Skip to content

Commit

Permalink
Support subdomain matching in trial tokens
Browse files Browse the repository at this point in the history
This CL adds support for subdomain trial tokens, such that a properly
issued token will enable a trial for multiple subdomains, rather than a
single specified origin.

In bug 653349, it was proposed to use a leading "*" to indicate a
wildcard token (e.g. *.example.com). In this implementation, an explicit
flag was added to the token format. The explicit flag keeping the token
parsing simple, by keeping the url::Origin as the representation of the
origin, and avoiding custom string parsing.

BUG=653349

Review-Url: https://codereview.chromium.org/2411803002
Cr-Commit-Position: refs/heads/master@{#425168}
  • Loading branch information
jpchase authored and Commit bot committed Oct 13, 2016
1 parent ffe7006 commit 4f0cb8e
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 20 deletions.
17 changes: 16 additions & 1 deletion content/common/origin_trials/trial_token.cc
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ std::unique_ptr<TrialToken> TrialToken::Parse(
return nullptr;
}

// The |isSubdomain| flag is optional. If found, ensure it is a valid boolean.
bool is_subdomain = false;
if (datadict->HasKey("isSubdomain")) {
if (!datadict->GetBoolean("isSubdomain", &is_subdomain)) {
return nullptr;
}
}

// Ensure that the feature name is a valid string.
if (feature_name.empty()) {
return nullptr;
Expand All @@ -168,10 +176,15 @@ std::unique_ptr<TrialToken> TrialToken::Parse(
}

return base::WrapUnique(
new TrialToken(origin, feature_name, expiry_timestamp));
new TrialToken(origin, is_subdomain, feature_name, expiry_timestamp));
}

bool TrialToken::ValidateOrigin(const url::Origin& origin) const {
if (match_subdomains_) {
return origin.scheme() == origin_.scheme() &&
origin.DomainIs(origin_.host()) &&
origin.port() == origin_.port();
}
return origin == origin_;
}

Expand Down Expand Up @@ -203,9 +216,11 @@ bool TrialToken::ValidateSignature(base::StringPiece signature,
}

TrialToken::TrialToken(const url::Origin& origin,
bool match_subdomains,
const std::string& feature_name,
uint64_t expiry_timestamp)
: origin_(origin),
match_subdomains_(match_subdomains),
feature_name_(feature_name),
expiry_time_(base::Time::FromDoubleT(expiry_timestamp)) {}

Expand Down
16 changes: 10 additions & 6 deletions content/common/origin_trials/trial_token.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ enum class WebOriginTrialTokenStatus;

