blob: d875bb3cd17f65f672e56e198e9520738f88748b [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/network/trust_token_browsertest.h"
#include <memory>
#include <string>
#include <string_view>
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "build/build_config.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.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_interceptor.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/trust_token_http_headers.h"
#include "services/network/public/cpp/trust_token_parameterization.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "services/network/test/trust_token_request_handler.h"
#include "services/network/test/trust_token_test_server_handler_registration.h"
#include "services/network/test/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_canon_stdstring.h"
namespace content {
namespace {
using network::test::TrustTokenRequestHandler;
using SignedRequest = network::test::TrustTokenSignedRequest;
using ::testing::AllOf;
using ::testing::DescribeMatcher;
using ::testing::Eq;
using ::testing::ExplainMatchResult;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::IsFalse;
using ::testing::IsSubsetOf;
using ::testing::Not;
using ::testing::Optional;
using ::testing::StrEq;
using ::testing::Truly;
MATCHER_P(HasHeader, name, base::StringPrintf("Has header %s", name)) {
if (!arg.headers.HasHeader(name)) {
*result_listener << base::StringPrintf("%s wasn't present", name);
return false;
}
*result_listener << base::StringPrintf("%s was present", name);
return true;
}
MATCHER_P2(HasHeader,
name,
other_matcher,
"has header " + std::string(name) + " that " +
DescribeMatcher<std::string>(other_matcher)) {
std::optional<std::string> header = arg.headers.GetHeader(name);
if (!header) {
*result_listener << base::StringPrintf("%s wasn't present", name);
return false;
}
return ExplainMatchResult(other_matcher, *header, result_listener);
}
MATCHER(
ReflectsSigningFailure,
"The given signed request reflects a client-side signing failure, having "
"an empty redemption record and no other related headers.") {
return ExplainMatchResult(
AllOf(HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord,
StrEq("")),
Not(HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))),
arg, result_listener);
}
} // namespace
TrustTokenBrowsertest::TrustTokenBrowsertest() = default;
void TrustTokenBrowsertest::SetUpOnMainThread() {
host_resolver()->AddRule("*", "127.0.0.1");
server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
server_.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("content/test/data")));
SetupCrossSiteRedirector(embedded_test_server());
SetupCrossSiteRedirector(&server_);
network::test::RegisterTrustTokenTestHandlers(&server_, &request_handler_);
TrustTokenBrowsertest::Observe(shell()->web_contents());
ASSERT_TRUE(server_.Start());
}
void TrustTokenBrowsertest::ProvideRequestHandlerKeyCommitmentsToNetworkService(
std::vector<std::string_view> hosts) {
base::flat_map<url::Origin, std::string_view> origins_and_commitments;
std::string key_commitments = request_handler_.GetKeyCommitmentRecord();
// TODO(davidvc): This could be extended to make the request handler aware
// of different origins, which would allow using different key commitments
// per origin.
for (std::string_view host : hosts) {
GURL::Replacements replacements;
replacements.SetHostStr(host);
origins_and_commitments.insert_or_assign(
url::Origin::Create(server_.base_url().ReplaceComponents(replacements)),
key_commitments);
}
if (origins_and_commitments.empty()) {
origins_and_commitments = {
{url::Origin::Create(server_.base_url()), key_commitments}};
}
base::RunLoop run_loop;
GetNetworkService()->SetTrustTokenKeyCommitments(
network::WrapKeyCommitmentsForIssuers(std::move(origins_and_commitments)),
run_loop.QuitClosure());
run_loop.Run();
}
std::string TrustTokenBrowsertest::IssuanceOriginFromHost(
const std::string& host) const {
auto ret = url::Origin::Create(server_.GetURL(host, "/")).Serialize();
return ret;
}
void TrustTokenBrowsertest::OnTrustTokensAccessed(
RenderFrameHost* render_frame_host,
const TrustTokenAccessDetails& details) {
access_count_++;
}
void TrustTokenBrowsertest::OnTrustTokensAccessed(
NavigationHandle* navigation_handle,
const TrustTokenAccessDetails& details) {
access_count_++;
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEnd) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
(async () => {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
await fetch("/sign", {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: [$1]}});
return "Success"; })(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ(
"Success",
EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test"))));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
// Fetch is called directly from top level (a.test), issuer origin (b.test)
// is different from top frame origin.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEndThirdParty) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"b.test"});
const GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
(async () => {
await fetch($1, {privateToken: {version: 1,
operation: 'token-request'}});
await fetch($2, {privateToken: {version: 1,
operation: 'token-redemption'}});
await fetch($3, {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: [$4]}});
return "Success"; })(); )";
const std::string issuer_origin = IssuanceOriginFromHost("b.test");
const std::string issuance_url = server_.GetURL("b.test", "/issue").spec();
const std::string redemption_url = server_.GetURL("b.test", "/redeem").spec();
const std::string signature_url = server_.GetURL("b.test", "/sign").spec();
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(command, issuance_url, redemption_url,
signature_url, issuer_origin)));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, XhrEndToEnd) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// If this isn't idiomatic JS, I don't know what is.
std::string command = R"(
(async () => {
let request = new XMLHttpRequest();
request.open('GET', '/issue');
request.setPrivateToken({
version: 1,
operation: '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.setPrivateToken({
version: 1,
operation: '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.setPrivateToken({
version: 1,
operation: 'send-redemption-record',
issuers: [$1]
});
promise = new Promise((res, rej) => {
request.onload = res; request.onerror = rej;
});
request.send();
await promise;
return "Success";
})(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ(
"Success",
EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test"))));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, IframeSendRedemptionRecord) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
std::string command = R"(
(async () => {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
return "Success";
})())";
GURL start_url = server_.GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_EQ("Success", EvalJs(shell(), command));
auto execute_op_via_iframe = [&](std::string_view path,
std::string_view 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.privateToken = $1;
myFrame.src = $2;)",
trust_token, path)));
TestNavigationObserver load_observer(shell()->web_contents());
load_observer.WaitForNavigationFinished();
};
execute_op_via_iframe("/sign", JsReplace(
R"({"version": 1,
"operation": "send-redemption-record",
"issuers": [$1]})",
IssuanceOriginFromHost("a.test")));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
IframeCanOnlySendRedemptionRecord) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
auto fail_to_execute_op_via_iframe = [&](std::string_view path,
std::string_view 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();
};
fail_to_execute_op_via_iframe("/issue", R"({"type": "token-request"})");
std::string command = JsReplace(R"(
(async () => {
return await document.hasPrivateToken($1);
})();)",
IssuanceOriginFromHost("a.test"));
EXPECT_EQ(false, EvalJs(shell(), command));
fail_to_execute_op_via_iframe("/redeem", R"({"type": "token-redemption"})");
command = JsReplace(R"(
(async () => {
return document.hasRedemptionRecord($1);
})();)",
IssuanceOriginFromHost("a.test"));
EXPECT_EQ(false, EvalJs(shell(), command));
fail_to_execute_op_via_iframe("/bad", R"({"type": "bad-type"})");
command = JsReplace(R"(
(async () => {
return await document.hasPrivateToken($1)
|| document.hasRedemptionRecord($1);
})();)",
IssuanceOriginFromHost("a.test"));
EXPECT_EQ(false, EvalJs(shell(), command));
// Expect zero accesses.
EXPECT_EQ(0, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, HasTrustTokenAfterIssuance) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = JsReplace(R"(
(async () => {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
return await document.hasPrivateToken($1);
})();)",
IssuanceOriginFromHost("a.test"));
// 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(), command));
// Expect one access for issue.
EXPECT_EQ(1, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
SigningWithNoRedemptionRecordDoesntCancelRequest) {
TrustTokenRequestHandler::Options options;
request_handler_.UpdateOptions(std::move(options));
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// This sign operation will fail, because we don't have a redemption record in
// storage, a prerequisite. However, the failure shouldn't be fatal.
std::string command = JsReplace(R"((async () => {
await fetch("/sign", {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: [$1]}});
return "Success";
})(); )",
IssuanceOriginFromHost("a.test"));
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ("Success", EvalJs(shell(), command));
EXPECT_THAT(request_handler_.last_incoming_signed_request(),
Optional(ReflectsSigningFailure()));
// Expect one access for sign.
EXPECT_EQ(1, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEndInIsolatedWorld) {
// Ensure an isolated world can execute Trust Tokens operations when its
// window's main world can. In particular, this ensures that the
// redemtion-and-signing permissions policy is appropriately propagated by the
// browser process.
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
(async () => {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
await fetch("/sign", {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: [$1]}});
return "Success"; })(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ(
"Success",
EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test")),
EXECUTE_SCRIPT_DEFAULT_OPTIONS,
/*world_id=*/30));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RecordsTimers) {
base::HistogramTester histograms;
// |completion_waiter| adds a synchronization point so that we can
// safely fetch all of the relevant histograms from the network process.
//
// Without this, there's a race between the fetch() promises resolving and the
// NetErrorForTrustTokenOperation histogram being logged. This likely has no
// practical impact during normal operation, but it makes this test flake: see
// https://crbug.com/1165862.
//
// The URLLoaderInterceptor's completion callback receives its
// URLLoaderCompletionStatus from URLLoaderClient::OnComplete, which happens
// after CorsURLLoader::NotifyCompleted, which records the final histogram.
base::RunLoop run_loop;
content::URLLoaderInterceptor completion_waiter(
base::BindRepeating([](URLLoaderInterceptor::RequestParams*) {
return false; // Don't intercept outbound requests.
}),
base::BindLambdaForTesting(
[&run_loop](const GURL& url,
const network::URLLoaderCompletionStatus& status) {
if (url.spec().find("sign") != std::string::npos)
run_loop.Quit();
}),
/*ready_callback=*/base::NullCallback());
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
(async () => {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
await fetch("/sign", {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: [$1]}});
return "Success"; })(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ(
"Success",
EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test"))));
run_loop.Run();
content::FetchHistogramsFromChildProcesses();
// Just check that the timers were populated: since we can't mock a clock in
// this browser test, it's hard to check the recorded values for
// reasonableness.
for (const std::string& op : {"Issuance", "Redemption", "Signing"}) {
histograms.ExpectTotalCount(
"Net.TrustTokens.OperationBeginTime.Success." + op, 1);
histograms.ExpectTotalCount(
"Net.TrustTokens.OperationTotalTime.Success." + op, 1);
histograms.ExpectTotalCount(
"Net.TrustTokens.OperationServerTime.Success." + op, 1);
histograms.ExpectTotalCount(
"Net.TrustTokens.OperationFinalizeTime.Success." + op, 1);
histograms.ExpectUniqueSample(
"Net.TrustTokens.NetErrorForTrustTokenOperation.Success." + op, net::OK,
1);
}
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RecordsNetErrorCodes) {
// Verify that the Net.TrustTokens.NetErrorForTrustTokenOperation.* metrics
// record successfully by testing two "success" cases where there's an
// unrelated net stack error and one case where the Trust Tokens operation
// itself fails.
base::HistogramTester histograms;
ProvideRequestHandlerKeyCommitmentsToNetworkService(
{"no-cert-for-this.domain"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_THAT(
EvalJs(shell(), JsReplace(
R"(fetch($1, {privateToken: {
version: 1,
operation: 'token-request'}})
.then(() => "Unexpected success!")
.catch(err => err.message);)",
IssuanceOriginFromHost("no-cert-for-this.domain")))
.ExtractString(),
HasSubstr("Failed to fetch"));
EXPECT_THAT(
EvalJs(shell(), JsReplace(
R"(fetch($1, {privateToken: {
version: 1,
operation: 'send-redemption-record',
issuers: ['https://nonexistent-issuer.example']}})
.then(() => "Unexpected success!")
.catch(err => err.message);)",
IssuanceOriginFromHost("no-cert-for-this.domain")))
.ExtractString(),
HasSubstr("Failed to fetch"));
content::FetchHistogramsFromChildProcesses();
// "Success" since we executed the outbound half of the Trust Tokens
// operation without issue:
histograms.ExpectUniqueSample(
"Net.TrustTokens.NetErrorForTrustTokenOperation.Success.Issuance",
net::ERR_CERT_COMMON_NAME_INVALID, 1);
// "Success" since signing can't fail:
histograms.ExpectUniqueSample(
"Net.TrustTokens.NetErrorForTrustTokenOperation.Success.Signing",
net::ERR_CERT_COMMON_NAME_INVALID, 1);
// Attempt a redemption against 'a.test'; we don't have a token for this
// domain, so it should fail.
EXPECT_EQ("InvalidStateError",
EvalJs(shell(), JsReplace(
R"(fetch($1, {privateToken: {
version: 1,
operation: 'token-redemption'}})
.then(() => "Unexpected success!")
.catch(err => err.name);)",
IssuanceOriginFromHost("a.test"))));
content::FetchHistogramsFromChildProcesses();
histograms.ExpectUniqueSample(
"Net.TrustTokens.NetErrorForTrustTokenOperation.Failure.Redemption",
net::ERR_TRUST_TOKEN_OPERATION_FAILED, 1);
// Expect three accesses, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RecordsFetchFailureReasons) {
// Verify that the Net.TrustTokens.NetErrorForFetchFailure.* metrics
// record successfully by testing one case with a blocked resource, one case
// with a generic net-stack failure, and one case with a Trust Tokens
// operation failure.
base::HistogramTester histograms;
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// This fetch will fail because we set `redirect: 'error'` and the
// /cross-site/ URL will redirect the request.
EXPECT_EQ("TypeError", EvalJs(shell(),
R"(fetch("/cross-site/b.test/issue", {
redirect: 'error',
privateToken: {version: 1,
operation: 'token-request'}
})
.then(() => "Unexpected success!")
.catch(err => err.name);)"));
content::FetchHistogramsFromChildProcesses();
histograms.ExpectUniqueSample(
"Net.TrustTokens.NetErrorForFetchFailure.Issuance", net::ERR_FAILED,
/*expected_count=*/1);
// Since issuance failed, there should be no tokens to redeem, so redemption
// should fail:
EXPECT_EQ("OperationError", EvalJs(shell(),
R"(fetch("/redeem", {privateToken: {
version: 1,
operation: 'token-redemption'}})
.then(() => "Unexpected success!")
.catch(err => err.name);)"));
content::FetchHistogramsFromChildProcesses();
histograms.ExpectUniqueSample(
"Net.TrustTokens.NetErrorForFetchFailure.Redemption",
net::ERR_TRUST_TOKEN_OPERATION_FAILED,
/*expected_count=*/1);
// Execute a cross-site b.test -> a.test issuance that would succeed, were it
// not for site b requiring CORP headers and none being present on the a.test
// issuance response:
ASSERT_TRUE(NavigateToURL(
shell(),
server_.GetURL("b.test",
"/cross-origin-opener-policy_redirect_final.html")));
GURL site_a_issuance_url =
GURL(IssuanceOriginFromHost("a.test")).Resolve("/issue");
EXPECT_THAT(EvalJs(shell(), JsReplace(R"(fetch($1, {
mode: 'no-cors',
privateToken: {version: 1,
operation: 'token-request'}})
.then(() => "Unexpected success!")
.catch(err => err.message);)",
site_a_issuance_url))
.ExtractString(),
HasSubstr("Failed to fetch"));
content::FetchHistogramsFromChildProcesses();
histograms.ExpectBucketCount(
"Net.TrustTokens.NetErrorForFetchFailure.Issuance",
net::ERR_BLOCKED_BY_RESPONSE,
/*expected_count=*/1);
// Expect three accesses, two for issue and one for redeem.
EXPECT_EQ(3, access_count_);
}
// Trust Tokens should require that their executing contexts be secure.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, OperationsRequireSecureContext) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL start_url =
embedded_test_server()->GetURL("insecure.test", "/page_with_iframe.html");
// Make sure that we are, in fact, using an insecure page.
ASSERT_FALSE(network::IsUrlPotentiallyTrustworthy(start_url));
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// 1. Confirm that the Fetch interface doesn't work:
std::string command =
R"(fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}})
.catch(error => error.message);)";
EXPECT_THAT(EvalJs(shell(), command).ExtractString(),
HasSubstr("secure context"));
// 2. Confirm that the XHR interface isn't present:
EXPECT_EQ(false, EvalJs(shell(), "'setTrustToken' in (new XMLHttpRequest);"));
// 3. Confirm that the iframe interface doesn't work by verifying that no
// Trust Tokens operation gets executed.
GURL issuance_url = server_.GetURL("/issue");
URLLoaderMonitor monitor({issuance_url});
// 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;)",
R"({"operation": "token-request"})", issuance_url)));
monitor.WaitForUrls();
EXPECT_THAT(monitor.GetRequestInfo(issuance_url),
Optional(Field(&network::ResourceRequest::trust_token_params,
IsFalse())));
// Expect zero accesses.
EXPECT_EQ(0, access_count_);
}
// Issuance should fail if we don't have keys for the issuer at hand.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, IssuanceRequiresKeys) {
ProvideRequestHandlerKeyCommitmentsToNetworkService(
{"not-the-right-server.example"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
fetch('/issue', {privateToken: {version: 1,
operation: 'token-request'}})
.then(() => 'Success').catch(err => err.name); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ("InvalidStateError", EvalJs(shell(), command));
// Expect one access of issue.
EXPECT_EQ(1, access_count_);
}
// When the server rejects issuance, the client-side issuance operation should
// fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
CorrectlyReportsServerErrorDuringIssuance) {
TrustTokenRequestHandler::Options options;
options.issuance_outcome =
TrustTokenRequestHandler::ServerOperationOutcome::kUnconditionalFailure;
request_handler_.UpdateOptions(std::move(options));
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_EQ("OperationError", EvalJs(shell(), R"(fetch('/issue',
{ privateToken: { version: 1, operation: 'token-request' } })
.then(()=>'Success').catch(err => err.name); )"));
// Expect one access of issue.
EXPECT_EQ(1, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, CrossOriginIssuanceWorks) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"sub1.b.test"});
GURL start_url = server_.GetURL("sub2.b.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// Using GetURL to generate the issuance location is important
// because it sets the port correctly.
EXPECT_EQ(
"Success",
EvalJs(shell(), JsReplace(R"(
fetch($1, { privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("sub1.b.test", "/issue"))));
// Expect one access of issue.
EXPECT_EQ(1, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, CrossSiteIssuanceWorks) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("b.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// Using GetURL to generate the issuance location is important
// because it sets the port correctly.
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(
fetch($1, { privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/issue"))));
// Expect one access of issue.
EXPECT_EQ(1, access_count_);
}
// Issuance should succeed only if the number of issuers associated with the
// requesting context's top frame origin is less than the limit on the number of
// such issuers.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
IssuanceRespectsAssociatedIssuersCap) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
static_assert(
network::kTrustTokenPerToplevelMaxNumberOfAssociatedIssuers < 10,
"Consider rewriting this test for performance's sake if the "
"number-of-issuers limit gets too large.");
// Each hasPrivateStateToken call adds the provided issuer to the calling
// context's list of associated issuers.
for (int i = 0;
i < network::kTrustTokenPerToplevelMaxNumberOfAssociatedIssuers; ++i) {
ASSERT_EQ("Success", EvalJs(shell(), "document.hasPrivateToken('https://a" +
base::NumberToString(i) +
".test').then(()=>'Success');"));
}
EXPECT_EQ("OperationError", EvalJs(shell(), R"(
fetch('/issue', { privateToken: { version: 1,
operation: 'token-request' } })
.then(() => 'Success').catch(error => error.name); )"));
// Expect one access for issue.
EXPECT_EQ(1, access_count_);
}
// When an issuance request is made in cors mode, a cross-origin redirect from
// issuer A to issuer B should result in a new issuance request to issuer B,
// obtaining issuer B tokens on success.
//
// Note: For more on the interaction between Trust Tokens and redirects, see the
// "Handling redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
TrustTokenBrowsertest,
CorsModeCrossOriginRedirectIssuanceUsesNewOriginAsIssuer) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(fetch($1, {privateToken: {
version: 1,
operation: 'token-request'}})
.then(() => "Success")
.catch(error => error.name);)";
EXPECT_EQ(
"Success",
EvalJs(shell(),
JsReplace(command,
server_.GetURL("a.test", "/cross-site/b.test/issue"))));
EXPECT_EQ(true, EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
IssuanceOriginFromHost("b.test"))));
EXPECT_EQ(false,
EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
IssuanceOriginFromHost("a.test"))));
// Expect two accesses for issues.
EXPECT_EQ(2, access_count_);
}
// When an issuance request is made in no-cors mode, a cross-origin redirect
// from issuer A to issuer B should result in recycling the original issuance
// request, obtaining issuer A tokens on success.
//
// Note: For more on the interaction between Trust Tokens and redirects, see the
// "Handling redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
TrustTokenBrowsertest,
NoCorsModeCrossOriginRedirectIssuanceUsesOriginalOriginAsIssuer) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(fetch($1, {mode: 'no-cors',
privateToken: {
version: 1,
operation: 'token-request'}})
.then(() => "Success")
.catch(error => error.name);)";
EXPECT_EQ(
"Success",
EvalJs(shell(),
JsReplace(command,
server_.GetURL("a.test", "/cross-site/b.test/issue"))));
EXPECT_EQ(true, EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
IssuanceOriginFromHost("a.test"))));
EXPECT_EQ(false,
EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
IssuanceOriginFromHost("b.test"))));
// Expect one access for issue.
EXPECT_EQ(1, access_count_);
}
// Issuance from a context with a secure-but-non-HTTP/S top frame origin
// should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
IssuanceRequiresSuitableTopFrameOrigin) {
ProvideRequestHandlerKeyCommitmentsToNetworkService();
GURL file_url = GetTestUrl(/*dir=*/nullptr, "title1.html");
ASSERT_TRUE(file_url.SchemeIsFile());
ASSERT_TRUE(NavigateToURL(shell(), file_url));
std::string command =
R"(fetch($1, {privateToken: {version: 1,
operation: 'token-request'}})
.catch(error => error.name);)";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ("InvalidStateError",
EvalJs(shell(), JsReplace(command, server_.GetURL("/issue"))));
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ(
false,
EvalJs(shell(),
JsReplace("document.hasPrivateToken($1);",
url::Origin::Create(server_.base_url()).Serialize())));
// Expect one access for issue.
EXPECT_EQ(1, access_count_);
}
// Redemption from a secure-but-non-HTTP(S) top frame origin should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
RedemptionRequiresSuitableTopFrameOrigin) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command =
R"(fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}})
.then(() => "Success")
.catch(error => error.name);)";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
EXPECT_EQ("Success", EvalJs(shell(), command));
GURL file_url = GetTestUrl(/*dir=*/nullptr, "title1.html");
ASSERT_TRUE(NavigateToURL(shell(), file_url));
// Redemption from a page with a file:// top frame origin should fail.
command = R"(fetch($1, {privateToken: {version: 1,
operation: 'token-redemption'}})
.catch(error => error.name);)";
EXPECT_EQ(
"InvalidStateError",
EvalJs(shell(), JsReplace(command, server_.GetURL("a.test", "/redeem"))));
// Expect two accesses, one for issue and one for redemption.
EXPECT_EQ(2, access_count_);
}
// hasPrivateToken from a context with a secure-but-non-HTTP/S top frame
// origin should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
HasTrustTokenRequiresSuitableTopFrameOrigin) {
GURL file_url = GetTestUrl(/*dir=*/nullptr, "title1.html");
ASSERT_TRUE(file_url.SchemeIsFile());
ASSERT_TRUE(NavigateToURL(shell(), file_url));
EXPECT_EQ("NotAllowedError",
EvalJs(shell(),
R"(document.hasPrivateToken('https://issuer.example')
.catch(error => error.name);)"));
EXPECT_EQ(0, access_count_);
}
// A hasPrivateToken call initiated from a secure context should succeed
// even if the initiating frame's origin is opaque (e.g. from a sandboxed
// iframe).
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
HasTrustTokenFromSecureSubframeWithOpaqueOrigin) {
ASSERT_TRUE(NavigateToURL(
shell(), server_.GetURL("a.test", "/page_with_sandboxed_iframe.html")));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ("Success",
EvalJs(root->child_at(0)->current_frame_host(),
R"(document.hasPrivateToken('https://davids.website')
.then(()=>'Success');)"));
EXPECT_EQ(0, access_count_);
}
// An operation initiated from a secure context should succeed even if the
// operation's associated request's initiator is opaque (e.g. from a sandboxed
// iframe with the right Permissions Policy).
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
OperationFromSecureSubframeWithOpaqueOrigin) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(
shell(), server_.GetURL("a.test", "/page_with_sandboxed_iframe.html")));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ("Success", EvalJs(root->child_at(0)->current_frame_host(),
JsReplace(R"(
fetch($1, {mode: 'no-cors',
privateToken: {
version: 1,
operation: 'token-request'}
}).then(()=>'Success');)",
server_.GetURL("a.test", "/issue"))));
// Expect one access for issue.
EXPECT_EQ(1, access_count_);
}
// If a server issues with a key not present in the client's collection of key
// commitments, the issuance operation should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, IssuanceWithAbsentKeyFails) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
// Reset the handler, so that the client's valid keys disagree with the
// server's keys. (This is theoretically flaky, but the chance of the client's
// random keys colliding with the server's random keys is negligible.)
request_handler_.UpdateOptions(TrustTokenRequestHandler::Options());
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command =
R"(fetch($1, {privateToken: {version: 1,
operation: 'token-request'}})
.then(() => "Success")
.catch(error => error.name);)";
EXPECT_EQ(
"OperationError",
EvalJs(shell(), JsReplace(command, server_.GetURL("a.test", "/issue"))));
// Expect one access for issue.
EXPECT_EQ(1, access_count_);
}
// This regression test for crbug.com/1111735 ensures it's possible to execute
// redemption from a nested same-origin frame that hasn't committed a
// navigation.
//
// How it works: The main frame embeds a same-origin iframe that does not
// commit a navigation (here, specifically because of an HTTP 204 return). From
// this iframe, we execute a Trust Tokens redemption operation via the iframe
// interface (in other words, the Trust Tokens operation executes during the
// process of navigating to a grandchild frame). The grandchild frame's load
// will result in a renderer kill without the fix for the bug applied.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
SignFromFrameLackingACommittedNavigation) {
GURL start_url = server_.GetURL(
"a.test", "/page-executing-trust-token-signing-from-204-subframe.html");
// Execute a signing operation from a child iframe that has not committed a
// navigation (see the html source).
ASSERT_TRUE(NavigateToURL(shell(), start_url));
// For good measure, make sure the analogous signing operation works from
// fetch, too, even though it wasn't broken by the same bug.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ("Success", EvalJs(root->child_at(0)->current_frame_host(),
JsReplace(R"(
fetch($1, {mode: 'no-cors',
privateToken: {
version: 1,
operation: 'send-redemption-record',
issuers: [
'https://issuer.example'
]}
}).then(()=>'Success');)",
server_.GetURL("a.test", "/issue"))));
// Expect one access for sign.
EXPECT_EQ(1, access_count_);
}
// Redemption should fail when there are no keys for the issuer.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RedemptionRequiresKeys) {
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("InvalidStateError",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(() => 'Success')
.catch(err => err.name); )",
server_.GetURL("a.test", "/redeem"))));
// Expect one access for redemption.
EXPECT_EQ(1, access_count_);
}
// Redemption should fail when there are no tokens to redeem.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RedemptionRequiresTokens) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("OperationError",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(() => 'Success')
.catch(err => err.name); )",
server_.GetURL("a.test", "/redeem"))));
// Expect one access for redemption.
EXPECT_EQ(1, access_count_);
}
// When we have tokens for one issuer A, redemption against a different issuer B
// should still fail if we don't have any tokens for B.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
RedemptionWithoutTokensForDesiredIssuerFails) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/issue"))));
EXPECT_EQ("OperationError",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(() => 'Success')
.catch(err => err.name); )",
server_.GetURL("b.test", "/redeem"))));
// Expect two accesses, one for issuance and one for redemption.
EXPECT_EQ(2, access_count_);
}
// When the server rejects redemption, the client-side redemption operation
// should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
CorrectlyReportsServerErrorDuringRedemption) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_EQ("Success", EvalJs(shell(), R"(fetch('/issue',
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )"));
// Send a redemption request to the issuance endpoint, which should error out
// for the obvious reason that it isn't an issuance request:
EXPECT_EQ("OperationError", EvalJs(shell(), R"(fetch('/issue',
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(() => 'Success')
.catch(err => err.name); )"));
// Expect two accesses, one for issuance and one for redemption.
EXPECT_EQ(2, access_count_);
}
// After a successful issuance and redemption, a subsequent redemption against
// the same issuer should hit the redemption record cache.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
RedemptionHitsRedemptionRecordCache) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/issue"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/redeem"))));
EXPECT_EQ("NoModificationAllowedError",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.catch(err => err.name); )",
server_.GetURL("a.test", "/redeem"))));
// Expect three accesses, one for issuance and two for redemption.
EXPECT_EQ(3, access_count_);
}
// Redemption with `refresh-policy: 'refresh'` from an issuer context should
// succeed, overwriting the existing redemption record.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
RefreshPolicyRefreshWorksInIssuerContext) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/issue"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/redeem"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption',
refreshPolicy: 'refresh' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/redeem"))));
// Expect three accesses, one for issuance and two for redemption.
EXPECT_EQ(3, access_count_);
}
// Redemption with `refresh-policy: 'refresh'` from a non-issuer context should
// still work.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
RefreshPolicyRefreshRequiresIssuerContext) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"b.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
// Execute the operations against issuer https://b.test:<port> from a
// different context; attempting to use refreshPolicy: 'refresh' should still
// succeed.
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("b.test", "/issue"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )",
server_.GetURL("b.test", "/redeem"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption',
refreshPolicy: 'refresh' } })
.then(()=>'Success').catch(err => err.name); )",
server_.GetURL("b.test", "/redeem"))));
// Expect three accesses, one for issuance and two for redemption.
EXPECT_EQ(3, access_count_);
}
// When a redemption request is made in cors mode, a cross-origin redirect from
// issuer A to issuer B should result in a new redemption request to issuer B,
// failing if there are no issuer B tokens.
//
// Note: For more on the interaction between Trust Tokens and redirects, see the
// "Handling redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
TrustTokenBrowsertest,
CorsModeCrossOriginRedirectRedemptionUsesNewOriginAsIssuer) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
// Obtain both https://a.test:<PORT> and https://b.test:<PORT> tokens, the
// former for the initial redemption request to https://a.test:<PORT> and the
// latter for the fresh post-redirect redemption request to
// https://b.test:<PORT>.
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/issue"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("b.test", "/issue"))));
// On the redemption request, `mode: 'cors'` (the default) has the effect that
// that redirecting a request will renew the request's Trust Tokens state.
EXPECT_EQ("Success", EvalJs(shell(), R"(
fetch('/cross-site/b.test/redeem',
{ privateToken: { mode: 'cors',
version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )"));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(
fetch('/sign',
{ privateToken: { version: 1,
operation: 'send-redemption-record',
issuers: [$1],
} }).then(()=>'Success');)",
IssuanceOriginFromHost("b.test"))));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// When a signing operation fails, it isn't fatal, so the requests
// should always get sent successfully.
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(
fetch('/sign',
{ privateToken: { version: 1,
operation: 'send-redemption-record',
issuers: [$1],
} }).then(()=>'Success');)",
IssuanceOriginFromHost("a.test"))));
// There shouldn't have been an a.test redemption record attached to the
// request.
EXPECT_THAT(request_handler_.last_incoming_signed_request(),
Optional(ReflectsSigningFailure()));
// Expect six accesses, four for issuance and two for redemption.
EXPECT_EQ(6, access_count_);
}
// When a redemption request is made in no-cors mode, a cross-origin redirect
// from issuer A to issuer B should result in recycling the original redemption
// request, obtaining an issuer A redemption record on success.
//
// Note: This isn't necessarily the behavior we'll end up wanting here; the test
// serves to document how redemption and redirects currently interact. For more
// on the interaction between Trust Tokens and redirects, see the "Handling
// redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
TrustTokenBrowsertest,
NoCorsModeCrossOriginRedirectRedemptionUsesOriginalOriginAsIssuer) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("Success", EvalJs(shell(), R"(
fetch('/issue',
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )"));
// `mode: 'no-cors'` on redemption has the effect that that redirecting a
// request will maintain the request's Trust Tokens state.
EXPECT_EQ("Success", EvalJs(shell(), R"(
fetch('/cross-site/b.test/redeem',
{ mode: 'no-cors',
privateToken: { version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )"));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(
fetch('/sign',
{ privateToken: { version: 1,
operation: 'send-redemption-record',
issuers: [$1]
} })
.then(()=>'Success'); )",
IssuanceOriginFromHost("a.test"))));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// When a signing operation fails, it isn't fatal, so the requests
// should always get sent successfully.
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(
fetch('/sign',
{ privateToken: { version: 1,
operation: 'send-redemption-record',
issuers: [$1]
} })
.then(()=>'Success'); )",
IssuanceOriginFromHost("b.test"))));
// There shouldn't have been a b.test redemption record attached to the
// request.
EXPECT_THAT(request_handler_.last_incoming_signed_request(),
Optional(ReflectsSigningFailure()));
// Expect four accesses, two for issuance and two for redemption.
EXPECT_EQ(4, access_count_);
}
// When a redemption request is made in no-cors mode, a cross-origin redirect
// from issuer A to issuer B should result in recycling the original redemption
// request and, in particular, sending the same token.
//
// Note: This isn't necessarily the behavior we'll end up wanting here; the test
// serves to document how redemption and redirects currently interact.
IN_PROC_BROWSER_TEST_F(
TrustTokenBrowsertest,
NoCorsModeCrossOriginRedirectRedemptionRecyclesSameRedemptionRequest) {
// Have issuance provide only a single token so that, if the redemption logic
// searches for a new token after redirect, the redemption will fail.
TrustTokenRequestHandler::Options options;
options.batch_size = 1;
request_handler_.UpdateOptions(std::move(options));
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
EXPECT_EQ("Success", EvalJs(shell(), R"(
fetch('/issue',
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )"));
// The redemption should succeed after the redirect, yielding an a.test
// redemption record (the redemption record correctly corresponding to a.test
// is covered by a prior test case).
EXPECT_EQ("Success", EvalJs(shell(), R"(
fetch('/cross-site/b.test/redeem',
{ mode: 'no-cors',
privateToken: { version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )"));
// Expect two accesses, one for issuance and one for redemption.
EXPECT_EQ(2, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
SigningRequiresRedemptionRecordInStorage) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
(async () => {
try {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
await fetch("/sign", {privateToken: {
version: 1,
operation: 'send-redemption-record',
issuers: [$1]} // b.test, set below
});
return "Requests succeeded";
} catch (err) {
return "Requests failed unexpectedly";
}
})(); )";
// We use EvalJs here, not ExecJs, because EvalJs waits for promises to
// resolve.
//
// When a signing operation fails, it isn't fatal, so the requests
// should always get sent successfully.
EXPECT_EQ(
"Requests succeeded",
EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("b.test"))));
EXPECT_THAT(request_handler_.last_incoming_signed_request(),
Optional(ReflectsSigningFailure()));
// Expect three access, one for issue, redeem, and sign.
EXPECT_EQ(3, access_count_);
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEndWithServiceWorker) {
ASSERT_TRUE(embedded_test_server()->Start());
const char* const hostname = "a.test";
ProvideRequestHandlerKeyCommitmentsToNetworkService({hostname});
const std::string origin = IssuanceOriginFromHost(hostname);
const GURL create_sw_url =
server_.GetURL(hostname, "/service_worker/create_service_worker.html");
EXPECT_TRUE(NavigateToURL(shell(), create_sw_url));
// call register function defined in create_sw_url with the service worker js
// file path
EXPECT_EQ("DONE",
EvalJs(shell(), "register('fetch_event_respond_with_fetch.js');"));
// Following navigate to empty html page makes fetch requests go through
// service worker. Requests do not go through service workers when commented
// out.
const GURL empty_page_url =
server_.GetURL(hostname, "/service_worker/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), empty_page_url));
const std::string trust_token_fetch_snippet = R"(
(async () => {
if (navigator.serviceWorker.controller === null) return "NotServiceWorker";
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
await fetch("/sign", {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: [$1]}});
return "TTSuccess"; })(); )";
EXPECT_EQ("TTSuccess",
EvalJs(shell(), JsReplace(trust_token_fetch_snippet, origin)));
EXPECT_THAT(
request_handler_.last_incoming_signed_request(),
Optional(AllOf(
HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));
// Expect three accesses, one for issue and one for redeem and one for sign.
EXPECT_EQ(3, access_count_);
}
// Test redemption limit. Make three refreshing redemption calls back to back
// and test whether the third one fails. This test does not mock time. It
// assumes time (in network process) elapsed between the first and the last
// redemption call is less than the hard coded limit (currently 48 hours).
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RedemptionLimit) {
// set request handler options batch size to more than 3
TrustTokenRequestHandler::Options options;
options.batch_size = 10;
request_handler_.UpdateOptions(std::move(options));
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
// issue options.batch_size many tokens
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-request' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/issue"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/redeem"))));
EXPECT_EQ("Success",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption',
refreshPolicy: 'refresh' } })
.then(()=>'Success'); )",
server_.GetURL("a.test", "/redeem"))));
// third redemption should fail
EXPECT_EQ("Error",
EvalJs(shell(), JsReplace(R"(fetch($1,
{ privateToken: { version: 1,
operation: 'token-redemption',
refreshPolicy: 'refresh' } })
.then(()=>'Success')
.catch(()=>'Error'); )",
server_.GetURL("a.test", "/redeem"))));
// Expect four accesses, one for issuance, one for redemption, and two for
// sign.
EXPECT_EQ(4, access_count_);
}
// Check whether depreciated fetch API where 'type' refers to operation
// type fails.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, CheckDepreciatedTypeField) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(fetch(
"/issue", {privateToken: {type: 'token-request'}})
.then(()=>'Success')
.catch(error => error.message); )";
EXPECT_THAT(EvalJs(shell(), command).ExtractString(),
HasSubstr("Failed to read the 'operation'\
property from 'PrivateToken': Required member is undefined."));
}
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
SendRedemptionRequestWithEmptyIssuers) {
ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
GURL start_url = server_.GetURL("a.test", "/title1.html");
ASSERT_TRUE(NavigateToURL(shell(), start_url));
std::string command = R"(
(async () => {
await fetch("/issue", {privateToken: {version: 1,
operation: 'token-request'}});
await fetch("/redeem", {privateToken: {version: 1,
operation: 'token-redemption'}});
return "Success"; })(); )";
ASSERT_EQ("Success", EvalJs(shell(), command));
command = R"(
fetch("/sign", {privateToken: {version: 1,
operation: 'send-redemption-record',
issuers: []}})
.then(() => 'Success')
.catch(error => error.message); )";
// fetch should throw due to empty issuer field
EXPECT_THAT(EvalJs(shell(), command).ExtractString(),
HasSubstr("Failed to execute 'fetch' on 'Window':\
privateToken: operation type 'send-redemption-record' requires that\
the 'issuers' field be present and contain at least one secure,\
HTTP(S) URL, but it was missing or empty."));
}
} // namespace content