blob: 492987da51c7aec444355a8365c423f9f2540e7e [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/trust_tokens/trust_token_request_redemption_helper.h"
#include <memory>
#include "base/callback.h"
#include "base/test/task_environment.h"
#include "net/base/load_flags.h"
#include "net/base/request_priority.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_test_util.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/trust_tokens/proto/public.pb.h"
#include "services/network/trust_tokens/test/trust_token_test_util.h"
#include "services/network/trust_tokens/trust_token_http_headers.h"
#include "services/network/trust_tokens/trust_token_key_commitment_getter.h"
#include "services/network/trust_tokens/trust_token_parameterization.h"
#include "services/network/trust_tokens/trust_token_store.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace network {
namespace {
using TrustTokenRequestRedemptionHelperTest = TrustTokenRequestHelperTest;
using ::testing::_;
using ::testing::Optional;
using ::testing::Property;
using ::testing::Return;
// FixedKeyCommitmentGetter returns the provided commitment result when
// |Get| is called by the tested code.
class FixedKeyCommitmentGetter : public TrustTokenKeyCommitmentGetter {
public:
FixedKeyCommitmentGetter() = default;
explicit FixedKeyCommitmentGetter(
const url::Origin& issuer,
mojom::TrustTokenKeyCommitmentResultPtr result)
: issuer_(issuer), result_(std::move(result)) {}
void Get(const url::Origin& origin,
base::OnceCallback<void(mojom::TrustTokenKeyCommitmentResultPtr)>
on_done) const override {
EXPECT_EQ(origin, issuer_);
std::move(on_done).Run(result_.Clone());
}
private:
url::Origin issuer_;
mojom::TrustTokenKeyCommitmentResultPtr result_;
};
base::NoDestructor<FixedKeyCommitmentGetter> g_fixed_key_commitment_getter{};
// MockCryptographer mocks out the cryptographic operations
// underlying Trust Tokens redemption.
class MockCryptographer
: public TrustTokenRequestRedemptionHelper::Cryptographer {
public:
MOCK_METHOD2(
Initialize,
bool(int issuer_configured_batch_size,
base::StringPiece signed_redemption_record_verification_key));
MOCK_METHOD3(
BeginRedemption,
base::Optional<std::string>(TrustToken token,
base::StringPiece verification_key,
const url::Origin& top_level_origin));
MOCK_METHOD1(ConfirmRedemption,
base::Optional<std::string>(base::StringPiece response_header));
};
class FakeKeyPairGenerator
: public TrustTokenRequestRedemptionHelper::KeyPairGenerator {
public:
bool Generate(std::string*, std::string*) override { return true; }
};
class FailingKeyPairGenerator
: public TrustTokenRequestRedemptionHelper::KeyPairGenerator {
public:
bool Generate(std::string*, std::string*) override { return false; }
};
class MockKeyPairGenerator
: public TrustTokenRequestRedemptionHelper::KeyPairGenerator {
public:
MockKeyPairGenerator(const std::string& signing,
const std::string& verification)
: signing_(signing), verification_(verification) {}
bool Generate(std::string* s, std::string* v) override {
s->swap(signing_);
v->swap(verification_);
return true;
}
private:
std::string signing_;
std::string verification_;
};
} // namespace
// Check that redemption fails if it would result in too many issuers being
// configured for the redemption top-level origin.
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsIfTooManyIssuers) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
auto issuer = *SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/"));
auto toplevel =
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/"));
// Associate the toplevel with the cap's worth of issuers different from
// |issuer|. (The cap is guaranteed to be quite small because of privacy
// requirements of the Trust Tokens protocol.)
for (int i = 0; i < kTrustTokenPerToplevelMaxNumberOfAssociatedIssuers; ++i) {
ASSERT_TRUE(store->SetAssociation(
*SuitableTrustTokenOrigin::Create(
GURL(base::StringPrintf("https://issuer%d.com/", i))),
toplevel));
}
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(),
&*g_fixed_key_commitment_getter, std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kResourceExhausted);
}
// Check that redemption fails if its key commitment request fails.
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsIfKeyCommitmentFails) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
// Have the key commitment getter return nullptr, denoting that the key
// commitment fetch failed.
FixedKeyCommitmentGetter getter(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")), nullptr);
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &getter,
std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kFailedPrecondition);
}
// Check that redemption fails with kResourceExhausted if there are no trust
// tokens stored for the (issuer, top-level origin) pair.
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsIfNoTokensToRedeem) {
// Establish the following state:
// * Initialize an _empty_ trust token store.
// * Successfully return from the key commitment query.
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
mojom::TrustTokenKeyCommitmentResult::New());
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kResourceExhausted);
}
// Check that redemption fails with kInternalError if there's an error during
// initializing the cryptography delegate.
TEST_F(TrustTokenRequestRedemptionHelperTest,
RejectsIfInitializingCryptographerFails) {
// Establish the following state:
// * Initialize an _empty_ trust token store.
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// Configure the cryptographer to fail to encode the redemption request.
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(false));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kInternalError);
}
// Check that redemption fails with kInternalError if there's an error during
// encoding of the request header.
TEST_F(TrustTokenRequestRedemptionHelperTest,
RejectsIfAddingRequestHeaderFails) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// Configure the cryptographer to fail to encode the redemption request.
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(Return(base::nullopt));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kInternalError);
}
// Check that redemption fails with kInternalError if there's an error during
// generating the signing/validation key pair.
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsIfKeyPairGenerationFails) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// Provide |helper| a FailingKeyPairGenerator to ensure that key pair
// generation does not succeed.
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FailingKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
// Since key pair generation failed, |Begin| should have failed and reported
// an internal error.
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kInternalError);
}
namespace {
class TrustTokenBeginRedemptionPostconditionsTest
: public TrustTokenRequestRedemptionHelperTest {
public:
void SetUp() override {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the
// key commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store =
TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// The value obtained from the cryptographer should be the exact
// Sec-Trust-Token header attached to the request.
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(
Return(std::string("this string contains a redemption request")));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
request_ = MakeURLRequest("https://issuer.com/");
request_->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request_.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
}
protected:
std::unique_ptr<net::URLRequest> request_;
};
} // namespace
// Check that the redemption helper sets the Sec-Trust-Token header on the
// outgoing request.
TEST_F(TrustTokenBeginRedemptionPostconditionsTest, SetsHeader) {
std::string attached_header;
EXPECT_TRUE(request_->extra_request_headers().GetHeader(
kTrustTokensSecTrustTokenHeader, &attached_header));
}
// Check that the redemption helper sets the LOAD_BYPASS_CACHE flag on the
// outgoing request.
TEST_F(TrustTokenBeginRedemptionPostconditionsTest, SetsLoadFlag) {
EXPECT_TRUE(request_->load_flags() & net::LOAD_BYPASS_CACHE);
}
// Check that the redemption helper rejects responses lacking the
// Sec-Trust-Token response header.
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsIfResponseOmitsHeader) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(
Return(std::string("this string contains a redemption request")));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
// Verify that the first half of the redemption operation succeeded, as a
// precursor to testing that the second half works, too.
ASSERT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
// Add an empty list of response headers. In particular, this is missing the
// Sec-Trust-Token redemption response header.
auto response_head = mojom::URLResponseHead::New();
response_head->headers =
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\r\n");
// As a consequence, |Finalize| should fail.
EXPECT_EQ(ExecuteFinalizeAndWaitForResult(&helper, response_head.get()),
mojom::TrustTokenOperationStatus::kBadResponse);
}
// Check that the redemption helper handles a redemption response rejected by
// the underlying cryptographic library.
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsIfResponseIsUnusable) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// Configure the cryptographer to reject the response header by returning
// nullopt on ConfirmRedemption.
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(
Return(std::string("this string contains a redemption request")));
EXPECT_CALL(*cryptographer, ConfirmRedemption(_))
.WillOnce(Return(base::nullopt));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
// Verify that the first half of the redemption operation succeeded, as a
// precursor to testing that the second half works, too.
ASSERT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
auto response_head = mojom::URLResponseHead::New();
response_head->headers =
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\r\n");
response_head->headers->SetHeader(kTrustTokensSecTrustTokenHeader, "");
// Since the cryptographer rejected the response header by returning nullopt
// on ConfirmRedemption, expect to fail with kBadResponse.
EXPECT_EQ(ExecuteFinalizeAndWaitForResult(&helper, response_head.get()),
mojom::TrustTokenOperationStatus::kBadResponse);
// Processing the response should have stripped the header.
EXPECT_FALSE(
response_head->headers->HasHeader(kTrustTokensSecTrustTokenHeader));
}
// Check that, when preconditions are met and the underlying cryptographic steps
// successfully complete, the begin/finalize methods succeed.
TEST_F(TrustTokenRequestRedemptionHelperTest, Success) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// Configure the cryptographer to succeed on both the outbound and inbound
// halves of the operation.
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(Return("well-formed redemption request"));
EXPECT_CALL(*cryptographer, ConfirmRedemption(_))
.WillOnce(Return("a successfully-extracted SRR"));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
// Since this test is testing the behavior on handling the response after
// successfully constructing a redemption request, sanity check that the setup
// has correctly caused constructing the request so succeed.
ASSERT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
auto response_head = mojom::URLResponseHead::New();
response_head->headers =
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\r\n");
response_head->headers->SetHeader(kTrustTokensSecTrustTokenHeader, "");
// After a successfully constructed request, when the response is well-formed
// and the delegate accepts the response, Finalize should succeed.
EXPECT_EQ(ExecuteFinalizeAndWaitForResult(&helper, response_head.get()),
mojom::TrustTokenOperationStatus::kOk);
// Processing the response should have stripped the header.
EXPECT_FALSE(
response_head->headers->HasHeader(kTrustTokensSecTrustTokenHeader));
}
// Check that a successful Begin call associates the issuer with the redemption
// toplevel origin.
TEST_F(TrustTokenRequestRedemptionHelperTest, AssociatesIssuerWithToplevel) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
// Configure the cryptographer to succeed on both the outbound and inbound
// halves of the operation.
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(Return("well-formed redemption request"));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<FakeKeyPairGenerator>(), std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
// Since this test is testing the behavior on handling the response after
// successfully constructing a redemption request, sanity check that the setup
// has correctly caused constructing the request so succeed.
ASSERT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
// After the operation has successfully begun, the issuer and the toplevel
// should be associated.
EXPECT_TRUE(store->IsAssociated(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/"))));
}
// Check that a successful end-to-end Begin/Finalize flow stores the obtained
// signed redemption record (and associated key pair) in the trust token store.
TEST_F(TrustTokenRequestRedemptionHelperTest, StoresObtainedRedemptionRecord) {
// Establish the following state:
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"token verification key");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(mojom::TrustTokenVerificationKey::New(
"token verification key", /*expiry=*/base::Time::Max()));
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(Return("well-formed redemption request"));
EXPECT_CALL(*cryptographer, ConfirmRedemption(_))
.WillOnce(Return("a successfully-extracted SRR"));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(), &*getter,
std::make_unique<MockKeyPairGenerator>("signing key", "verification key"),
std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
auto response_head = mojom::URLResponseHead::New();
response_head->headers =
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\r\n");
response_head->headers->SetHeader(kTrustTokensSecTrustTokenHeader, "");
EXPECT_EQ(ExecuteFinalizeAndWaitForResult(&helper, response_head.get()),
mojom::TrustTokenOperationStatus::kOk);
// After the operation has successfully finished, the SRR parsed from the
// server response should be in the store.
EXPECT_THAT(
store->RetrieveNonstaleRedemptionRecord(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/"))),
Optional(AllOf(
Property(&SignedTrustTokenRedemptionRecord::body,
"a successfully-extracted SRR"),
Property(&SignedTrustTokenRedemptionRecord::public_key,
"verification key"),
Property(&SignedTrustTokenRedemptionRecord::token_verification_key,
"token verification key"),
Property(&SignedTrustTokenRedemptionRecord::signing_key,
"signing key"))));
}
// Check that a "refresh" refresh mode is rejected unless the request's
// initiating origin is the issuer origin.
TEST_F(TrustTokenRequestRedemptionHelperTest,
RejectsRefreshFromNonissuerOrigin) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kRefresh, store.get(),
&*g_fixed_key_commitment_getter, std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
// kRefresh should mean that redemption fails on requests with
// non-issuer initiators.
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://not-issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kFailedPrecondition);
}
// On a redemption operation parameterized by kUseCachedSrr, if there's an SRR
// present in the store for the given issuer-toplevel pair, the request should
// return early with kAlreadyExists.
TEST_F(TrustTokenRequestRedemptionHelperTest, RedemptionRecordCacheHit) {
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->SetRedemptionRecord(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")),
SignedTrustTokenRedemptionRecord());
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(),
&*g_fixed_key_commitment_getter, std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kAlreadyExists);
}
// Check that a successful end-to-end Begin/Finalize flow with kRefresh
// overwrites the previously stored signed redemption record (and associated key
// pair) in the trust token store.
TEST_F(TrustTokenRequestRedemptionHelperTest,
SuccessUsingRefreshSrrOverwritesStoredSrr) {
// Establish the following state:
// * A signed redemption record is already stored for the issuer, toplevel
// pair at hand.
// * One key commitment returned from the key commitment registry, with one
// key, with body "".
// * One token stored corresponding to the key "" (this will be the token
// that the redemption request redeems; its key needs to match the key
// commitment's key so that it does not get evicted from storage after the key
// commitment is updated to reflect the key commitment result).
std::unique_ptr<TrustTokenStore> store = TrustTokenStore::CreateForTesting();
store->SetRedemptionRecord(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com")),
SignedTrustTokenRedemptionRecord());
store->AddTokens(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
std::vector<std::string>{"a token"},
/*key=*/"");
auto key_commitment_result = mojom::TrustTokenKeyCommitmentResult::New();
key_commitment_result->keys.push_back(
mojom::TrustTokenVerificationKey::New());
key_commitment_result->batch_size =
static_cast<int>(kMaximumTrustTokenIssuanceBatchSize);
auto getter = std::make_unique<FixedKeyCommitmentGetter>(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com")),
std::move(key_commitment_result));
auto cryptographer = std::make_unique<MockCryptographer>();
EXPECT_CALL(*cryptographer, Initialize(_, _)).WillOnce(Return(true));
EXPECT_CALL(*cryptographer, BeginRedemption(_, _, _))
.WillOnce(Return("well-formed redemption request"));
EXPECT_CALL(*cryptographer, ConfirmRedemption(_))
.WillOnce(Return("a successfully-extracted SRR"));
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kRefresh, store.get(), &*getter,
std::make_unique<MockKeyPairGenerator>("signing key", "verification key"),
std::move(cryptographer));
auto request = MakeURLRequest("https://issuer.com/");
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
// Set the initiator in order to be able to use refresh mode
// kRefresh.
request->set_initiator(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")));
mojom::TrustTokenOperationStatus result =
ExecuteBeginOperationAndWaitForResult(&helper, request.get());
EXPECT_EQ(result, mojom::TrustTokenOperationStatus::kOk);
auto response_head = mojom::URLResponseHead::New();
response_head->headers =
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\r\n");
response_head->headers->SetHeader(kTrustTokensSecTrustTokenHeader, "");
EXPECT_EQ(ExecuteFinalizeAndWaitForResult(&helper, response_head.get()),
mojom::TrustTokenOperationStatus::kOk);
// After the operation has successfully finished, the SRR parsed from the
// server response should be in the store.
EXPECT_THAT(
store->RetrieveNonstaleRedemptionRecord(
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.com/")),
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/"))),
Optional(AllOf(Property(&SignedTrustTokenRedemptionRecord::body,
"a successfully-extracted SRR"),
Property(&SignedTrustTokenRedemptionRecord::public_key,
"verification key"),
Property(&SignedTrustTokenRedemptionRecord::signing_key,
"signing key"))));
}
TEST_F(TrustTokenRequestRedemptionHelperTest, RejectsUnsuitableInsecureIssuer) {
auto store = TrustTokenStore::CreateForTesting();
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(),
&*g_fixed_key_commitment_getter, std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("http://insecure-issuer.com/");
EXPECT_EQ(ExecuteBeginOperationAndWaitForResult(&helper, request.get()),
mojom::TrustTokenOperationStatus::kInvalidArgument);
}
TEST_F(TrustTokenRequestRedemptionHelperTest,
RejectsUnsuitableNonHttpNonHttpsIssuer) {
auto store = TrustTokenStore::CreateForTesting();
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kUseCached, store.get(),
&*g_fixed_key_commitment_getter, std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("file:///non-https-issuer.txt");
EXPECT_EQ(ExecuteBeginOperationAndWaitForResult(&helper, request.get()),
mojom::TrustTokenOperationStatus::kInvalidArgument);
}
TEST_F(TrustTokenRequestRedemptionHelperTest, RequiresInitiatorForSrrRefresh) {
// Refresh mode "refresh" requires that the request's initiator to
// be same-origin with the request's issuer. Test that, in this case, the
// redemption helper requires that the request have an initiator.
auto store = TrustTokenStore::CreateForTesting();
TrustTokenRequestRedemptionHelper helper(
*SuitableTrustTokenOrigin::Create(GURL("https://toplevel.com/")),
mojom::TrustTokenRefreshPolicy::kRefresh, store.get(),
&*g_fixed_key_commitment_getter, std::make_unique<FakeKeyPairGenerator>(),
std::make_unique<MockCryptographer>());
auto request = MakeURLRequest("https://issuer.example");
request->set_initiator(base::nullopt);
EXPECT_EQ(ExecuteBeginOperationAndWaitForResult(&helper, request.get()),
mojom::TrustTokenOperationStatus::kFailedPrecondition);
}
} // namespace network