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:

committed by
Commit Bot

parent
0c3e8c06e2
commit
6e339a26c7
content/test
services/network/trust_tokens
BUILD.gn
test
signed_request_verification_util.ccsigned_request_verification_util.htest_server_handler_registration.cctest_server_handler_registration.htrust_token_request_handler.cctrust_token_request_handler.h
trust_token_request_canonicalizer.cctrust_token_request_canonicalizer.htrust_token_request_canonicalizer_unittest.cctrust_token_request_signing_helper.cctrust_token_request_signing_helper_unittest.cc@ -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",
|
||||
|
245
content/test/trust_token_browsertest.cc
Normal file
245
content/test/trust_token_browsertest.cc
Normal file
@ -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 = ↦
|
||||
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.
|
||||
|
Reference in New Issue
Block a user