1 // Copyright 2020 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 "services/network/trust_tokens/trust_token_key_commitment_parser.h"
6
7 #include "base/base64.h"
8 #include "base/json/json_reader.h"
9 #include "base/numerics/safe_conversions.h"
10 #include "base/ranges/algorithm.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_piece.h"
13 #include "base/values.h"
14 #include "services/network/public/mojom/trust_tokens.mojom.h"
15 #include "services/network/trust_tokens/suitable_trust_token_origin.h"
16
17 namespace network {
18
19 const char kTrustTokenKeyCommitmentProtocolVersionField[] = "protocol_version";
20 const char kTrustTokenKeyCommitmentIDField[] = "id";
21 const char kTrustTokenKeyCommitmentBatchsizeField[] = "batchsize";
22 const char kTrustTokenKeyCommitmentExpiryField[] = "expiry";
23 const char kTrustTokenKeyCommitmentKeyField[] = "Y";
24 const char kTrustTokenKeyCommitmentRequestIssuanceLocallyOnField[] =
25 "request_issuance_locally_on";
26 const char kTrustTokenLocalIssuanceOsAndroid[] = "android";
27 const char kTrustTokenKeyCommitmentUnavailableLocalIssuanceFallbackField[] =
28 "unavailable_local_issuance_fallback";
29 const char kTrustTokenLocalIssuanceFallbackWebIssuance[] = "web_issuance";
30 const char kTrustTokenLocalIssuanceFallbackReturnWithError[] =
31 "return_with_error";
32
33 namespace {
34
35 // Parses a single key label. If |in| is the string representation of an integer
36 // in in the representable range of uint32_t, returns true. Otherwise, returns
37 // false.
ParseSingleKeyLabel(base::StringPiece in)38 bool ParseSingleKeyLabel(base::StringPiece in) {
39 uint64_t key_label_in_uint64;
40 if (!base::StringToUint64(in, &key_label_in_uint64))
41 return false;
42 if (!base::IsValueInRangeForNumericType<uint32_t>(key_label_in_uint64))
43 return false;
44 return true;
45 }
46
47 enum class ParseKeyResult {
48 // Continue as if the key didn't exist.
49 kIgnore,
50 // Fail parsing totally.
51 kFail,
52 // Parsing the key succeeded.
53 kSucceed
54 };
55
56 // Parses a single key, consisting of a body (the key material) and an expiry
57 // timestamp. Fails the parse if either field is missing or malformed. If the
58 // key has expired but is otherwise valid, ignores the key rather than failing
59 // the prase.
ParseSingleKeyExceptLabel(const base::Value & in,mojom::TrustTokenVerificationKey * out)60 ParseKeyResult ParseSingleKeyExceptLabel(
61 const base::Value& in,
62 mojom::TrustTokenVerificationKey* out) {
63 CHECK(in.is_dict());
64
65 const std::string* expiry =
66 in.FindStringKey(kTrustTokenKeyCommitmentExpiryField);
67 const std::string* key_body =
68 in.FindStringKey(kTrustTokenKeyCommitmentKeyField);
69 if (!expiry || !key_body)
70 return ParseKeyResult::kFail;
71
72 uint64_t expiry_microseconds_since_unix_epoch;
73 if (!base::StringToUint64(*expiry, &expiry_microseconds_since_unix_epoch))
74 return ParseKeyResult::kFail;
75
76 if (!base::Base64Decode(*key_body, &out->body))
77 return ParseKeyResult::kFail;
78
79 out->expiry =
80 base::Time::UnixEpoch() +
81 base::TimeDelta::FromMicroseconds(expiry_microseconds_since_unix_epoch);
82 if (out->expiry <= base::Time::Now())
83 return ParseKeyResult::kIgnore;
84
85 return ParseKeyResult::kSucceed;
86 }
87
ParseOs(base::StringPiece os_string)88 base::Optional<mojom::TrustTokenKeyCommitmentResult::Os> ParseOs(
89 base::StringPiece os_string) {
90 if (os_string == kTrustTokenLocalIssuanceOsAndroid)
91 return mojom::TrustTokenKeyCommitmentResult::Os::kAndroid;
92 return base::nullopt;
93 }
94
95 // Attempts to parse a string representation of a member of the
96 // UnavailableLocalIssuanceFallback enum, returning true on success and false on
97 // failure.
ParseUnavailableLocalIssuanceFallback(base::StringPiece fallback_string,mojom::TrustTokenKeyCommitmentResult::UnavailableLocalIssuanceFallback * fallback_out)98 bool ParseUnavailableLocalIssuanceFallback(
99 base::StringPiece fallback_string,
100 mojom::TrustTokenKeyCommitmentResult::UnavailableLocalIssuanceFallback*
101 fallback_out) {
102 if (fallback_string == kTrustTokenLocalIssuanceFallbackWebIssuance) {
103 *fallback_out = mojom::TrustTokenKeyCommitmentResult::
104 UnavailableLocalIssuanceFallback::kWebIssuance;
105 return true;
106 }
107 if (fallback_string == kTrustTokenLocalIssuanceFallbackReturnWithError) {
108 *fallback_out = mojom::TrustTokenKeyCommitmentResult::
109 UnavailableLocalIssuanceFallback::kReturnWithError;
110 return true;
111 }
112 return false;
113 }
114
115 // Given a per-issuer key commitment dictionary, looks for the local Trust
116 // Tokens issuance-related fields request_issuance_locally_on and
117 // unavailable_local_issuance_fallback.
118 //
119 // Returns true if both are absent, or if both are present and well-formed; in
120 // the latter case, updates |result| to with their parsed values. Otherwise,
121 // returns false.
ParseLocalIssuanceFieldsIfPresent(const base::Value & value,mojom::TrustTokenKeyCommitmentResult * result)122 bool ParseLocalIssuanceFieldsIfPresent(
123 const base::Value& value,
124 mojom::TrustTokenKeyCommitmentResult* result) {
125 const base::Value* maybe_request_issuance_locally_on =
126 value.FindKey(kTrustTokenKeyCommitmentRequestIssuanceLocallyOnField);
127
128 // The local issuance field is optional...
129 if (!maybe_request_issuance_locally_on)
130 return true;
131
132 // ...but needs to be the right type if it's provided.
133 if (!maybe_request_issuance_locally_on->is_list())
134 return false;
135
136 for (const base::Value& maybe_os_value :
137 maybe_request_issuance_locally_on->GetList()) {
138 if (!maybe_os_value.is_string())
139 return false;
140 base::Optional<mojom::TrustTokenKeyCommitmentResult::Os> maybe_os =
141 ParseOs(maybe_os_value.GetString());
142 if (!maybe_os)
143 return false;
144 result->request_issuance_locally_on.push_back(*maybe_os);
145 }
146
147 // Deduplicate the OS values:
148 auto& oses = result->request_issuance_locally_on;
149 base::ranges::sort(oses);
150 auto to_remove = base::ranges::unique(oses);
151 oses.erase(to_remove, oses.end());
152
153 const std::string* maybe_fallback = value.FindStringKey(
154 kTrustTokenKeyCommitmentUnavailableLocalIssuanceFallbackField);
155 if (!maybe_fallback ||
156 !ParseUnavailableLocalIssuanceFallback(
157 *maybe_fallback, &result->unavailable_local_issuance_fallback)) {
158 return false;
159 }
160
161 return true;
162 }
163
ParseSingleIssuer(const base::Value & value)164 mojom::TrustTokenKeyCommitmentResultPtr ParseSingleIssuer(
165 const base::Value& value) {
166 if (!value.is_dict())
167 return nullptr;
168
169 auto result = mojom::TrustTokenKeyCommitmentResult::New();
170
171 // Confirm that the protocol_version field is present.
172 const std::string* maybe_version =
173 value.FindStringKey(kTrustTokenKeyCommitmentProtocolVersionField);
174 if (!maybe_version)
175 return nullptr;
176 if (*maybe_version == "TrustTokenV2PMB") {
177 result->protocol_version =
178 mojom::TrustTokenProtocolVersion::kTrustTokenV2Pmb;
179 } else if (*maybe_version == "TrustTokenV2VOPRF") {
180 result->protocol_version =
181 mojom::TrustTokenProtocolVersion::kTrustTokenV2Voprf;
182 } else {
183 return nullptr;
184 }
185
186 // Confirm that the id field is present and type-safe.
187 base::Optional<int> maybe_id =
188 value.FindIntKey(kTrustTokenKeyCommitmentIDField);
189 if (!maybe_id || *maybe_id <= 0)
190 return nullptr;
191 result->id = *maybe_id;
192
193 // Confirm that the batchsize field is present and type-safe.
194 base::Optional<int> maybe_batch_size =
195 value.FindIntKey(kTrustTokenKeyCommitmentBatchsizeField);
196 if (!maybe_batch_size || *maybe_batch_size <= 0)
197 return nullptr;
198 result->batch_size = *maybe_batch_size;
199
200 if (!ParseLocalIssuanceFieldsIfPresent(value, result.get()))
201 return nullptr;
202
203 // Parse the key commitments in the result (these are exactly the
204 // key-value pairs in the dictionary with dictionary-typed values).
205 for (const auto& kv : value.DictItems()) {
206 const base::Value& item = kv.second;
207 if (!item.is_dict())
208 continue;
209
210 auto key = mojom::TrustTokenVerificationKey::New();
211
212 if (!ParseSingleKeyLabel(kv.first))
213 return nullptr;
214
215 switch (ParseSingleKeyExceptLabel(item, key.get())) {
216 case ParseKeyResult::kFail:
217 return nullptr;
218 case ParseKeyResult::kIgnore:
219 continue;
220 case ParseKeyResult::kSucceed:
221 result->keys.push_back(std::move(key));
222 }
223 }
224
225 return result;
226 }
227
228 // Entry is a convenience struct used as an intermediate representation when
229 // parsing multiple issuers. In addition to a parsed canonicalized issuer, it
230 // preserves the raw JSON string key (the second entry) in order
231 // deterministically to deduplicate entries with keys canonicalizing to the same
232 // issuer.
233 using Entry = std::tuple<SuitableTrustTokenOrigin, // canonicalized issuer
234 std::string, // raw key from the JSON
235 mojom::TrustTokenKeyCommitmentResultPtr>;
canonicalized_issuer(Entry & e)236 SuitableTrustTokenOrigin& canonicalized_issuer(Entry& e) {
237 return std::get<0>(e);
238 }
commitment(Entry & e)239 mojom::TrustTokenKeyCommitmentResultPtr& commitment(Entry& e) {
240 return std::get<2>(e);
241 }
242
243 } // namespace
244
245 // https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#bookmark=id.6wh9crbxdizi
246 // {
247 // "protocol_version" : ..., // Protocol Version; value of type string.
248 // "id" : ..., // ID; value of type int.
249 // "batchsize" : ..., // Batch size; value of type int.
250 //
251 // // Optional operating systems on which to request issuance via system
252 // // mediation (valid values are: "android"), and (required if at least one
253 // // OS is specified) fallback behavior on other operating systems:
254 // "request_issuance_locally_on": [<os 1>, ..., <os N>],
255 // "unavailable_local_issuance_fallback": "web_issuance" | "return_with_error"
256 //
257 // "1" : { // Key label, a number in uint32_t range; ignored
258 // // except for checking that it is present and
259 // // type-safe.
260 // "Y" : ..., // Required token issuance verification key, in
261 // // base64.
262 // "expiry" : ..., // Required token issuance key expiry time, in
263 // // microseconds since the Unix epoch.
264 // },
265 // "17" : { // No guarantee that key labels (1, 7) are dense.
266 // "Y" : ...,
267 // "expiry" : ...,
268 // }
269 // }
Parse(base::StringPiece response_body)270 mojom::TrustTokenKeyCommitmentResultPtr TrustTokenKeyCommitmentParser::Parse(
271 base::StringPiece response_body) {
272 base::Optional<base::Value> maybe_value =
273 base::JSONReader::Read(response_body);
274 if (!maybe_value)
275 return nullptr;
276
277 return ParseSingleIssuer(std::move(*maybe_value));
278 }
279
280 std::unique_ptr<base::flat_map<SuitableTrustTokenOrigin,
281 mojom::TrustTokenKeyCommitmentResultPtr>>
ParseMultipleIssuers(base::StringPiece response_body)282 TrustTokenKeyCommitmentParser::ParseMultipleIssuers(
283 base::StringPiece response_body) {
284 base::Optional<base::Value> maybe_value =
285 base::JSONReader::Read(response_body);
286 if (!maybe_value)
287 return nullptr;
288
289 if (!maybe_value->is_dict())
290 return nullptr;
291
292 // The configuration might contain conflicting lists of keys for issuers with
293 // the same canonicalized URLs but different string representations provided
294 // by the server. In order to handle these deterministically, first transfer
295 // the entries to intermediate storage while maintaining the initial JSON
296 // keys; then deduplicate based on identical entries' JSON keys' lexicographic
297 // value.
298
299 std::vector<Entry> parsed_entries;
300
301 for (const auto& kv : maybe_value->DictItems()) {
302 const std::string& raw_key_from_json = kv.first;
303 base::Optional<SuitableTrustTokenOrigin> maybe_issuer =
304 SuitableTrustTokenOrigin::Create(GURL(raw_key_from_json));
305
306 if (!maybe_issuer)
307 continue;
308
309 mojom::TrustTokenKeyCommitmentResultPtr commitment_result =
310 ParseSingleIssuer(kv.second);
311
312 if (!commitment_result)
313 continue;
314
315 parsed_entries.emplace_back(Entry(std::move(*maybe_issuer),
316 raw_key_from_json,
317 std::move(commitment_result)));
318 }
319
320 // Deterministically deduplicate entries corresponding to the same issuer,
321 // with the largest JSON key lexicographically winning.
322 std::sort(parsed_entries.begin(), parsed_entries.end(), std::greater<>());
323 parsed_entries.erase(std::unique(parsed_entries.begin(), parsed_entries.end(),
324 [](Entry& lhs, Entry& rhs) -> bool {
325 return canonicalized_issuer(lhs) ==
326 canonicalized_issuer(rhs);
327 }),
328 parsed_entries.end());
329
330 // Finally, discard the raw JSON strings and construct a map to return.
331 std::vector<std::pair<SuitableTrustTokenOrigin,
332 mojom::TrustTokenKeyCommitmentResultPtr>>
333 map_storage;
334 map_storage.reserve(parsed_entries.size());
335 for (Entry& e : parsed_entries) {
336 map_storage.emplace_back(std::move(canonicalized_issuer(e)),
337 std::move(commitment(e)));
338 }
339
340 // Please don't remove this VLOG without first checking with
341 // trust_tokens/OWNERS to see if it's still being used for manual
342 // testing.
343 VLOG(1) << "Successfully parsed " << map_storage.size()
344 << " issuers' Trust Tokens key commitments.";
345
346 return std::make_unique<base::flat_map<
347 SuitableTrustTokenOrigin, mojom::TrustTokenKeyCommitmentResultPtr>>(
348 std::move(map_storage));
349 }
350
351 } // namespace network
352