| // Copyright 2016 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 "third_party/blink/public/common/origin_trials/trial_token_validator.h" |
| |
| #include <memory> |
| #include <set> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/simple_test_clock.h" |
| #include "base/time/time.h" |
| #include "net/http/http_response_headers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/origin_trials/origin_trial_policy.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token.h" |
| #include "url/gurl.h" |
| |
| namespace blink { |
| namespace trial_token_validator_unittest { |
| |
| // This is a sample public key for testing the API. The corresponding private |
| // key (use this to generate new samples for this test file) is: |
| // |
| // 0x83, 0x67, 0xf4, 0xcd, 0x2a, 0x1f, 0x0e, 0x04, 0x0d, 0x43, 0x13, |
| // 0x4c, 0x67, 0xc4, 0xf4, 0x28, 0xc9, 0x90, 0x15, 0x02, 0xe2, 0xba, |
| // 0xfd, 0xbb, 0xfa, 0xbc, 0x92, 0x76, 0x8a, 0x2c, 0x4b, 0xc7, 0x75, |
| // 0x10, 0xac, 0xf9, 0x3a, 0x1c, 0xb8, 0xa9, 0x28, 0x70, 0xd2, 0x9a, |
| // 0xd0, 0x0b, 0x59, 0xe1, 0xac, 0x2b, 0xb7, 0xd5, 0xca, 0x1f, 0x64, |
| // 0x90, 0x08, 0x8e, 0xa8, 0xe0, 0x56, 0x3a, 0x04, 0xd0 |
| const uint8_t kTestPublicKey[] = { |
| 0x75, 0x10, 0xac, 0xf9, 0x3a, 0x1c, 0xb8, 0xa9, 0x28, 0x70, 0xd2, |
| 0x9a, 0xd0, 0x0b, 0x59, 0xe1, 0xac, 0x2b, 0xb7, 0xd5, 0xca, 0x1f, |
| 0x64, 0x90, 0x08, 0x8e, 0xa8, 0xe0, 0x56, 0x3a, 0x04, 0xd0, |
| }; |
| |
| // 0x21, 0xee, 0xfa, 0x81, 0x6a, 0xff, 0xdf, 0xb8, 0xc1, 0xdd, 0x75, |
| // 0x05, 0x04, 0x29, 0x68, 0x67, 0x60, 0x85, 0x91, 0xd0, 0x50, 0x16, |
| // 0x0a, 0xcf, 0xa2, 0x37, 0xa3, 0x2e, 0x11, 0x7a, 0x17, 0x96, 0x50, |
| // 0x07, 0x4d, 0x76, 0x55, 0x56, 0x42, 0x17, 0x2d, 0x8a, 0x9c, 0x47, |
| // 0x96, 0x25, 0xda, 0x70, 0xaa, 0xb9, 0xfd, 0x53, 0x5d, 0x51, 0x3e, |
| // 0x16, 0xab, 0xb4, 0x86, 0xea, 0xf3, 0x35, 0xc6, 0xca |
| const uint8_t kTestPublicKey2[] = { |
| 0x50, 0x07, 0x4d, 0x76, 0x55, 0x56, 0x42, 0x17, 0x2d, 0x8a, 0x9c, |
| 0x47, 0x96, 0x25, 0xda, 0x70, 0xaa, 0xb9, 0xfd, 0x53, 0x5d, 0x51, |
| 0x3e, 0x16, 0xab, 0xb4, 0x86, 0xea, 0xf3, 0x35, 0xc6, 0xca, |
| }; |
| |
| // 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[] = |
| "AuR/1mg+/w5ROLN54Ok20rApK3opgR7Tq9ZfzhATQmnCa+BtPA1RRw4Nigf336r+" |
| "O4fM3Sa+MEd+5JcIgSZafw8AAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" |
| "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" |
| "IjogMjAwMDAwMDAwMH0="; |
| const uint8_t kSampleTokenSignature[] = { |
| 0xe4, 0x7f, 0xd6, 0x68, 0x3e, 0xff, 0x0e, 0x51, 0x38, 0xb3, 0x79, |
| 0xe0, 0xe9, 0x36, 0xd2, 0xb0, 0x29, 0x2b, 0x7a, 0x29, 0x81, 0x1e, |
| 0xd3, 0xab, 0xd6, 0x5f, 0xce, 0x10, 0x13, 0x42, 0x69, 0xc2, 0x6b, |
| 0xe0, 0x6d, 0x3c, 0x0d, 0x51, 0x47, 0x0e, 0x0d, 0x8a, 0x07, 0xf7, |
| 0xdf, 0xaa, 0xfe, 0x3b, 0x87, 0xcc, 0xdd, 0x26, 0xbe, 0x30, 0x47, |
| 0x7e, 0xe4, 0x97, 0x08, 0x81, 0x26, 0x5a, 0x7f, 0x0f}; |
| |
| // The token should be valid for this origin and for this feature. |
| const char kAppropriateOrigin[] = "https://valid.example.com"; |
| const char kAppropriateFeatureName[] = "Frobulate"; |
| |
| const char kInappropriateFeatureName[] = "Grokalyze"; |
| 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[] = |
| "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[] = |
| "AmHPUIXMaXe9jWW8kJeDFXolVjT93p4XMnK4+jMYd2pjqtFcYB1bUmdD8PunQKM+" |
| "RrOtlAwa0gPqqn+A8GTD3AQAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l" |
| "eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5" |
| "IjogMTAwMDAwMDAwMH0="; |
| const uint8_t kExpiredTokenSignature[] = { |
| 0x61, 0xcf, 0x50, 0x85, 0xcc, 0x69, 0x77, 0xbd, 0x8d, 0x65, 0xbc, |
| 0x90, 0x97, 0x83, 0x15, 0x7a, 0x25, 0x56, 0x34, 0xfd, 0xde, 0x9e, |
| 0x17, 0x32, 0x72, 0xb8, 0xfa, 0x33, 0x18, 0x77, 0x6a, 0x63, 0xaa, |
| 0xd1, 0x5c, 0x60, 0x1d, 0x5b, 0x52, 0x67, 0x43, 0xf0, 0xfb, 0xa7, |
| 0x40, 0xa3, 0x3e, 0x46, 0xb3, 0xad, 0x94, 0x0c, 0x1a, 0xd2, 0x03, |
| 0xea, 0xaa, 0x7f, 0x80, 0xf0, 0x64, 0xc3, 0xdc, 0x04}; |
| |
| const char kUnparsableToken[] = "abcde"; |
| |
| // Well-formed token, for an insecure origin. |
| // Generate this token with the command (in tools/origin_trials): |
| // generate_token.py http://valid.example.com Frobulate |
| // --expire-timestamp=2000000000 |
| const char kInsecureOriginToken[] = |
| "AjfC47H1q8/Ho5ALFkjkwf9CBK6oUUeRTlFc50Dj+eZEyGGKFIY2WTxMBfy8cLc3" |
| "E0nmFroDA3OmABmO5jMCFgkAAABXeyJvcmlnaW4iOiAiaHR0cDovL3ZhbGlkLmV4" |
| "YW1wbGUuY29tOjgwIiwgImZlYXR1cmUiOiAiRnJvYnVsYXRlIiwgImV4cGlyeSI6" |
| "IDIwMDAwMDAwMDB9"; |
| |
| // This timestamp is set to a time after the expiry timestamp of kExpiredToken, |
| // but before the expiry timestamp of kValidToken. |
| double kNowTimestamp = 1500000000; |
| |
| class TestOriginTrialPolicy : public OriginTrialPolicy { |
| public: |
| bool IsOriginTrialsSupported() const override { return true; } |
| bool IsOriginSecure(const GURL& url) const override { |
| return url.SchemeIs("https"); |
| } |
| base::StringPiece GetPublicKey() const override { |
| return base::StringPiece(reinterpret_cast<const char*>(key_), |
| base::size(kTestPublicKey)); |
| } |
| bool IsFeatureDisabled(base::StringPiece feature) const override { |
| return disabled_features_.count(feature.as_string()) > 0; |
| } |
| |
| // Test setup methods |
| void SetPublicKey(const uint8_t* key) { key_ = key; } |
| void DisableFeature(const std::string& feature) { |
| disabled_features_.insert(feature); |
| } |
| void DisableToken(const std::string& token) { |
| disabled_tokens_.insert(token); |
| } |
| |
| protected: |
| bool IsTokenDisabled(base::StringPiece token_signature) const override { |
| return disabled_tokens_.count(token_signature.as_string()) > 0; |
| } |
| |
| private: |
| const uint8_t* key_ = nullptr; |
| std::set<std::string> disabled_features_; |
| std::set<std::string> disabled_tokens_; |
| }; |
| |
| class TrialTokenValidatorTest : public testing::Test { |
| public: |
| TrialTokenValidatorTest() |
| : appropriate_origin_(url::Origin::Create(GURL(kAppropriateOrigin))), |
| inappropriate_origin_(url::Origin::Create(GURL(kInappropriateOrigin))), |
| insecure_origin_(url::Origin::Create(GURL(kInsecureOrigin))), |
| valid_token_signature_( |
| std::string(reinterpret_cast<const char*>(kSampleTokenSignature), |
| base::size(kSampleTokenSignature))), |
| expired_token_signature_( |
| std::string(reinterpret_cast<const char*>(kExpiredTokenSignature), |
| base::size(kExpiredTokenSignature))), |
| response_headers_(new net::HttpResponseHeaders("")) { |
| TrialTokenValidator::SetOriginTrialPolicyGetter( |
| base::BindRepeating([](OriginTrialPolicy* policy) { return policy; }, |
| base::Unretained(&policy_))); |
| SetPublicKey(kTestPublicKey); |
| } |
| |
| ~TrialTokenValidatorTest() override { |
| TrialTokenValidator::ResetOriginTrialPolicyGetter(); |
| } |
| |
| void SetPublicKey(const uint8_t* key) { policy_.SetPublicKey(key); } |
| |
| void DisableFeature(const std::string& feature) { |
| policy_.DisableFeature(feature); |
| } |
| |
| void DisableToken(const std::string& token_signature) { |
| policy_.DisableToken(token_signature); |
| } |
| |
| base::Time Now() { return base::Time::FromDoubleT(kNowTimestamp); } |
| |
| const url::Origin appropriate_origin_; |
| const url::Origin inappropriate_origin_; |
| const url::Origin insecure_origin_; |
| |
| std::string valid_token_signature_; |
| std::string expired_token_signature_; |
| |
| scoped_refptr<net::HttpResponseHeaders> response_headers_; |
| |
| TestOriginTrialPolicy policy_; |
| const TrialTokenValidator validator_; |
| }; |
| |
| TEST_F(TrialTokenValidatorTest, ValidateValidToken) { |
| std::string feature; |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kSuccess, |
| validator_.ValidateToken(kSampleToken, appropriate_origin_, |
| &feature, Now())); |
| EXPECT_EQ(kAppropriateFeatureName, feature); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateInappropriateOrigin) { |
| std::string feature; |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kWrongOrigin, |
| validator_.ValidateToken(kSampleToken, inappropriate_origin_, |
| &feature, Now())); |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kWrongOrigin, |
| validator_.ValidateToken(kSampleToken, insecure_origin_, &feature, |
| Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateInvalidSignature) { |
| std::string feature; |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kInvalidSignature, |
| validator_.ValidateToken(kInvalidSignatureToken, |
| appropriate_origin_, &feature, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateUnparsableToken) { |
| std::string feature; |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kMalformed, |
| validator_.ValidateToken(kUnparsableToken, appropriate_origin_, |
| &feature, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateExpiredToken) { |
| std::string feature; |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kExpired, |
| validator_.ValidateToken(kExpiredToken, appropriate_origin_, |
| &feature, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateValidTokenWithIncorrectKey) { |
| std::string feature; |
| SetPublicKey(kTestPublicKey2); |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kInvalidSignature, |
| validator_.ValidateToken(kSampleToken, appropriate_origin_, |
| &feature, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidatorRespectsDisabledFeatures) { |
| std::string feature; |
| // Disable an irrelevant feature; token should still validate |
| DisableFeature(kInappropriateFeatureName); |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kSuccess, |
| validator_.ValidateToken(kSampleToken, appropriate_origin_, |
| &feature, Now())); |
| EXPECT_EQ(kAppropriateFeatureName, feature); |
| // Disable the token's feature; it should no longer be valid |
| DisableFeature(kAppropriateFeatureName); |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kFeatureDisabled, |
| validator_.ValidateToken(kSampleToken, appropriate_origin_, |
| &feature, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidatorRespectsDisabledTokens) { |
| std::string feature; |
| // Disable an irrelevant token; token should still validate |
| DisableToken(expired_token_signature_); |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kSuccess, |
| validator_.ValidateToken(kSampleToken, appropriate_origin_, |
| &feature, Now())); |
| EXPECT_EQ(kAppropriateFeatureName, feature); |
| // Disable the token; it should no longer be valid |
| DisableToken(valid_token_signature_); |
| EXPECT_EQ(blink::OriginTrialTokenStatus::kTokenDisabled, |
| validator_.ValidateToken(kSampleToken, appropriate_origin_, |
| &feature, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateRequestInsecure) { |
| response_headers_->AddHeader(std::string("Origin-Trial: ") + |
| kInsecureOriginToken); |
| EXPECT_FALSE(validator_.RequestEnablesFeature( |
| GURL(kInsecureOrigin), response_headers_.get(), kAppropriateFeatureName, |
| Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateRequestValidToken) { |
| response_headers_->AddHeader(std::string("Origin-Trial: ") + kSampleToken); |
| EXPECT_TRUE(validator_.RequestEnablesFeature(GURL(kAppropriateOrigin), |
| response_headers_.get(), |
| kAppropriateFeatureName, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateRequestNoTokens) { |
| EXPECT_FALSE(validator_.RequestEnablesFeature( |
| GURL(kAppropriateOrigin), response_headers_.get(), |
| kAppropriateFeatureName, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateRequestMultipleHeaders) { |
| response_headers_->AddHeader(std::string("Origin-Trial: ") + kSampleToken); |
| response_headers_->AddHeader(std::string("Origin-Trial: ") + kExpiredToken); |
| EXPECT_TRUE(validator_.RequestEnablesFeature(GURL(kAppropriateOrigin), |
| response_headers_.get(), |
| kAppropriateFeatureName, Now())); |
| EXPECT_FALSE(validator_.RequestEnablesFeature( |
| GURL(kAppropriateOrigin), response_headers_.get(), |
| kInappropriateFeatureName, Now())); |
| EXPECT_FALSE(validator_.RequestEnablesFeature( |
| GURL(kInappropriateOrigin), response_headers_.get(), |
| kAppropriateFeatureName, Now())); |
| } |
| |
| TEST_F(TrialTokenValidatorTest, ValidateRequestMultipleHeaderValues) { |
| response_headers_->AddHeader(std::string("Origin-Trial: ") + kExpiredToken + |
| ", " + kSampleToken); |
| EXPECT_TRUE(validator_.RequestEnablesFeature(GURL(kAppropriateOrigin), |
| response_headers_.get(), |
| kAppropriateFeatureName, Now())); |
| EXPECT_FALSE(validator_.RequestEnablesFeature( |
| GURL(kAppropriateOrigin), response_headers_.get(), |
| kInappropriateFeatureName, Now())); |
| EXPECT_FALSE(validator_.RequestEnablesFeature( |
| GURL(kInappropriateOrigin), response_headers_.get(), |
| kAppropriateFeatureName, Now())); |
| } |
| |
| } // namespace trial_token_validator_unittest |
| } // namespace blink |