Commit 1fafce25 authored by David Van Cleve's avatar David Van Cleve Committed by Commit Bot

Add a Trust Tokens request signing helper.

This CL implements the request signing operation of the Trust Tokens
protocol by adding a signing helper to //services/network. Request
signing involves the following steps:
1. Retrieve a Signed Redemption Record (SRR) from storage and attach
it as a request header.
2. Optionally, add a timestamp header.
3. Construct a canonical representation of the request, including a
collection of the request headers specified by the caller, and compute a
signature over this canonical representation using a stored public key
associated with the SRR.
4. Attach this signature---but _not_ the request's canonical
representation, which server-side consumers will be able to
reconstruct---as a request header.

The Trust Tokens design doc [*] contains the normative description
of how to construct this canonical signing data.

[*]: https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.6a92f2gfl9le

Bug: 1042962
Change-Id: I0acf0a7ba29d193013411db911f612d694667a17
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2050756
Commit-Queue: David Van Cleve <davidvc@chromium.org>
Reviewed-by: default avatarChris Palmer <palmer@chromium.org>
Reviewed-by: default avatarBalazs Engedy <engedy@chromium.org>
Reviewed-by: default avatarMatt Menke <mmenke@chromium.org>
Reviewed-by: default avatarCharlie Harrison <csharrison@chromium.org>
Cr-Commit-Position: refs/heads/master@{#748051}
parent d2c5a96b
...@@ -26,7 +26,11 @@ source_set("trust_tokens") { ...@@ -26,7 +26,11 @@ source_set("trust_tokens") {
"trust_token_operation_status.h", "trust_token_operation_status.h",
"trust_token_parameterization.h", "trust_token_parameterization.h",
"trust_token_persister.h", "trust_token_persister.h",
"trust_token_request_canonicalizer.cc",
"trust_token_request_canonicalizer.h",
"trust_token_request_helper.h", "trust_token_request_helper.h",
"trust_token_request_signing_helper.cc",
"trust_token_request_signing_helper.h",
"trust_token_store.cc", "trust_token_store.cc",
"trust_token_store.h", "trust_token_store.h",
"types.cc", "types.cc",
...@@ -36,6 +40,7 @@ source_set("trust_tokens") { ...@@ -36,6 +40,7 @@ source_set("trust_tokens") {
deps = [ deps = [
":storage_proto", ":storage_proto",
"//base", "//base",
"//components/cbor",
"//components/sqlite_proto", "//components/sqlite_proto",
"//services/network/public/cpp", "//services/network/public/cpp",
"//services/network/public/mojom", "//services/network/public/mojom",
...@@ -74,6 +79,8 @@ source_set("tests") { ...@@ -74,6 +79,8 @@ source_set("tests") {
"trust_token_database_owner_unittest.cc", "trust_token_database_owner_unittest.cc",
"trust_token_key_commitment_controller_unittest.cc", "trust_token_key_commitment_controller_unittest.cc",
"trust_token_persister_unittest.cc", "trust_token_persister_unittest.cc",
"trust_token_request_canonicalizer_unittest.cc",
"trust_token_request_signing_helper_unittest.cc",
"trust_token_store_unittest.cc", "trust_token_store_unittest.cc",
"types_unittest.cc", "types_unittest.cc",
] ]
...@@ -84,6 +91,7 @@ source_set("tests") { ...@@ -84,6 +91,7 @@ source_set("tests") {
":trust_tokens", ":trust_tokens",
"//base", "//base",
"//base/test:test_support", "//base/test:test_support",
"//components/cbor",
"//components/sqlite_proto", "//components/sqlite_proto",
"//net", "//net",
"//net:test_support", "//net:test_support",
......
include_rules = [ include_rules = [
"+components/cbor",
"+components/sqlite_proto", "+components/sqlite_proto",
"+third_party/protobuf/src/google/protobuf", "+third_party/protobuf/src/google/protobuf",
"+sql", "+sql",
......
...@@ -32,6 +32,11 @@ constexpr char kTrustTokensRequestHeaderSecSignature[] = "Sec-Signature"; ...@@ -32,6 +32,11 @@ constexpr char kTrustTokensRequestHeaderSecSignature[] = "Sec-Signature";
constexpr char kTrustTokensRequestHeaderSecSignedRedemptionRecord[] = constexpr char kTrustTokensRequestHeaderSecSignedRedemptionRecord[] =
"Sec-Signed-Redemption-Record"; "Sec-Signed-Redemption-Record";
// As a request header during the request signing operation, provides the list
// of headers included in the signing data's canonical request data. An absent
// header denotes an empty list.
constexpr char kTrustTokensRequestHeaderSignedHeaders[] = "Signed-Headers";
} // namespace network } // namespace network
#endif // SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_HTTP_HEADERS_H_ #endif // SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_HTTP_HEADERS_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include <string>
#include "base/strings/string_piece.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
#include "services/network/trust_tokens/trust_token_http_headers.h"
#include "services/network/trust_tokens/trust_token_request_signing_helper.h"
namespace network {
base::Optional<std::vector<uint8_t>>
TrustTokenRequestCanonicalizer::Canonicalize(
net::URLRequest* request,
base::StringPiece public_key,
mojom::TrustTokenSignRequestData sign_request_data) const {
DCHECK(sign_request_data == mojom::TrustTokenSignRequestData::kInclude ||
sign_request_data == mojom::TrustTokenSignRequestData::kHeadersOnly);
// It seems like there's no conceivable way in which keys could be empty
// during normal use, so reject in this case as a common-sense safety measure.
if (public_key.empty())
return base::nullopt;
cbor::Value::MapValue canonicalized_request;
// Here and below, the lines beginning with numbers are a reproduction of the
// normative pseudocode form the design doc.
// 1. If sign-request-data is 'include', add 'url': <request_url> to the
// structure.
// 1a. The key and value are both of CBOR type “text string”.
if (sign_request_data == mojom::TrustTokenSignRequestData::kInclude) {
canonicalized_request.emplace(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataUrlKey,
request->url().spec());
}
// 2. If sign-request-data is 'include' or 'headers-only', for each value
// header_name in the Signed-Headers request header, if the request has a
// header with a name that is a case-insensitive match of header_name, add
// <lowercased(header_name)>: <header value> to the map.
// - Each key and value are of CBOR type “text string”.
std::vector<std::string> headers_to_add;
std::string signed_headers_header;
if (request->extra_request_headers().GetHeader(
kTrustTokensRequestHeaderSignedHeaders, &signed_headers_header)) {
base::Optional<std::vector<std::string>> maybe_headers_to_add =
internal::ParseTrustTokenSignedHeadersHeader(signed_headers_header);
if (!maybe_headers_to_add)
return base::nullopt;
headers_to_add.swap(*maybe_headers_to_add);
}
for (const std::string& header_name : headers_to_add) {
std::string header_value;
// GetHeader matches case-insensitive names.
if (request->extra_request_headers().GetHeader(header_name,
&header_value)) {
canonicalized_request.emplace(base::ToLowerASCII(header_name),
header_value);
}
}
// 3. Add 'public-key': <pk> to the map
// - The key is of CBOR type “text string”; the value is of CBOR type “byte
// string”.
canonicalized_request.emplace(
std::piecewise_construct,
std::forward_as_tuple(TrustTokenRequestSigningHelper::
kCanonicalizedRequestDataPublicKeyKey),
std::forward_as_tuple(public_key, cbor::Value::Type::BYTE_STRING));
return cbor::Writer::Write(cbor::Value(std::move(canonicalized_request)));
}
} // namespace network
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_REQUEST_CANONICALIZER_H_
#define SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_REQUEST_CANONICALIZER_H_
#include <vector>
#include "base/optional.h"
#include "base/strings/string_piece_forward.h"
#include "net/url_request/url_request.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
namespace network {
// A TrustTokenRequestCanonicalizer turns a (URLRequest, public key) pair into
// the corresponding "canonical request data," which is a serialized CBOR
// structure comprising the public key and a collection of request data.
//
// Constructing this is a step in the Trust Tokens protocol's request signing
// operation. Exactly what request data is included alongside the public key
// depends on the parameterization of the operation, but it will always include
// a (potentially empty) caller-specified collection of request headers chosen
// from the TrustTokenRequestSigningHelper::kSignableRequestHeaders allowlist.
//
// The normative pseudocode for this operation currently lives in the Trust
// Tokens design doc's "Signature generation" section.
class TrustTokenRequestCanonicalizer {
public:
TrustTokenRequestCanonicalizer() = default;
virtual ~TrustTokenRequestCanonicalizer() = default;
TrustTokenRequestCanonicalizer(const TrustTokenRequestCanonicalizer&) =
delete;
TrustTokenRequestCanonicalizer& operator=(
const TrustTokenRequestCanonicalizer&) = delete;
// Attempts to canonicalize |request| according to the pseudocode in the
// design doc's "Signature generation" section, obtaining the headers to sign
// by inspecting |request|'s Signed-Headers header. |sign_request_data|'s
// value denotes whether the signing data should be more (kInclude) or less
// (kHeadersOnly) descriptive; refer to the normative pseudocode for details.
//
// |request| is passed as a mutable argument because, in the future, some
// forms of canonicalization may involve temporarily mutating |request|, in
// particular by reading its upload data.
//
// Returns nullopt if |request|'s Signed-Headers header is malformed (i.e.,
// not a valid Structured Headers list of atoms); if |public_key| is empty; or
// if there is an internal error during serialization.
//
// REQUIRES: |sign_request_data| is kInclude or kHeadersOnly.
virtual base::Optional<std::vector<uint8_t>> Canonicalize(
net::URLRequest* request,
base::StringPiece public_key,
mojom::TrustTokenSignRequestData sign_request_data) const;
};
} // namespace network
#endif // SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_REQUEST_CANONICALIZER_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include <memory>
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "net/url_request/url_request.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
#include "services/network/trust_tokens/trust_token_http_headers.h"
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include "services/network/trust_tokens/trust_token_request_signing_helper.h"
#include "services/network/trust_tokens/trust_token_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace network {
// Adopt the Trust Tokens fixture to create URLRequests without boilerplate
using TrustTokenRequestCanonicalizerTest = TrustTokenRequestHelperTest;
// Check that an empty request with an empty public key (and no headers to sign)
// serializes correctly. Expected CBOR maps:
//
// SignRequestData::kHeadersOnly:
// { "public_key": b"key" }
//
// SignRequestData::kInclude:
// { "url": "", "public_key": b"key" }
TEST_F(TrustTokenRequestCanonicalizerTest, Empty) {
TrustTokenRequestCanonicalizer canonicalizer;
cbor::Value::MapValue expected_cbor;
expected_cbor[cbor::Value(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataPublicKeyKey)] =
cbor::Value("key", cbor::Value::Type::BYTE_STRING);
std::unique_ptr<net::URLRequest> request = MakeURLRequest("");
EXPECT_EQ(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kHeadersOnly),
cbor::Writer::Write(cbor::Value(expected_cbor)));
expected_cbor[cbor::Value(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataUrlKey)] =
cbor::Value("");
EXPECT_EQ(
canonicalizer.Canonicalize(request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kInclude),
cbor::Writer::Write(cbor::Value(expected_cbor)));
}
// Canonicalize a request with a nonempty public key and a nonempty URL.
//
// SignRequestData::kHeadersOnly:
// { "public_key": b"key" }
//
// SignRequestData::kInclude:
// { "url": "https://issuer.com/", "public_key": b"key" }
TEST_F(TrustTokenRequestCanonicalizerTest, Simple) {
TrustTokenRequestCanonicalizer canonicalizer;
cbor::Value::MapValue expected_cbor;
expected_cbor[cbor::Value(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataPublicKeyKey)] =
cbor::Value("key", cbor::Value::Type::BYTE_STRING);
std::unique_ptr<net::URLRequest> request =
MakeURLRequest("https://issuer.com/");
EXPECT_EQ(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kHeadersOnly),
cbor::Writer::Write(cbor::Value(expected_cbor)));
expected_cbor[cbor::Value(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataUrlKey)] =
cbor::Value("https://issuer.com/");
EXPECT_EQ(
canonicalizer.Canonicalize(request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kInclude),
cbor::Writer::Write(cbor::Value(expected_cbor)));
}
// Canonicalize a request with a nonempty public key, some signed headers, and a
// nonempty URL.
//
// Expected CBOR maps:
//
// SignRequestData::kHeadersOnly:
// { "public_key": b"key", "first_header": "first_header_value",
// "second_header": "second_header_value" }
//
// SignRequestData::kInclude:
// { "url": "https://issuer.com/", "public_key": b"key",
// "first_header": "first_header_value", "second_header":
// "second_header_value" }
TEST_F(TrustTokenRequestCanonicalizerTest, WithSignedHeaders) {
TrustTokenRequestCanonicalizer canonicalizer;
cbor::Value::MapValue expected_cbor;
expected_cbor[cbor::Value(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataPublicKeyKey)] =
cbor::Value("key", cbor::Value::Type::BYTE_STRING);
std::unique_ptr<net::URLRequest> request =
MakeURLRequest("https://issuer.com/");
// Capitalization should be normalized.
request->SetExtraRequestHeaderByName("First_HeadER", "first_header_value",
/*overwrite=*/true);
request->SetExtraRequestHeaderByName("second_header", "second_header_value",
/*overwrite=*/true);
request->SetExtraRequestHeaderByName(kTrustTokensRequestHeaderSignedHeaders,
" first_header , second_header ",
/*overwrite=*/true);
expected_cbor[cbor::Value("first_header")] =
cbor::Value("first_header_value");
expected_cbor[cbor::Value("second_header")] =
cbor::Value("second_header_value");
EXPECT_EQ(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kHeadersOnly),
cbor::Writer::Write(cbor::Value(expected_cbor)));
expected_cbor[cbor::Value(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataUrlKey)] =
cbor::Value("https://issuer.com/");
EXPECT_EQ(
canonicalizer.Canonicalize(request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kInclude),
cbor::Writer::Write(cbor::Value(expected_cbor)));
}
// Canonicalizing a request with a malformed Signed-Headers header should fail.
TEST_F(TrustTokenRequestCanonicalizerTest, RejectsMalformedSignedHeaders) {
TrustTokenRequestCanonicalizer canonicalizer;
std::unique_ptr<net::URLRequest> request =
MakeURLRequest("https://issuer.com/");
// Set the Signed-Headers header to something that is *not* the serialization
// of a Structured Headers token. (Tokens can't start with quotes.)
request->SetExtraRequestHeaderByName(kTrustTokensRequestHeaderSignedHeaders,
"\"", /*overwrite=*/true);
EXPECT_FALSE(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kHeadersOnly));
}
// Canonicalizing a request with an empty key should fail.
TEST_F(TrustTokenRequestCanonicalizerTest, RejectsEmptyKey) {
TrustTokenRequestCanonicalizer canonicalizer;
std::unique_ptr<net::URLRequest> request =
MakeURLRequest("https://issuer.com/");
EXPECT_FALSE(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"",
mojom::TrustTokenSignRequestData::kHeadersOnly));
}
} // namespace network
...@@ -21,6 +21,7 @@ namespace network { ...@@ -21,6 +21,7 @@ namespace network {
// attaching cached redemption records). // attaching cached redemption records).
class TrustTokenRequestHelper { class TrustTokenRequestHelper {
public: public:
TrustTokenRequestHelper() = default;
virtual ~TrustTokenRequestHelper() = default; virtual ~TrustTokenRequestHelper() = default;
TrustTokenRequestHelper(const TrustTokenRequestHelper&) = delete; TrustTokenRequestHelper(const TrustTokenRequestHelper&) = delete;
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/trust_tokens/trust_token_request_signing_helper.h"
#include <iterator>
#include <memory>
#include <string>
#include "base/base64.h"
#include "base/containers/flat_set.h"
#include "base/optional.h"
#include "base/stl_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time_to_iso8601.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "net/http/structured_headers.h"
#include "net/url_request/url_request.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
#include "services/network/trust_tokens/proto/public.pb.h"
#include "services/network/trust_tokens/trust_token_http_headers.h"
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include "services/network/trust_tokens/trust_token_store.h"
#include "url/url_constants.h"
namespace network {
namespace internal {
// Parse the Signed-Headers input header as a Structured Headers Draft 15 list
// of "tokens" (unquoted strings with a constrained alphabet).
base::Optional<std::vector<std::string>> ParseTrustTokenSignedHeadersHeader(
base::StringPiece header) {
base::Optional<net::structured_headers::List> maybe_list =
net::structured_headers::ParseList(header);
if (!maybe_list)
return base::nullopt;
std::vector<std::string> ret;
for (const net::structured_headers::ParameterizedMember&
parameterized_member : *maybe_list) {
if (!parameterized_member.params.empty() ||
parameterized_member.member.size() != 1) {
return base::nullopt;
}
const net::structured_headers::ParameterizedItem& parameterized_item =
parameterized_member.member.front();
if (!parameterized_item.params.empty())
return base::nullopt;
if (!parameterized_item.item.is_token())
return base::nullopt;
ret.push_back(parameterized_item.item.GetString());
}
return ret;
}
} // namespace internal
const char* const TrustTokenRequestSigningHelper::kSignableRequestHeaders[]{
kTrustTokensRequestHeaderSecSignedRedemptionRecord,
kTrustTokensRequestHeaderSecTime,
};
constexpr char
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataUrlKey[];
constexpr char
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataPublicKeyKey[];
constexpr uint8_t
TrustTokenRequestSigningHelper::kRequestSigningDomainSeparator[];
namespace {
using Params = TrustTokenRequestSigningHelper::Params;
// Constants for keys and values in the Sec-Signature header:
const char kSignatureHeaderSignRequestDataIncludeValue[] = "include";
const char kSignatureHeaderSignRequestDataHeadersOnlyValue[] = "headers-only";
const char kSignatureHeaderSignRequestDataKey[] = "sign-request-data";
const char kSignatureHeaderPublicKeyKey[] = "public-key";
const char kSignatureHeaderSignatureKey[] = "sig";
std::vector<std::string> Lowercase(std::vector<std::string> in) {
for (std::string& str : in) {
for (auto& ch : str) {
ch = base::ToLowerASCII(ch);
}
}
return in;
}
// In order to check whether all of the header names given by the client are
// signable, perform a single initial computation of the lower-cased versions
// of |kSignableRequestHeaders|.
const base::flat_set<std::string>& LowercaseSignableHeaders() {
static base::NoDestructor<base::flat_set<std::string>>
kLowercaseSignableHeaders{Lowercase(
{std::begin(TrustTokenRequestSigningHelper::kSignableRequestHeaders),
std::end(TrustTokenRequestSigningHelper::kSignableRequestHeaders)})};
return *kLowercaseSignableHeaders;
}
// Attempts to combine the (comma-delimited) header names in |request|'s
// Signed-Headers header, if any, and the members of |additional_headers|.
//
// Returns nullopt, and removes |request|'s Signed-Headers header, if any
// provided header name is not present in the signable headers allowlist
// TrustTokenRequestSigningHelper::kSignableRequestHeaders.
//
// Otherwise:
// - updates |request|'s Signed-Headers header to contain the union of the
// lower-cased members of |additional_headers| and the lower-cased elements of
// |request|'s previous header value; and
// - returns the list of these header names.
base::Optional<std::vector<std::string>>
GetHeadersToSignAndUpdateSignedHeadersHeader(
net::URLRequest* request,
const std::vector<std::string>& additional_headers) {
std::string signed_headers_header;
ignore_result(request->extra_request_headers().GetHeader(
kTrustTokensRequestHeaderSignedHeaders, &signed_headers_header));
// Because of the characteristics of the protocol, there are expected to be
// roughly 2-5 total headers to sign.
base::flat_set<std::string> deduped_lowercase_headers_to_sign(
Lowercase(additional_headers));
base::Optional<std::vector<std::string>> maybe_parsed_header_names =
internal::ParseTrustTokenSignedHeadersHeader(signed_headers_header);
// Remove the Signed-Headers header:
// - On failure, or on success with no headers to sign, this will stay removed
// in order to denote that no headers are being signed.
// - On success, it will be added back to the request.
request->RemoveRequestHeaderByName(kTrustTokensRequestHeaderSignedHeaders);
// Fail if the request's Signed-Headers header existed but failed to parse.
if (!maybe_parsed_header_names)
return base::nullopt;
for (const std::string& header_name : Lowercase(*maybe_parsed_header_names))
deduped_lowercase_headers_to_sign.insert(header_name);
// If there are no headers to sign, don't bother readding the Signed-Headers
// header.
if (deduped_lowercase_headers_to_sign.empty())
return std::vector<std::string>();
if (!base::STLIncludes(LowercaseSignableHeaders(),
deduped_lowercase_headers_to_sign)) {
return base::nullopt;
}
std::vector<std::string> out(
std::make_move_iterator(deduped_lowercase_headers_to_sign.begin()),
std::make_move_iterator(deduped_lowercase_headers_to_sign.end()));
request->SetExtraRequestHeaderByName(kTrustTokensRequestHeaderSignedHeaders,
base::JoinString(out, ","),
/*overwrite=*/true);
return out;
}
void AttachSignedRedemptionRecordHeader(net::URLRequest* request,
const std::string& value) {
request->SetExtraRequestHeaderByName(
kTrustTokensRequestHeaderSecSignedRedemptionRecord, value,
/*overwrite=*/true);
}
} // namespace
TrustTokenRequestSigningHelper::TrustTokenRequestSigningHelper(
TrustTokenStore* token_store,
Params params,
std::unique_ptr<Signer> signer,
std::unique_ptr<TrustTokenRequestCanonicalizer> canonicalizer)
: token_store_(token_store),
params_(params),
signer_(std::move(signer)),
canonicalizer_(std::move(canonicalizer)) {
DCHECK(params_.issuer.scheme() == url::kHttpsScheme ||
(params_.issuer.scheme() == url::kHttpScheme &&
IsOriginPotentiallyTrustworthy(params_.issuer)));
DCHECK(params_.toplevel.scheme() == url::kHttpsScheme ||
(params_.toplevel.scheme() == url::kHttpScheme &&
IsOriginPotentiallyTrustworthy(params_.toplevel)));
}
TrustTokenRequestSigningHelper::~TrustTokenRequestSigningHelper() = default;
Params::Params() = default;
Params::~Params() = default;
Params::Params(const Params&) = default;
// The type alias causes a linter false positive.
// NOLINTNEXTLINE(misc-unconventional-assign-operator)
Params& Params::operator=(const Params&) = default;
void TrustTokenRequestSigningHelper::Begin(
net::URLRequest* request,
base::OnceCallback<void(TrustTokenOperationStatus)> done) {
DCHECK(request);
DCHECK(request->url().SchemeIsHTTPOrHTTPS() &&
IsUrlPotentiallyTrustworthy(request->url()));
DCHECK(request->initiator() &&
request->initiator()->scheme() == url::kHttpsScheme ||
(request->initiator()->scheme() == url::kHttpScheme &&
IsOriginPotentiallyTrustworthy(*request->initiator())));
// This class is responsible for adding these headers; callers should not add
// them.
DCHECK(!request->extra_request_headers().HasHeader(
kTrustTokensRequestHeaderSecSignedRedemptionRecord));
DCHECK(!request->extra_request_headers().HasHeader(
kTrustTokensRequestHeaderSecTime));
DCHECK(!request->extra_request_headers().HasHeader(
kTrustTokensRequestHeaderSecSignature));
base::Optional<SignedTrustTokenRedemptionRecord> maybe_redemption_record =
token_store_->RetrieveNonstaleRedemptionRecord(params_.issuer,
params_.toplevel);
if (!maybe_redemption_record) {
AttachSignedRedemptionRecordHeader(request, std::string());
std::move(done).Run(TrustTokenOperationStatus::kResourceExhausted);
return;
}
base::Optional<std::vector<std::string>> maybe_headers_to_sign =
GetHeadersToSignAndUpdateSignedHeadersHeader(
request, params_.additional_headers_to_sign);
if (!maybe_headers_to_sign) {
AttachSignedRedemptionRecordHeader(request, std::string());
std::move(done).Run(TrustTokenOperationStatus::kInvalidArgument);
return;
}
AttachSignedRedemptionRecordHeader(
request, base::Base64Encode(base::as_bytes(
base::make_span(maybe_redemption_record->body()))));
if (params_.should_add_timestamp) {
request->SetExtraRequestHeaderByName(kTrustTokensRequestHeaderSecTime,
base::TimeToISO8601(base::Time::Now()),
/*overwrite=*/true);
}
if (params_.sign_request_data == mojom::TrustTokenSignRequestData::kOmit) {
std::move(done).Run(TrustTokenOperationStatus::kOk);
return;
}
base::Optional<std::vector<uint8_t>> maybe_signature =
GetSignature(request, *maybe_redemption_record, *maybe_headers_to_sign);
if (!maybe_signature) {
AttachSignedRedemptionRecordHeader(request, std::string());
request->RemoveRequestHeaderByName(kTrustTokensRequestHeaderSecTime);
request->RemoveRequestHeaderByName(kTrustTokensRequestHeaderSignedHeaders);
std::move(done).Run(TrustTokenOperationStatus::kInternalError);
return;
}
base::Optional<std::string> maybe_signature_header = BuildSignatureHeader(
maybe_redemption_record->public_key(),
base::StringPiece(reinterpret_cast<const char*>(maybe_signature->data()),
maybe_signature->size()));
// Error serializing the header. Not expected.
if (!maybe_signature_header) {
std::move(done).Run(TrustTokenOperationStatus::kInternalError);
return;
}
request->SetExtraRequestHeaderByName(kTrustTokensRequestHeaderSecSignature,
*maybe_signature_header,
/*overwrite=*/true);
std::move(done).Run(TrustTokenOperationStatus::kOk);
}
TrustTokenOperationStatus TrustTokenRequestSigningHelper::Finalize(
mojom::URLResponseHead* response) {
return TrustTokenOperationStatus::kOk;
}
base::Optional<std::string>
TrustTokenRequestSigningHelper::BuildSignatureHeader(
base::StringPiece public_key,
base::StringPiece signature) {
net::structured_headers::Dictionary header_items;
header_items[kSignatureHeaderPublicKeyKey] =
net::structured_headers::ParameterizedMember(
net::structured_headers::Item(
std::string(public_key),
net::structured_headers::Item::ItemType::kByteSequenceType),
{});
header_items[kSignatureHeaderSignatureKey] =
net::structured_headers::ParameterizedMember(
net::structured_headers::Item(
std::string(signature),
net::structured_headers::Item::ItemType::kByteSequenceType),
{});
// A value of kOmit denotes not wanting the request signed at all, so it'd be
// a caller error if we were trying to sign the request with it set.
DCHECK_NE(params_.sign_request_data, mojom::TrustTokenSignRequestData::kOmit);
const char* sign_request_data_value =
params_.sign_request_data == mojom::TrustTokenSignRequestData::kInclude
? kSignatureHeaderSignRequestDataIncludeValue
: kSignatureHeaderSignRequestDataHeadersOnlyValue;
header_items[kSignatureHeaderSignRequestDataKey] =
net::structured_headers::ParameterizedMember(
net::structured_headers::Item(
sign_request_data_value,
net::structured_headers::Item::ItemType::kTokenType),
{});
return net::structured_headers::SerializeDictionary(header_items);
}
base::Optional<std::vector<uint8_t>>
TrustTokenRequestSigningHelper::GetSignature(
net::URLRequest* request,
const SignedTrustTokenRedemptionRecord& redemption_record,
const std::vector<std::string>& headers_to_sign) {
// (This follows the normative pseudocode, labeled "signature
// generation," in the Trust Tokens design doc.)
//
// 1. Generate a CBOR-encoded dictionary, the canonical request data.
// 2. Sign the concatenation of “Trust Token v0” and the CBOR-encoded
// dictionary. (The domain separator string “Trust Token v0” allows versioning
// otherwise-forward-compatible protocol structures, which is useful in case
// the semantics change across versions.)
base::Optional<std::vector<uint8_t>> maybe_request_in_cbor =
canonicalizer_->Canonicalize(request, redemption_record.public_key(),
params_.sign_request_data);
if (!maybe_request_in_cbor)
return base::nullopt;
// kRequestSigningDomainSeparator is an explicitly-specified char array, not
// a string literal, so this will, as intended, not include a null terminator.
std::vector<uint8_t> signing_data(std::begin(kRequestSigningDomainSeparator),
std::end(kRequestSigningDomainSeparator));
signing_data.insert(signing_data.end(), maybe_request_in_cbor->begin(),
maybe_request_in_cbor->end());
signer_->Init(
base::as_bytes(base::make_span(redemption_record.signing_key())));
return signer_->Sign(base::make_span(signing_data));
}
} // namespace network
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_REQUEST_SIGNING_HELPER_H_
#define SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_REQUEST_SIGNING_HELPER_H_
#include <memory>
#include <vector>
#include "base/callback_forward.h"
#include "base/component_export.h"
#include "base/containers/span.h"
#include "base/optional.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom-forward.h"
#include "services/network/trust_tokens/trust_token_operation_status.h"
#include "services/network/trust_tokens/trust_token_request_helper.h"
#include "url/origin.h"
namespace network {
class TrustTokenRequestCanonicalizer;
namespace internal {
// Given a string representation of a Trust Tokens Signed-Headers header,
// returns the list of header names given in the header, or nullopt on parsing
// error.
base::Optional<std::vector<std::string>> ParseTrustTokenSignedHeadersHeader(
base::StringPiece header);
} // namespace internal
class TrustTokenStore;
class SignedTrustTokenRedemptionRecord;
// Class TrustTokenRequestSigningHelper executes a single trust token signing
// operation (https://github.com/wicg/trust-token-api): it searches storage for
// a Signed Redemption Record (SRR), attaches the SRR to the request, and,
// depending on how the operation is parameterized, potentially also computes
// and attaches a signature over the SRR, a canonical representation of some
// of the request's data (for instance, a collection of the request's headers),
// and some additional metadata.
// To compute this signature, it uses a signing key associated with the SRR
// and generated during the previous Trust Tokens redemption operation that
// yielded the SRR.
class TrustTokenRequestSigningHelper : public TrustTokenRequestHelper {
public:
// The list of headers that callers are allowed to specify
// for signing. This allowlist exists in part because some headers are added
// much later in request construction. For the Trust Tokens MVP ("v0"), this
// is limited to the signed redemption record and added timestamp
// (see Params::should_add_timestamp) headers.
static const char* const kSignableRequestHeaders[];
// These are magic strings used in request signing. The canonicalized request
// data keys are used when constructing a CBOR dictionary; they are the keys
// to the values of request URL, POST body, and signing public key
// (if any).
static constexpr char kCanonicalizedRequestDataUrlKey[] = "url";
static constexpr char kCanonicalizedRequestDataPublicKeyKey[] = "public-key";
// |kRequestSigningDomainSeparator| is a static (fixed major per protocol
// version) string included in the signing data immediately prior to the
// request's canonical representation. This allows rendering otherwise valid
// signatures forwards-incompatible, which is useful in case the signing
// data's semantics change across protocol versions but its syntax does not.
static constexpr uint8_t kRequestSigningDomainSeparator[] = {
'T', 'r', 'u', 's', 't', ' ', 'T', 'o', 'k', 'e', 'n', ' ', 'v', '0'};
struct Params {
Params();
~Params();
Params(const Params&);
Params& operator=(const Params&);
// |issuer| is the Trust Tokens issuer origin for which to retrieve a Signed
// Redemption Record and matching signing key. This must be both (1) HTTP or
// HTTPS and (2) "potentially trustworthy". This precondition is slightly
// involved because there are two needs:
// 1. HTTP or HTTPS so that the scheme serializes in a sensible manner in
// order to serve as a key for persisting state.
// 2. potentially trustworthy origin to satisfy Web security requirements.
url::Origin issuer;
// |toplevel| is the top-level origin of the initiating request. This must
// satisfy the same preconditions as |issuer|.
url::Origin toplevel;
// |additional_headers_to_sign| is a list of headers to sign, in addition to
// those specified by the request's Signed-Headers header. If these are not
// case-insensitive versions of headers in the |kSignableRequestHeaders|
// allowlist, signing will fail with error kInvalidArgument.
std::vector<std::string> additional_headers_to_sign;
// If |should_add_timestamp| is true, successful signing operations will add
// a Sec-Time header to the request bearing a current timestamp. "Sec-Time"
// may be specified in kRequest
bool should_add_timestamp;
// If |sign_request_data| is kInclude, the request's URL will be
// included in the canonical request data used for signing. If it is
// kHeadersOnly, the request's headers will be the only request data used.
// If it is kOmit, no signature will be attached.
mojom::TrustTokenSignRequestData sign_request_data;
};
// Class Signer is responsible for the actual generation of signatures over
// request data.
class Signer {
public:
virtual ~Signer() = default;
// Initializes signer state with the given key. Must be called at least once
// before the first call to |Sign|.
virtual void Init(base::span<const uint8_t> key) = 0;
// Returns a one-shot signature over the given data, or an error. |Init|
// must have been called before the first call to |Sign|.
virtual base::Optional<std::vector<uint8_t>> Sign(
base::span<const uint8_t> data) = 0;
// Verifies the given signature. Does not depend on the current state of the
// signer (in particular, |Init| need not have been called).
virtual bool Verify(base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key) = 0;
};
// Creates a request signing helper with behavior determined by |params|,
// relying on |token_store| to provide protocol state; |canonicalizer| to
// generate the request's canonical request data; and |signer| to generate a
// signature over the request's signing data once it has been constructed from
// the canonical request data.
//
// |token_store| must outlive this object.
TrustTokenRequestSigningHelper(
TrustTokenStore* token_store,
Params params,
std::unique_ptr<Signer> signer,
std::unique_ptr<TrustTokenRequestCanonicalizer> canonicalizer);
~TrustTokenRequestSigningHelper() override;
TrustTokenRequestSigningHelper(const TrustTokenRequestSigningHelper&) =
delete;
TrustTokenRequestSigningHelper& operator=(
const TrustTokenRequestSigningHelper&) = delete;
// Attempts to attach a Signed Redemption Record (SRR) corresponding
// to |request|'s initiating top-level origin and the provided issuer origin.
//
// PRECONDITIONS:
// (0. |request|'s destination's origin and its initiator must satisfy the
// same conditions as the issuer origin in |params_|. This is DCHECKed, since
// it is not a protocol-level precondition.)
//
// 1. If the request already contains a Sec-Signed-Redemption-Record,
// Sec-Time, or Sec-Signature header, returns kInvalidArgument without
// touching the request.
// 2. If the caller specified headers for signing other than those in
// kSignableRequestHeaders (or if the request has a malformed or otherwise
// invalid signed issuers list in its Signed-Headers header), returns
// kInvalidArgument and attaches an empty Sec-Signed-Redemption-Record header
// to the request.
// 3. If |token_store_| contains no SRR for this issuer-toplevel pair,
// returns kResourceExhausted and attaches an empty
// Sec-Signed-Redemption-Record header.
//
// ATTACHING THE REDEMPTION RECORD:
// In the case that an SRR is found and the requested headers to sign are
// well-formed, attaches a Sec-Signed-Redemption-Record header
// bearing the SRR and:
// 1. if the request is configured for adding a Trust Tokens timestamp,
// adds a timestamp header;
// 2. if the request is configured for signing, computes the request's
// canonical request data and adds a signature header, following the algorithm
// in the explainer:
// https://github.com/WICG/trust-token-api#extension-trust-bound-keypair-and-request-signing
//
// RETURNS:
// - On success, returns kOk.
// - On internal error during signing, returns kInternalError and attaches an
// empty SRR header, no signature header, and no timestamp header.
// - On precondition failure, returns an error code and possibly attaches an
// empty SRR header; see PRECONDITIONS section above.
void Begin(net::URLRequest* request,
base::OnceCallback<void(TrustTokenOperationStatus)> done) override;
// Immediately returns kOk with no other effect. (Signing is an operation that
// only needs to process requests, not their corresponding responses.)
TrustTokenOperationStatus Finalize(mojom::URLResponseHead* response) override;
private:
// Given (unencoded) bytestrings |public_key| and |signature|, returns the
// Trust Tokens signature header, a serialized Structured Headers Draft 13
// dictionary looking roughly like (order not guaranteed):
// public-key=<pk>,
// sig=<signature>,
// sign-request-data=include | headers-only
//
// Returns nullopt on serialization error.
base::Optional<std::string> BuildSignatureHeader(base::StringPiece public_key,
base::StringPiece signature);
// Returns a signature over |request|'s pertinent data (public key,
// user-specified headers and, possibly, destination URL), or nullopt in case
// of internal error.
base::Optional<std::vector<uint8_t>> GetSignature(
net::URLRequest* request,
const SignedTrustTokenRedemptionRecord& record,
const std::vector<std::string>& headers_to_sign);
TrustTokenStore* token_store_;
// Temporary representation of the signing-related Fetch parameters until
// they're implemented.
// TODO(crbug.com/1043118): When integrating this with URLLoader/the signing
// helper factory, update Params's fields, or perhaps remove the struct.
Params params_;
std::unique_ptr<Signer> signer_;
std::unique_ptr<TrustTokenRequestCanonicalizer> canonicalizer_;
};
} // namespace network
#endif // SERVICES_NETWORK_TRUST_TOKENS_TRUST_TOKEN_REQUEST_SIGNING_HELPER_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/trust_tokens/trust_token_request_signing_helper.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <string>
#include <vector>
#include "base/base64.h"
#include "base/containers/span.h"
#include "base/optional.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/test/bind_test_util.h"
#include "base/test/task_environment.h"
#include "base/time/time_to_iso8601.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "net/base/request_priority.h"
#include "net/http/structured_headers.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_test_util.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
#include "services/network/trust_tokens/proto/public.pb.h"
#include "services/network/trust_tokens/trust_token_operation_status.h"
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include "services/network/trust_tokens/trust_token_store.h"
#include "services/network/trust_tokens/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
using ::testing::AnyOf;
using ::testing::IsEmpty;
using ::testing::Matches;
using ::testing::Not;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;
namespace network {
namespace {
using TrustTokenRequestSigningHelperTest = TrustTokenRequestHelperTest;
// FakeSigner returns a successful, nonempty, but meaningless, signature over
// its given signing data. It should be used for tests involving only signing,
// not verification.
class FakeSigner : public TrustTokenRequestSigningHelper::Signer {
public:
void Init(base::span<const uint8_t> key) override {}
base::Optional<std::vector<uint8_t>> Sign(
base::span<const uint8_t> data) override {
return std::vector<uint8_t>{'s', 'i', 'g', 'n', 'e', 'd'};
}
bool Verify(base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key) override {
NOTREACHED();
return false;
}
};
// IdentitySigner returns a "signature" over given signing data whose value
// equals that of the signing data. This makes verifying the signature easy:
// just check if the signature being provided equals the data it's supposed to
// be signing over.
class IdentitySigner : public TrustTokenRequestSigningHelper::Signer {
public:
void Init(base::span<const uint8_t> key) override {}
base::Optional<std::vector<uint8_t>> Sign(
base::span<const uint8_t> data) override {
return std::vector<uint8_t>(data.begin(), data.end());
}
bool Verify(base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key) override {
return std::equal(data.begin(), data.end(), signature.begin());
}
};
// FailingSigner always fails the Sign and Verify options.
class FailingSigner : public TrustTokenRequestSigningHelper::Signer {
public:
void Init(base::span<const uint8_t> key) override {}
base::Optional<std::vector<uint8_t>> Sign(
base::span<const uint8_t> data) override {
return base::nullopt;
}
bool Verify(base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key) override {
return false;
}
};
base::Optional<base::flat_map<std::string, net::structured_headers::Item>>
DeserializeSecSignatureHeader(base::StringPiece header) {
base::StringPairs kvs;
if (!base::SplitStringIntoKeyValuePairs(header, '=', ',', &kvs))
return base::nullopt;
base::flat_map<std::string, net::structured_headers::Item> ret;
for (const std::pair<std::string, std::string>& kv : kvs) {
auto maybe_item = net::structured_headers::ParseItem(kv.second);
if (!maybe_item || !maybe_item->params.empty())
return base::nullopt;
ret[kv.first] = std::move(maybe_item->item);
}
return ret;
}
// Reconstructs |request|'s canonical request data, extracts the signature from
// |request|'s Sec-Signature header, and uses the verification algorithm
// provided by the template parameter |Signer| to check that the Sec-Signature
// header's contained signature verifies.
template <typename Signer>
void ReconstructSigningDataAndAssertSignatureVerifies(
net::URLRequest* request,
const TrustTokenRequestCanonicalizer& canonicalizer) {
std::string signature_header;
ASSERT_TRUE(request->extra_request_headers().GetHeader("Sec-Signature",
&signature_header));
base::Optional<base::flat_map<std::string, net::structured_headers::Item>>
maybe_map = DeserializeSecSignatureHeader(signature_header);
ASSERT_TRUE(maybe_map);
auto it = maybe_map->find("sig");
ASSERT_TRUE(it != maybe_map->end());
ASSERT_TRUE(it->second.is_byte_sequence());
base::StringPiece signature = it->second.GetString();
it = maybe_map->find("public-key");
ASSERT_TRUE(it != maybe_map->end());
ASSERT_TRUE(it->second.is_byte_sequence());
base::StringPiece public_key = it->second.GetString();
it = maybe_map->find("sign-request-data");
ASSERT_TRUE(it != maybe_map->end());
ASSERT_TRUE(it->second.is_token());
base::StringPiece sign_request_data = it->second.GetString();
ASSERT_THAT(sign_request_data,
AnyOf(StrEq("include"), StrEq("headers-only")));
base::Optional<std::vector<uint8_t>> written_reconstructed_cbor =
canonicalizer.Canonicalize(
request, public_key,
sign_request_data == "include"
? mojom::TrustTokenSignRequestData::kInclude
: mojom::TrustTokenSignRequestData::kHeadersOnly);
ASSERT_TRUE(written_reconstructed_cbor);
std::vector<uint8_t> reconstructed_signing_data(
std::begin(
TrustTokenRequestSigningHelper::kRequestSigningDomainSeparator),
std::end(TrustTokenRequestSigningHelper::kRequestSigningDomainSeparator));
reconstructed_signing_data.insert(reconstructed_signing_data.end(),
written_reconstructed_cbor->begin(),
written_reconstructed_cbor->end());
ASSERT_TRUE(Signer().Verify(base::make_span(reconstructed_signing_data),
base::as_bytes(base::make_span(signature)),
base::as_bytes(base::make_span(public_key))));
}
// Verifies that |request| has a Sec-Signature header with a "sig" field and
// extracts the request's signature from this field.
void AssertHasSignatureAndExtract(const net::URLRequest& request,
std::string* signature_out) {
std::string signature_header;
ASSERT_TRUE(request.extra_request_headers().GetHeader("Sec-Signature",
&signature_header));
base::StringPairs kvs;
base::SplitStringIntoKeyValuePairs(signature_header, '=', ',', &kvs);
base::Optional<base::flat_map<std::string, net::structured_headers::Item>>
maybe_map = DeserializeSecSignatureHeader(std::move(signature_header));
ASSERT_TRUE(maybe_map);
auto it = maybe_map->find("sig");
ASSERT_TRUE(it != maybe_map->end());
ASSERT_TRUE(it->second.is_byte_sequence());
*signature_out = it->second.GetString();
}
// Assert that the given signing data is a concatenation of the domain separator
// defined in TrustTokenRequestSigningHelper (initially "Trust Token v0") and a
// valid CBOR struct, and that the struct contains a field of name |field_name|;
// extract the corresponding value.
void AssertDecodesToCborAndExtractField(base::StringPiece signing_data,
base::StringPiece field_name,
std::string* field_value_out) {
base::Optional<cbor::Value> parsed = cbor::Reader::Read(base::as_bytes(
// Skip over the "Trust Token v0" domain separator.
base::make_span(signing_data)
.subspan(base::size(TrustTokenRequestSigningHelper::
kRequestSigningDomainSeparator))));
ASSERT_TRUE(parsed);
const cbor::Value::MapValue& map = parsed->GetMap();
auto it = map.find(cbor::Value(field_name));
ASSERT_TRUE(it != map.end());
const cbor::Value& value = it->second;
*field_value_out = value.is_string()
? value.GetString()
: std::string(value.GetBytestringAsString());
}
MATCHER_P(Header, name, base::StringPrintf("The header %s is present", name)) {
return arg.extra_request_headers().HasHeader(name);
}
MATCHER_P2(Header,
name,
other_matcher,
"Evaluate the given matcher on the given header, if "
"present.") {
std::string header;
if (!arg.extra_request_headers().GetHeader(name, &header))
return false;
return Matches(other_matcher)(header);
}
} // namespace
TEST_F(TrustTokenRequestSigningHelperTest, WontSignIfNoRedemptionRecord) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<FakeSigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kResourceExhausted);
EXPECT_THAT(*my_request, Header("Sec-Signed-Redemption-Record", IsEmpty()));
EXPECT_THAT(*my_request, Not(Header("Sec-Signature")));
}
TEST_F(TrustTokenRequestSigningHelperTest, MergesHeaders) {
// The signing operation should fuse and lowercase the headers from the
// "Signed-Headers" request header and the additionalSignedHeaders Fetch
// param.
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
params.additional_headers_to_sign = std::vector<std::string>{"Sec-Time"};
SignedTrustTokenRedemptionRecord my_record;
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<FakeSigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
my_request->SetExtraRequestHeaderByName(
"Signed-Headers", "Sec-Signed-Redemption-Record", /*overwrite=*/true);
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kOk);
std::string signed_headers_header_value;
ASSERT_TRUE(my_request->extra_request_headers().GetHeader(
"Signed-Headers", &signed_headers_header_value));
// Headers should have been merged and lower-cased.
EXPECT_THAT(base::SplitString(signed_headers_header_value, ",",
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL),
UnorderedElementsAre(StrEq("sec-time"),
StrEq("sec-signed-redemption-record")));
}
TEST_F(TrustTokenRequestSigningHelperTest,
RejectsOnUnsignableHeaderNameInSignedHeadersHeader) {
// The signing operation should fail if there's an unsignable header (as
// specified by TrustTokenRequestSigningHelper::kSignableRequestHeaders) in
// the "Signed-Headers" request header or the additionalSignedHeaders Fetch
// param; this tests the former.
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
SignedTrustTokenRedemptionRecord my_record;
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<FakeSigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
my_request->SetExtraRequestHeaderByName(
"Signed-Headers",
"this header name is definitely not in"
"TrustTokenRequestSigningHelper::kSignableRequestHeaders",
/*overwrite=*/true);
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kInvalidArgument);
EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
}
TEST_F(TrustTokenRequestSigningHelperTest,
RejectsOnUnsignableHeaderNameInAdditionalHeadersList) {
// The signing operation should fail if there's an unsignable header (as
// specified by TrustTokenRequestSigningHelper::kSignableRequestHeaders) in
// the "Signed-Headers" request header or the additionalSignedHeaders Fetch
// param; this tests the latter.
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
params.additional_headers_to_sign = std::vector<std::string>{
"this header name is definitely not in "
"TrustTokenRequestSigningHelper::kSignableRequestHeaders"};
SignedTrustTokenRedemptionRecord my_record;
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<FakeSigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kInvalidArgument);
EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
}
class TrustTokenRequestSigningHelperTestWithMockTime
: public TrustTokenRequestSigningHelperTest {
public:
TrustTokenRequestSigningHelperTestWithMockTime()
: TrustTokenRequestSigningHelperTest(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~TrustTokenRequestSigningHelperTestWithMockTime() override = default;
};
TEST_F(TrustTokenRequestSigningHelperTestWithMockTime, ProvidesTimeHeader) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.should_add_timestamp = true;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
SignedTrustTokenRedemptionRecord my_record;
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<FakeSigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kOk);
EXPECT_THAT(
*my_request,
Header("Sec-Time", StrEq(base::TimeToISO8601(base::Time::Now()))));
}
// Test SRR attachment without request signing:
TEST_F(TrustTokenRequestSigningHelperTest,
RedemptionRecordAttachmentWithoutSigning) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
params.should_add_timestamp = true;
params.sign_request_data = mojom::TrustTokenSignRequestData::kOmit;
SignedTrustTokenRedemptionRecord my_record;
my_record.set_body("look at me! I'm a signed redemption record");
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<IdentitySigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
ASSERT_EQ(result, TrustTokenOperationStatus::kOk);
EXPECT_THAT(*my_request, Header("Sec-Signed-Redemption-Record",
StrEq(base::Base64Encode(base::as_bytes(
base::make_span(my_record.body()))))));
EXPECT_THAT(*my_request, Header("Sec-Time"));
EXPECT_THAT(*my_request, Not(Header("Sec-Signature")));
}
// Test a round-trip sign-and-verify with no headers.
TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyMinimal) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
SignedTrustTokenRedemptionRecord my_record;
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
// Giving an IdentitySigner to |helper| will mean that |helper| should provide
// its entire signing data in the request's Sec-Signature header's "sig"
// field. ReconstructSigningDataAndAssertSignatureVerifies then reproduces
// this canonical data's construction and checks that the reconstructed data
// matches what |helper| produced.
auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
auto* raw_canonicalizer = canonicalizer.get();
TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
std::make_unique<IdentitySigner>(),
std::move(canonicalizer));
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kOk);
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
}
// Test a round-trip sign-and-verify with signed headers.
TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyWithHeaders) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
SignedTrustTokenRedemptionRecord record;
record.set_body("I am a signed token redemption record");
record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, record);
params.additional_headers_to_sign =
std::vector<std::string>{"Sec-Signed-Redemption-Record"};
auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
auto* raw_canonicalizer = canonicalizer.get();
TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
std::make_unique<IdentitySigner>(),
std::move(canonicalizer));
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(url::Origin::Create(GURL("https://issuer.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kOk);
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
}
// Test a round-trip sign-and-verify with signed headers when adding a timestamp
// header via |should_add_timestamp|.
TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyTimestampHeader) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
params.additional_headers_to_sign = std::vector<std::string>{"sec-time"};
params.should_add_timestamp = true;
SignedTrustTokenRedemptionRecord record;
record.set_body("I am a signed token redemption record");
record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, record);
auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
auto* raw_canonicalizer = canonicalizer.get();
TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
std::make_unique<IdentitySigner>(),
std::move(canonicalizer));
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kOk);
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
std::string signature_string;
ASSERT_NO_FATAL_FAILURE(
AssertHasSignatureAndExtract(*my_request, &signature_string));
std::string retrieved_url_spec;
ASSERT_NO_FATAL_FAILURE(AssertDecodesToCborAndExtractField(
signature_string, "sec-time", &retrieved_url_spec));
}
// Test a round-trip sign-and-verify additionally signing over the destination
// URL (signRequestData = "include").
TEST_F(TrustTokenRequestSigningHelperTest,
SignAndVerifyWithHeadersAndDestinationUrl) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
params.sign_request_data = mojom::TrustTokenSignRequestData::kInclude;
SignedTrustTokenRedemptionRecord record;
record.set_body("I am a signed token redemption record");
record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, record);
params.additional_headers_to_sign =
std::vector<std::string>{"Sec-Signed-Redemption-Record"};
auto canonicalizer = std::make_unique<TrustTokenRequestCanonicalizer>();
auto* raw_canonicalizer = canonicalizer.get();
TrustTokenRequestSigningHelper helper(store.get(), std::move(params),
std::make_unique<IdentitySigner>(),
std::move(canonicalizer));
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
// In addition to testing that the signing data equals
// ReconstructSigningDataAndAssertSignatureVerifies's reconstruction of the
// data, explicitly check that it contains a "url" field with the right value.
EXPECT_EQ(result, TrustTokenOperationStatus::kOk);
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
// Because we're using an IdentitySigner, |signature_string| will have value
// equal to the base64-encoded request signing data.
std::string signature_string;
ASSERT_NO_FATAL_FAILURE(
AssertHasSignatureAndExtract(*my_request, &signature_string));
std::string retrieved_url_spec;
ASSERT_NO_FATAL_FAILURE(AssertDecodesToCborAndExtractField(
signature_string, "url", &retrieved_url_spec));
ASSERT_EQ(retrieved_url_spec, GURL("https://destination.com").spec());
}
// When signing fails, the request should have an empty
// Sec-Signed-Redemption-Record header attached, and none of the other headers
// that could potentially be added during signing.
TEST_F(TrustTokenRequestSigningHelperTest, CatchesSignatureFailure) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateInMemory();
TrustTokenRequestSigningHelper::Params params;
params.sign_request_data = mojom::TrustTokenSignRequestData::kHeadersOnly;
params.issuer = url::Origin::Create(GURL("https://issuer.com"));
params.toplevel = url::Origin::Create(GURL("https://toplevel.com"));
SignedTrustTokenRedemptionRecord my_record;
my_record.set_public_key("key");
store->SetRedemptionRecord(params.issuer, params.toplevel, my_record);
params.should_add_timestamp = true;
params.additional_headers_to_sign =
std::vector<std::string>{"Sec-Signed-Redemption-Record"};
// FailingSigner will fail to sign the request, so we should see the operation
// fail.
TrustTokenRequestSigningHelper helper(
store.get(), std::move(params), std::make_unique<FailingSigner>(),
std::make_unique<TrustTokenRequestCanonicalizer>());
auto my_request = MakeURLRequest("https://destination.com/");
my_request->set_initiator(
url::Origin::Create(GURL("https://initiator.com/")));
TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, my_request.get());
EXPECT_EQ(result, TrustTokenOperationStatus::kInternalError);
EXPECT_THAT(*my_request, Not(Header("Signed-Headers")));
EXPECT_THAT(*my_request, Not(Header("Sec-Time")));
EXPECT_THAT(*my_request, Not(Header("Sec-Signature")));
EXPECT_THAT(*my_request, Header("Sec-Signed-Redemption-Record", IsEmpty()));
}
} // namespace network
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
namespace network { namespace network {
TrustTokenRequestHelperTest::TrustTokenRequestHelperTest() = default; TrustTokenRequestHelperTest::TrustTokenRequestHelperTest(
base::test::TaskEnvironment::TimeSource time_source)
: env_(time_source) {}
TrustTokenRequestHelperTest::~TrustTokenRequestHelperTest() = default; TrustTokenRequestHelperTest::~TrustTokenRequestHelperTest() = default;
std::unique_ptr<net::URLRequest> TrustTokenRequestHelperTest::MakeURLRequest( std::unique_ptr<net::URLRequest> TrustTokenRequestHelperTest::MakeURLRequest(
......
...@@ -27,7 +27,9 @@ namespace network { ...@@ -27,7 +27,9 @@ namespace network {
// constructing net::URLRequests. // constructing net::URLRequests.
class TrustTokenRequestHelperTest : public ::testing::Test { class TrustTokenRequestHelperTest : public ::testing::Test {
public: public:
TrustTokenRequestHelperTest(); explicit TrustTokenRequestHelperTest(
base::test::TaskEnvironment::TimeSource time_source =
base::test::TaskEnvironment::TimeSource::DEFAULT);
~TrustTokenRequestHelperTest() override; ~TrustTokenRequestHelperTest() override;
TrustTokenRequestHelperTest(const TrustTokenRequestHelperTest&) = delete; TrustTokenRequestHelperTest(const TrustTokenRequestHelperTest&) = delete;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment