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_request_signing_helper.h"
6 
7 #include <algorithm>
8 #include <iterator>
9 #include <memory>
10 #include <string>
11 #include <vector>
12 
13 #include "base/base64.h"
14 #include "base/containers/span.h"
15 #include "base/optional.h"
16 #include "base/ranges/algorithm.h"
17 #include "base/strings/string_split.h"
18 #include "base/strings/string_util.h"
19 #include "base/test/bind.h"
20 #include "base/test/task_environment.h"
21 #include "base/time/time_to_iso8601.h"
22 #include "components/cbor/reader.h"
23 #include "components/cbor/values.h"
24 #include "components/cbor/writer.h"
25 #include "net/base/request_priority.h"
26 #include "net/http/structured_headers.h"
27 #include "net/log/test_net_log.h"
28 #include "net/log/test_net_log_util.h"
29 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
30 #include "net/url_request/url_request.h"
31 #include "net/url_request/url_request_test_util.h"
32 #include "services/network/public/cpp/trust_token_parameterization.h"
33 #include "services/network/public/mojom/trust_tokens.mojom-shared.h"
34 #include "services/network/trust_tokens/proto/public.pb.h"
35 #include "services/network/trust_tokens/test/signed_request_verification_util.h"
36 #include "services/network/trust_tokens/test/trust_token_test_util.h"
37 #include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
38 #include "services/network/trust_tokens/trust_token_store.h"
39 #include "testing/gmock/include/gmock/gmock.h"
40 #include "testing/gtest/include/gtest/gtest.h"
41 #include "url/gurl.h"
42 #include "url/origin.h"
43 
44 using ::testing::IsEmpty;
45 using ::testing::Matches;
46 using ::testing::Not;
47 using ::testing::StrEq;
48 using ::testing::UnorderedElementsAre;
49 
50 namespace network {
51 
52 namespace {
53 
54 using TrustTokenRequestSigningHelperTest = TrustTokenRequestHelperTest;
55 
56 // FakeSigner returns a successful, nonempty, but meaningless, signature over
57 // its given signing data. It should be used for tests involving only signing,
58 // not verification.
59 class FakeSigner : public TrustTokenRequestSigningHelper::Signer {
60  public:
Sign(base::span<const uint8_t> key,base::span<const uint8_t> data)61   base::Optional<std::vector<uint8_t>> Sign(
62       base::span<const uint8_t> key,
63       base::span<const uint8_t> data) override {
64     return std::vector<uint8_t>{'s', 'i', 'g', 'n', 'e', 'd'};
65   }
Verify(base::span<const uint8_t> data,base::span<const uint8_t> signature,base::span<const uint8_t> verification_key)66   bool Verify(base::span<const uint8_t> data,
67               base::span<const uint8_t> signature,
68               base::span<const uint8_t> verification_key) override {
69     NOTREACHED();
70     return false;
71   }
72 
GetAlgorithmIdentifier()73   std::string GetAlgorithmIdentifier() override { return "fake-signer"; }
74 };
75 
76 // IdentitySigner returns a "signature" over given signing data whose value
77 // equals that of the signing data. This makes verifying the signature easy:
78 // just check if the signature being provided equals the data it's supposed to
79 // be signing over.
80 class IdentitySigner : public TrustTokenRequestSigningHelper::Signer {
81  public:
Sign(base::span<const uint8_t> key,base::span<const uint8_t> data)82   base::Optional<std::vector<uint8_t>> Sign(
83       base::span<const uint8_t> key,
84       base::span<const uint8_t> data) override {
85     return std::vector<uint8_t>(data.begin(), data.end());
86   }
Verify(base::span<const uint8_t> data,base::span<const uint8_t> signature,base::span<const uint8_t> verification_key)87   bool Verify(base::span<const uint8_t> data,
88               base::span<const uint8_t> signature,
89               base::span<const uint8_t> verification_key) override {
90     return std::equal(data.begin(), data.end(), signature.begin());
91   }
GetAlgorithmIdentifier()92   std::string GetAlgorithmIdentifier() override { return "identity-signer"; }
93 };
94 
95 // FailingSigner always fails the Sign and Verify options.
96 class FailingSigner : public TrustTokenRequestSigningHelper::Signer {
97  public:
Sign(base::span<const uint8_t> key,base::span<const uint8_t> data)98   base::Optional<std::vector<uint8_t>> Sign(
99       base::span<const uint8_t> key,
100       base::span<const uint8_t> data) override {
101     return base::nullopt;
102   }
Verify(base::span<const uint8_t> data,base::span<const uint8_t> signature,base::span<const uint8_t> verification_key)103   bool Verify(base::span<const uint8_t> data,
104               base::span<const uint8_t> signature,
105               base::span<const uint8_t> verification_key) override {
106     return false;
107   }
GetAlgorithmIdentifier()108   std::string GetAlgorithmIdentifier() override { return "failing-signer"; }
109 };
110 
111 // Reconstructs |request|'s canonical request data, extracts the signatures from
112 // |request|'s Sec-Signature header, and uses the verification algorithm
113 // provided by the template parameter |Signer| to check that the Sec-Signature
114 // header's contained signatures verify.
115 template <typename Signer>
ReconstructSigningDataAndAssertSignaturesVerify(net::URLRequest * request,size_t num_expected_signatures)116 void ReconstructSigningDataAndAssertSignaturesVerify(
117     net::URLRequest* request,
118     size_t num_expected_signatures) {
119   std::string error;
120 
121   std::map<std::string, std::string> verification_keys_per_issuer;
122   bool success = test::ReconstructSigningDataAndVerifySignatures(
123       request->url(), request->extra_request_headers(),
124       base::BindRepeating([](base::span<const uint8_t> data,
125                              base::span<const uint8_t> signature,
126                              base::span<const uint8_t> verification_key,
127                              const std::string& signing_alg) {
128         return Signer().Verify(data, signature, verification_key) &&
129                signing_alg == Signer().GetAlgorithmIdentifier();
130       }),
131       &error, &verification_keys_per_issuer);
132 
133   ASSERT_TRUE(success) << error;
134   ASSERT_EQ(verification_keys_per_issuer.size(), num_expected_signatures);
135 }
136 
137 // Verifies that |request| has a Sec-Signature header containing signatures and
138 // extracts the signature for each issuer to |signatures_out|.
AssertHasSignaturesAndExtract(const net::URLRequest & request,std::map<std::string,std::string> * signatures_out)139 void AssertHasSignaturesAndExtract(
140     const net::URLRequest& request,
141     std::map<std::string, std::string>* signatures_out) {
142   std::string signature_header;
143   ASSERT_TRUE(request.extra_request_headers().GetHeader("Sec-Signature",
144                                                         &signature_header));
145 
146   base::Optional<net::structured_headers::Dictionary> maybe_dictionary =
147       net::structured_headers::ParseDictionary(signature_header);
148   ASSERT_TRUE(maybe_dictionary);
149   ASSERT_TRUE(maybe_dictionary->contains("signatures"));
150 
151   for (auto& issuer_and_params : maybe_dictionary->at("signatures").member) {
152     net::structured_headers::Item& issuer_item = issuer_and_params.item;
153     ASSERT_TRUE(issuer_item.is_string());
154 
155     auto signature_iterator = std::find_if(
156         issuer_and_params.params.begin(), issuer_and_params.params.end(),
157         [](auto& param) { return param.first == "sig"; });
158 
159     ASSERT_TRUE(signature_iterator != issuer_and_params.params.end())
160         << "Missing signature";
161     ASSERT_TRUE(signature_iterator->second.is_byte_sequence());
162     signatures_out->emplace(issuer_item.GetString(),
163                             signature_iterator->second.GetString());
164   }
165 }
166 
167 // Assert that the given signing data is a concatenation of the domain separator
168 // defined in TrustTokenRequestSigningHelper (initially "Trust Token v0") and a
169 // valid CBOR struct, and that the struct contains a field of name |field_name|;
170 // extract the corresponding value.
AssertDecodesToCborAndExtractField(base::StringPiece signing_data,base::StringPiece field_name,std::string * field_value_out)171 void AssertDecodesToCborAndExtractField(base::StringPiece signing_data,
172                                         base::StringPiece field_name,
173                                         std::string* field_value_out) {
174   base::Optional<cbor::Value> parsed = cbor::Reader::Read(base::as_bytes(
175       // Skip over the domain separator (e.g. "Trust Token v0").
176       base::make_span(signing_data)
177           .subspan(base::size(TrustTokenRequestSigningHelper::
178                                   kRequestSigningDomainSeparator))));
179   ASSERT_TRUE(parsed);
180 
181   const cbor::Value::MapValue& map = parsed->GetMap();
182   auto it = map.find(cbor::Value(field_name));
183   ASSERT_TRUE(it != map.end());
184   const cbor::Value& value = it->second;
185   *field_value_out = value.is_string()
186                          ? value.GetString()
187                          : std::string(value.GetBytestringAsString());
188 }
189 
190 MATCHER_P(Header, name, base::StringPrintf("The header %s is present", name)) {
191   return arg.extra_request_headers().HasHeader(name);
192 }
193 MATCHER_P2(Header,
194            name,
195            other_matcher,
196            "Evaluate the given matcher on the given header, if "
197            "present.") {
198   std::string header;
199   if (!arg.extra_request_headers().GetHeader(name, &header))
200     return false;
201   return Matches(other_matcher)(header);
202 }
203 
CreateSuitableOriginOrDie(base::StringPiece spec)204 SuitableTrustTokenOrigin CreateSuitableOriginOrDie(base::StringPiece spec) {
205   base::Optional<SuitableTrustTokenOrigin> maybe_origin =
206       SuitableTrustTokenOrigin::Create(GURL(spec));
207   CHECK(maybe_origin) << "Failed to create a SuitableTrustTokenOrigin!";
208   return *maybe_origin;
209 }
210 
211 }  // namespace
212 
TEST_F(TrustTokenRequestSigningHelperTest,WontSignIfNoRedemptionRecord)213 TEST_F(TrustTokenRequestSigningHelperTest, WontSignIfNoRedemptionRecord) {
214   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
215 
216   TrustTokenRequestSigningHelper::Params params(
217       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
218       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
219   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
220 
221   TrustTokenRequestSigningHelper helper(
222       store.get(), std::move(params), std::make_unique<FakeSigner>(),
223       std::make_unique<TrustTokenRequestCanonicalizer>());
224 
225   auto my_request = MakeURLRequest("https://destination.com/");
226   my_request->set_initiator(
227       url::Origin::Create(GURL("https://initiator.com/")));
228 
229   mojom::TrustTokenOperationStatus result =
230       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
231 
232   // In failure cases---in particular, in this case where none of the provided
233   // issuers has a redemption record in storage---the signing helper should
234   // return kOk but attach an empty RR header.
235   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
236   EXPECT_THAT(*my_request, Header("Sec-Redemption-Record", IsEmpty()));
237   EXPECT_THAT(*my_request, Not(Header("Sec-Signature")));
238 }
239 
TEST_F(TrustTokenRequestSigningHelperTest,MergesHeaders)240 TEST_F(TrustTokenRequestSigningHelperTest, MergesHeaders) {
241   // The signing operation should fuse and lowercase the headers from the
242   // "Signed-Headers" request header and the additionalSignedHeaders Fetch
243   // param.
244 
245   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
246 
247   TrustTokenRequestSigningHelper::Params params(
248       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
249       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
250   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
251   params.additional_headers_to_sign = std::vector<std::string>{"Sec-Time"};
252 
253   TrustTokenRedemptionRecord my_record;
254   my_record.set_public_key("key");
255   my_record.set_body("RR body");
256   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
257                              my_record);
258 
259   TrustTokenRequestSigningHelper helper(
260       store.get(), std::move(params), std::make_unique<FakeSigner>(),
261       std::make_unique<TrustTokenRequestCanonicalizer>());
262 
263   auto my_request = MakeURLRequest("https://destination.com/");
264   my_request->set_initiator(
265       url::Origin::Create(GURL("https://initiator.com/")));
266 
267   my_request->SetExtraRequestHeaderByName(
268       "Signed-Headers", "Sec-Redemption-Record", /*overwrite=*/true);
269 
270   mojom::TrustTokenOperationStatus result =
271       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
272 
273   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
274   std::string signed_headers_header_value;
275   ASSERT_TRUE(my_request->extra_request_headers().GetHeader(
276       "Signed-Headers", &signed_headers_header_value));
277 
278   // Headers should have been merged and lower-cased.
279   EXPECT_THAT(
280       base::SplitString(signed_headers_header_value, ",", base::KEEP_WHITESPACE,
281                         base::SPLIT_WANT_ALL),
282       UnorderedElementsAre(StrEq("sec-time"), StrEq("sec-redemption-record")));
283 }
284 
TEST_F(TrustTokenRequestSigningHelperTest,RejectsOnUnsignableHeaderNameInSignedHeadersHeader)285 TEST_F(TrustTokenRequestSigningHelperTest,
286        RejectsOnUnsignableHeaderNameInSignedHeadersHeader) {
287   // The signing operation should fail if there's an unsignable header (as
288   // specified by TrustTokenRequestSigningHelper::kSignableRequestHeaders) in
289   // the "Signed-Headers" request header or the additionalSignedHeaders Fetch
290   // param; this tests the former.
291 
292   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
293 
294   TrustTokenRequestSigningHelper::Params params(
295       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
296       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
297   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
298 
299   TrustTokenRedemptionRecord my_record;
300   my_record.set_public_key("key");
301   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
302                              my_record);
303 
304   TrustTokenRequestSigningHelper helper(
305       store.get(), std::move(params), std::make_unique<FakeSigner>(),
306       std::make_unique<TrustTokenRequestCanonicalizer>());
307 
308   auto my_request = MakeURLRequest("https://destination.com/");
309   my_request->set_initiator(
310       url::Origin::Create(GURL("https://initiator.com/")));
311 
312   my_request->SetExtraRequestHeaderByName(
313       "Signed-Headers",
314       "this header name is definitely not in"
315       "TrustTokenRequestSigningHelper::kSignableRequestHeaders",
316       /*overwrite=*/true);
317 
318   mojom::TrustTokenOperationStatus result =
319       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
320 
321   // In failure cases, the signing helper should return kOk but attach an empty
322   // RR header.
323   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
324   EXPECT_THAT(*my_request, Header("Sec-Redemption-Record", IsEmpty()));
325   EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
326 }
327 
TEST_F(TrustTokenRequestSigningHelperTest,RejectsOnUnsignableHeaderNameInAdditionalHeadersList)328 TEST_F(TrustTokenRequestSigningHelperTest,
329        RejectsOnUnsignableHeaderNameInAdditionalHeadersList) {
330   // The signing operation should fail if there's an unsignable header (as
331   // specified by TrustTokenRequestSigningHelper::kSignableRequestHeaders) in
332   // the "Signed-Headers" request header or the additionalSignedHeaders Fetch
333   // param; this tests the latter.
334 
335   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
336 
337   TrustTokenRequestSigningHelper::Params params(
338       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
339       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
340   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
341   params.additional_headers_to_sign = std::vector<std::string>{
342       "this header name is definitely not in "
343       "TrustTokenRequestSigningHelper::kSignableRequestHeaders"};
344 
345   TrustTokenRedemptionRecord my_record;
346   my_record.set_public_key("key");
347   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
348                              my_record);
349 
350   TrustTokenRequestSigningHelper helper(
351       store.get(), std::move(params), std::make_unique<FakeSigner>(),
352       std::make_unique<TrustTokenRequestCanonicalizer>());
353 
354   auto my_request = MakeURLRequest("https://destination.com/");
355   my_request->set_initiator(
356       url::Origin::Create(GURL("https://initiator.com/")));
357   mojom::TrustTokenOperationStatus result =
358       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
359 
360   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
361   EXPECT_THAT(*my_request, Header("Sec-Redemption-Record", IsEmpty()));
362   EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
363 }
364 
365 class TrustTokenRequestSigningHelperTestWithMockTime
366     : public TrustTokenRequestSigningHelperTest {
367  public:
TrustTokenRequestSigningHelperTestWithMockTime()368   TrustTokenRequestSigningHelperTestWithMockTime()
369       : TrustTokenRequestSigningHelperTest(
370             base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
371   ~TrustTokenRequestSigningHelperTestWithMockTime() override = default;
372 };
373 
TEST_F(TrustTokenRequestSigningHelperTestWithMockTime,ProvidesTimeHeader)374 TEST_F(TrustTokenRequestSigningHelperTestWithMockTime, ProvidesTimeHeader) {
375   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
376 
377   TrustTokenRequestSigningHelper::Params params(
378       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
379       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
380   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
381   params.should_add_timestamp = true;
382 
383   TrustTokenRedemptionRecord my_record;
384   my_record.set_public_key("key");
385   my_record.set_body("look at me, I'm an RR body");
386   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
387                              my_record);
388 
389   TrustTokenRequestSigningHelper helper(
390       store.get(), std::move(params), std::make_unique<FakeSigner>(),
391       std::make_unique<TrustTokenRequestCanonicalizer>());
392 
393   auto my_request = MakeURLRequest("https://destination.com/");
394   my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
395   mojom::TrustTokenOperationStatus result =
396       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
397 
398   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
399   EXPECT_THAT(
400       *my_request,
401       Header("Sec-Time", StrEq(base::TimeToISO8601(base::Time::Now()))));
402 }
403 
404 // Test RR attachment without request signing:
405 // - The two issuers with stored redemption records should appear in the header.
406 // - A third issuer without a corresponding redemption record in storage
407 // shouldn't appear in the header.
TEST_F(TrustTokenRequestSigningHelperTest,RedemptionRecordAttachmentWithoutSigning)408 TEST_F(TrustTokenRequestSigningHelperTest,
409        RedemptionRecordAttachmentWithoutSigning) {
410   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
411 
412   TrustTokenRequestSigningHelper::Params params(
413       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
414       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
415   params.should_add_timestamp = true;
416   params.sign_request_data = mojom::TrustTokenSignRequestData::kOmit;
417   params.issuers.push_back(
418       *SuitableTrustTokenOrigin::Create(GURL("https://second-issuer.example")));
419 
420   TrustTokenRedemptionRecord first_issuer_record;
421   first_issuer_record.set_body("look at me! I'm a redemption record");
422   first_issuer_record.set_public_key("key");
423   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
424                              first_issuer_record);
425 
426   TrustTokenRedemptionRecord second_issuer_record;
427   second_issuer_record.set_body(
428       "I'm another redemption record, distinct from the first");
429   second_issuer_record.set_public_key("some other key");
430   store->SetRedemptionRecord(params.issuers.back(), params.toplevel,
431                              second_issuer_record);
432 
433   // Attempting to sign with an issuer with no redemption record in storage
434   // should be fine, resulting in the issuer getting ignored.
435   params.issuers.push_back(
436       *SuitableTrustTokenOrigin::Create(GURL("https://third-issuer.example")));
437 
438   TrustTokenRequestSigningHelper helper(
439       store.get(), std::move(params), std::make_unique<IdentitySigner>(),
440       std::make_unique<TrustTokenRequestCanonicalizer>());
441 
442   auto my_request = MakeURLRequest("https://destination.com/");
443   my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
444   mojom::TrustTokenOperationStatus result =
445       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
446 
447   ASSERT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
448 
449   std::string redemption_record_header;
450   ASSERT_TRUE(my_request->extra_request_headers().GetHeader(
451       "Sec-Redemption-Record", &redemption_record_header));
452   std::map<SuitableTrustTokenOrigin, std::string> redemption_records_per_issuer;
453   std::string error;
454   ASSERT_TRUE(test::ExtractRedemptionRecordsFromHeader(
455       redemption_record_header, &redemption_records_per_issuer, &error))
456       << error;
457 
458   EXPECT_THAT(
459       redemption_records_per_issuer,
460       UnorderedElementsAre(
461           Pair(CreateSuitableOriginOrDie("https://issuer.com"),
462                StrEq(first_issuer_record.body())),
463           Pair(CreateSuitableOriginOrDie("https://second-issuer.example"),
464                StrEq(second_issuer_record.body()))));
465   EXPECT_THAT(*my_request, Header("Sec-Time"));
466   EXPECT_THAT(*my_request, Not(Header("Sec-Signature")));
467 }
468 
469 // Test a round-trip sign-and-verify with no headers.
TEST_F(TrustTokenRequestSigningHelperTest,SignAndVerifyMinimal)470 TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyMinimal) {
471   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
472 
473   TrustTokenRequestSigningHelper::Params params(
474       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
475       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
476   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
477 
478   TrustTokenRedemptionRecord my_record;
479   my_record.set_public_key("key");
480   my_record.set_body("look at me, I'm an RR body");
481   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
482                              my_record);
483 
484   // Giving an IdentitySigner to |helper| will mean that |helper| should provide
485   // its entire signing data in the request's Sec-Signature header's "sig"
486   // field. ReconstructSigningDataAndAssertSignaturesVerify then reproduces
487   // this canonical data's construction and checks that the reconstructed data
488   // matches what |helper| produced.
489   auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
490   TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
491                                         std::make_unique<IdentitySigner>(),
492                                         std::move(canonicalizer));
493 
494   auto my_request = MakeURLRequest("https://destination.com/");
495   my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
496   mojom::TrustTokenOperationStatus result =
497       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
498 
499   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
500 
501   ASSERT_NO_FATAL_FAILURE(
502       ReconstructSigningDataAndAssertSignaturesVerify<IdentitySigner>(
503           my_request.get(), /*num_expected_signatures=*/1));
504 }
505 
506 // Test a round-trip sign-and-verify with signed headers and multiple issuers.
TEST_F(TrustTokenRequestSigningHelperTest,SignAndVerifyWithHeaders)507 TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyWithHeaders) {
508   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
509 
510   TrustTokenRequestSigningHelper::Params params(
511       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
512       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
513   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
514   TrustTokenRedemptionRecord record;
515   record.set_body("I am a token redemption record");
516   record.set_public_key("key");
517   store->SetRedemptionRecord(params.issuers.front(), params.toplevel, record);
518   params.additional_headers_to_sign =
519       std::vector<std::string>{"Sec-Redemption-Record"};
520 
521   params.issuers.push_back(
522       *SuitableTrustTokenOrigin::Create(GURL("https://second-issuer.example")));
523   TrustTokenRedemptionRecord other_record;
524   other_record.set_body("I am a different token redemption record");
525   other_record.set_public_key("some other key");
526   store->SetRedemptionRecord(params.issuers.back(), params.toplevel,
527                              other_record);
528 
529   auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
530   TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
531                                         std::make_unique<IdentitySigner>(),
532                                         std::move(canonicalizer));
533 
534   auto my_request = MakeURLRequest("https://destination.com/");
535   my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
536   mojom::TrustTokenOperationStatus result =
537       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
538 
539   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
540   ASSERT_NO_FATAL_FAILURE(
541       ReconstructSigningDataAndAssertSignaturesVerify<IdentitySigner>(
542           my_request.get(), /*num_expected_signatures=*/2));
543 }
544 
545 // Test a round-trip sign-and-verify with signed headers when adding a timestamp
546 // header via |should_add_timestamp|.
TEST_F(TrustTokenRequestSigningHelperTest,SignAndVerifyTimestampHeader)547 TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyTimestampHeader) {
548   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
549 
550   TrustTokenRequestSigningHelper::Params params(
551       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
552       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
553   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
554   params.additional_headers_to_sign = std::vector<std::string>{"sec-time"};
555   params.should_add_timestamp = true;
556 
557   TrustTokenRedemptionRecord record;
558   record.set_body("I am a token redemption record");
559   record.set_public_key("key");
560   store->SetRedemptionRecord(params.issuers.front(), params.toplevel, record);
561 
562   auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
563   TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
564                                         std::make_unique<IdentitySigner>(),
565                                         std::move(canonicalizer));
566 
567   auto my_request = MakeURLRequest("https://destination.com/");
568   my_request->set_initiator(
569       url::Origin::Create(GURL("https://initiator.com/")));
570   mojom::TrustTokenOperationStatus result =
571       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
572 
573   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
574   ASSERT_NO_FATAL_FAILURE(
575       ReconstructSigningDataAndAssertSignaturesVerify<IdentitySigner>(
576           my_request.get(), /*num_expected_signatures=*/1));
577 
578   // Because we're using an IdentitySigner, each signature will have value
579   // equal to the base64-encoded request signing data.
580   std::map<std::string, std::string> signatures;
581   ASSERT_NO_FATAL_FAILURE(
582       AssertHasSignaturesAndExtract(*my_request, &signatures));
583   std::string retrieved_timestamp;
584   ASSERT_NO_FATAL_FAILURE(AssertDecodesToCborAndExtractField(
585       signatures.begin()->second, "sec-time", &retrieved_timestamp));
586 }
587 
588 // Test a round-trip sign-and-verify additionally signing over the destination
589 // eTLD+1 (signRequestData = "include").
TEST_F(TrustTokenRequestSigningHelperTest,SignAndVerifyWithHeadersAndDestinationUrl)590 TEST_F(TrustTokenRequestSigningHelperTest,
591        SignAndVerifyWithHeadersAndDestinationUrl) {
592   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
593 
594   TrustTokenRequestSigningHelper::Params params(
595       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
596       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
597   params.sign_request_data = mojom::TrustTokenSignRequestData::kInclude;
598 
599   TrustTokenRedemptionRecord record;
600   record.set_body("I am a token redemption record");
601   record.set_public_key("key");
602   store->SetRedemptionRecord(params.issuers.front(), params.toplevel, record);
603   params.additional_headers_to_sign =
604       std::vector<std::string>{"Sec-Redemption-Record"};
605 
606   auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
607   TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
608                                         std::make_unique<IdentitySigner>(),
609                                         std::move(canonicalizer));
610 
611   auto my_request = MakeURLRequest("https://sub.destination.com/path?query");
612   my_request->set_initiator(
613       url::Origin::Create(GURL("https://initiator.com/")));
614   mojom::TrustTokenOperationStatus result =
615       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
616 
617   // In addition to testing that the signing data equals
618   // ReconstructSigningDataAndAssertSignaturesVerify's reconstruction of the
619   // data, explicitly check that it contains a "destination" field with the
620   // right value.
621   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
622 
623   ASSERT_NO_FATAL_FAILURE(
624       ReconstructSigningDataAndAssertSignaturesVerify<IdentitySigner>(
625           my_request.get(), /*num_expected_signatures=*/1));
626 
627   // Because we're using an IdentitySigner, each signature will have value
628   // equal to the base64-encoded request signing data.
629   std::map<std::string, std::string> signatures;
630   ASSERT_NO_FATAL_FAILURE(
631       AssertHasSignaturesAndExtract(*my_request, &signatures));
632   std::string retrieved_url;
633   ASSERT_NO_FATAL_FAILURE(AssertDecodesToCborAndExtractField(
634       signatures.begin()->second, "destination", &retrieved_url));
635   ASSERT_EQ(retrieved_url, "destination.com");
636 }
637 
638 // When signing fails, the request should have an empty
639 // Sec-Redemption-Record header attached, and none of the other headers
640 // that could potentially be added during signing.
TEST_F(TrustTokenRequestSigningHelperTest,CatchesSignatureFailure)641 TEST_F(TrustTokenRequestSigningHelperTest, CatchesSignatureFailure) {
642   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
643 
644   TrustTokenRequestSigningHelper::Params params(
645       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
646       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
647   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
648 
649   TrustTokenRedemptionRecord my_record;
650   my_record.set_public_key("key");
651   my_record.set_signing_key("signing key");
652   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
653                              my_record);
654 
655   params.should_add_timestamp = true;
656   params.additional_headers_to_sign =
657       std::vector<std::string>{"Sec-Redemption-Record"};
658 
659   // FailingSigner will fail to sign the request, so we should see the operation
660   // fail.
661   net::RecordingTestNetLog net_log;
662   TrustTokenRequestSigningHelper helper(
663       store.get(), std::move(params), std::make_unique<FailingSigner>(),
664       std::make_unique<TrustTokenRequestCanonicalizer>(),
665       net::NetLogWithSource::Make(&net_log,
666                                   net::NetLogSourceType::URL_REQUEST));
667 
668   auto my_request = MakeURLRequest("https://destination.com/");
669   my_request->set_initiator(
670       url::Origin::Create(GURL("https://initiator.com/")));
671   mojom::TrustTokenOperationStatus result =
672       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
673 
674   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
675   EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
676   EXPECT_THAT(*my_request, Not(Header("Sec-Time")));
677   EXPECT_THAT(*my_request, Not(Header("Sec-Signature")));
678   EXPECT_THAT(*my_request, Header("Sec-Redemption-Record", IsEmpty()));
679   EXPECT_TRUE(base::ranges::any_of(
680       net_log.GetEntriesWithType(
681           net::NetLogEventType::TRUST_TOKEN_OPERATION_BEGIN_SIGNING),
682       [](const net::NetLogEntry& entry) {
683         base::Optional<std::string> key = net::GetOptionalStringValueFromParams(
684             entry, "failed_signing_params.key");
685         base::Optional<std::string> issuer =
686             net::GetOptionalStringValueFromParams(
687                 entry, "failed_signing_params.issuer");
688         return key && *key == "signing key" && issuer &&
689                *issuer == "https://issuer.com";
690       }));
691 }
692 
693 // Test a round-trip sign-and-verify with signed headers when adding additional
694 // signing data.
TEST_F(TrustTokenRequestSigningHelperTest,SignAndVerifyAdditionalSigningData)695 TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyAdditionalSigningData) {
696   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
697 
698   TrustTokenRequestSigningHelper::Params params(
699       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
700       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
701   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
702   params.possibly_unsafe_additional_signing_data =
703       "some additional data to sign";
704 
705   TrustTokenRedemptionRecord record;
706   record.set_body("I am a token redemption record");
707   record.set_public_key("key");
708   store->SetRedemptionRecord(params.issuers.front(), params.toplevel, record);
709 
710   auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
711   TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
712                                         std::make_unique<IdentitySigner>(),
713                                         std::move(canonicalizer));
714 
715   auto my_request = MakeURLRequest("https://destination.com/");
716   my_request->set_initiator(
717       url::Origin::Create(GURL("https://initiator.com/")));
718   mojom::TrustTokenOperationStatus result =
719       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
720 
721   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
722   ASSERT_NO_FATAL_FAILURE(
723       ReconstructSigningDataAndAssertSignaturesVerify<IdentitySigner>(
724           my_request.get(), /*num_expected_signatures=*/1));
725 
726   // Because we're using an IdentitySigner, each signature will have value
727   // equal to the base64-encoded request signing data.
728   std::map<std::string, std::string> signatures;
729   ASSERT_NO_FATAL_FAILURE(
730       AssertHasSignaturesAndExtract(*my_request, &signatures));
731   std::string retrieved_additional_signing_data;
732   ASSERT_NO_FATAL_FAILURE(AssertDecodesToCborAndExtractField(
733       signatures.begin()->second, "sec-trust-tokens-additional-signing-data",
734       &retrieved_additional_signing_data));
735 
736   EXPECT_EQ(retrieved_additional_signing_data, "some additional data to sign");
737 }
738 
TEST_F(TrustTokenRequestSigningHelperTest,RejectsOnOverlongAdditionalSigningData)739 TEST_F(TrustTokenRequestSigningHelperTest,
740        RejectsOnOverlongAdditionalSigningData) {
741   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
742 
743   TrustTokenRequestSigningHelper::Params params(
744       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
745       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
746   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
747   params.possibly_unsafe_additional_signing_data =
748       std::string(kTrustTokenAdditionalSigningDataMaxSizeBytes + 1, 'a');
749 
750   TrustTokenRedemptionRecord my_record;
751   my_record.set_public_key("key");
752   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
753                              my_record);
754 
755   TrustTokenRequestSigningHelper helper(
756       store.get(), std::move(params), std::make_unique<FakeSigner>(),
757       std::make_unique<TrustTokenRequestCanonicalizer>());
758 
759   auto my_request = MakeURLRequest("https://destination.com/");
760   my_request->set_initiator(
761       url::Origin::Create(GURL("https://initiator.com/")));
762 
763   mojom::TrustTokenOperationStatus result =
764       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
765 
766   // In failure cases, the signing helper should return kOk but attach an empty
767   // RR header.
768   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
769   EXPECT_THAT(*my_request, Header("Sec-Redemption-Record", IsEmpty()));
770   EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
771   EXPECT_THAT(*my_request,
772               Not(Header("Sec-Trust-Tokens-Additional-Signing-Data")));
773 }
774 
TEST_F(TrustTokenRequestSigningHelperTest,RejectsOnAdditionalSigningDataThatIsNotAValidHeaderValue)775 TEST_F(TrustTokenRequestSigningHelperTest,
776        RejectsOnAdditionalSigningDataThatIsNotAValidHeaderValue) {
777   std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
778 
779   TrustTokenRequestSigningHelper::Params params(
780       *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
781       *SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")));
782   params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
783   params.possibly_unsafe_additional_signing_data = "\r";
784 
785   TrustTokenRedemptionRecord my_record;
786   my_record.set_public_key("key");
787   store->SetRedemptionRecord(params.issuers.front(), params.toplevel,
788                              my_record);
789 
790   TrustTokenRequestSigningHelper helper(
791       store.get(), std::move(params), std::make_unique<FakeSigner>(),
792       std::make_unique<TrustTokenRequestCanonicalizer>());
793 
794   auto my_request = MakeURLRequest("https://destination.com/");
795   my_request->set_initiator(
796       url::Origin::Create(GURL("https://initiator.com/")));
797 
798   mojom::TrustTokenOperationStatus result =
799       ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
800 
801   // In failure cases, the signing helper should return kOk but attach an empty
802   // RR header.
803   EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
804   EXPECT_THAT(*my_request, Header("Sec-Redemption-Record", IsEmpty()));
805   EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
806   EXPECT_THAT(*my_request,
807               Not(Header("Sec-Trust-Tokens-Additional-Signing-Data")));
808 }
809 
810 }  // namespace network
811