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