0

Trust Tokens: add (disabled) initial end-to-end tests

This CL comprises some test-only changes adding rigging to support
end-to-end Trust Tokens tests, as well as some first-pass tests
themselves, disabled pending the necessary code landing in the child CL.

The additions:
1. TrustTokenRequestHandler, which is a server responsible for
satisfying issuance and redemption requests and inspecting subsequent
signed requests to confirm invariants hold;
2. signed_request_verification_util.{h, cc} get more utility methods for
parsing and verifying the contents of Sec-Signature headers and SRRs
3. TrustTokenRequestCanonicalizer and TrustTokenRequestSigningHelper's
unit tests get some refactoring in order to share code with the new
end-to-end test infra
4. The tests! They live in //content/test. I'm starting out with simple
end-to-end tests using the Fetch, XHR, and iframe APIs, as well as one
test that makes sure hasTrustToken returns true after a successful
issuance.

R=csharrison

Test: Tests pass in the child CL.
Bug: 1063140
Change-Id: I0ad51d7559bb5c066693e82150ed3c40a2f58102
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2163845
Commit-Queue: David Van Cleve <davidvc@chromium.org>
Reviewed-by: Charlie Harrison <csharrison@chromium.org>
Cr-Commit-Position: refs/heads/master@{#763822}
This commit is contained in:
David Van Cleve
2020-04-29 15:17:52 +00:00
committed by Commit Bot
parent 0c3e8c06e2
commit 6e339a26c7
14 changed files with 1219 additions and 122 deletions

@ -1130,6 +1130,7 @@ test("content_browsertests") {
"../test/browser_test_utils_browsertest.cc",
"../test/content_browser_test_test.cc",
"../test/top_frame_population_browsertest.cc",
"../test/trust_token_browsertest.cc",
"../test/trust_token_parameters_browsertest.cc",
"../test/url_loader_interceptor_test.cc",
"../test/webui_resource_browsertest.cc",

@ -0,0 +1,245 @@
// 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 <memory>
#include <string>
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/strings/string_piece.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
#include "net/base/escape.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
#include "services/network/trust_tokens/test/test_server_handler_registration.h"
#include "services/network/trust_tokens/test/trust_token_request_handler.h"
#include "services/network/trust_tokens/test/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
// Given a well-formed key commitment record JSON and an issuer origin, returns
// a serialized one-item dictionary mapping the commitment to the issuer.
std::string WrapKeyCommitmentForIssuer(const url::Origin& issuer,
base::StringPiece commitment) {
std::string ret;
JSONStringValueSerializer serializer(&ret);
CHECK_NE(issuer.Serialize(),
""); // guard against accidentally passing an opaque origin
base::Value to_serialize(base::Value::Type::DICTIONARY);
to_serialize.SetKey(issuer.Serialize(), *base::JSONReader::Read(commitment));
CHECK(serializer.Serialize(to_serialize));
return ret;
}
} // namespace
// TrustTokenBrowsertest is a fixture containing boilerplate for initializing an
// HTTPS test server and passing requests through to an embedded instance of
// network::test::TrustTokenRequestHandler, which contains the guts of the
// "server-side" token issuance and redemption logic as well as some consistency
// checks for subsequent signed requests.
class TrustTokenBrowsertest : public ContentBrowserTest {
public:
TrustTokenBrowsertest() {
features_.InitAndEnableFeature(network::features::kTrustTokens);
}
// Registers the following handlers:
// - default //content/test/data files;
// - a special "/issue" endpoint executing Trust Tokens issuance;
// - a special "/redeem" endpoint executing redemption; and
// - a special "/sign" endpoint that verifies that the received signed request
// data is correctly structured and that the provided Sec-Signature header's
// verification key was previously bound to a successful token redemption.
void SetUpOnMainThread() override {
server_.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("content/test/data")));
network::test::RegisterTrustTokenTestHandlers(&server_, &request_handler_);
ASSERT_TRUE(server_.Start());
}
protected:
base::test::ScopedFeatureList features_;
// TODO(davidvc): Extend this to support more than one key set.
network::test::TrustTokenRequestHandler request_handler_{/*num_keys=*/1};
net::EmbeddedTestServer server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, DISABLED_FetchEndToEnd) {
base::RunLoop run_loop;
GetNetworkService()->SetTrustTokenKeyCommitments(
WrapKeyCommitmentForIssuer(url::Origin::Create(server_.base_url()),
request_handler_.GetKeyCommitmentRecord()),
run_loop.QuitClosure());
run_loop.Run();
GURL start_url(server_.GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
std::string cmd = R"(
(async () => {
await fetch("/issue", {trustToken: {type: 'token-request'}});
await fetch("/redeem", {trustToken: {type: 'srr-token-redemption'}});
await fetch("/sign", {trustToken: {type: 'send-srr',
signRequestData: 'include',
issuer: $1}}); })(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_TRUE(
EvalJs(
shell(),
JsReplace(cmd, url::Origin::Create(server_.base_url()).Serialize()))
.error.empty());
EXPECT_EQ(request_handler_.LastVerificationError(), base::nullopt);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, DISABLED_XhrEndToEnd) {
base::RunLoop run_loop;
GetNetworkService()->SetTrustTokenKeyCommitments(
WrapKeyCommitmentForIssuer(url::Origin::Create(server_.base_url()),
request_handler_.GetKeyCommitmentRecord()),
run_loop.QuitClosure());
run_loop.Run();
GURL start_url(server_.GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
// If this isn't idiomatic JS, I don't know what is.
std::string cmd = R"(
(async () => {
let request = new XMLHttpRequest();
request.open('GET', '/issue');
request.setTrustToken({
type: 'token-request'
});
let promise = new Promise((res, rej) => {
request.onload = res; request.onerror = rej;
});
request.send();
await promise;
request = new XMLHttpRequest();
request.open('GET', '/redeem');
request.setTrustToken({
type: 'srr-token-redemption'
});
promise = new Promise((res, rej) => {
request.onload = res; request.onerror = rej;
});
request.send();
await promise;
request = new XMLHttpRequest();
request.open('GET', '/sign');
request.setTrustToken({
type: 'send-srr',
signRequestData: 'include',
issuer: $1
});
promise = new Promise((res, rej) => {
request.onload = res; request.onerror = rej;
});
request.send();
await promise;
})(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_TRUE(
EvalJs(
shell(),
JsReplace(cmd, url::Origin::Create(server_.base_url()).Serialize()))
.error.empty());
EXPECT_EQ(request_handler_.LastVerificationError(), base::nullopt);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, DISABLED_IframeEndToEnd) {
base::RunLoop run_loop;
GetNetworkService()->SetTrustTokenKeyCommitments(
WrapKeyCommitmentForIssuer(url::Origin::Create(server_.base_url()),
request_handler_.GetKeyCommitmentRecord()),
run_loop.QuitClosure());
run_loop.Run();
GURL start_url(server_.GetURL("/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
auto execute_op_via_iframe = [&](base::StringPiece path,
base::StringPiece trust_token) {
// It's important to set the trust token arguments before updating src, as
// the latter triggers a load.
EXPECT_TRUE(ExecJs(
shell(), JsReplace(
R"( const myFrame = document.getElementById("test_iframe");
myFrame.trustToken = $1;
myFrame.src = $2;)",
trust_token, path)));
TestNavigationObserver load_observer(shell()->web_contents());
load_observer.WaitForNavigationFinished();
};
execute_op_via_iframe("/issue", R"({"type": "token-request"})");
execute_op_via_iframe("/redeem", R"({"type": "srr-token-redemption"})");
execute_op_via_iframe(
"/sign",
JsReplace(
R"({"type": "send-srr", "signRequestData": "include", "issuer": $1})",
url::Origin::Create(server_.base_url()).Serialize()));
EXPECT_EQ(request_handler_.LastVerificationError(), base::nullopt);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
DISABLED_HasTrustTokenAfterIssuance) {
base::RunLoop run_loop;
GetNetworkService()->SetTrustTokenKeyCommitments(
WrapKeyCommitmentForIssuer(url::Origin::Create(server_.base_url()),
request_handler_.GetKeyCommitmentRecord()),
run_loop.QuitClosure());
run_loop.Run();
GURL start_url(server_.GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
std::string cmd =
JsReplace(R"(
(async () => {
await fetch("/issue", {trustToken: {type: 'token-request'}});
return await document.hasTrustToken($1);
})();)",
url::Origin::Create(server_.base_url()).Serialize());
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
//
// Note: EvalJs's EXPECT_EQ type-conversion magic only supports the
// "Yoda-style" EXPECT_EQ(expected, actual).
EXPECT_EQ(true, EvalJs(shell(), cmd));
}
} // namespace content

@ -91,6 +91,10 @@ source_set("test_support") {
sources = [
"test/signed_request_verification_util.cc",
"test/signed_request_verification_util.h",
"test/test_server_handler_registration.cc",
"test/test_server_handler_registration.h",
"test/trust_token_request_handler.cc",
"test/trust_token_request_handler.h",
"test/trust_token_test_util.cc",
"test/trust_token_test_util.h",
]
@ -99,6 +103,7 @@ source_set("test_support") {
":trust_tokens",
"//base",
"//base/test:test_support",
"//components/cbor",
"//net",
"//net:test_support",
"//net/traffic_annotation:test_support",

@ -4,13 +4,42 @@
#include "services/network/trust_tokens/test/signed_request_verification_util.h"
#include <vector>
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "net/http/http_request_headers.h"
#include "net/http/structured_headers.h"
#include "services/network/trust_tokens/ed25519_trust_token_request_signer.h"
#include "services/network/trust_tokens/trust_token_http_headers.h"
#include "services/network/trust_tokens/trust_token_parameterization.h"
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include "services/network/trust_tokens/trust_token_request_signing_helper.h"
#include "third_party/boringssl/src/include/openssl/curve25519.h"
namespace network {
namespace test {
namespace {
base::Optional<base::flat_map<std::string, net::structured_headers::Item>>
DeserializeSecSignatureHeader(base::StringPiece header) {
base::Optional<net::structured_headers::Dictionary> maybe_dictionary =
net::structured_headers::ParseDictionary(header);
if (!maybe_dictionary)
return base::nullopt;
base::flat_map<std::string, net::structured_headers::Item> ret;
for (const auto& kv : *maybe_dictionary) {
ret[kv.first] = kv.second.member.front().item;
}
return ret;
}
} // namespace
// From the design doc:
//
@ -23,7 +52,8 @@ namespace test {
// commitment registry.
SrrVerificationStatus VerifyTrustTokenSignedRedemptionRecord(
base::StringPiece record,
base::StringPiece verification_key) {
base::StringPiece verification_key,
std::string* srr_body_out) {
base::Optional<net::structured_headers::Dictionary> maybe_dictionary =
net::structured_headers::ParseDictionary(record);
if (!maybe_dictionary)
@ -67,8 +97,188 @@ SrrVerificationStatus VerifyTrustTokenSignedRedemptionRecord(
return SrrVerificationStatus::kSignatureVerificationError;
}
if (srr_body_out)
srr_body_out->swap(body);
return SrrVerificationStatus::kSuccess;
}
bool ReconstructSigningDataAndVerifySignature(
const GURL& destination,
const net::HttpRequestHeaders& headers,
base::OnceCallback<bool(base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key)>
verifier,
std::string* error_out,
std::string* verification_key_out) {
// Make it possible to set the error without needing to check for
// |error_out|'s presence.
std::string dummy_error;
if (!error_out)
error_out = &dummy_error;
std::string signature_header;
if (!headers.GetHeader(kTrustTokensRequestHeaderSecSignature,
&signature_header)) {
*error_out = "Missing Sec-Signature header";
return false;
}
base::Optional<base::flat_map<std::string, net::structured_headers::Item>>
map = DeserializeSecSignatureHeader(signature_header);
if (!map) {
*error_out = "Malformed Sec-Signature header";
return false;
}
auto it = map->find("sig");
if (it == map->end()) {
*error_out = "Missing 'sig' element in the Sec-Signature header";
return false;
}
if (!it->second.is_byte_sequence()) {
*error_out = "'sig' element in Sec-Signature header is type-unsafe";
return false;
}
// GetString is also the method one uses to get a byte sequence.
base::StringPiece signature = it->second.GetString();
it = map->find("public-key");
if (it == map->end()) {
*error_out = "Missing 'public-key' element in the Sec-Signature header";
return false;
}
if (!it->second.is_byte_sequence()) {
*error_out = "'public-key' element in Sec-Signature header is type-unsafe";
return false;
}
base::StringPiece public_key = it->second.GetString();
it = map->find("sign-request-data");
if (it == map->end()) {
*error_out =
"Missing 'sign-request-data' element in the Sec-Signature header";
return false;
}
if (!it->second.is_token()) {
*error_out =
"'sign-request-data' element in Sec-Signature header is type-unsafe";
return false;
}
// GetString is also the method one uses to get a token.
base::StringPiece sign_request_data = it->second.GetString();
if (sign_request_data != "headers-only" && sign_request_data != "include") {
*error_out =
"'sign-request-data' element in Sec-Signature header had a bad value";
return false;
}
base::Optional<std::vector<uint8_t>> written_reconstructed_cbor =
TrustTokenRequestCanonicalizer().Canonicalize(
destination, headers, public_key,
sign_request_data == "include"
? mojom::TrustTokenSignRequestData::kInclude
: mojom::TrustTokenSignRequestData::kHeadersOnly);
if (!written_reconstructed_cbor) {
*error_out = "Error reconstructing canonical request data";
return false;
}
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());
if (!verifier) {
verifier =
base::BindOnce(&Ed25519TrustTokenRequestSigner::Verify,
std::make_unique<Ed25519TrustTokenRequestSigner>());
}
if (!std::move(verifier).Run(base::make_span(reconstructed_signing_data),
base::as_bytes(base::make_span(signature)),
base::as_bytes(base::make_span(public_key)))) {
*error_out = "Error verifying signature";
return false;
}
if (verification_key_out)
*verification_key_out = std::string(public_key);
return true;
}
bool ConfirmSrrBodyIntegrity(base::StringPiece srr_body,
std::string* error_out) {
std::string dummy_error;
std::string& error = error_out ? *error_out : dummy_error;
base::Optional<cbor::Value> maybe_map =
cbor::Reader::Read(base::as_bytes(base::make_span(srr_body)));
if (!maybe_map) {
error = "SRR body wasn't valid CBOR";
return false;
}
if (!maybe_map->is_map()) {
error = "SRR body wasn't a CBOR map";
return false;
}
const cbor::Value::MapValue& map = maybe_map->GetMap();
if (map.size() != 3) {
error = "SRR body is a map of unexpected size";
return false;
}
// check_field is a convenience function automating some of the work of
// verifying that the CBOR map has the desired structure. It takes a (possibly
// two-level compound) field name and a type-checker cbor::Value member
// function pointer (e.g. &cbor::Value::is_string) and verifies that the field
// exists and satisfies the given type predicate.
auto check_field = [&](base::StringPiece key, auto type_checker) -> bool {
const cbor::Value::MapValue* submap = &map;
if (base::Contains(key, ".")) {
auto keys = base::SplitStringPiece(key, ".", base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
cbor::Value submap_key(keys[0], cbor::Value::Type::STRING);
if (!map.contains(submap_key) || !map.at(submap_key).is_map()) {
return false;
}
submap = &map.at(submap_key).GetMap();
key = keys[1];
}
cbor::Value cbor_key(key, cbor::Value::Type::STRING);
return submap->contains(cbor_key) && (submap->at(cbor_key).*type_checker)();
};
for (const auto& tup : {
std::make_tuple("client-data", &cbor::Value::is_map),
std::make_tuple("client-data.key-hash", &cbor::Value::is_bytestring),
std::make_tuple("client-data.redemption-timestamp",
&cbor::Value::is_unsigned),
std::make_tuple("client-data.redeeming-origin",
&cbor::Value::is_string),
std::make_tuple("metadata", &cbor::Value::is_map),
std::make_tuple("metadata.public", &cbor::Value::is_unsigned),
std::make_tuple("metadata.private", &cbor::Value::is_unsigned),
std::make_tuple("expiry-timestamp", &cbor::Value::is_unsigned),
}) {
if (!check_field(std::get<0>(tup), std::get<1>(tup))) {
error = "Missing or type-unsafe " + std::string(std::get<0>(tup));
return false;
}
}
return true;
}
} // namespace test
} // namespace network

@ -5,7 +5,14 @@
#ifndef SERVICES_NETWORK_TRUST_TOKENS_TEST_SIGNED_REQUEST_VERIFICATION_UTIL_H_
#define SERVICES_NETWORK_TRUST_TOKENS_TEST_SIGNED_REQUEST_VERIFICATION_UTIL_H_
#include "base/strings/string_piece_forward.h"
#include <string>
#include "base/callback.h"
#include "base/containers/span.h"
#include "base/optional.h"
#include "base/strings/string_piece.h"
#include "net/http/http_request_headers.h"
#include "url/gurl.h"
namespace network {
namespace test {
@ -14,6 +21,9 @@ namespace test {
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#bookmark=id.omg78vbnmjid,
// extracts the signature and body, and uses the given verification key to
// verify the signature.
//
// On success, if |srr_body_out| is non-null, sets |srr_body_out| to the
// obtained SRR body.
enum class SrrVerificationStatus {
kParseError,
kSignatureVerificationError,
@ -21,7 +31,36 @@ enum class SrrVerificationStatus {
};
SrrVerificationStatus VerifyTrustTokenSignedRedemptionRecord(
base::StringPiece record,
base::StringPiece verification_key);
base::StringPiece verification_key,
std::string* srr_body_out = nullptr);
// Reconstructs a request's canonical request data, extracts the signature from
// its Sec-Signature header, checks that the Sec-Signature header's contained
// signature verifies.
//
// Optionally:
// - if |verification_key_out| is non-null, on success, returns the verification
// key so that the caller can verify further state concerning the key (like
// confirming that the key was bound to a previous redemption).
// - if |error_out| is non-null, on failure, sets it to a human-readable
// description of the reason the verification failed.
// - if |verifier| is non-null, uses the given verifier to verify the
// signature instead of Ed25519
bool ReconstructSigningDataAndVerifySignature(
const GURL& destination,
const net::HttpRequestHeaders& headers,
base::OnceCallback<bool(base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key)>
verifier = {}, // defaults to Ed25519
std::string* error_out = nullptr,
std::string* verification_key_out = nullptr);
// Returns true if |srr_body| a valid CBOR encoding of an "SRR body" struct, as
// defined in the design doc. Otherwise, returns false and, if |error_out| is
// non-null, sets |error_out| to a helpful error message.
bool ConfirmSrrBodyIntegrity(base::StringPiece srr_body,
std::string* error_out = nullptr);
} // namespace test
} // namespace network

@ -0,0 +1,115 @@
// 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/test/test_server_handler_registration.h"
#include <memory>
#include "base/base64.h"
#include "base/check.h"
#include "base/logging.h"
#include "base/optional.h"
#include "base/strings/string_piece.h"
#include "base/test/bind_test_util.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/trust_tokens/test/trust_token_request_handler.h"
namespace network {
namespace test {
namespace {
const char kIssuanceRelativePath[] = "/issue";
const char kRedemptionRelativePath[] = "/redeem";
const char kSignedRequestVerificationRelativePath[] = "/sign";
std::unique_ptr<net::test_server::HttpResponse>
MakeTrustTokenFailureResponse() {
// No need to report a failure HTTP code here: returning a vanilla OK should
// fail the Trust Tokens operation client-side.
return std::make_unique<net::test_server::BasicHttpResponse>();
}
// Constructs and returns an HTTP response bearing the given base64-encoded
// Trust Tokens issuance or redemption protocol response message.
std::unique_ptr<net::test_server::HttpResponse> MakeTrustTokenResponse(
base::StringPiece contents) {
CHECK([&]() {
std::string temp;
return base::Base64Decode(contents, &temp);
}());
auto ret = std::make_unique<net::test_server::BasicHttpResponse>();
ret->AddCustomHeader("Sec-Trust-Token", std::string(contents));
return ret;
}
} // namespace
void RegisterTrustTokenTestHandlers(net::EmbeddedTestServer* test_server,
TrustTokenRequestHandler* handler) {
test_server->RegisterRequestHandler(base::BindLambdaForTesting(
[handler](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
// Decline to handle the request if it isn't destined for this
// endpoint.
if (request.relative_url != kIssuanceRelativePath)
return nullptr;
if (!base::Contains(request.headers, "Sec-Trust-Token"))
return MakeTrustTokenFailureResponse();
base::Optional<std::string> operation_result =
handler->Issue(request.headers.at("Sec-Trust-Token"));
if (!operation_result)
return MakeTrustTokenFailureResponse();
return MakeTrustTokenResponse(*operation_result);
}));
test_server->RegisterRequestHandler(base::BindLambdaForTesting(
[handler](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url != kRedemptionRelativePath)
return nullptr;
if (!base::Contains(request.headers, "Sec-Trust-Token"))
return MakeTrustTokenFailureResponse();
base::Optional<std::string> operation_result =
handler->Redeem(request.headers.at("Sec-Trust-Token"));
if (!operation_result)
return MakeTrustTokenFailureResponse();
return MakeTrustTokenResponse(*operation_result);
}));
test_server->RegisterRequestHandler(base::BindLambdaForTesting(
[handler](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url != kSignedRequestVerificationRelativePath)
return nullptr;
std::string error;
net::HttpRequestHeaders headers;
for (const auto& name_and_value : request.headers)
headers.SetHeader(name_and_value.first, name_and_value.second);
bool success =
handler->VerifySignedRequest(request.GetURL(), headers, &error);
LOG_IF(ERROR, !success) << error;
// Unlike issuance and redemption, there's no special state to return
// on success for signing.
return std::make_unique<net::test_server::BasicHttpResponse>();
}));
}
} // namespace test
} // namespace network

@ -0,0 +1,37 @@
// 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_TEST_TEST_SERVER_HANDLER_REGISTRATION_H_
#define SERVICES_NETWORK_TRUST_TOKENS_TEST_TEST_SERVER_HANDLER_REGISTRATION_H_
#include "net/test/embedded_test_server/embedded_test_server.h"
namespace network {
namespace test {
class TrustTokenRequestHandler;
// Wires |handler|'s issuance, redemption, and signed request verification
// methods up to |test_server| at standard endpoint locations ("/issue",
// "/redeem", and "/sign", relative to |test_server|'s base URL). This lets
// browser test files relying on Trust Tokens server logic share this
// initialization boilerplate.
//
// Usage:
// - This must be called before starting |test_server|.
// - Feel free to call it multiple times with distinct values of |base_url| and
// |handler|, in order to emulate multiple Trust Tokens issuer servers.
//
// Lifetime: because this registers callbacks on |test_server| that call into
// |handler|, |handler| needs to live long enough to service all requests (e.g.
// in a test) that will arrive at |test_server|.
void RegisterTrustTokenTestHandlers(net::EmbeddedTestServer* test_server,
TrustTokenRequestHandler* handler);
} // namespace test
} // namespace network
#endif // SERVICES_NETWORK_TRUST_TOKENS_TEST_TEST_SERVER_HANDLER_REGISTRATION_H_

@ -0,0 +1,396 @@
// 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/test/trust_token_request_handler.h"
#include "base/base64.h"
#include "base/check.h"
#include "base/containers/span.h"
#include "base/json/json_string_value_serializer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/test/bind_test_util.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "crypto/sha2.h"
#include "net/http/http_request_headers.h"
#include "net/http/structured_headers.h"
#include "services/network/trust_tokens/ed25519_trust_token_request_signer.h"
#include "services/network/trust_tokens/scoped_boringssl_bytes.h"
#include "services/network/trust_tokens/test/signed_request_verification_util.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 "third_party/boringssl/src/include/openssl/curve25519.h"
#include "third_party/boringssl/src/include/openssl/evp.h"
#include "third_party/boringssl/src/include/openssl/trust_token.h"
namespace network {
namespace test {
namespace {
struct IssuanceKeyPair {
// Token signing and verification keys:
std::vector<uint8_t> signing;
std::vector<uint8_t> verification;
// Default to a very long expiry time, but allow this to be overridden when
// specific tests want to do so.
base::Time expiry = base::Time::Max();
};
IssuanceKeyPair GenerateIssuanceKeyPair(int id) {
IssuanceKeyPair keys;
keys.signing.resize(TRUST_TOKEN_MAX_PRIVATE_KEY_SIZE);
keys.verification.resize(TRUST_TOKEN_MAX_PUBLIC_KEY_SIZE);
size_t signing_key_len, verification_key_len;
TRUST_TOKEN_generate_key(keys.signing.data(), &signing_key_len,
keys.signing.size(), keys.verification.data(),
&verification_key_len, keys.verification.size(), id);
keys.signing.resize(signing_key_len);
keys.verification.resize(verification_key_len);
return keys;
}
// This convenience helper prevents forgetting whether the inequality is weak or
// strict.
bool HasKeyPairExpired(const IssuanceKeyPair& p) {
return p.expiry <= base::Time::Now();
}
} // namespace
struct TrustTokenRequestHandler::Rep {
// Issue at most this many tokens per issuance.
int batch_size;
// Signed redemption record (SRR) signing and verification keys:
std::vector<uint8_t> srr_signing;
std::vector<uint8_t> srr_verification;
std::vector<IssuanceKeyPair> issuance_keys;
// Creates a BoringSSL token issuer context suitable for issuance or
// redemption, using only the unexpired key pairs from |issuance_keys|.
bssl::UniquePtr<TRUST_TOKEN_ISSUER> CreateIssuerContextFromUnexpiredKeys()
const;
// Verifies the redemption request's client datal is a valid CBOR
// encoding of a structure matching the format specified in the design doc.
//
// If this is the case, returns true and stores the contained
// browser-generated public key hash in |hashes_of_redemption_bound_key_pairs|
// for comparison against subsequent signed requests. Otherwise, returns false
// and, if |error| is not null, sets |error| to a human-readable explanation
// of why the input was not valid.
bool ConfirmClientDataIntegrityAndStoreKeyHash(
base::span<const uint8_t> client_data,
std::string* error = nullptr);
// Maintains all key pairs bound to successful redemptions.
// TODO(davidvc): This can be expanded to map per top-frame origin for
// tests across multiple origins.
// TODO(davidvc): We can also expand the verification logic here to confirm
// the private metadata field decodes appropriately.
std::set<std::string> hashes_of_redemption_bound_key_pairs;
// Contains a human-readable string explaining why the most recent signed
// request verification to fail failed, or nullopt if no verification has
// failed.
base::Optional<std::string> last_verification_error;
};
bssl::UniquePtr<TRUST_TOKEN_ISSUER>
TrustTokenRequestHandler::Rep::CreateIssuerContextFromUnexpiredKeys() const {
bssl::UniquePtr<TRUST_TOKEN_ISSUER> ret(TRUST_TOKEN_ISSUER_new(batch_size));
if (!ret)
return nullptr;
for (const IssuanceKeyPair& key_pair : issuance_keys) {
if (HasKeyPairExpired(key_pair))
continue;
if (!TRUST_TOKEN_ISSUER_add_key(ret.get(), key_pair.signing.data(),
key_pair.signing.size())) {
return nullptr;
}
}
// Copying the comment from evp.h:
// The [Ed25519] RFC 8032 private key format is the 32-byte prefix of
// |ED25519_sign|'s 64-byte private key.
bssl::UniquePtr<EVP_PKEY> issuer_srr_key(EVP_PKEY_new_raw_private_key(
EVP_PKEY_ED25519, /*unused=*/nullptr, srr_signing.data(),
/*len=*/32));
if (!issuer_srr_key)
return nullptr;
if (!TRUST_TOKEN_ISSUER_set_srr_key(ret.get(), issuer_srr_key.get()))
return nullptr;
return ret;
}
bool TrustTokenRequestHandler::Rep::ConfirmClientDataIntegrityAndStoreKeyHash(
base::span<const uint8_t> client_data,
std::string* error) {
std::string dummy_error;
if (!error)
error = &dummy_error;
base::Optional<cbor::Value> maybe_value = cbor::Reader::Read(client_data);
if (!maybe_value) {
*error = "client data was invalid CBOR";
return false;
}
if (!maybe_value->is_map()) {
*error = "client data was valid CBOR but not a map";
return false;
}
const cbor::Value::MapValue& map = maybe_value->GetMap();
if (map.size() != 3u) {
*error = "Unexpected number of fields in client data";
return false;
}
auto it = map.find(cbor::Value("key-hash", cbor::Value::Type::STRING));
if (it == map.end()) {
*error = "client data was missing a 'key-hash' field";
return false;
}
if (!it->second.is_bytestring()) {
*error = "client data 'key-hash' field was not a bytestring";
return false;
}
base::StringPiece key_hash = it->second.GetBytestringAsString();
// Even though we don't yet examine the remaining fields in detail, perform
// some structural integrity checks to make sure all's generally well:
cbor::Value redeeming_origin_key("redeeming-origin",
cbor::Value::Type::STRING);
if (!map.contains(redeeming_origin_key) ||
!map.at(redeeming_origin_key).is_string()) {
*error = "Missing or type-unsafe redeeming-origin field in client data";
return false;
}
cbor::Value redemption_timestamp_key("redemption-timestamp",
cbor::Value::Type::STRING);
if (!map.contains(redemption_timestamp_key) ||
!map.at(redemption_timestamp_key).is_unsigned()) {
*error = "Missing or type-unsafe redemption-timestamp field in client data";
return false;
}
hashes_of_redemption_bound_key_pairs.insert(std::string(key_hash));
return true;
}
TrustTokenRequestHandler::TrustTokenRequestHandler(int num_keys, int batch_size)
: rep_(std::make_unique<Rep>()) {
rep_->batch_size = batch_size;
rep_->srr_signing.resize(ED25519_PRIVATE_KEY_LEN);
rep_->srr_verification.resize(ED25519_PUBLIC_KEY_LEN);
ED25519_keypair(rep_->srr_verification.data(), rep_->srr_signing.data());
for (int i = 0; i < num_keys; ++i) {
rep_->issuance_keys.push_back(GenerateIssuanceKeyPair(i));
}
}
TrustTokenRequestHandler::~TrustTokenRequestHandler() = default;
std::string TrustTokenRequestHandler::GetKeyCommitmentRecord() const {
base::AutoLock lock(mutex_);
std::string ret;
JSONStringValueSerializer serializer(&ret);
base::Value value(base::Value::Type::DICTIONARY);
value.SetStringKey(
"srrkey", base::Base64Encode(base::make_span(rep_->srr_verification)));
value.SetIntKey("batchsize", rep_->batch_size);
for (size_t i = 0; i < rep_->issuance_keys.size(); ++i) {
value.SetStringPath(base::NumberToString(i) + ".Y",
base::Base64Encode(base::make_span(
rep_->issuance_keys[i].verification)));
value.SetStringPath(base::NumberToString(i) + ".expiry",
base::NumberToString((rep_->issuance_keys[i].expiry -
base::Time::UnixEpoch())
.InMicroseconds()));
}
// It's OK to be a bit crashy in exceptional failure cases because it
// indicates a serious coding error in this test-only code; we'd like to find
// this out sooner rather than later.
CHECK(serializer.Serialize(value));
return ret;
}
base::Optional<std::string> TrustTokenRequestHandler::Issue(
base::StringPiece issuance_request) {
base::AutoLock lock(mutex_);
bssl::UniquePtr<TRUST_TOKEN_ISSUER> issuer_ctx =
rep_->CreateIssuerContextFromUnexpiredKeys();
std::string decoded_issuance_request;
if (!base::Base64Decode(issuance_request, &decoded_issuance_request))
return base::nullopt;
// TODO(davidvc): Perhaps make this configurable? Not a high priority, though.
constexpr uint8_t kPrivateMetadata = 0;
ScopedBoringsslBytes decoded_issuance_response;
uint8_t num_tokens_issued = 0;
bool ok = false;
for (size_t i = 0; i < rep_->issuance_keys.size(); ++i) {
if (HasKeyPairExpired(rep_->issuance_keys[i]))
continue;
if (TRUST_TOKEN_ISSUER_issue(
issuer_ctx.get(), decoded_issuance_response.mutable_ptr(),
decoded_issuance_response.mutable_len(), &num_tokens_issued,
base::as_bytes(base::make_span(decoded_issuance_request)).data(),
decoded_issuance_request.size(),
/*public_metadata=*/static_cast<uint32_t>(i), kPrivateMetadata,
rep_->batch_size)) {
ok = true;
break;
}
}
if (!ok)
return base::nullopt;
return base::Base64Encode(decoded_issuance_response.as_span());
}
constexpr base::TimeDelta TrustTokenRequestHandler::kSrrLifetime =
base::TimeDelta::FromDays(100);
base::Optional<std::string> TrustTokenRequestHandler::Redeem(
base::StringPiece redemption_request) {
base::AutoLock lock(mutex_);
bssl::UniquePtr<TRUST_TOKEN_ISSUER> issuer_ctx =
rep_->CreateIssuerContextFromUnexpiredKeys();
std::string decoded_redemption_request;
if (!base::Base64Decode(redemption_request, &decoded_redemption_request))
return base::nullopt;
ScopedBoringsslBytes decoded_redemption_response;
TRUST_TOKEN* redeemed_token;
ScopedBoringsslBytes redeemed_client_data;
uint64_t received_redemption_timestamp;
if (!TRUST_TOKEN_ISSUER_redeem(
issuer_ctx.get(), decoded_redemption_response.mutable_ptr(),
decoded_redemption_response.mutable_len(), &redeemed_token,
redeemed_client_data.mutable_ptr(),
redeemed_client_data.mutable_len(), &received_redemption_timestamp,
base::as_bytes(base::make_span(decoded_redemption_request)).data(),
decoded_redemption_request.size(), kSrrLifetime.InSeconds())) {
return base::nullopt;
}
rep_->ConfirmClientDataIntegrityAndStoreKeyHash(
redeemed_client_data.as_span());
// Put the issuer-receied token in a smart pointer so it will get deleted on
// leaving scope.
bssl::UniquePtr<TRUST_TOKEN> redeemed_token_scoper(redeemed_token);
return base::Base64Encode(decoded_redemption_response.as_span());
}
bool TrustTokenRequestHandler::VerifySignedRequest(
const GURL& destination,
const net::HttpRequestHeaders& headers,
std::string* error_out) {
std::string dummy_error;
if (!error_out)
error_out = &dummy_error;
// In order to avoid deadlock, this must be before VerifySignedRequest's
// |lock|'s definition. This is so that |set_last_error_on_return|'s
// destructor (and associated callback) are run after the function-scoped
// AutoLock is destroyed (and releases the mutex).
base::ScopedClosureRunner set_last_error_on_return(
base::BindLambdaForTesting([error_out, this]() {
base::AutoLock lock(mutex_);
if (!error_out->empty())
rep_->last_verification_error = *error_out;
}));
base::AutoLock lock(mutex_);
std::string verification_key;
if (!ReconstructSigningDataAndVerifySignature(destination, headers,
/*verifier=*/{}, error_out,
&verification_key)) {
return false;
}
if (!base::Contains(rep_->hashes_of_redemption_bound_key_pairs,
crypto::SHA256HashString(verification_key))) {
if (error_out) {
*error_out =
"Got a request signed with a verification key whose hash was not "
"previously bound to a redemption request.";
}
return false;
}
std::string sec_signed_redemption_record_header;
if (!headers.GetHeader(kTrustTokensRequestHeaderSecSignedRedemptionRecord,
&sec_signed_redemption_record_header)) {
if (error_out)
*error_out = "Request missing its SRR header";
return false;
}
std::string srr_body;
switch (VerifyTrustTokenSignedRedemptionRecord(
sec_signed_redemption_record_header,
base::StringPiece(
reinterpret_cast<const char*>(rep_->srr_verification.data()),
rep_->srr_verification.size()),
&srr_body)) {
case SrrVerificationStatus::kSignatureVerificationError:
if (error_out) {
*error_out = "Request SRR signature failed to verify";
}
return false;
case SrrVerificationStatus::kParseError:
if (error_out) {
*error_out = "Request SRR header failed to parse";
}
return false;
case SrrVerificationStatus::kSuccess:
break;
}
if (!ConfirmSrrBodyIntegrity(srr_body, error_out))
return false; // On failure, |ConfirmSrrBodyIntegrity| has set the error.
return true;
}
base::Optional<std::string> TrustTokenRequestHandler::LastVerificationError() {
base::AutoLock lock(mutex_);
return rep_->last_verification_error;
}
} // namespace test
} // namespace network

@ -0,0 +1,98 @@
// 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_TEST_TRUST_TOKEN_REQUEST_HANDLER_H_
#define SERVICES_NETWORK_TRUST_TOKENS_TEST_TRUST_TOKEN_REQUEST_HANDLER_H_
#include <string>
#include "base/optional.h"
#include "base/strings/string_piece.h"
#include "base/synchronization/lock.h"
#include "base/time/time.h"
#include "net/http/http_request_headers.h"
#include "url/gurl.h"
namespace network {
namespace test {
// TrustTokenRequestHandler encapsulates server-side Trust Tokens issuance and
// redemption logic and implements some integrity and correctness checks for
// requests subsequently signed with keys bound to token redemptions.
//
// It's thread-safe so that the methods can be called by test code directly and
// by net::EmbeddedTestServer handlers.
class TrustTokenRequestHandler {
public:
// Initializes server-side Trust Tokens logic by generating |num_keys| many
// issuance key pairs and a Signed Redemption Record (SRR)
// signing-and-verification key pair.
//
// If |batch_size| is provided, the issuer will be willing to issue at most
// that many tokens per issuance operation.
static constexpr int kDefaultIssuerBatchSize = 10;
explicit TrustTokenRequestHandler(int num_keys,
int batch_size = kDefaultIssuerBatchSize);
~TrustTokenRequestHandler();
// TODO(davidvc): Provide a way to specify when keys expire.
// Returns a key commitment record suitable for inserting into a {issuer:
// commitment} dictionary passed to the network service via
// NetworkService::SetTrustTokenKeyCommitments. This comprises |num_keys|
// token verification keys and a batch size of |batch_size| (or none if
// |batch_size| is nullopt).
std::string GetKeyCommitmentRecord() const;
// Given a base64-encoded issuance request, processes the
// request and returns either nullopt (on error) or a base64-encoded response.
base::Optional<std::string> Issue(base::StringPiece issuance_request);
// Given a base64-encoded redemption request, processes the
// request and returns either nullopt (on error) or a base64-encoded response.
// On success, the response's signed redemption record will have a lifetime of
// |kSrrLifetime|. We use a ludicrously long lifetime because there's no way
// to mock time in browser tests, and we don't want the SRR expiring
// unexpectedly.
//
// TODO(davidvc): This needs to be expanded to be able to provide
// SRRs that have already expired. (This seems like the easiest way of
// exercising client-side SRR expiry logic in end-to-end tests, because
// there's no way to fast-forward a clock past an expiry time.)
static const base::TimeDelta kSrrLifetime;
base::Optional<std::string> Redeem(base::StringPiece redemption_request);
// Inspects |request| and returns true exactly when:
// - the request bears a well-formed Sec-Signature header with a valid
// signature over the request's canonical signing data;
// - the signature's public key's hash was bound to a previous redemption
// request; and
// - the request contains a well-formed signed redemption record whose
// signature verifies against the issuer's published SRR key.
//
// Otherwise, returns false and, if |error_out| is not null, sets |error_out|
// to a helpful error message.
//
// TODO(davidvc): This currently doesn't support signRequestData: 'omit'.
bool VerifySignedRequest(const GURL& destination,
const net::HttpRequestHeaders& headers,
std::string* error_out = nullptr);
// Returns the verification error from the most recent unsuccessful
// VerifySignedRequest call, if any.
base::Optional<std::string> LastVerificationError();
private:
struct Rep; // Contains state internal to this class's implementation.
// Guards this class's internal state.
mutable base::Lock mutex_;
std::unique_ptr<Rep> rep_ GUARDED_BY(mutex_);
};
} // namespace test
} // namespace network
#endif // SERVICES_NETWORK_TRUST_TOKENS_TEST_TRUST_TOKEN_REQUEST_HANDLER_H_

@ -17,7 +17,8 @@ namespace network {
base::Optional<std::vector<uint8_t>>
TrustTokenRequestCanonicalizer::Canonicalize(
net::URLRequest* request,
const GURL& destination,
const net::HttpRequestHeaders& headers,
base::StringPiece public_key,
mojom::TrustTokenSignRequestData sign_request_data) const {
DCHECK(sign_request_data == mojom::TrustTokenSignRequestData::kInclude ||
@ -38,7 +39,7 @@ TrustTokenRequestCanonicalizer::Canonicalize(
if (sign_request_data == mojom::TrustTokenSignRequestData::kInclude) {
canonicalized_request.emplace(
TrustTokenRequestSigningHelper::kCanonicalizedRequestDataUrlKey,
request->url().spec());
destination.spec());
}
// 2. If sign-request-data is 'include' or 'headers-only', for each value
@ -48,8 +49,8 @@ TrustTokenRequestCanonicalizer::Canonicalize(
// - 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)) {
if (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)
@ -59,9 +60,7 @@ TrustTokenRequestCanonicalizer::Canonicalize(
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)) {
if (headers.GetHeader(header_name, &header_value)) {
canonicalized_request.emplace(base::ToLowerASCII(header_name),
header_value);
}

@ -9,6 +9,7 @@
#include "base/optional.h"
#include "base/strings/string_piece_forward.h"
#include "net/http/http_request_headers.h"
#include "net/url_request/url_request.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
@ -36,23 +37,22 @@ class TrustTokenRequestCanonicalizer {
TrustTokenRequestCanonicalizer& operator=(
const TrustTokenRequestCanonicalizer&) = delete;
// Attempts to canonicalize |request| according to the pseudocode in the
// Attempts to canonicalize a 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
// by inspecting the 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.
// |destination| and |headers| together represent an outgoing request.
//
// Returns nullopt if |request|'s Signed-Headers header is malformed (i.e.,
// Returns nullopt if the 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,
const GURL& destination,
const net::HttpRequestHeaders& headers,
base::StringPiece public_key,
mojom::TrustTokenSignRequestData sign_request_data) const;
};

@ -38,17 +38,19 @@ TEST_F(TrustTokenRequestCanonicalizerTest, Empty) {
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)));
EXPECT_EQ(
canonicalizer.Canonicalize(
request->url(), request->extra_request_headers(),
/*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),
canonicalizer.Canonicalize(
request->url(), request->extra_request_headers(),
/*public_key=*/"key", mojom::TrustTokenSignRequestData::kInclude),
cbor::Writer::Write(cbor::Value(expected_cbor)));
}
@ -69,17 +71,19 @@ TEST_F(TrustTokenRequestCanonicalizerTest, Simple) {
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)));
EXPECT_EQ(
canonicalizer.Canonicalize(
request->url(), request->extra_request_headers(),
/*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),
canonicalizer.Canonicalize(
request->url(), request->extra_request_headers(),
/*public_key=*/"key", mojom::TrustTokenSignRequestData::kInclude),
cbor::Writer::Write(cbor::Value(expected_cbor)));
}
@ -122,17 +126,19 @@ TEST_F(TrustTokenRequestCanonicalizerTest, WithSignedHeaders) {
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)));
EXPECT_EQ(
canonicalizer.Canonicalize(
request->url(), request->extra_request_headers(),
/*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),
canonicalizer.Canonicalize(
request->url(), request->extra_request_headers(),
/*public_key=*/"key", mojom::TrustTokenSignRequestData::kInclude),
cbor::Writer::Write(cbor::Value(expected_cbor)));
}
@ -149,8 +155,8 @@ TEST_F(TrustTokenRequestCanonicalizerTest, RejectsMalformedSignedHeaders) {
"\"", /*overwrite=*/true);
EXPECT_FALSE(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"key",
mojom::TrustTokenSignRequestData::kHeadersOnly));
request->url(), request->extra_request_headers(),
/*public_key=*/"key", mojom::TrustTokenSignRequestData::kHeadersOnly));
}
// Canonicalizing a request with an empty key should fail.
@ -161,7 +167,7 @@ TEST_F(TrustTokenRequestCanonicalizerTest, RejectsEmptyKey) {
MakeURLRequest("https://issuer.com/");
EXPECT_FALSE(canonicalizer.Canonicalize(
request.get(), /*public_key=*/"",
mojom::TrustTokenSignRequestData::kHeadersOnly));
request->url(), request->extra_request_headers(),
/*public_key=*/"", mojom::TrustTokenSignRequestData::kHeadersOnly));
}
} // namespace network

