From 0709f4ee3881c0c97c7e765ba824b20d5464771f Mon Sep 17 00:00:00 2001 From: iclelland Date: Thu, 14 Apr 2016 09:21:17 -0700 Subject: [PATCH] Change origin trial token format This CL introduces a new format for origin trial tokens. The tokens are JSON-encoded data, wrapped in a base64-encoded signed binary structure. The token format is documented at https://docs.google.com/document/d/1v5fi0EUV_QHckVHVF2K4P72iNywnrJtNhNZ6i2NPt0M Additionally, this CL changes the order of operations when processing tokens. Now signature verification is always performed as part of parsing, and the token's applicability can be checked after it has been verified and parsed. BUG=601090 Review URL: https://codereview.chromium.org/1858763003 Cr-Commit-Position: refs/heads/master@{#387333} --- content/common/origin_trials/trial_token.cc | 171 +++++++----- content/common/origin_trials/trial_token.h | 69 ++--- .../origin_trials/trial_token_unittest.cc | 255 +++++++++++------- .../origin_trials/trial_token_validator.cc | 14 +- .../origin_trials/trial_token_validator.h | 2 +- .../trial_token_validator_unittest.cc | 23 +- .../origin_trials/sample-api-enabled.html | 6 +- .../origin_trials/sample-api-expired.html | 5 +- .../sample-api-multiple-tokens.html | 11 +- .../origin_trials/sample-api-stolen.html | 5 +- tools/origin_trials/generate_token.py | 22 +- 11 files changed, 345 insertions(+), 238 deletions(-) diff --git a/content/common/origin_trials/trial_token.cc b/content/common/origin_trials/trial_token.cc index cc91aae2bee23c..0a137edc68b43e 100644 --- a/content/common/origin_trials/trial_token.cc +++ b/content/common/origin_trials/trial_token.cc @@ -9,13 +9,13 @@ #include #include "base/base64.h" +#include "base/big_endian.h" +#include "base/json/json_reader.h" #include "base/macros.h" #include "base/memory/ptr_util.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/strings/string_piece.h" #include "base/time/time.h" +#include "base/values.h" #include "url/gurl.h" #include "url/origin.h" @@ -23,84 +23,133 @@ namespace content { namespace { -// Version 1 is the only token version currently supported -const uint8_t kVersion1 = 1; +// Version is a 1-byte field at offset 0. +const size_t kVersionOffset = 0; +const size_t kVersionSize = 1; -const char kFieldSeparator[] = "|"; +// These constants define the Version 2 field sizes and offsets. +const size_t kSignatureOffset = kVersionOffset + kVersionSize; +const size_t kSignatureSize = 64; +const size_t kPayloadLengthOffset = kSignatureOffset + kSignatureSize; +const size_t kPayloadLengthSize = 4; +const size_t kPayloadOffset = kPayloadLengthOffset + kPayloadLengthSize; + +// Version 2 is the only token version currently supported. Version 1 was +// introduced in Chrome M50, and removed in M51. There were no experiments +// enabled in the stable M50 release which would have used those tokens. +const uint8_t kVersion2 = 2; } // namespace TrialToken::~TrialToken() {} -std::unique_ptr TrialToken::Parse(const std::string& token_text) { - if (token_text.empty()) { +// static +std::unique_ptr TrialToken::From(const std::string& token_text, + base::StringPiece public_key) { + std::unique_ptr token_payload = Extract(token_text, public_key); + if (!token_payload) { return nullptr; } + return Parse(*token_payload); +} + +bool TrialToken::IsValidForFeature(const url::Origin& origin, + base::StringPiece feature_name, + const base::Time& now) const { + return ValidateOrigin(origin) && ValidateFeatureName(feature_name) && + ValidateDate(now); +} - // Extract the version from the token. The version must be the first part of - // the token, separated from the remainder, as: - // version| - size_t version_end = token_text.find(kFieldSeparator); - if (version_end == std::string::npos) { +std::unique_ptr TrialToken::Extract( + const std::string& token_payload, + base::StringPiece public_key) { + if (token_payload.empty()) { return nullptr; } - std::string version_string = token_text.substr(0, version_end); - unsigned int version = 0; - if (!base::StringToUint(version_string, &version) || version > UINT8_MAX) { + // Token is base64-encoded; decode first. + std::string token_contents; + if (!base::Base64Decode(token_payload, &token_contents)) { return nullptr; } - // Only version 1 currently supported - if (version != kVersion1) { + // Only version 2 currently supported. + if (token_contents.length() < (kVersionOffset + kVersionSize)) { + return nullptr; + } + uint8_t version = token_contents[kVersionOffset]; + if (version != kVersion2) { return nullptr; } - // Extract the version-specific contents of the token - std::string token_contents = token_text.substr(version_end + 1); + // Token must be large enough to contain a version, signature, and payload + // length. + if (token_contents.length() < (kPayloadLengthOffset + kPayloadLengthSize)) { + return nullptr; + } + + // Extract the length of the signed data (Big-endian). + uint32_t payload_length; + base::ReadBigEndian(&(token_contents[kPayloadLengthOffset]), &payload_length); - // The contents of a valid version 1 token should resemble: - // signature|origin|feature_name|expiry_timestamp - std::vector parts = - SplitString(token_contents, kFieldSeparator, base::KEEP_WHITESPACE, - base::SPLIT_WANT_ALL); - if (parts.size() != 4) { + // Validate that the stated length matches the actual payload length. + if (payload_length != token_contents.length() - kPayloadOffset) { return nullptr; } - const std::string& signature = parts[0]; - const std::string& origin_string = parts[1]; - const std::string& feature_name = parts[2]; - const std::string& expiry_string = parts[3]; + // Extract the version-specific contents of the token. + const char* token_bytes = token_contents.data(); + base::StringPiece version_piece(token_bytes + kVersionOffset, kVersionSize); + base::StringPiece signature(token_bytes + kSignatureOffset, kSignatureSize); + base::StringPiece payload_piece(token_bytes + kPayloadLengthOffset, + kPayloadLengthSize + payload_length); - uint64_t expiry_timestamp; - if (!base::StringToUint64(expiry_string, &expiry_timestamp)) { + // The data which is covered by the signature is (version + length + payload). + std::string signed_data = + version_piece.as_string() + payload_piece.as_string(); + + // Validate the signature on the data before proceeding. + if (!TrialToken::ValidateSignature(signature, signed_data, public_key)) { return nullptr; } - // Ensure that the origin is a valid (non-unique) origin URL + // Return just the payload, as a new string. + return base::WrapUnique( + new std::string(token_contents, kPayloadOffset, payload_length)); +} + +std::unique_ptr TrialToken::Parse(const std::string& token_json) { + std::unique_ptr datadict = + base::DictionaryValue::From(base::JSONReader::Read(token_json)); + if (!datadict) { + return nullptr; + } + + std::string origin_string; + std::string feature_name; + int expiry_timestamp = 0; + datadict->GetString("origin", &origin_string); + datadict->GetString("feature", &feature_name); + datadict->GetInteger("expiry", &expiry_timestamp); + + // Ensure that the origin is a valid (non-unique) origin URL. url::Origin origin = url::Origin(GURL(origin_string)); if (origin.unique()) { return nullptr; } - // Signed data is (origin + "|" + feature_name + "|" + expiry). - std::string data = token_contents.substr(signature.length() + 1); - - return base::WrapUnique(new TrialToken(version, signature, data, origin, - feature_name, expiry_timestamp)); -} + // Ensure that the feature name is a valid string. + if (feature_name.empty()) { + return nullptr; + } -bool TrialToken::IsAppropriate(const url::Origin& origin, - base::StringPiece feature_name) const { - return ValidateOrigin(origin) && ValidateFeatureName(feature_name); -} + // Ensure that the expiry timestamp is a valid (positive) integer. + if (expiry_timestamp <= 0) { + return nullptr; + } -bool TrialToken::IsValid(const base::Time& now, - base::StringPiece public_key) const { - // TODO(iclelland): Allow for multiple signing keys, and iterate over all - // active keys here. https://crbug.com/543220 - return ValidateDate(now) && ValidateSignature(public_key); + return base::WrapUnique( + new TrialToken(origin, feature_name, expiry_timestamp)); } bool TrialToken::ValidateOrigin(const url::Origin& origin) const { @@ -115,24 +164,14 @@ bool TrialToken::ValidateDate(const base::Time& now) const { return expiry_time_ > now; } -bool TrialToken::ValidateSignature(base::StringPiece public_key) const { - return ValidateSignature(signature_, data_, public_key); -} - // static -bool TrialToken::ValidateSignature(const std::string& signature_text, +bool TrialToken::ValidateSignature(base::StringPiece signature, const std::string& data, base::StringPiece public_key) { // Public key must be 32 bytes long for Ed25519. CHECK_EQ(public_key.length(), 32UL); - std::string signature; - // signature_text is base64-encoded; decode first. - if (!base::Base64Decode(signature_text, &signature)) { - return false; - } - - // Signature must be 64 bytes long + // Signature must be 64 bytes long. if (signature.length() != 64) { return false; } @@ -144,16 +183,10 @@ bool TrialToken::ValidateSignature(const std::string& signature_text, return (result != 0); } -TrialToken::TrialToken(uint8_t version, - const std::string& signature, - const std::string& data, - const url::Origin& origin, +TrialToken::TrialToken(const url::Origin& origin, const std::string& feature_name, uint64_t expiry_timestamp) - : version_(version), - signature_(signature), - data_(data), - origin_(origin), + : origin_(origin), feature_name_(feature_name), expiry_time_(base::Time::FromDoubleT(expiry_timestamp)) {} diff --git a/content/common/origin_trials/trial_token.h b/content/common/origin_trials/trial_token.h index 3143a7efec5582..fa05e3713b5b94 100644 --- a/content/common/origin_trials/trial_token.h +++ b/content/common/origin_trials/trial_token.h @@ -32,23 +32,20 @@ class CONTENT_EXPORT TrialToken { public: ~TrialToken(); - // Returns a token object if the string represents a well-formed token, or - // nullptr otherwise. (This does not mean that the token is valid, just that - // it can be parsed.) - static std::unique_ptr Parse(const std::string& token_text); - - // Returns true if this feature is appropriate for use by the given origin, - // for the given feature name. This does not check whether the signature is - // valid, or whether the token itself has expired. - bool IsAppropriate(const url::Origin& origin, - base::StringPiece feature_name) const; - - // Returns true if this token has a valid signature, and has not expired. - bool IsValid(const base::Time& now, base::StringPiece public_key) const; - - uint8_t version() { return version_; } - std::string signature() { return signature_; } - std::string data() { return data_; } + // Returns a token object if the string represents a signed well-formed token, + // or nullptr otherwise. (This does not mean that the token is currently + // valid, or appropriate for a given origin / feature, just that it is + // correctly formatted and signed by the supplied public key, and can be + // parsed.) + static std::unique_ptr From(const std::string& token_text, + base::StringPiece public_key); + + // Returns true if this token is appropriate for use by the given origin, + // for the given feature name, and has not yet expired. + bool IsValidForFeature(const url::Origin& origin, + base::StringPiece feature_name, + const base::Time& now) const; + url::Origin origin() { return origin_; } std::string feature_name() { return feature_name_; } base::Time expiry_time() { return expiry_time_; } @@ -56,44 +53,28 @@ class CONTENT_EXPORT TrialToken { protected: friend class TrialTokenTest; + // Returns the payload of a signed token, or nullptr if the token is not + // properly signed, or is not well-formed. + static std::unique_ptr Extract(const std::string& token_text, + base::StringPiece public_key); + + // Returns a token object if the string represents a well-formed JSON token + // payload, or nullptr otherwise. + static std::unique_ptr Parse(const std::string& token_json); + bool ValidateOrigin(const url::Origin& origin) const; bool ValidateFeatureName(base::StringPiece feature_name) const; bool ValidateDate(const base::Time& now) const; - bool ValidateSignature(base::StringPiece public_key) const; - static bool ValidateSignature(const std::string& signature_text, + static bool ValidateSignature(base::StringPiece signature_text, const std::string& data, base::StringPiece public_key); private: - TrialToken(uint8_t version, - const std::string& signature, - const std::string& data, - const url::Origin& origin, + TrialToken(const url::Origin& origin, const std::string& feature_name, uint64_t expiry_timestamp); - // The version number for this token. The version identifies the structure of - // the token, as well as the algorithm used to generate/validate the token. - // The version number is only incremented when incompatible changes are made - // to either the structure (e.g. adding a field), or the algorithm (e.g. - // changing the hash or signing algorithm). - // NOTE: The version number is not part of the token signature and validation. - // Thus it is possible to modify a token to use a different version from - // that used to generate the signature. To mitigate cross-version - // attacks, the signing key(s) should be changed whenever there are - // changes to the token structure or algorithms. - uint8_t version_; - - // The base64-encoded-signature portion of the token. For the token to be - // valid, this must be a valid signature for the data portion of the token, as - // verified by the public key in trial_token.cc. - std::string signature_; - - // The portion of the token string which is signed, and whose signature is - // contained in the signature_ member. - std::string data_; - // The origin for which this token is valid. Must be a secure origin. url::Origin origin_; diff --git a/content/common/origin_trials/trial_token_unittest.cc b/content/common/origin_trials/trial_token_unittest.cc index f5a2527a1a8723..ebb5b05a4cf550 100644 --- a/content/common/origin_trials/trial_token_unittest.cc +++ b/content/common/origin_trials/trial_token_unittest.cc @@ -49,14 +49,14 @@ const uint8_t kTestPublicKey2[] = { }; // This is a good trial token, signed with the above test private key. +// Generate this token with the command (in tools/origin_trials): +// generate_token.py valid.example.com Frobulate --expire-timestamp=1458766277 const char* kSampleToken = - "1|UsEO0cNxoUtBnHDJdGPWTlXuLENjXcEIPL7Bs7sbvicPCcvAtyqhQuTJ9h/u1R3VZpWigtI+" - "SdUwk7Dyk/qbDw==|https://valid.example.com|Frobulate|1458766277"; -const uint8_t kExpectedVersion = 1; -const char* kExpectedSignature = - "UsEO0cNxoUtBnHDJdGPWTlXuLENjXcEIPL7Bs7sbvicPCcvAtyqhQuTJ9h/u1R3VZpWigtI+S" - "dUwk7Dyk/qbDw=="; -const char* kExpectedData = "https://valid.example.com|Frobulate|1458766277"; + "Ap+Q/Qm0ELadZql+dlEGSwnAVsFZKgCEtUZg8idQC3uekkIeSZIY1tftoYdrwhqj" + "7FO5L22sNvkZZnacLvmfNwsAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMTQ1ODc2NjI3N30="; + const char* kExpectedFeatureName = "Frobulate"; const char* kExpectedOrigin = "https://valid.example.com"; const uint64_t kExpectedExpiry = 1458766277; @@ -75,49 +75,76 @@ double kInvalidTimestamp = 1458766278.0; // Well-formed trial token with an invalid signature. const char* kInvalidSignatureToken = - "1|CO8hDne98QeFeOJ0DbRZCBN3uE0nyaPgaLlkYhSWnbRoDfEAg+TXELaYfQPfEvKYFauBg/" - "hnxmba765hz0mXMc==|https://valid.example.com|Frobulate|1458766277"; + "AYeNXSDktgG9p4ns5B1WKsLq2TytMxfR4whfbi+oyT0rXyzh+qXYfxbDMGmyjU2Z" + "lEJ16vQObMXJoOaYUqd8xwkAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMTQ1ODc2NjI3N30="; + +// Trial token truncated in the middle of the length field; too short to +// possibly be valid. +const char kTruncatedToken[] = + "Ap+Q/Qm0ELadZql+dlEGSwnAVsFZKgCEtUZg8idQC3uekkIeSZIY1tftoYdrwhqj" + "7FO5L22sNvkZZnacLvmfNwsA"; + +// Trial token with an incorrectly-declared length, but with a valid signature. +const char kIncorrectLengthToken[] = + "Ao06eNl/CZuM88qurWKX4RfoVEpHcVHWxdOTrEXZkaC1GUHyb/8L4sthADiVWdc9" + "kXFyF1BW5bbraqp6MBVr3wEAAABaeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMTQ1ODc2NjI3N30="; + +// Trial token with a misidentified version (42). +const char kIncorrectVersionToken[] = + "KlH8wVLT5o59uDvlJESorMDjzgWnvG1hmIn/GiT9Ng3f45ratVeiXCNTeaJheOaG" + "A6kX4ir4Amv8aHVC+OJHZQkAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMTQ1ODc2NjI3N30="; + +const char kSampleTokenJSON[] = + "{\"origin\": \"https://valid.example.com:443\", \"feature\": " + "\"Frobulate\", \"expiry\": 1458766277}"; // Various ill-formed trial tokens. These should all fail to parse. const char* kInvalidTokens[] = { - // Invalid - only one part + // Invalid - Not JSON at all "abcde", - // Not enough parts - "https://valid.example.com|FeatureName|1458766277", - "Signature|https://valid.example.com|FeatureName|1458766277", - // Non-numeric version - "a|Signature|https://valid.example.com|FeatureName|1458766277", - "1x|Signature|https://valid.example.com|FeatureName|1458766277", - // Unsupported version (< min, > max, negative, overflow) - "0|Signature|https://valid.example.com|FeatureName|1458766277", - "2|Signature|https://valid.example.com|FeatureName|1458766277", - "-1|Signature|https://valid.example.com|FeatureName|1458766277", - "99999|Signature|https://valid.example.com|FeatureName|1458766277", - // Delimiter in feature name - "1|Signature|https://valid.example.com|Feature|Name|1458766277", - // Extra string field - "1|Signature|https://valid.example.com|FeatureName|1458766277|ExtraField", - // Extra numeric field - "1|Signature|https://valid.example.com|FeatureName|1458766277|1458766277", - // Non-numeric expiry timestamp - "1|Signature|https://valid.example.com|FeatureName|abcdefghij", - "1|Signature|https://valid.example.com|FeatureName|1458766277x", + // Invalid JSON + "{", + // Not an object + "\"abcde\"", + "123.4", + "[0, 1, 2]", + // Missing keys + "{}", + "{\"something\": 1}", + "{\"origin\": \"https://a.a\"}", + "{\"origin\": \"https://a.a\", \"feature\": \"a\"}" + "{\"origin\": \"https://a.a\", \"expiry\": 1458766277}", + "{\"feature\": \"FeatureName\", \"expiry\": 1458766277}", + // Incorrect types + "{\"origin\": 1, \"feature\": \"a\", \"expiry\": 1458766277}", + "{\"origin\": \"https://a.a\", \"feature\": 1, \"expiry\": 1458766277}", + "{\"origin\": \"https://a.a\", \"feature\": \"a\", \"expiry\": \"1\"}", // Negative expiry timestamp - "1|Signature|https://valid.example.com|FeatureName|-1458766277", + "{\"origin\": \"https://a.a\", \"feature\": \"a\", \"expiry\": -1}", // Origin not a proper origin URL - "1|Signature|abcdef|FeatureName|1458766277", - "1|Signature|data:text/plain,abcdef|FeatureName|1458766277", - "1|Signature|javascript:alert(1)|FeatureName|1458766277"}; -const size_t kNumInvalidTokens = arraysize(kInvalidTokens); + "{\"origin\": \"abcdef\", \"feature\": \"a\", \"expiry\": 1458766277}", + "{\"origin\": \"data:text/plain,abcdef\", \"feature\": \"a\", \"expiry\": " + "1458766277}", + "{\"origin\": \"javascript:alert(1)\", \"feature\": \"a\", \"expiry\": " + "1458766277}"}; } // namespace -class TrialTokenTest : public testing::Test { +class TrialTokenTest : public testing::TestWithParam { public: TrialTokenTest() : expected_origin_(GURL(kExpectedOrigin)), invalid_origin_(GURL(kInvalidOrigin)), insecure_origin_(GURL(kInsecureOrigin)), + expected_expiry_(base::Time::FromDoubleT(kExpectedExpiry)), + valid_timestamp_(base::Time::FromDoubleT(kValidTimestamp)), + invalid_timestamp_(base::Time::FromDoubleT(kInvalidTimestamp)), correct_public_key_( base::StringPiece(reinterpret_cast(kTestPublicKey), arraysize(kTestPublicKey))), @@ -126,6 +153,14 @@ class TrialTokenTest : public testing::Test { arraysize(kTestPublicKey2))) {} protected: + std::unique_ptr Extract(const std::string& token_text, + base::StringPiece public_key) { + return TrialToken::Extract(token_text, public_key); + } + std::unique_ptr Parse(const std::string& token_payload) { + return TrialToken::Parse(token_payload); + } + bool ValidateOrigin(TrialToken* token, const url::Origin origin) { return token->ValidateOrigin(origin); } @@ -138,11 +173,6 @@ class TrialTokenTest : public testing::Test { return token->ValidateDate(now); } - bool ValidateSignature(TrialToken* token, - const base::StringPiece& public_key) { - return token->ValidateSignature(public_key); - } - base::StringPiece correct_public_key() { return correct_public_key_; } base::StringPiece incorrect_public_key() { return incorrect_public_key_; } @@ -150,38 +180,88 @@ class TrialTokenTest : public testing::Test { const url::Origin invalid_origin_; const url::Origin insecure_origin_; + const base::Time expected_expiry_; + const base::Time valid_timestamp_; + const base::Time invalid_timestamp_; + private: base::StringPiece correct_public_key_; base::StringPiece incorrect_public_key_; }; +// Test the extraction of the signed payload from token strings. This includes +// checking the included version identifier, payload length, and cryptographic +// signature. + +// Test verification of signature and extraction of token JSON from signed +// token. +TEST_F(TrialTokenTest, ValidateValidSignature) { + std::unique_ptr token_payload = + Extract(kSampleToken, correct_public_key()); + ASSERT_TRUE(token_payload); + EXPECT_STREQ(kSampleTokenJSON, token_payload.get()->c_str()); +} + +TEST_F(TrialTokenTest, ValidateInvalidSignature) { + std::unique_ptr token_payload = + Extract(kInvalidSignatureToken, correct_public_key()); + ASSERT_FALSE(token_payload); +} + +TEST_F(TrialTokenTest, ValidateSignatureWithIncorrectKey) { + std::unique_ptr token_payload = + Extract(kSampleToken, incorrect_public_key()); + ASSERT_FALSE(token_payload); +} + +TEST_F(TrialTokenTest, ValidateEmptyToken) { + std::unique_ptr token_payload = + Extract("", correct_public_key()); + ASSERT_FALSE(token_payload); +} + +TEST_F(TrialTokenTest, ValidateShortToken) { + std::unique_ptr token_payload = + Extract(kTruncatedToken, correct_public_key()); + ASSERT_FALSE(token_payload); +} + +TEST_F(TrialTokenTest, ValidateUnsupportedVersion) { + std::unique_ptr token_payload = + Extract(kIncorrectVersionToken, correct_public_key()); + ASSERT_FALSE(token_payload); +} + +TEST_F(TrialTokenTest, ValidateSignatureWithIncorrectLength) { + std::unique_ptr token_payload = + Extract(kIncorrectLengthToken, correct_public_key()); + ASSERT_FALSE(token_payload); +} + +// Test parsing of fields from JSON token. + TEST_F(TrialTokenTest, ParseEmptyString) { - std::unique_ptr empty_token = TrialToken::Parse(""); + std::unique_ptr empty_token = Parse(""); EXPECT_FALSE(empty_token); } -TEST_F(TrialTokenTest, ParseInvalidStrings) { - for (size_t i = 0; i < kNumInvalidTokens; ++i) { - std::unique_ptr empty_token = - TrialToken::Parse(kInvalidTokens[i]); - EXPECT_FALSE(empty_token) << "Invalid trial token should not parse: " - << kInvalidTokens[i]; - } +TEST_P(TrialTokenTest, ParseInvalidString) { + std::unique_ptr empty_token = Parse(GetParam()); + EXPECT_FALSE(empty_token) << "Invalid trial token should not parse."; } +INSTANTIATE_TEST_CASE_P(, TrialTokenTest, ::testing::ValuesIn(kInvalidTokens)); + TEST_F(TrialTokenTest, ParseValidToken) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); + std::unique_ptr token = Parse(kSampleTokenJSON); ASSERT_TRUE(token); - EXPECT_EQ(kExpectedVersion, token->version()); EXPECT_EQ(kExpectedFeatureName, token->feature_name()); - EXPECT_EQ(kExpectedSignature, token->signature()); - EXPECT_EQ(kExpectedData, token->data()); EXPECT_EQ(expected_origin_, token->origin()); - EXPECT_EQ(base::Time::FromDoubleT(kExpectedExpiry), token->expiry_time()); + EXPECT_EQ(expected_expiry_, token->expiry_time()); } TEST_F(TrialTokenTest, ValidateValidToken) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); + std::unique_ptr token = Parse(kSampleTokenJSON); ASSERT_TRUE(token); EXPECT_TRUE(ValidateOrigin(token.get(), expected_origin_)); EXPECT_FALSE(ValidateOrigin(token.get(), invalid_origin_)); @@ -192,54 +272,29 @@ TEST_F(TrialTokenTest, ValidateValidToken) { token.get(), base::ToUpperASCII(kExpectedFeatureName).c_str())); EXPECT_FALSE(ValidateFeatureName( token.get(), base::ToLowerASCII(kExpectedFeatureName).c_str())); - EXPECT_TRUE( - ValidateDate(token.get(), base::Time::FromDoubleT(kValidTimestamp))); - EXPECT_FALSE( - ValidateDate(token.get(), base::Time::FromDoubleT(kInvalidTimestamp))); -} - -TEST_F(TrialTokenTest, TokenIsAppropriateForOriginAndFeature) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); - ASSERT_TRUE(token); - EXPECT_TRUE(token->IsAppropriate(expected_origin_, kExpectedFeatureName)); - EXPECT_FALSE(token->IsAppropriate(expected_origin_, - base::ToUpperASCII(kExpectedFeatureName))); - EXPECT_FALSE(token->IsAppropriate(expected_origin_, - base::ToLowerASCII(kExpectedFeatureName))); - EXPECT_FALSE(token->IsAppropriate(invalid_origin_, kExpectedFeatureName)); - EXPECT_FALSE(token->IsAppropriate(insecure_origin_, kExpectedFeatureName)); - EXPECT_FALSE(token->IsAppropriate(expected_origin_, kInvalidFeatureName)); -} - -TEST_F(TrialTokenTest, ValidateValidSignature) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); - ASSERT_TRUE(token); - EXPECT_TRUE(ValidateSignature(token.get(), correct_public_key())); -} - -TEST_F(TrialTokenTest, ValidateInvalidSignature) { - std::unique_ptr token = TrialToken::Parse(kInvalidSignatureToken); - ASSERT_TRUE(token); - EXPECT_FALSE(ValidateSignature(token.get(), correct_public_key())); -} - -TEST_F(TrialTokenTest, ValidateTokenWithCorrectKey) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); - ASSERT_TRUE(token); - EXPECT_TRUE(token->IsValid(base::Time::FromDoubleT(kValidTimestamp), - correct_public_key())); -} - -TEST_F(TrialTokenTest, ValidateSignatureWithIncorrectKey) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); - ASSERT_TRUE(token); - EXPECT_FALSE(token->IsValid(base::Time::FromDoubleT(kValidTimestamp), - incorrect_public_key())); + EXPECT_TRUE(ValidateDate(token.get(), valid_timestamp_)); + EXPECT_FALSE(ValidateDate(token.get(), invalid_timestamp_)); } -TEST_F(TrialTokenTest, ValidateWhenNotExpired) { - std::unique_ptr token = TrialToken::Parse(kSampleToken); +TEST_F(TrialTokenTest, TokenIsValidForFeature) { + std::unique_ptr token = Parse(kSampleTokenJSON); ASSERT_TRUE(token); + EXPECT_TRUE(token->IsValidForFeature(expected_origin_, kExpectedFeatureName, + valid_timestamp_)); + EXPECT_FALSE(token->IsValidForFeature( + expected_origin_, base::ToUpperASCII(kExpectedFeatureName), + valid_timestamp_)); + EXPECT_FALSE(token->IsValidForFeature( + expected_origin_, base::ToLowerASCII(kExpectedFeatureName), + valid_timestamp_)); + EXPECT_FALSE(token->IsValidForFeature(invalid_origin_, kExpectedFeatureName, + valid_timestamp_)); + EXPECT_FALSE(token->IsValidForFeature(insecure_origin_, kExpectedFeatureName, + valid_timestamp_)); + EXPECT_FALSE(token->IsValidForFeature(expected_origin_, kInvalidFeatureName, + valid_timestamp_)); + EXPECT_FALSE(token->IsValidForFeature(expected_origin_, kExpectedFeatureName, + invalid_timestamp_)); } } // namespace content diff --git a/content/common/origin_trials/trial_token_validator.cc b/content/common/origin_trials/trial_token_validator.cc index 2dd4d8aaa46db2..6eef75a5e9a40b 100644 --- a/content/common/origin_trials/trial_token_validator.cc +++ b/content/common/origin_trials/trial_token_validator.cc @@ -12,17 +12,19 @@ namespace content { bool TrialTokenValidator::ValidateToken(const std::string& token, const url::Origin& origin, - base::StringPiece featureName) { - std::unique_ptr trial_token = TrialToken::Parse(token); - + base::StringPiece feature_name) { // TODO(iclelland): Allow for multiple signing keys, and iterate over all // active keys here. https://crbug.com/543220 ContentClient* content_client = GetContentClient(); base::StringPiece public_key = content_client->GetOriginTrialPublicKey(); + if (public_key.empty()) { + return false; + } + std::unique_ptr trial_token = TrialToken::From(token, public_key); - return !public_key.empty() && trial_token && - trial_token->IsAppropriate(origin, featureName) && - trial_token->IsValid(base::Time::Now(), public_key); + return trial_token && + trial_token->IsValidForFeature(origin, feature_name, + base::Time::Now()); } } // namespace content diff --git a/content/common/origin_trials/trial_token_validator.h b/content/common/origin_trials/trial_token_validator.h index d19c112169dcbf..a8406350d71c48 100644 --- a/content/common/origin_trials/trial_token_validator.h +++ b/content/common/origin_trials/trial_token_validator.h @@ -17,7 +17,7 @@ namespace TrialTokenValidator { // This method is thread-safe. CONTENT_EXPORT bool ValidateToken(const std::string& token, const url::Origin& origin, - base::StringPiece featureName); + base::StringPiece feature_name); } // namespace TrialTokenValidator diff --git a/content/common/origin_trials/trial_token_validator_unittest.cc b/content/common/origin_trials/trial_token_validator_unittest.cc index e54a8d51c52232..59011e785f2e87 100644 --- a/content/common/origin_trials/trial_token_validator_unittest.cc +++ b/content/common/origin_trials/trial_token_validator_unittest.cc @@ -48,9 +48,13 @@ const uint8_t kTestPublicKey2[] = { // This is a good trial token, signed with the above test private key. // TODO(iclelland): This token expires in 2033. Update it or find a way // to autogenerate it before then. +// Generate this token with the command (in tools/origin_trials): +// generate_token.py valid.example.com Frobulate --expire-timestamp=2000000000 const char kSampleToken[] = - "1|w694328Rl8l2vd96nkbAumpwvOOnvhWTj9/pfBRkvcWMDAsmiMEhZGEPzdBRy5Yao6il5qC" - "OyS6Ah7uuHf7JAQ==|https://valid.example.com|Frobulate|2000000000"; + "AuR/1mg+/w5ROLN54Ok20rApK3opgR7Tq9ZfzhATQmnCa+BtPA1RRw4Nigf336r+" + "O4fM3Sa+MEd+5JcIgSZafw8AAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMjAwMDAwMDAwMH0="; // The token should be valid for this origin and for this feature. const char kAppropriateOrigin[] = "https://valid.example.com"; @@ -61,14 +65,21 @@ const char kInappropriateOrigin[] = "https://invalid.example.com"; const char kInsecureOrigin[] = "http://valid.example.com"; // Well-formed trial token with an invalid signature. +// This token is a corruption of the above valid token. const char kInvalidSignatureToken[] = - "1|CO8hDne98QeFeOJ0DbRZCBN3uE0nyaPgaLlkYhSWnbRoDfEAg+TXELaYfQPfEvKYFauBg/h" - "nxmba765hz0mXMc==|https://valid.example.com|Frobulate|2000000000"; + "AuR/1mg+/w5ROLN54Ok20rApK3opgR7Tq9ZfzhATQmnCa+BtPA1RRw4Nigf336r+" + "RrOtlAwa0gPqqn+A8GTD3AQAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMjAwMDAwMDAwMH0="; // Well-formed, but expired, trial token. (Expired in 2001) +// Generate this token with the command (in tools/origin_trials): +// generate_token.py valid.example.com Frobulate --expire-timestamp=1000000000 const char kExpiredToken[] = - "1|Vtzq/H0qMxsMXPThIgGEvI13d3Fd8K3W11/0E+FrJJXqBpx6n/dFkeFkEUsPaP3KeT8PCPF" - "1zpZ7kVgWYRLpAA==|https://valid.example.com|Frobulate|1000000000"; + "AmHPUIXMaXe9jWW8kJeDFXolVjT93p4XMnK4+jMYd2pjqtFcYB1bUmdD8PunQKM+" + "RrOtlAwa0gPqqn+A8GTD3AQAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" + "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" + "IjogMTAwMDAwMDAwMH0="; const char kUnparsableToken[] = "abcde"; diff --git a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-enabled.html b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-enabled.html index bcad26b4aab93f..6b9ed43dbf9915 100644 --- a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-enabled.html +++ b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-enabled.html @@ -3,8 +3,10 @@ - -Test Sample API when trial is enabled + + diff --git a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-expired.html b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-expired.html index 53641c5f254020..acdae4d048650f 100644 --- a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-expired.html +++ b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-expired.html @@ -4,7 +4,10 @@ below has a valid signature for the current signing key, but it would be better to always have a token which is guaranteed to be valid when the tests are run. --> - + + Test Sample API when trial has expired diff --git a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-multiple-tokens.html b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-multiple-tokens.html index 40ad0f152d15f3..cc8a98b995bd12 100644 --- a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-multiple-tokens.html +++ b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-multiple-tokens.html @@ -3,9 +3,14 @@ - - - + + + + Test Sample API when trial is enabled and multiple tokens are present diff --git a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-stolen.html b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-stolen.html index 50e60a37f98dc3..44a4af7b585d25 100644 --- a/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-stolen.html +++ b/third_party/WebKit/LayoutTests/http/tests/origin_trials/sample-api-stolen.html @@ -3,7 +3,10 @@ - + + Test Sample API when token is present for a different origin diff --git a/tools/origin_trials/generate_token.py b/tools/origin_trials/generate_token.py index 54e61c64482544..3b4447bbaac400 100755 --- a/tools/origin_trials/generate_token.py +++ b/tools/origin_trials/generate_token.py @@ -14,8 +14,10 @@ """ import argparse import base64 +import json import re import os +import struct import sys import time import urlparse @@ -29,6 +31,9 @@ # no longer than 63 ASCII characters) DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?I",len(data)) + data def Sign(private_key, data): return ed25519.signature(data, private_key[:32], private_key[32:]) def FormatToken(version, signature, data): - return version + "|" + base64.b64encode(signature) + "|" + data + return base64.b64encode(version + signature + + struct.pack(">I",len(data)) + data) def main(): parser = argparse.ArgumentParser( @@ -123,11 +134,12 @@ def main(): sys.exit(1) token_data = GenerateTokenData(args.origin, args.trial_name, expiry) - signature = Sign(private_key, token_data) + data_to_sign = GenerateDataToSign(VERSION, token_data) + signature = Sign(private_key, data_to_sign) # Verify that that the signature is correct before printing it. try: - ed25519.checkvalid(signature, token_data, private_key[32:]) + ed25519.checkvalid(signature, data_to_sign, private_key[32:]) except Exception, exc: print "There was an error generating the signature." print "(The original error was: %s)" % exc @@ -135,7 +147,7 @@ def main(): # Output a properly-formatted token. Version 1 is hard-coded, as it is # the only defined token version. - print FormatToken("1", signature, token_data) + print FormatToken(VERSION, signature, token_data) if __name__ == "__main__": main()