namespace content {

// The Experimental Framework (EF) provides limited access to experimental
// features, on a per-origin basis (origin trials). This class defines the trial
// token data structure, used to securely provide access to an experimental
// feature.
// The Origin Trials Framework (OT) provides limited access to experimental
// features, on a per-origin basis. This class defines the trial token data
// structure, used to securely provide access to an experimental feature.
//
// Features are defined by string names, provided by the implementers. The EF
// Features are defined by string names, provided by the implementers. The OT
// code does not maintain an enum or constant list for feature names. Instead,
// the EF validates the name provided by the feature implementation against any
// it validates the name provided by the feature implementation against any
// provided tokens.
//
// More documentation on the token format can be found at
Expand Down Expand Up @@ -58,6 +57,7 @@ class CONTENT_EXPORT TrialToken {
const base::Time& now) const;

url::Origin origin() { return origin_; }
bool match_subdomains() const { return match_subdomains_; }
std::string feature_name() { return feature_name_; }
base::Time expiry_time() { return expiry_time_; }

Expand Down Expand Up @@ -92,12 +92,16 @@ class CONTENT_EXPORT TrialToken {

private:
TrialToken(const url::Origin& origin,
bool match_subdomains,
const std::string& feature_name,
uint64_t expiry_timestamp);

// The origin for which this token is valid. Must be a secure origin.
url::Origin origin_;

// Indicates if the token should match all subdomains of the origin.
bool match_subdomains_;

// The name of the experimental feature which this token enables.
std::string feature_name_;

Expand Down
127 changes: 125 additions & 2 deletions content/common/origin_trials/trial_token_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,39 @@ const char* kSampleToken =
"eGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5"
"IjogMTQ1ODc2NjI3N30=";

// This is a good subdomain trial token, signed with the above test private key.
// Generate this token with the command (in tools/origin_trials):
// generate_token.py example.com Frobulate --is-subdomain
// --expire-timestamp=1458766277
const char* kSampleSubdomainToken =
"Auu+j9nXAQoy5+t00MiWakZwFExcdNC8ENkRdK1gL4OMFHS0AbZCscslDTcP1fjN"
"FjpbmQG+VCPk1NrldVXZng4AAABoeyJvcmlnaW4iOiAiaHR0cHM6Ly9leGFtcGxl"
"LmNvbTo0NDMiLCAiaXNTdWJkb21haW4iOiB0cnVlLCAiZmVhdHVyZSI6ICJGcm9i"
"dWxhdGUiLCAiZXhwaXJ5IjogMTQ1ODc2NjI3N30=";

// This is a good trial token, explicitly not a subdomain, signed with the above
// test private key. Generate this token with the command:
// generate_token.py valid.example.com Frobulate --no-subdomain
// --expire-timestamp=1458766277
const char* kSampleNonSubdomainToken =
"AreD979D7tO0luSZTr1+/+J6E0SSj/GEUyLK41o1hXFzXw1R7Z1hCDHs0gXWVSu1"
"lvH52Winvy39tHbsU2gJJQYAAABveyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5l"
"eGFtcGxlLmNvbTo0NDMiLCAiaXNTdWJkb21haW4iOiBmYWxzZSwgImZlYXR1cmUi"
"OiAiRnJvYnVsYXRlIiwgImV4cGlyeSI6IDE0NTg3NjYyNzd9";

const char* kExpectedFeatureName = "Frobulate";
const char* kExpectedOrigin = "https://valid.example.com";
const char* kExpectedSubdomainOrigin = "https://example.com";
const char* kExpectedMultipleSubdomainOrigin =
"https://part1.part2.part3.example.com";
const uint64_t kExpectedExpiry = 1458766277;

// The token should not be valid for this origin, or for this feature.
const char* kInvalidOrigin = "https://invalid.example.com";
const char* kInsecureOrigin = "http://valid.example.com";
const char* kIncorrectPortOrigin = "https://valid.example.com:444";
const char* kIncorrectDomainOrigin = "https://valid.example2.com";
const char* kInvalidTLDOrigin = "https://com";
const char* kInvalidFeatureName = "Grokalyze";

// The token should be valid if the current time is kValidTimestamp or earlier.
Expand Down Expand Up @@ -112,6 +138,14 @@ const char kSampleTokenJSON[] =
"{\"origin\": \"https://valid.example.com:443\", \"feature\": "
"\"Frobulate\", \"expiry\": 1458766277}";

const char kSampleNonSubdomainTokenJSON[] =
"{\"origin\": \"https://valid.example.com:443\", \"isSubdomain\": false, "
"\"feature\": \"Frobulate\", \"expiry\": 1458766277}";

const char kSampleSubdomainTokenJSON[] =
"{\"origin\": \"https://example.com:443\", \"isSubdomain\": true, "
"\"feature\": \"Frobulate\", \"expiry\": 1458766277}";

// Various ill-formed trial tokens. These should all fail to parse.
const char* kInvalidTokens[] = {
// Invalid - Not JSON at all
Expand All @@ -126,30 +160,41 @@ const char* kInvalidTokens[] = {
"{}",
"{\"something\": 1}",
"{\"origin\": \"https://a.a\"}",
"{\"origin\": \"https://a.a\", \"feature\": \"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\"}",
"{\"origin\": \"https://a.a\", \"isSubdomain\": \"true\", \"feature\": "
"\"a\", \"expiry\": 1458766277}",
"{\"origin\": \"https://a.a\", \"isSubdomain\": 1, \"feature\": \"a\", "
"\"expiry\": 1458766277}",
// Negative expiry timestamp
"{\"origin\": \"https://a.a\", \"feature\": \"a\", \"expiry\": -1}",
// Origin not a proper origin URL
"{\"origin\": \"abcdef\", \"feature\": \"a\", \"expiry\": 1458766277}",
"{\"origin\": \"data:text/plain,abcdef\", \"feature\": \"a\", \"expiry\": "
"1458766277}",
"{\"origin\": \"javascript:alert(1)\", \"feature\": \"a\", \"expiry\": "
"1458766277}"};
"1458766277}",
};

} // namespace

class TrialTokenTest : public testing::TestWithParam<const char*> {
public:
TrialTokenTest()
: expected_origin_(GURL(kExpectedOrigin)),
expected_subdomain_origin_(GURL(kExpectedSubdomainOrigin)),
expected_multiple_subdomain_origin_(
GURL(kExpectedMultipleSubdomainOrigin)),
invalid_origin_(GURL(kInvalidOrigin)),
insecure_origin_(GURL(kInsecureOrigin)),
incorrect_port_origin_(GURL(kIncorrectPortOrigin)),
incorrect_domain_origin_(GURL(kIncorrectDomainOrigin)),
invalid_tld_origin_(GURL(kInvalidTLDOrigin)),
expected_expiry_(base::Time::FromDoubleT(kExpectedExpiry)),
valid_timestamp_(base::Time::FromDoubleT(kValidTimestamp)),
invalid_timestamp_(base::Time::FromDoubleT(kInvalidTimestamp)),
Expand Down Expand Up @@ -194,8 +239,13 @@ class TrialTokenTest : public testing::TestWithParam<const char*> {
base::StringPiece incorrect_public_key() { return incorrect_public_key_; }

const url::Origin expected_origin_;
const url::Origin expected_subdomain_origin_;
const url::Origin expected_multiple_subdomain_origin_;
const url::Origin invalid_origin_;
const url::Origin insecure_origin_;
const url::Origin incorrect_port_origin_;
const url::Origin incorrect_domain_origin_;
const url::Origin invalid_tld_origin_;

const base::Time expected_expiry_;
const base::Time valid_timestamp_;
Expand All @@ -220,6 +270,22 @@ TEST_F(TrialTokenTest, ValidateValidSignature) {
EXPECT_STREQ(kSampleTokenJSON, token_payload.c_str());
}

TEST_F(TrialTokenTest, ValidateSubdomainValidSignature) {
std::string token_payload;
blink::WebOriginTrialTokenStatus status =
Extract(kSampleSubdomainToken, correct_public_key(), &token_payload);
ASSERT_EQ(blink::WebOriginTrialTokenStatus::Success, status);
EXPECT_STREQ(kSampleSubdomainTokenJSON, token_payload.c_str());
}

TEST_F(TrialTokenTest, ValidateNonSubdomainValidSignature) {
std::string token_payload;
blink::WebOriginTrialTokenStatus status =
Extract(kSampleNonSubdomainToken, correct_public_key(), &token_payload);
ASSERT_EQ(blink::WebOriginTrialTokenStatus::Success, status);
EXPECT_STREQ(kSampleNonSubdomainTokenJSON, token_payload.c_str());
}

TEST_F(TrialTokenTest, ValidateInvalidSignature) {
blink::WebOriginTrialTokenStatus status =
ExtractIgnorePayload(kInvalidSignatureToken, correct_public_key());
Expand Down Expand Up @@ -274,16 +340,39 @@ TEST_F(TrialTokenTest, ParseValidToken) {
std::unique_ptr<TrialToken> token = Parse(kSampleTokenJSON);
ASSERT_TRUE(token);
EXPECT_EQ(kExpectedFeatureName, token->feature_name());
EXPECT_FALSE(token->match_subdomains());
EXPECT_EQ(expected_origin_, token->origin());
EXPECT_EQ(expected_expiry_, token->expiry_time());
}

TEST_F(TrialTokenTest, ParseValidNonSubdomainToken) {
std::unique_ptr<TrialToken> token = Parse(kSampleNonSubdomainTokenJSON);
ASSERT_TRUE(token);
EXPECT_EQ(kExpectedFeatureName, token->feature_name());
EXPECT_FALSE(token->match_subdomains());
EXPECT_EQ(expected_origin_, token->origin());
EXPECT_EQ(expected_expiry_, token->expiry_time());
}

TEST_F(TrialTokenTest, ParseValidSubdomainToken) {
std::unique_ptr<TrialToken> token = Parse(kSampleSubdomainTokenJSON);
ASSERT_TRUE(token);
EXPECT_EQ(kExpectedFeatureName, token->feature_name());
EXPECT_TRUE(token->match_subdomains());
EXPECT_EQ(kExpectedSubdomainOrigin, token->origin().Serialize());
EXPECT_EQ(expected_subdomain_origin_, token->origin());
EXPECT_EQ(expected_expiry_, token->expiry_time());
}

TEST_F(TrialTokenTest, ValidateValidToken) {
std::unique_ptr<TrialToken> token = Parse(kSampleTokenJSON);
ASSERT_TRUE(token);
EXPECT_TRUE(ValidateOrigin(token.get(), expected_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), invalid_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), insecure_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), incorrect_port_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), incorrect_domain_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), invalid_tld_origin_));
EXPECT_TRUE(ValidateFeatureName(token.get(), kExpectedFeatureName));
EXPECT_FALSE(ValidateFeatureName(token.get(), kInvalidFeatureName));
EXPECT_FALSE(ValidateFeatureName(
Expand All @@ -294,6 +383,18 @@ TEST_F(TrialTokenTest, ValidateValidToken) {
EXPECT_FALSE(ValidateDate(token.get(), invalid_timestamp_));
}

TEST_F(TrialTokenTest, ValidateValidSubdomainToken) {
std::unique_ptr<TrialToken> token = Parse(kSampleSubdomainTokenJSON);
ASSERT_TRUE(token);
EXPECT_TRUE(ValidateOrigin(token.get(), expected_origin_));
EXPECT_TRUE(ValidateOrigin(token.get(), expected_subdomain_origin_));
EXPECT_TRUE(ValidateOrigin(token.get(), expected_multiple_subdomain_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), insecure_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), incorrect_port_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), incorrect_domain_origin_));
EXPECT_FALSE(ValidateOrigin(token.get(), invalid_tld_origin_));
}