@ -347,8 +347,9 @@ TrustTokenRequestSigningHelper::GetSignature(
// 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);
canonicalizer_->Canonicalize(
request->url(), request->extra_request_headers(),
redemption_record.public_key(), params_.sign_request_data);
if (!maybe_request_in_cbor)
return base::nullopt;

@ -28,6 +28,7 @@
#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/test/signed_request_verification_util.h"
#include "services/network/trust_tokens/test/trust_token_test_util.h"
#include "services/network/trust_tokens/trust_token_request_canonicalizer.h"
#include "services/network/trust_tokens/trust_token_store.h"
@ -100,76 +101,24 @@ class FailingSigner : public TrustTokenRequestSigningHelper::Signer {
}
};
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));
net::URLRequest* request) {
std::string error;
bool success = test::ReconstructSigningDataAndVerifySignature(
request->url(), request->extra_request_headers(),
base::BindOnce([](base::span<const uint8_t> data,
base::span<const uint8_t> signature,
base::span<const uint8_t> verification_key) {
return Signer().Verify(data, signature, verification_key);
}),
&error);
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))));
ASSERT_TRUE(success) << error;
}
// Verifies that |request| has a Sec-Signature header with a "sig" field and
@ -180,15 +129,15 @@ void AssertHasSignatureAndExtract(const net::URLRequest& request,
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();
base::Optional<net::structured_headers::Dictionary> maybe_dictionary =
net::structured_headers::ParseDictionary(signature_header);
ASSERT_TRUE(maybe_dictionary);
ASSERT_TRUE(maybe_dictionary->contains("sig"));
net::structured_headers::Item& sig_item =
maybe_dictionary->at("sig").member.front().item;
ASSERT_TRUE(sig_item.is_byte_sequence());
*signature_out = sig_item.GetString();
}
// Assert that the given signing data is a concatenation of the domain separator
@ -459,7 +408,6 @@ TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyMinimal) {
// 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));
@ -473,7 +421,7 @@ TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyMinimal) {
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
my_request.get()));
}
// Test a round-trip sign-and-verify with signed headers.
@ -492,7 +440,6 @@ TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyWithHeaders) {
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));
@ -505,7 +452,7 @@ TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyWithHeaders) {
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
my_request.get()));
}
// Test a round-trip sign-and-verify with signed headers when adding a timestamp
@ -526,7 +473,6 @@ TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyTimestampHeader) {
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));
@ -540,7 +486,7 @@ TEST_F(TrustTokenRequestSigningHelperTest, SignAndVerifyTimestampHeader) {
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
my_request.get()));
std::string signature_string;
ASSERT_NO_FATAL_FAILURE(
@ -569,7 +515,6 @@ TEST_F(TrustTokenRequestSigningHelperTest,
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));
@ -587,7 +532,7 @@ TEST_F(TrustTokenRequestSigningHelperTest,
ASSERT_NO_FATAL_FAILURE(
ReconstructSigningDataAndAssertSignatureVerifies<IdentitySigner>(
my_request.get(), *raw_canonicalizer));
my_request.get()));
// Because we're using an IdentitySigner, |signature_string| will have value
// equal to the base64-encoded request signing data.