1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/public/common/origin_trials/trial_token.h"
6
7 #include "base/base64.h"
8 #include "base/big_endian.h"
9 #include "base/json/json_reader.h"
10 #include "base/logging.h"
11 #include "base/macros.h"
12 #include "base/memory/ptr_util.h"
13 #include "base/optional.h"
14 #include "base/strings/string_piece.h"
15 #include "base/time/time.h"
16 #include "base/values.h"
17 #include "third_party/boringssl/src/include/openssl/curve25519.h"
18 #include "url/gurl.h"
19 #include "url/origin.h"
20
21 namespace blink {
22
23 namespace {
24
25 // Token payloads can be at most 4KB in size, as a guard against trying to parse
26 // excessively large tokens (see crbug.com/802377). The origin is the only part
27 // of the payload that is user-supplied. The 4KB payload limit allows for the
28 // origin to be ~3900 chars. In some cases, 2KB is suggested as the practical
29 // limit for URLs, e.g.:
30 // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
31 // This means tokens can contain origins that are nearly twice as long as any
32 // expected to be seen in the wild.
33 const size_t kMaxPayloadSize = 4096;
34 // Encoded tokens can be at most 6KB in size. Based on the 4KB payload limit,
35 // this allows for the payload, signature, and other format bits, plus the
36 // Base64 encoding overhead (~4/3 of the input).
37 const size_t kMaxTokenSize = 6144;
38
39 // Version is a 1-byte field at offset 0.
40 const size_t kVersionOffset = 0;
41 const size_t kVersionSize = 1;
42
43 // These constants define the Version 2 field sizes and offsets.
44 const size_t kSignatureOffset = kVersionOffset + kVersionSize;
45 const size_t kSignatureSize = 64;
46 const size_t kPayloadLengthOffset = kSignatureOffset + kSignatureSize;
47 const size_t kPayloadLengthSize = 4;
48 const size_t kPayloadOffset = kPayloadLengthOffset + kPayloadLengthSize;
49
50 // Version 3 introduced support to match tokens against third party origins (see
51 // design doc
52 // https://docs.google.com/document/d/1xALH9W7rWmX0FpjudhDeS2TNTEOXuPn4Tlc9VmuPdHA
53 // for more details).
54 const uint8_t kVersion3 = 3;
55 // Version 2 is also currently supported. Version 1 was
56 // introduced in Chrome M50, and removed in M51. There were no experiments
57 // enabled in the stable M50 release which would have used those tokens.
58 const uint8_t kVersion2 = 2;
59
60 const char* kUsageSubset = "subset";
61
62 } // namespace
63
64 TrialToken::~TrialToken() = default;
65
66 // static
From(base::StringPiece token_text,base::StringPiece public_key,OriginTrialTokenStatus * out_status)67 std::unique_ptr<TrialToken> TrialToken::From(
68 base::StringPiece token_text,
69 base::StringPiece public_key,
70 OriginTrialTokenStatus* out_status) {
71 DCHECK(out_status);
72 std::string token_payload;
73 std::string token_signature;
74 uint8_t token_version;
75 *out_status = Extract(token_text, public_key, &token_payload,
76 &token_signature, &token_version);
77 if (*out_status != OriginTrialTokenStatus::kSuccess) {
78 DVLOG(2) << "Malformed origin trial token found (unable to extract)";
79 return nullptr;
80 }
81 std::unique_ptr<TrialToken> token = Parse(token_payload, token_version);
82 if (token) {
83 token->signature_ = token_signature;
84 *out_status = OriginTrialTokenStatus::kSuccess;
85 } else {
86 DVLOG(2) << "Malformed origin trial token found (unable to parse)";
87 *out_status = OriginTrialTokenStatus::kMalformed;
88 }
89 DVLOG(2) << "Valid origin trial token found for feature "
90 << token->feature_name();
91 return token;
92 }
93
IsValid(const url::Origin & origin,const base::Time & now) const94 OriginTrialTokenStatus TrialToken::IsValid(const url::Origin& origin,
95 const base::Time& now) const {
96 // The order of these checks is intentional. For example, will only report a
97 // token as expired if it is valid for the origin.
98 if (!ValidateOrigin(origin)) {
99 DVLOG(2) << "Origin trial token from different origin";
100 return OriginTrialTokenStatus::kWrongOrigin;
101 }
102 if (!ValidateDate(now)) {
103 DVLOG(2) << "Origin trial token expired";
104 return OriginTrialTokenStatus::kExpired;
105 }
106 return OriginTrialTokenStatus::kSuccess;
107 }
108
109 // static
Extract(base::StringPiece token_text,base::StringPiece public_key,std::string * out_token_payload,std::string * out_token_signature,uint8_t * out_token_version)110 OriginTrialTokenStatus TrialToken::Extract(base::StringPiece token_text,
111 base::StringPiece public_key,
112 std::string* out_token_payload,
113 std::string* out_token_signature,
114 uint8_t* out_token_version) {
115 if (token_text.empty()) {
116 return OriginTrialTokenStatus::kMalformed;
117 }
118
119 // Protect against attempting to extract arbitrarily large tokens.
120 // See crbug.com/802377.
121 if (token_text.length() > kMaxTokenSize) {
122 return OriginTrialTokenStatus::kMalformed;
123 }
124
125 // Token is base64-encoded; decode first.
126 std::string token_contents;
127 if (!base::Base64Decode(token_text, &token_contents)) {
128 return OriginTrialTokenStatus::kMalformed;
129 }
130
131 // Only version 2 and 3 currently supported.
132 if (token_contents.length() < (kVersionOffset + kVersionSize)) {
133 return OriginTrialTokenStatus::kMalformed;
134 }
135 uint8_t version = token_contents[kVersionOffset];
136 if (version != kVersion2 && version != kVersion3) {
137 return OriginTrialTokenStatus::kWrongVersion;
138 }
139
140 // Token must be large enough to contain a version, signature, and payload
141 // length.
142 if (token_contents.length() < (kPayloadLengthOffset + kPayloadLengthSize)) {
143 return OriginTrialTokenStatus::kMalformed;
144 }
145
146 // Extract the length of the signed data (Big-endian).
147 uint32_t payload_length;
148 base::ReadBigEndian(&(token_contents[kPayloadLengthOffset]), &payload_length);
149
150 // Validate that the stated length matches the actual payload length.
151 if (payload_length != token_contents.length() - kPayloadOffset) {
152 return OriginTrialTokenStatus::kMalformed;
153 }
154
155 // Extract the version-specific contents of the token.
156 const char* token_bytes = token_contents.data();
157 base::StringPiece version_piece(token_bytes + kVersionOffset, kVersionSize);
158 base::StringPiece signature(token_bytes + kSignatureOffset, kSignatureSize);
159 base::StringPiece payload_piece(token_bytes + kPayloadLengthOffset,
160 kPayloadLengthSize + payload_length);
161
162 // The data which is covered by the signature is (version + length + payload).
163 std::string signed_data =
164 version_piece.as_string() + payload_piece.as_string();
165
166 // Validate the signature on the data before proceeding.
167 if (!TrialToken::ValidateSignature(signature, signed_data, public_key)) {
168 return OriginTrialTokenStatus::kInvalidSignature;
169 }
170
171 // Return the payload and signature, as new strings.
172 *out_token_version = version;
173 *out_token_payload = token_contents.substr(kPayloadOffset, payload_length);
174 *out_token_signature = signature.as_string();
175 return OriginTrialTokenStatus::kSuccess;
176 }
177
178 // static
Parse(const std::string & token_payload,const uint8_t version)179 std::unique_ptr<TrialToken> TrialToken::Parse(const std::string& token_payload,
180 const uint8_t version) {
181 // Protect against attempting to parse arbitrarily large tokens. This check is
182 // required here because the fuzzer calls Parse() directly, bypassing the size
183 // check in Extract().
184 // See crbug.com/802377.
185 if (token_payload.length() > kMaxPayloadSize) {
186 return nullptr;
187 }
188
189 base::Optional<base::Value> datadict = base::JSONReader::Read(token_payload);
190 if (!datadict || !datadict->is_dict()) {
191 return nullptr;
192 }
193
194 // Ensure that the origin is a valid (non-opaque) origin URL.
195 std::string* origin_string = datadict->FindStringKey("origin");
196 if (!origin_string) {
197 return nullptr;
198 }
199 url::Origin origin = url::Origin::Create(GURL(*origin_string));
200 if (origin.opaque()) {
201 return nullptr;
202 }
203
204 // The |isSubdomain| flag is optional. If found, ensure it is a valid boolean.
205 bool is_subdomain = false;
206 base::Value* is_subdomain_value = datadict->FindKey("isSubdomain");
207 if (is_subdomain_value) {
208 if (!is_subdomain_value->is_bool()) {
209 return nullptr;
210 }
211 is_subdomain = is_subdomain_value->GetBool();
212 }
213
214 // Ensure that the feature name is a valid string.
215 std::string* feature_name = datadict->FindStringKey("feature");
216 if (!feature_name || feature_name->empty()) {
217 return nullptr;
218 }
219
220 // Ensure that the expiry timestamp is a valid (positive) integer.
221 int expiry_timestamp = datadict->FindIntKey("expiry").value_or(0);
222 if (expiry_timestamp <= 0) {
223 return nullptr;
224 }
225
226 // Initialize optional version 3 fields to default values.
227 bool is_third_party = false;
228 UsageRestriction usage = UsageRestriction::kNone;
229
230 if (version == kVersion3) {
231 // The |isThirdParty| flag is optional. If found, ensure it is a valid
232 // boolean.
233 base::Value* is_third_party_value = datadict->FindKey("isThirdParty");
234 if (is_third_party_value) {
235 if (!is_third_party_value->is_bool()) {
236 return nullptr;
237 }
238 is_third_party = is_third_party_value->GetBool();
239 }
240
241 // The |usage| field is optional. If found, ensure its value is either empty
242 // or "subset".
243 std::string* usage_value = datadict->FindStringKey("usage");
244 if (usage_value) {
245 if (usage_value->empty()) {
246 usage = UsageRestriction::kNone;
247 } else if (*usage_value == kUsageSubset) {
248 usage = UsageRestriction::kSubset;
249 } else {
250 return nullptr;
251 }
252 }
253 }
254
255 return base::WrapUnique(new TrialToken(origin, is_subdomain, *feature_name,
256 expiry_timestamp, is_third_party,
257 usage));
258 }
259
ValidateOrigin(const url::Origin & origin) const260 bool TrialToken::ValidateOrigin(const url::Origin& origin) const {
261 if (match_subdomains_) {
262 return origin.scheme() == origin_.scheme() &&
263 origin.DomainIs(origin_.host()) && origin.port() == origin_.port();
264 }
265 return origin == origin_;
266 }
267
ValidateFeatureName(base::StringPiece feature_name) const268 bool TrialToken::ValidateFeatureName(base::StringPiece feature_name) const {
269 return feature_name == feature_name_;
270 }
271
ValidateDate(const base::Time & now) const272 bool TrialToken::ValidateDate(const base::Time& now) const {
273 return expiry_time_ > now;
274 }
275
276 // static
ValidateSignature(base::StringPiece signature,const std::string & data,base::StringPiece public_key)277 bool TrialToken::ValidateSignature(base::StringPiece signature,
278 const std::string& data,
279 base::StringPiece public_key) {
280 // Public key must be 32 bytes long for Ed25519.
281 CHECK_EQ(public_key.length(), 32UL);
282
283 // Signature must be 64 bytes long.
284 if (signature.length() != 64) {
285 return false;
286 }
287
288 int result = ED25519_verify(
289 reinterpret_cast<const uint8_t*>(data.data()), data.length(),
290 reinterpret_cast<const uint8_t*>(signature.data()),
291 reinterpret_cast<const uint8_t*>(public_key.data()));
292 return (result != 0);
293 }
294
TrialToken(const url::Origin & origin,bool match_subdomains,const std::string & feature_name,uint64_t expiry_timestamp,bool is_third_party,UsageRestriction usage_restriction)295 TrialToken::TrialToken(const url::Origin& origin,
296 bool match_subdomains,
297 const std::string& feature_name,
298 uint64_t expiry_timestamp,
299 bool is_third_party,
300 UsageRestriction usage_restriction)
301 : origin_(origin),
302 match_subdomains_(match_subdomains),
303 feature_name_(feature_name),
304 expiry_time_(base::Time::FromDoubleT(expiry_timestamp)),
305 is_third_party_(is_third_party),
306 usage_restriction_(usage_restriction) {}
307
308 } // namespace blink
309