TEST_F(TrialTokenTest, TokenIsValid) {
std::unique_ptr<TrialToken> token = Parse(kSampleTokenJSON);
ASSERT_TRUE(token);
Expand All @@ -303,6 +404,28 @@ TEST_F(TrialTokenTest, TokenIsValid) {
token->IsValid(invalid_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::WrongOrigin,
token->IsValid(insecure_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::WrongOrigin,
token->IsValid(incorrect_port_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::Expired,
token->IsValid(expected_origin_, invalid_timestamp_));
}

TEST_F(TrialTokenTest, SubdomainTokenIsValid) {
std::unique_ptr<TrialToken> token = Parse(kSampleSubdomainTokenJSON);
ASSERT_TRUE(token);
EXPECT_EQ(blink::WebOriginTrialTokenStatus::Success,
token->IsValid(expected_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::Success,
token->IsValid(expected_subdomain_origin_, valid_timestamp_));
EXPECT_EQ(
blink::WebOriginTrialTokenStatus::Success,
token->IsValid(expected_multiple_subdomain_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::WrongOrigin,
token->IsValid(incorrect_domain_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::WrongOrigin,
token->IsValid(insecure_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::WrongOrigin,
token->IsValid(incorrect_port_origin_, valid_timestamp_));
EXPECT_EQ(blink::WebOriginTrialTokenStatus::Expired,
token->IsValid(expected_origin_, invalid_timestamp_));
}
Expand Down
1 change: 1 addition & 0 deletions content/test/data/fuzzer_corpus/origin_trial_token_data/24
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"feature":"WebBluetooth","isSubdomain":true,origin":"https://example.com:443","expiry":1458766277}
6 changes: 6 additions & 0 deletions content/test/data/fuzzer_corpus/origin_trial_token_data/25
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"feature": "WebBluetooth",
"isSubdomain": false,
"origin": "https://example.com:443",
"expiry": 1458766277
}
1 change: 1 addition & 0 deletions content/test/data/fuzzer_corpus/origin_trial_token_data/26
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"origin":"https://example.com:443","isSubdomain": 1,"something":"else","feature":"WebBluetooth","expiry":1458766277}
1 change: 1 addition & 0 deletions content/test/data/fuzzer_corpus/origin_trial_token_data/27
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"origin":"https://example.com:443","isSubdomain":"something","feature":"WebBluetooth","expiry":1458766277}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
# found in the LICENSE file.

"\"origin\""
"\"isSubdomain\""
"\"feature\""
"\"expiry\""
"https://"
"example.com"
":443"
"true"
"false"
Loading

0 comments on commit 4f0cb8e

Please sign in to comment.