diff options
author | iclelland <iclelland@chromium.org> | 2016-01-06 12:35:55 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-01-06 20:36:59 +0000 |
commit | 672676d8ae5f598f56714bb81b45498476952e9f (patch) | |
tree | 2e5ed7306da8477f77007196bfe4178842e80c5d /content | |
parent | 15a33c499fb52b3f1d53db0c5351291973664d8e (diff) | |
download | chromium_src-672676d8ae5f598f56714bb81b45498476952e9f.zip chromium_src-672676d8ae5f598f56714bb81b45498476952e9f.tar.gz chromium_src-672676d8ae5f598f56714bb81b45498476952e9f.tar.bz2 |
Add API Key parsing for experimental APIs
This adds the experimental framework API key parsing mechanism to Chromium.
This code deals with origins, API names, and dates. The validation currently checks for general well-formedness, and can determine, given an origin and API name, whether a given key is appropriate for use. (This is parsing only; actual signature validation is a separate CL)
BUG=543146
Review URL: https://codereview.chromium.org/1521063003
Cr-Commit-Position: refs/heads/master@{#367893}
Diffstat (limited to 'content')
-rw-r--r-- | content/common/experiments/api_key.cc | 98 | ||||
-rw-r--r-- | content/common/experiments/api_key.h | 89 | ||||
-rw-r--r-- | content/common/experiments/api_key_unittest.cc | 129 | ||||
-rw-r--r-- | content/content_common.gypi | 2 | ||||
-rw-r--r-- | content/content_tests.gypi | 1 |
5 files changed, 319 insertions, 0 deletions
diff --git a/content/common/experiments/api_key.cc b/content/common/experiments/api_key.cc new file mode 100644 index 0000000..09b607f --- /dev/null +++ b/content/common/experiments/api_key.cc @@ -0,0 +1,98 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/common/experiments/api_key.h" + +#include <vector> + +#include "base/base64.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "url/origin.h" + +namespace content { + +namespace { + +const char* kApiKeyFieldSeparator = "|"; +} + +ApiKey::~ApiKey() {} + +scoped_ptr<ApiKey> ApiKey::Parse(const std::string& key_text) { + if (key_text.empty()) { + return nullptr; + } + + // API Key should resemble: + // signature|origin|api_name|expiry_timestamp + std::vector<std::string> parts = + SplitString(key_text, kApiKeyFieldSeparator, base::KEEP_WHITESPACE, + base::SPLIT_WANT_ALL); + if (parts.size() != 4) { + return nullptr; + } + + const std::string& signature = parts[0]; + const std::string& origin_string = parts[1]; + const std::string& api_name = parts[2]; + const std::string& expiry_string = parts[3]; + + uint64_t expiry_timestamp; + if (!base::StringToUint64(expiry_string, &expiry_timestamp)) { + return nullptr; + } + + // Ensure that the origin is a valid (non-unique) origin URL + GURL origin_url(origin_string); + if (url::Origin(origin_url).unique()) { + return nullptr; + } + + // signed data is (origin + "|" + api_name + "|" + expiry). + std::string data = key_text.substr(signature.length() + 1); + + return make_scoped_ptr( + new ApiKey(signature, data, origin_url, api_name, expiry_timestamp)); +} + +ApiKey::ApiKey(const std::string& signature, + const std::string& data, + const GURL& origin, + const std::string& api_name, + uint64_t expiry_timestamp) + : signature_(signature), + data_(data), + origin_(origin), + api_name_(api_name), + expiry_timestamp_(expiry_timestamp) {} + +bool ApiKey::IsAppropriate(const std::string& origin, + const std::string& api_name) const { + return ValidateOrigin(origin) && ValidateApiName(api_name); +} + +bool ApiKey::IsValid(const base::Time& now) const { + // TODO(iclelland): Validate signature on key data here as well. + // https://crbug.com/543215 + return ValidateDate(now); +} + +bool ApiKey::ValidateOrigin(const std::string& origin) const { + return GURL(origin) == origin_; +} + +bool ApiKey::ValidateApiName(const std::string& api_name) const { + return base::EqualsCaseInsensitiveASCII(api_name, api_name_); +} + +bool ApiKey::ValidateDate(const base::Time& now) const { + base::Time expiry_time = base::Time::FromDoubleT((double)expiry_timestamp_); + return expiry_time > now; +} + +} // namespace content diff --git a/content/common/experiments/api_key.h b/content/common/experiments/api_key.h new file mode 100644 index 0000000..6b06ea2 --- /dev/null +++ b/content/common/experiments/api_key.h @@ -0,0 +1,89 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_COMMON_EXPERIMENTS_API_KEY_H_ +#define CONTENT_COMMON_EXPERIMENTS_API_KEY_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/time/time.h" +#include "content/common/content_export.h" +#include "url/gurl.h" + +namespace content { + +// The Experimental Framework (EF) provides limited access to experimental APIs, +// on a per-origin basis. This class defines the API key data structure, used +// to securely provide access to an experimental API. +// +// Experimental APIs are defined by string names, provided by the implementers. +// The EF code does not maintain an enum or constant list for experiment names. +// Instead, the EF validates the name provided by the API implementation against +// any provided API keys. +// +// More documentation on the key format can be found at +// https://docs.google.com/document/d/1v5fi0EUV_QHckVHVF2K4P72iNywnrJtNhNZ6i2NPt0M + +class CONTENT_EXPORT ApiKey { + public: + ~ApiKey(); + + // Returns a key object if the string represents a well-formed key, or + // nullptr otherwise. (This does not mean that the key is valid, just that it + // can be parsed.) + static scoped_ptr<ApiKey> Parse(const std::string& key_text); + + // Returns true if this API is appropriate for use by the given origin, for + // the given API name. This does not check whether the signature is valid, or + // whether the key itself has expired. + bool IsAppropriate(const std::string& origin, + const std::string& apiName) const; + + // Returns true if this API key has a valid signature, and has not expired. + bool IsValid(const base::Time& now) const; + + std::string signature() { return signature_; } + std::string data() { return data_; } + GURL origin() { return origin_; } + std::string api_name() { return api_name_; } + uint64_t expiry_timestamp() { return expiry_timestamp_; } + + protected: + friend class ApiKeyTest; + + bool ValidateOrigin(const std::string& origin) const; + bool ValidateApiName(const std::string& api_name) const; + bool ValidateDate(const base::Time& now) const; + + private: + ApiKey(const std::string& signature, + const std::string& data, + const GURL& origin, + const std::string& api_name, + uint64_t expiry_timestamp); + + // The base64-encoded-signature portion of the key. For the key to be valid, + // this must be a valid signature for the data portion of the key, as verified + // by the public key in api_key.cc. + std::string signature_; + + // The portion of the key string which is signed, and whose signature is + // contained in the signature_ member. + std::string data_; + + // The origin for which this key is valid. Must be a secure origin. + GURL origin_; + + // The name of the API experiment which this key enables. + std::string api_name_; + + // The time until which this key should be considered valid, in UTC, as + // seconds since the Unix epoch. + uint64_t expiry_timestamp_; +}; + +} // namespace content + +#endif // CONTENT_COMMON_EXPERIMENTS_API_KEY_H_ diff --git a/content/common/experiments/api_key_unittest.cc b/content/common/experiments/api_key_unittest.cc new file mode 100644 index 0000000..697456d --- /dev/null +++ b/content/common/experiments/api_key_unittest.cc @@ -0,0 +1,129 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/common/experiments/api_key.h" + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_util.h" +#include "base/test/simple_test_clock.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +const char* kSampleAPIKey = + "Signature|https://valid.example.com|Frobulate|1458766277"; + +const char* kExpectedAPIKeySignature = "Signature"; +const char* kExpectedAPIKeyData = + "https://valid.example.com|Frobulate|1458766277"; +const char* kExpectedAPIName = "Frobulate"; +const char* kExpectedOrigin = "https://valid.example.com"; +const uint64_t kExpectedExpiry = 1458766277; + +// The key should not be valid for this origin, or for this API. +const char* kInvalidOrigin = "https://invalid.example.com"; +const char* kInsecureOrigin = "http://valid.example.com"; +const char* kInvalidAPIName = "Grokalyze"; + +// The key should be valid if the current time is kValidTimestamp or earlier. +double kValidTimestamp = 1458766276.0; + +// The key should be invalid if the current time is kInvalidTimestamp or later. +double kInvalidTimestamp = 1458766278.0; + +// Various ill-formed API keys. These should all fail to parse. +const char* kInvalidAPIKeys[] = { + // Invalid - only one part + "abcde", + // Not enough parts + "https://valid.example.com|APIName|1458766277", + // Delimiter in API Name + "Signature|https://valid.example.com|API|Name|1458766277", + // Extra string field + "Signature|https://valid.example.com|APIName|1458766277|SomethingElse", + // Extra numeric field + "Signature|https://valid.example.com|APIName|1458766277|1458766277", + // Non-numeric expiry timestamp + "Signature|https://valid.example.com|APIName|abcdefghij", + "Signature|https://valid.example.com|APIName|1458766277x", + // Negative expiry timestamp + "Signature|https://valid.example.com|APIName|-1458766277", + // Origin not a proper origin URL + "Signature|abcdef|APIName|1458766277", + "Signature|data:text/plain,abcdef|APIName|1458766277", + "Signature|javascript:alert(1)|APIName|1458766277"}; +const size_t kNumInvalidAPIKeys = arraysize(kInvalidAPIKeys); + +} // namespace + +class ApiKeyTest : public testing::Test { + protected: + bool ValidateOrigin(ApiKey* api_key, const char* origin) { + return api_key->ValidateOrigin(origin); + } + + bool ValidateApiName(ApiKey* api_key, const char* api_name) { + return api_key->ValidateApiName(api_name); + } + + bool ValidateDate(ApiKey* api_key, const base::Time& now) { + return api_key->ValidateDate(now); + } +}; + +TEST_F(ApiKeyTest, ParseEmptyString) { + scoped_ptr<ApiKey> empty_key = ApiKey::Parse(""); + EXPECT_FALSE(empty_key); +} + +TEST_F(ApiKeyTest, ParseInvalidStrings) { + for (size_t i = 0; i < kNumInvalidAPIKeys; ++i) { + scoped_ptr<ApiKey> empty_key = ApiKey::Parse(kInvalidAPIKeys[i]); + EXPECT_FALSE(empty_key) << "Invalid API Key should not parse: " + << kInvalidAPIKeys[i]; + } +} + +TEST_F(ApiKeyTest, ParseValidKeyString) { + scoped_ptr<ApiKey> key = ApiKey::Parse(kSampleAPIKey); + ASSERT_TRUE(key); + EXPECT_EQ(kExpectedAPIName, key->api_name()); + EXPECT_EQ(kExpectedAPIKeySignature, key->signature()); + EXPECT_EQ(kExpectedAPIKeyData, key->data()); + EXPECT_EQ(GURL(kExpectedOrigin), key->origin()); + EXPECT_EQ(kExpectedExpiry, key->expiry_timestamp()); +} + +TEST_F(ApiKeyTest, ValidateValidKey) { + scoped_ptr<ApiKey> key = ApiKey::Parse(kSampleAPIKey); + ASSERT_TRUE(key); + EXPECT_TRUE(ValidateOrigin(key.get(), kExpectedOrigin)); + EXPECT_FALSE(ValidateOrigin(key.get(), kInvalidOrigin)); + EXPECT_FALSE(ValidateOrigin(key.get(), kInsecureOrigin)); + EXPECT_TRUE(ValidateApiName(key.get(), kExpectedAPIName)); + EXPECT_FALSE(ValidateApiName(key.get(), kInvalidAPIName)); + EXPECT_TRUE( + ValidateDate(key.get(), base::Time::FromDoubleT(kValidTimestamp))); + EXPECT_FALSE( + ValidateDate(key.get(), base::Time::FromDoubleT(kInvalidTimestamp))); +} + +TEST_F(ApiKeyTest, KeyIsAppropriateForOriginAndAPI) { + scoped_ptr<ApiKey> key = ApiKey::Parse(kSampleAPIKey); + ASSERT_TRUE(key); + EXPECT_TRUE(key->IsAppropriate(kExpectedOrigin, kExpectedAPIName)); + EXPECT_TRUE(key->IsAppropriate(kExpectedOrigin, + base::ToUpperASCII(kExpectedAPIName))); + EXPECT_TRUE(key->IsAppropriate(kExpectedOrigin, + base::ToLowerASCII(kExpectedAPIName))); + EXPECT_FALSE(key->IsAppropriate(kInvalidOrigin, kExpectedAPIName)); + EXPECT_FALSE(key->IsAppropriate(kInsecureOrigin, kExpectedAPIName)); + EXPECT_FALSE(key->IsAppropriate(kExpectedOrigin, kInvalidAPIName)); +} + +} // namespace content diff --git a/content/content_common.gypi b/content/content_common.gypi index 7eb25c8..5509c88 100644 --- a/content/content_common.gypi +++ b/content/content_common.gypi @@ -253,6 +253,8 @@ 'common/dwrite_font_platform_win.cc', 'common/dwrite_font_proxy_messages.h', 'common/edit_command.h', + 'common/experiments/api_key.cc', + 'common/experiments/api_key.h', 'common/file_utilities_messages.h', 'common/fileapi/file_system_messages.h', 'common/fileapi/webblob_messages.h', diff --git a/content/content_tests.gypi b/content/content_tests.gypi index 29a11df..297fd7d 100644 --- a/content/content_tests.gypi +++ b/content/content_tests.gypi @@ -657,6 +657,7 @@ 'common/discardable_shared_memory_heap_unittest.cc', 'common/dom_storage/dom_storage_map_unittest.cc', 'common/dwrite_font_platform_win_unittest.cc', + 'common/experiments/api_key_unittest.cc', 'common/fileapi/file_system_util_unittest.cc', 'common/font_warmup_win_unittest.cc', 'common/gpu/client/gpu_memory_buffer_impl_shared_memory_unittest.cc', |