blob: 4ee301e528b105a88d4696709c607a2f64b521f7 [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 "base/strings/string_util.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/thread_annotations.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
#include "services/network/trust_tokens/test/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
// These integration tests cover the interaction between the Trust Token API's
// Fetch and iframe surfaces and various configuration requiring Origin Trial
// tokens to execute some or all of the Trust Tokens operations (issuance,
// redemption, and signing).
//
// There are two configuration modes:
// - "third-party origin trial": all Trust Tokens operations require an origin
// trial token to execute and, if a token is missing, the Trust Tokens interface
// disppears so that attempts to execute operations will silently no-op. This is
// because the Trust Tokens interface manifests itself as an additional argument
// in fetch's RequestInit dictionary, which does not throw errors when
// unexpected arguments are provided.
// - "standard origin trial": only Trust Tokens issuance requires an origin
// trial token to execute and, if a token is missing, issuance will fail.
//
// As an example, consider
//
// fetch("https://chromium.org", {trustToken: {type: 'token-request'}}),
//
// a representative fetch with an associated Trust Tokens issuance operation.
// When Trust Tokens is completely disabled (e.g. "third-party origin trial"
// mode with no token), the trustToken argument will be ignored. On the other
// hand, when Trust Tokens is enabled but issuance is forbidden ("standard
// origin trial" mode with no token), this will reject with an exception.
namespace content {
namespace {
using ::testing::Combine;
using ::testing::Values;
using ::testing::ValuesIn;
// Trust Tokens has three interfaces: fetch, XHR, and iframe. However, the XHR
// and fetch interfaces use essentially identical code paths, so we exclude the
// XHR interface in order to save some test duration.
enum class Interface {
kFetch,
kIframe,
};
// Prints a string representation to use for generating test names.
std::string ToString(Interface interface) {
switch (interface) {
case Interface::kFetch:
return "Fetch";
case Interface::kIframe:
return "Iframe";
}
}
using Op = network::mojom::TrustTokenOperationType;
enum class Outcome {
// A request with Trust Tokens parameters should reach the network stack.
kSuccess,
// A request without Trust Tokens parameters should reach the network stack.
kSuccessWithoutTrustTokenParams,
// The Trust Tokens operation should error out. For the Fetch interface, this
// means an exception gets thrown; for the iframe interface, it means we
// continue the request with no Trust Tokens parameters (i.e., it's equivalent
// to kSuccessWithoutTrustTokenParams).
kFailure,
};
enum class TrialEnabled {
// The Trust Tokens operation at hand will be executed from a context with an
// origin trial token.
kEnabled,
// The Trust Tokens operation at hand will be executed from a context lacking
// an origin trial token.
kDisabled,
};
// Prints a string representation to use for generating test names.
std::string ToString(TrialEnabled trial_enabled) {
switch (trial_enabled) {
case TrialEnabled::kEnabled:
return "TrialEnabled";
case TrialEnabled::kDisabled:
return "TrialDisabled";
}
}
using TrialType = network::features::TrustTokenOriginTrialSpec;
// Prints a string representation to use for generating test names.
std::string ToString(TrialType trial_type) {
switch (trial_type) {
case TrialType::kAllOperationsRequireOriginTrial:
return "AllOpsNeedTrial";
case TrialType::kOnlyIssuanceRequiresOriginTrial:
return "OnlyIssuanceNeedsTrial";
default:
NOTREACHED();
return "";
}
}
struct TestDescription {
Op op;
Outcome outcome;
TrialType trial_type;
TrialEnabled trial_enabled;
};
class TrustTokenOriginTrialBrowsertest
: public ContentBrowserTest,
public ::testing::WithParamInterface<
std::tuple<Interface, TestDescription>> {
public:
TrustTokenOriginTrialBrowsertest() {
auto& field_trial_param =
network::features::kTrustTokenOperationsRequiringOriginTrial;
features_.InitAndEnableFeatureWithParameters(
network::features::kTrustTokens,
{{field_trial_param.name,
field_trial_param.GetName(std::get<1>(GetParam()).trial_type)}});
}
// kPageWithOriginTrialToken is a landing page from which we execute Trust
// Tokens operations in test cases that require an origin trial token to be
// present. We use a deterministic port and swap in the landing page with
// URLLoaderInterceptor, rather than serving the page from
// EmbeddedTestServer, because the token is generated offline and bound to a
// specific origin.
const GURL kPageWithOriginTrialToken{"http://localhost:5555"};
// kTrustTokenUrl is the destination URL of the executed Trust Tokens
// operations. It's arbitrary, since the tests just need to intercept
// requests en route to check if they bear Trust Tokens parameters.
const GURL kTrustTokenUrl{kPageWithOriginTrialToken.Resolve("/trust-token")};
protected:
// OnRequest is a URLLoaderInterceptor callback. It:
// - serves the Origin Trials token when navigating to the landing page
// - quits the run loop, and stores the obtained request, when receiving a
// request to the Trust Tokens URL
// - declines to intercept otherwise (e.g. on favicon load)
//
// For the reasons discussed in |kPageWithOriginTrialToken|'s member comment,
// we need to use an interceptor to serve the landing page. Since we're
// already stuck with having an interceptor around, we use the same
// interceptor---instead of, say, registering an EmbeddedTestServer
// callback---to verify that requests sent to the Trust Tokens endpoint bear
// (or omit) Trust Tokens parameters. (This also lets us avoid having to set
// up all of the server-side logic necessary for executing a Trust Tokens
// operation end to end.)
bool OnRequest(URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url == kPageWithOriginTrialToken) {
// Origin Trials key generated with:
//
// tools/origin_trials/generate_token.py --expire-days 5000 --version 3 \
// http://localhost:5555 TrustTokens
//
// Note that you can't have an origin trial token with expiry more than
// 2^31-1 seconds past the epoch, so (for instance) --expire-days 10000
// would not have generated a valid token.
URLLoaderInterceptor::WriteResponse(
base::ReplaceStringPlaceholders(
"HTTP/1.1 200 OK\n"
"Content-type: text/html\n"
"Origin-Trial: $1\n\n",
{"A220DaFwmOb78vs8TojpryN1mfL9+zHjNDdo+rJTwRcaPkCIzU4/"
"vP9pnSHyI2ye8WsoxToBprvd7YH+"
"SdR0FgAAAABTeyJvcmlnaW4iOiAiaHR0cDovL2xvY2FsaG9zdDo1NTU1IiwgImZ"
"lYXR1cmUiOiAiVHJ1c3RUb2tlbnMiLCAiZXhwaXJ5IjogMjAyNTQ1OTI0MX0="},
/*offsets=*/nullptr),
/*body=*/"", params->client.get());
return true;
}
if (params->url_request.url == kTrustTokenUrl) {
{
base::AutoLock lock(mutex_);
CHECK(!trust_token_request_)
<< "Unexpected second Trust Tokens request";
trust_token_request_ = params->url_request;
}
// Write a response here so that the request doesn't fail: this is
// necessary so that tests expecting kFailure do not erroneously pass in
// cases where the request does not error out.
URLLoaderInterceptor::WriteResponse(
"HTTP/1.1 200 OK\nContent-type: text/html\n\n", /*body=*/"",
params->client.get());
base::OnceClosure done;
{
base::AutoLock lock(mutex_);
done = std::move(on_received_request_);
}
std::move(done).Run();
return true;
}
return false;
}
base::test::ScopedFeatureList features_;
// The request data is written on the IO sequence and read on the main
// sequence.
base::Lock mutex_;
// |on_received_request_| is called once a request arrives at
// |kTrustTokenUrl|; the request is then placed in |trust_token_request_|.
base::OnceClosure on_received_request_ GUARDED_BY(mutex_);
base::Optional<network::ResourceRequest> trust_token_request_
GUARDED_BY(mutex_);
};
const TestDescription kTestDescriptions[] = {
{Op::kIssuance, Outcome::kSuccess,
TrialType::kOnlyIssuanceRequiresOriginTrial, TrialEnabled::kEnabled},
{Op::kIssuance, Outcome::kFailure,
TrialType::kOnlyIssuanceRequiresOriginTrial, TrialEnabled::kDisabled},
{Op::kRedemption, Outcome::kSuccess,
TrialType::kOnlyIssuanceRequiresOriginTrial, TrialEnabled::kEnabled},
{Op::kRedemption, Outcome::kSuccess,
TrialType::kOnlyIssuanceRequiresOriginTrial, TrialEnabled::kDisabled},
{Op::kIssuance, Outcome::kSuccess,
TrialType::kAllOperationsRequireOriginTrial, TrialEnabled::kEnabled},
{Op::kIssuance, Outcome::kSuccessWithoutTrustTokenParams,
TrialType::kAllOperationsRequireOriginTrial, TrialEnabled::kDisabled},
{Op::kRedemption, Outcome::kSuccess,
TrialType::kAllOperationsRequireOriginTrial, TrialEnabled::kEnabled},
{Op::kRedemption, Outcome::kSuccessWithoutTrustTokenParams,
TrialType::kAllOperationsRequireOriginTrial, TrialEnabled::kDisabled},
};
// Prints a string representation to use for generating test names.
std::string ToString(Op op) {
switch (op) {
case Op::kIssuance:
return "Issuance";
case Op::kRedemption:
return "Redemption";
default:
NOTREACHED();
return "";
}
}
std::string TestParamToString(
const ::testing::TestParamInfo<std::tuple<Interface, TestDescription>>&
info) {
Interface interface = std::get<0>(info.param);
const TestDescription& test_description = std::get<1>(info.param);
return base::ReplaceStringPlaceholders(
"$1_$2_$3_$4",
{ToString(interface), ToString(test_description.op),
ToString(test_description.trial_type),
ToString(test_description.trial_enabled)},
nullptr);
}
} // namespace
// Each parameter has to be a valid JSON encoding of a TrustToken JS object
// *and* valid to directly substitute into JS: this is because the iframe API
// requires a JSON encoding of the parameters object, while the Fetch and XHR
// APIs require actual objects.
INSTANTIATE_TEST_SUITE_P(ExecutingAllOperations,
TrustTokenOriginTrialBrowsertest,
Combine(Values(Interface::kFetch, Interface::kIframe),
ValuesIn(kTestDescriptions)),
&TestParamToString);
// Test that a Trust Tokens request passes parameters to the network stack
// only when permitted by the origin trials framework (either because
// configuration specifies that no origin trial token is required, or because an
// origin trial token is present in the executing context).
IN_PROC_BROWSER_TEST_P(TrustTokenOriginTrialBrowsertest,
ProvidesParamsOnlyWhenAllowed) {
TestDescription test_description = std::get<1>(GetParam());
URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[this](URLLoaderInterceptor::RequestParams* params) {
return OnRequest(params);
}));
switch (test_description.trial_enabled) {
case TrialEnabled::kEnabled:
ASSERT_TRUE(NavigateToURL(shell(), kPageWithOriginTrialToken));
break;
case TrialEnabled::kDisabled:
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/title1.html")));
break;
}
base::RunLoop run_loop;
{
base::AutoLock lock(mutex_);
on_received_request_ = run_loop.QuitClosure();
}
network::TrustTokenTestParameters trust_token_params(
test_description.op, base::nullopt, base::nullopt, base::nullopt,
base::nullopt, base::nullopt, base::nullopt);
network::TrustTokenParametersAndSerialization
expected_params_and_serialization =
network::SerializeTrustTokenParametersAndConstructExpectation(
trust_token_params);
std::string command;
switch (std::get<0>(GetParam()) /* interface */) {
case Interface::kFetch:
command = JsReplace("fetch($1, {trustToken: ", kTrustTokenUrl) +
expected_params_and_serialization.serialized_params + "});";
break;
case Interface::kIframe:
command = JsReplace(
"let iframe = document.createElement('iframe');"
"iframe.src = $1;"
"iframe.trustToken = $2;"
"document.body.appendChild(iframe);",
kTrustTokenUrl, expected_params_and_serialization.serialized_params);
// When a Trust Tokens operation fails via the iframe interface, the
// request itself will still execute, just without an associated Trust
// Tokens operation.
if (test_description.outcome == Outcome::kFailure)
test_description.outcome = Outcome::kSuccessWithoutTrustTokenParams;
break;
}
if (test_description.outcome == Outcome::kFailure) {
// Use EvalJs here to wait for promises to resolve.
EXPECT_FALSE(EvalJs(shell(), command).error.empty());
return;
}
ASSERT_TRUE(ExecJs(shell(), command));
run_loop.Run();
// URLLoaderInterceptor writes to trust_token_request_ on the IO sequence.
base::AutoLock lock(mutex_);
ASSERT_TRUE(trust_token_request_);
switch (test_description.outcome) {
case Outcome::kSuccess:
EXPECT_TRUE(trust_token_request_->trust_token_params);
break;
case Outcome::kSuccessWithoutTrustTokenParams:
EXPECT_FALSE(trust_token_request_->trust_token_params);
break;
case Outcome::kFailure:
NOTREACHED(); // Handled earlier.
}
}
} // namespace content