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