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