blob: 57edb2bcf0625604770212d01f721c011b46a4e3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <tuple>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/loader/prefetch_url_loader_service_context.h"
#include "content/browser/loader/subresource_proxying_url_loader_service.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/web_package/prefetched_signed_exchange_cache.h"
#include "content/browser/web_package/signed_exchange_handler.h"
#include "content/browser/web_package/signed_exchange_utils.h"
#include "content/common/content_constants_internal.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/network_service_util.h"
#include "content/public/browser/ssl_status.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/page_type.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_cert_verifier_browser_test.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/signed_exchange_browser_test_helper.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_navigation_throttle.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_download_manager_delegate.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/mock_reduce_accept_language_controller_delegate.h"
#include "media/media_buildflags.h"
#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
#include "net/base/features.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/ct_policy_status.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/cert/test_root_certs.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_request_headers.h"
#include "net/http/transport_security_state.h"
#include "net/http/transport_security_state_test_util.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/test_data_directory.h"
#include "net/test/url_request/url_request_mock_http_job.h"
#include "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
constexpr char kLoadResultHistogram[] = "SignedExchange.LoadResult2";
constexpr char kPrefetchResultHistogram[] =
"SignedExchange.Prefetch.LoadResult2";
constexpr char kRedirectLoopHistogram[] = "SignedExchange.FallbackRedirectLoop";
class RedirectObserver : public WebContentsObserver {
public:
explicit RedirectObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
RedirectObserver(const RedirectObserver&) = delete;
RedirectObserver& operator=(const RedirectObserver&) = delete;
~RedirectObserver() override = default;
void DidRedirectNavigation(NavigationHandle* handle) override {
const net::HttpResponseHeaders* response = handle->GetResponseHeaders();
if (response)
response_code_ = response->response_code();
}
const std::optional<int>& response_code() const { return response_code_; }
private:
std::optional<int> response_code_;
};
class AssertNavigationHandleFlagObserver : public WebContentsObserver {
public:
explicit AssertNavigationHandleFlagObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
AssertNavigationHandleFlagObserver(
const AssertNavigationHandleFlagObserver&) = delete;
AssertNavigationHandleFlagObserver& operator=(
const AssertNavigationHandleFlagObserver&) = delete;
~AssertNavigationHandleFlagObserver() override = default;
void DidFinishNavigation(NavigationHandle* handle) override {
EXPECT_TRUE(handle->IsSignedExchangeInnerResponse());
}
};
class FinishNavigationObserver : public WebContentsObserver {
public:
FinishNavigationObserver(WebContents* contents,
base::OnceClosure done_closure)
: WebContentsObserver(contents), done_closure_(std::move(done_closure)) {}
FinishNavigationObserver(const FinishNavigationObserver&) = delete;
FinishNavigationObserver& operator=(const FinishNavigationObserver&) = delete;
void DidFinishNavigation(NavigationHandle* navigation_handle) override {
error_code_ = navigation_handle->GetNetErrorCode();
std::move(done_closure_).Run();
}
const std::optional<net::Error>& error_code() const { return error_code_; }
private:
base::OnceClosure done_closure_;
std::optional<net::Error> error_code_;
};
class MockContentBrowserClient final
: public ContentBrowserTestContentBrowserClient {
public:
std::string GetAcceptLangs(BrowserContext* context) override {
return accept_langs_;
}
void SetAcceptLangs(const std::string& langs) { accept_langs_ = langs; }
private:
std::string accept_langs_ = "en";
};
} // namespace
class SignedExchangeRequestHandlerBrowserTestBase
: public CertVerifierBrowserTest {
public:
explicit SignedExchangeRequestHandlerBrowserTestBase(
bool use_prefetch = false)
: use_prefetch_(use_prefetch) {
// Enable BackForwardCache for now as some tests are flaky when the previous
// RenderFrameHost doesn't change on navigation (the histograms are not
// recoded correctly).
// TODO(crbug.com/40242189): Figure out why and fix.
feature_list_.InitAndEnableFeature(features::kBackForwardCache);
}
SignedExchangeRequestHandlerBrowserTestBase(
const SignedExchangeRequestHandlerBrowserTestBase&) = delete;
SignedExchangeRequestHandlerBrowserTestBase& operator=(
const SignedExchangeRequestHandlerBrowserTestBase&) = delete;
void SetUp() override {
sxg_test_helper_.SetUp();
CertVerifierBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
CertVerifierBrowserTest::SetUpOnMainThread();
inactive_rfh_deletion_observer_ =
std::make_unique<InactiveRenderFrameHostDeletionObserver>(
shell()->web_contents());
client_ = std::make_unique<MockContentBrowserClient>();
}
void TearDownOnMainThread() override {
sxg_test_helper_.TearDownOnMainThread();
client_.reset();
}
protected:
bool UsePrefetch() const { return use_prefetch_; }
void MaybeTriggerPrefetchSXG(const GURL& url, bool expect_success) {
if (!UsePrefetch()) {
return;
}
const GURL prefetch_html_url = embedded_test_server()->GetURL(
std::string("/sxg/prefetch.html#") + url.spec());
std::u16string expected_title =
base::ASCIIToUTF16(expect_success ? "OK" : "FAIL");
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
EXPECT_TRUE(NavigateToURL(shell(), prefetch_html_url));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
if (expect_success) {
WaitUntilSXGIsCached(url);
}
}
void RunSimpleTest(std::string_view sxg_path);
void InstallUrlInterceptor(const GURL& url, const std::string& data_path) {
sxg_test_helper_.InstallUrlInterceptor(url, data_path);
}
void InstallMockCert() {
sxg_test_helper_.InstallMockCert(mock_cert_verifier());
}
void InstallMockCertChainInterceptor() {
sxg_test_helper_.InstallMockCertChainInterceptor();
}
// Make the MockCertVerifier treat the signed exchange's certificate
// "prime256v1-sha256.public.pem" as valid for "test.example.org", but
// issued by a known root.
void InstallMockCertByKnownRoot() {
scoped_refptr<net::X509Certificate> original_cert =
SignedExchangeBrowserTestHelper::LoadCertificate();
net::CertVerifyResult dummy_result;
dummy_result.verified_cert = original_cert;
dummy_result.cert_status = net::OK;
dummy_result.ocsp_result.response_status = bssl::OCSPVerifyResult::PROVIDED;
dummy_result.ocsp_result.revocation_status =
bssl::OCSPRevocationStatus::GOOD;
dummy_result.is_issued_by_known_root = true;
mock_cert_verifier()->AddResultForCertAndHost(
original_cert, "test.example.org", dummy_result, net::OK);
}
void SetAcceptLangs(const std::string& langs) {
client_->SetAcceptLangs(langs);
StoragePartitionImpl* partition =
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition());
partition->GetSubresourceProxyingURLLoaderService()
->prefetch_url_loader_service_context_for_testing()
.SetAcceptLanguages(langs);
// Set the Accept-Language for delegate in order to get correct
// Accept-Language in navigation requests instead of always using the
// default shell Accept-Language.
MockReduceAcceptLanguageControllerDelegate* delegate =
static_cast<MockReduceAcceptLanguageControllerDelegate*>(
shell()
->web_contents()
->GetBrowserContext()
->GetReduceAcceptLanguageControllerDelegate());
delegate->SetUserAcceptLanguages(langs);
}
std::unique_ptr<InactiveRenderFrameHostDeletionObserver>
inactive_rfh_deletion_observer_;
const base::HistogramTester histogram_tester_;
std::unique_ptr<MockContentBrowserClient> client_;
private:
class CacheObserver : public PrefetchedSignedExchangeCache::TestObserver {
public:
CacheObserver(const GURL& outer_url, base::OnceClosure quit_closure)
: outer_url_(outer_url), quit_closure_(std::move(quit_closure)) {}
CacheObserver(const CacheObserver&) = delete;
CacheObserver& operator=(const CacheObserver&) = delete;
~CacheObserver() override = default;
void OnStored(PrefetchedSignedExchangeCache* cache,
const GURL& outer_url) override {
if (quit_closure_ && (outer_url_ == outer_url)) {
std::move(quit_closure_).Run();
}
}
private:
const GURL outer_url_;
base::OnceClosure quit_closure_;
};
void WaitUntilSXGIsCached(const GURL& url) {
scoped_refptr<PrefetchedSignedExchangeCache> cache =
static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame())
->EnsurePrefetchedSignedExchangeCache();
if (cache->GetExchanges().find(url) != cache->GetExchanges().end()) {
return;
}
base::RunLoop run_loop;
auto observer =
std::make_unique<CacheObserver>(url, run_loop.QuitClosure());
cache->AddObserverForTesting(observer.get());
run_loop.Run();
cache->RemoveObserverForTesting(observer.get());
}
const bool use_prefetch_ = false;
base::test::ScopedFeatureList feature_list_;
SignedExchangeBrowserTestHelper sxg_test_helper_;
};
void SignedExchangeRequestHandlerBrowserTestBase::RunSimpleTest(
std::string_view sxg_path) {
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(sxg_path);
MaybeTriggerPrefetchSXG(url, true);
if (!UsePrefetch()) {
// Need to be in a page to execute JavaScript to trigger renderer initiated
// navigation.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
}
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
AssertNavigationHandleFlagObserver assert_navigation_handle_flag_observer(
shell()->web_contents());
// PrefetchedSignedExchangeCache is used only for renderer initiated
// navigation. So use JavaScript to trigger renderer initiated navigation.
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
base::StringPrintf("location.href = '%s';", url.spec().c_str())));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetVisibleEntry();
EXPECT_TRUE(entry->GetSSL().initialized);
EXPECT_FALSE(!!(entry->GetSSL().content_status &
SSLStatus::DISPLAYED_INSECURE_CONTENT));
ASSERT_TRUE(entry->GetSSL().certificate);
// "test.example.org.public.pem.cbor" is generated from
// "prime256v1-sha256.public.pem". So the SHA256 of the certificates must
// match.
const net::SHA256HashValue fingerprint =
net::X509Certificate::CalculateFingerprint256(
entry->GetSSL().certificate->cert_buffer());
scoped_refptr<net::X509Certificate> original_cert =
SignedExchangeBrowserTestHelper::LoadCertificate();
const net::SHA256HashValue original_fingerprint =
net::X509Certificate::CalculateFingerprint256(
original_cert->cert_buffer());
EXPECT_EQ(original_fingerprint, fingerprint);
// Wait for the previous page's RFH to be deleted (if it changed) so that the
// histograms will get updated.
inactive_rfh_deletion_observer_->Wait();
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kSuccess, 1);
histogram_tester_.ExpectTotalCount(
"SignedExchange.Time.CertificateFetch.Success", 1);
if (UsePrefetch()) {
histogram_tester_.ExpectUniqueSample(kPrefetchResultHistogram,
SignedExchangeLoadResult::kSuccess, 1);
histogram_tester_.ExpectTotalCount("PrefetchedSignedExchangeCache.Count",
1);
}
}
class SignedExchangeRequestHandlerBrowserTest
: public testing::WithParamInterface<bool>,
public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRequestHandlerBrowserTest()
: SignedExchangeRequestHandlerBrowserTestBase(GetParam()) {}
~SignedExchangeRequestHandlerBrowserTest() override = default;
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return info.param ? "WithPrefetch" : "WithoutPrefetch";
}
};
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, Simple) {
RunSimpleTest("/sxg/test.example.org_test.sxg");
}
class SignedExchangeRendererSideContentDecodingBrowserTest
: public testing::WithParamInterface<std::tuple<bool, bool>>,
public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRendererSideContentDecodingBrowserTest()
: SignedExchangeRequestHandlerBrowserTestBase(std::get<0>(GetParam())) {
if (std::get<1>(GetParam())) {
features_.InitWithFeatures(
{network::features::kRendererSideContentDecoding}, {});
} else {
features_.InitWithFeatures(
{}, {network::features::kRendererSideContentDecoding});
}
}
~SignedExchangeRendererSideContentDecodingBrowserTest() override = default;
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return base::StrCat({
std::get<0>(info.param) ? "WithPrefetch" : "WithoutPrefetch",
std::get<1>(info.param) ? "FeatureEnabled" : "FeatureDisabled",
});
}
private:
base::test::ScopedFeatureList features_;
};
INSTANTIATE_TEST_SUITE_P(
All,
SignedExchangeRendererSideContentDecodingBrowserTest,
::testing::Combine(::testing::Bool(), ::testing::Bool()),
&SignedExchangeRendererSideContentDecodingBrowserTest::DescribeParams);
IN_PROC_BROWSER_TEST_P(SignedExchangeRendererSideContentDecodingBrowserTest,
Compressed) {
RunSimpleTest("/sxg/test.example.org_test.sxg.gz");
}
class SignedExchangeRendererSideContentDecodingFailureBrowserTest
: public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRendererSideContentDecodingFailureBrowserTest() {
features_.InitWithFeaturesAndParameters(
{{network::features::kRendererSideContentDecoding,
{{"RendererSideContentDecodingForceMojoFailureForTesting", "true"}}}},
{});
}
~SignedExchangeRendererSideContentDecodingFailureBrowserTest() override =
default;
private:
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(
SignedExchangeRendererSideContentDecodingFailureBrowserTest,
Compressed) {
std::string_view sxg_path = "/sxg/test.example.org_test.sxg.gz";
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url = embedded_test_server()->GetURL(sxg_path);
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(shell()->web_contents(),
run_loop.QuitClosure());
EXPECT_FALSE(NavigateToURL(shell()->web_contents(), url));
run_loop.Run();
EXPECT_THAT(finish_navigation_observer.error_code(),
net::ERR_INSUFFICIENT_RESOURCES);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, VariantMatch) {
SetAcceptLangs("fr,en-US");
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_fr_variant.sxg");
MaybeTriggerPrefetchSXG(url, true);
if (!UsePrefetch()) {
// Need to be in a page to execute JavaScript to trigger renderer initiated
// navigation.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
}
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
// PrefetchedSignedExchangeCache is used only for renderer initiated
// navigation. So use JavaScript to trigger renderer initiated navigation.
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
base::StringPrintf("location.href = '%s';", url.spec().c_str())));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// Wait for the previous page's RFH to be deleted (if it changed) so that the
// histograms will get updated.
inactive_rfh_deletion_observer_->Wait();
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kSuccess, 1);
if (UsePrefetch()) {
histogram_tester_.ExpectTotalCount("PrefetchedSignedExchangeCache.Count",
1);
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
VariantMismatch) {
SetAcceptLangs("en-US,ja");
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_fr_variant.sxg");
MaybeTriggerPrefetchSXG(url, false);
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kVariantMismatch,
UsePrefetch() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
MissingNosniff) {
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_test_missing_nosniff.sxg");
MaybeTriggerPrefetchSXG(url, false);
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kSXGServedWithoutNosniff,
UsePrefetch() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
InvalidContentType) {
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_test_invalid_content_type.sxg");
MaybeTriggerPrefetchSXG(url, false);
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kVersionMismatch,
UsePrefetch() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, Expired) {
signed_exchange_utils::SetVerificationTimeForTesting(
base::Time::UnixEpoch() +
base::Seconds(SignedExchangeBrowserTestHelper::kSignatureHeaderExpires +
1));
InstallMockCertChainInterceptor();
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram,
SignedExchangeLoadResult::kSignatureVerificationError, 1);
histogram_tester_.ExpectUniqueSample(
"SignedExchange.SignatureVerificationResult",
SignedExchangeSignatureVerifier::Result::kErrExpired, 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
RedirectBrokenSignedExchanges) {
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
constexpr const char* kBrokenExchanges[] = {
"/sxg/test.example.org_test_invalid_magic_string.sxg",
"/sxg/test.example.org_test_invalid_cbor_header.sxg",
};
for (const auto* broken_exchange : kBrokenExchanges) {
SCOPED_TRACE(testing::Message()
<< "testing broken exchange: " << broken_exchange);
GURL url = embedded_test_server()->GetURL(broken_exchange);
MaybeTriggerPrefetchSXG(url, false);
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
histogram_tester_.ExpectTotalCount(kLoadResultHistogram,
UsePrefetch() ? 4 : 2);
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kVersionMismatch,
UsePrefetch() ? 2 : 1);
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kHeaderParseError,
UsePrefetch() ? 2 : 1);
}
// TODO(crbug.com/41460883): Fails pretty often on Android.
// TODO(crbug.com/40201215): Fails flakily on all platforms with Synchronous
// HTML Parsing enabled.
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
DISABLED_BadMICE) {
InstallMockCertChainInterceptor();
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_test_bad_mice.sxg");
MaybeTriggerPrefetchSXG(url, false);
const std::u16string title_good = u"Reached End: false";
const std::u16string title_bad = u"Reached End: true";
TitleWatcher title_watcher(shell()->web_contents(), title_good);
title_watcher.AlsoWaitForTitle(title_bad);
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title_good, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectTotalCount(kLoadResultHistogram,
UsePrefetch() ? 2 : 1);
{
SCOPED_TRACE(testing::Message()
<< "testing SignedExchangeLoadResult::kMerkleIntegrityError");
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kMerkleIntegrityError,
UsePrefetch() ? 2 : 1);
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, BadMICESmall) {
InstallMockCertChainInterceptor();
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_test_bad_mice_small.sxg");
MaybeTriggerPrefetchSXG(url, false);
// Note: TitleWatcher is not needed. NavigateToURL waits until navigation
// complete.
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
histogram_tester_.ExpectTotalCount(kLoadResultHistogram,
UsePrefetch() ? 2 : 1);
{
SCOPED_TRACE(testing::Message()
<< "testing SignedExchangeLoadResult::kMerkleIntegrityError");
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kMerkleIntegrityError,
UsePrefetch() ? 2 : 1);
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, CertNotFound) {
InstallUrlInterceptor(GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/404.msg");
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
MaybeTriggerPrefetchSXG(url, false);
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kCertFetchError,
UsePrefetch() ? 2 : 1);
histogram_tester_.ExpectTotalCount(
"SignedExchange.Time.CertificateFetch.Failure", UsePrefetch() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
VaryCookieSxgUsed) {
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_vary_cookie.sxg");
MaybeTriggerPrefetchSXG(url, true);
if (!UsePrefetch()) {
// Need to be in a page to execute JavaScript to trigger renderer initiated
// navigation.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
}
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
// PrefetchedSignedExchangeCache is used only for renderer initiated
// navigation. So use JavaScript to trigger renderer initiated navigation.
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
base::StringPrintf("location.href = '%s';", url.spec().c_str())));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kSuccess, 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
VaryCookieSxgNotUsed) {
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(),
GURL("https://test.example.org/test/"), "milk=1"));
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_vary_cookie.sxg");
MaybeTriggerPrefetchSXG(url, true);
if (!UsePrefetch()) {
// Need to be in a page to execute JavaScript to trigger renderer initiated
// navigation.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
}
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
// PrefetchedSignedExchangeCache is used only for renderer initiated
// navigation. So use JavaScript to trigger renderer initiated navigation.
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
base::StringPrintf("location.href = '%s';", url.spec().c_str())));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
if (UsePrefetch()) {
// kSuccess is recorded for successful prefetch, and
// kHadCookieForCookielessOnlySXG is recorded by not being used for actual
// navigation.
histogram_tester_.ExpectBucketCount(kLoadResultHistogram,
SignedExchangeLoadResult::kSuccess, 1);
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram,
SignedExchangeLoadResult::kHadCookieForCookielessOnlySXG, 1);
histogram_tester_.ExpectTotalCount(kLoadResultHistogram, 2);
} else {
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram,
SignedExchangeLoadResult::kHadCookieForCookielessOnlySXG, 1);
}
}
INSTANTIATE_TEST_SUITE_P(
All,
SignedExchangeRequestHandlerBrowserTest,
::testing::Bool(),
&SignedExchangeRequestHandlerBrowserTest::DescribeParams);
class SignedExchangeRequestHandlerDownloadBrowserTest
: public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRequestHandlerDownloadBrowserTest() = default;
~SignedExchangeRequestHandlerDownloadBrowserTest() override = default;
protected:
class DownloadObserver : public DownloadManager::Observer {
public:
DownloadObserver(DownloadManager* manager) : manager_(manager) {
manager_->AddObserver(this);
}
~DownloadObserver() override { manager_->RemoveObserver(this); }
void WaitUntilDownloadCreated() { run_loop_.Run(); }
const GURL& observed_url() const { return url_; }
const std::string& observed_content_disposition() const {
return content_disposition_;
}
// content::DownloadManager::Observer implementation.
void OnDownloadCreated(content::DownloadManager* manager,
download::DownloadItem* item) override {
url_ = item->GetURL();
content_disposition_ = item->GetContentDisposition();
run_loop_.Quit();
}
private:
raw_ptr<DownloadManager> manager_;
base::RunLoop run_loop_;
GURL url_;
std::string content_disposition_;
};
void SetUpOnMainThread() override {
SignedExchangeRequestHandlerBrowserTestBase::SetUpOnMainThread();
ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir());
ShellDownloadManagerDelegate* delegate =
static_cast<ShellDownloadManagerDelegate*>(
shell()
->web_contents()
->GetBrowserContext()
->GetDownloadManagerDelegate());
delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath());
}
private:
base::ScopedTempDir downloads_directory_;
};
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest,
Download) {
std::unique_ptr<DownloadObserver> observer =
std::make_unique<DownloadObserver>(
shell()->web_contents()->GetBrowserContext()->GetDownloadManager());
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
const std::string load_sxg =
"const iframe = document.createElement('iframe');"
"iframe.src = './sxg/test.example.org_test_download.sxg';"
"document.body.appendChild(iframe);";
EXPECT_TRUE(ExecJs(shell()->web_contents(), load_sxg));
observer->WaitUntilDownloadCreated();
EXPECT_EQ(
embedded_test_server()->GetURL("/sxg/test.example.org_test_download.sxg"),
observer->observed_url());
EXPECT_EQ("attachment; filename=test.sxg",
observer->observed_content_disposition());
}
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest,
DownloadInnerResponse) {
InstallMockCert();
InstallMockCertChainInterceptor();
std::unique_ptr<DownloadObserver> observer =
std::make_unique<DownloadObserver>(
shell()->web_contents()->GetBrowserContext()->GetDownloadManager());
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
const std::string load_sxg =
"const iframe = document.createElement('iframe');"
"iframe.src = './sxg/test.example.org_bad_content_type.sxg';"
"document.body.appendChild(iframe);";
// Since the inner response has an invalid Content-Type and MIME sniffing
// is disabled for Signed Exchange inner response, this should download the
// inner response of the exchange.
EXPECT_TRUE(ExecJs(shell()->web_contents(), load_sxg));
observer->WaitUntilDownloadCreated();
EXPECT_EQ(GURL("https://test.example.org/test/"), observer->observed_url());
}
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest,
DataURLDownload) {
const GURL sxg_url = GURL("data:application/signed-exchange,");
std::unique_ptr<DownloadObserver> observer =
std::make_unique<DownloadObserver>(
shell()->web_contents()->GetBrowserContext()->GetDownloadManager());
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
const std::string load_sxg = base::StringPrintf(
"const iframe = document.createElement('iframe');"
"iframe.src = '%s';"
"document.body.appendChild(iframe);",
sxg_url.spec().c_str());
EXPECT_TRUE(ExecJs(shell()->web_contents(), load_sxg));
observer->WaitUntilDownloadCreated();
EXPECT_EQ(sxg_url, observer->observed_url());
}
class SignedExchangeRequestHandlerRealCertVerifierBrowserTest
: public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRequestHandlerRealCertVerifierBrowserTest() {
// Use "real" CertVerifier.
disable_mock_cert_verifier();
// This installs "root_ca_cert.pem" from which our test certificates are
// created. (Needed for the tests that use real certificate, i.e.
// RealCertVerifier)
scoped_test_root_ = net::EmbeddedTestServer::RegisterTestCerts();
}
void SetUp() override {
SignedExchangeHandler::SetShouldIgnoreCertValidityPeriodErrorForTesting(
true);
SignedExchangeRequestHandlerBrowserTestBase::SetUp();
}
void TearDown() override {
SignedExchangeRequestHandlerBrowserTestBase::TearDown();
SignedExchangeHandler::SetShouldIgnoreCertValidityPeriodErrorForTesting(
false);
}
private:
net::ScopedTestRoot scoped_test_root_;
};
// If this fails with ERR_CERT_DATE_INVALID, try to regenerate test data
// by running generate-test-certs.sh and generate-test-sxgs.sh in
// src/content/test/data/sxg. See https://crbug.com/1279652.
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerRealCertVerifierBrowserTest,
Basic) {
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org-long-validity.public.pem.cbor");
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_long_cert_validity.sxg");
// This signed exchange should pass CertVerifier::Verify() and then fail at
// SignedExchangeHandler::CheckOCSPStatus() because of the dummy OCSP
// response.
// TODO(crbug.com/40564303): Make this test pass the OCSP check. We'll
// need to either generate an OCSP response on the fly, or override the OCSP
// verification time.
std::u16string title = u"Fallback URL response";
TitleWatcher title_watcher(shell()->web_contents(), title);
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// Verify that it failed at the OCSP check step.
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kOCSPError, 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
LogicalUrlInServiceWorkerScope) {
// SW-scope: https://test.example.org/test/
// SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
const GURL install_sw_url =
GURL("https://test.example.org/test/publisher-service-worker.html");
InstallUrlInterceptor(install_sw_url,
"content/test/data/sxg/publisher-service-worker.html");
InstallUrlInterceptor(
GURL("https://test.example.org/test/publisher-service-worker.js"),
"content/test/data/sxg/publisher-service-worker.js");
{
std::u16string title = u"Done";
TitleWatcher title_watcher(shell()->web_contents(), title);
EXPECT_TRUE(NavigateToURL(shell(), install_sw_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
title_watcher.AlsoWaitForTitle(u"Generated");
GURL expected_commit_url = GURL("https://test.example.org/test/");
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
// The page content shoud be served from the signed exchange.
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
// TODO(crbug.com/40890897): Re-enable this test when de-flaked.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_NotControlledByDistributorsSW \
DISABLED_NotControlledByDistributorsSW
#else
#define MAYBE_NotControlledByDistributorsSW NotControlledByDistributorsSW
#endif
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
MAYBE_NotControlledByDistributorsSW) {
// SW-scope: http://127.0.0.1:PORT/sxg/
// SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
const GURL install_sw_url = embedded_test_server()->GetURL(
"/sxg/no-respond-with-service-worker.html");
{
std::u16string title = u"Done";
TitleWatcher title_watcher(shell()->web_contents(), title);
EXPECT_TRUE(NavigateToURL(shell(), install_sw_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg"),
GURL("https://test.example.org/test/") /* expected_commit_url */));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// The page must not be controlled by the service worker of the physical URL.
EXPECT_EQ(false, EvalJs(shell(), "!!navigator.serviceWorker.controller"));
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
NotControlledBySameOriginDistributorsSW) {
// SW-scope: https://test.example.org/scope/
// SXG physical URL: https://test.example.org/scope/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
InstallUrlInterceptor(GURL("https://test.example.org/scope/test.sxg"),
"content/test/data/sxg/test.example.org_test.sxg");
const GURL install_sw_url = GURL(
"https://test.example.org/scope/no-respond-with-service-worker.html");
InstallUrlInterceptor(
install_sw_url,
"content/test/data/sxg/no-respond-with-service-worker.html");
InstallUrlInterceptor(
GURL("https://test.example.org/scope/no-respond-with-service-worker.js"),
"content/test/data/sxg/no-respond-with-service-worker.js");
{
std::u16string title = u"Done";
TitleWatcher title_watcher(shell()->web_contents(), title);
EXPECT_TRUE(NavigateToURL(shell(), install_sw_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
EXPECT_TRUE(NavigateToURL(
shell(), GURL("https://test.example.org/scope/test.sxg"),
GURL("https://test.example.org/test/") /* expected_commit_url */));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// The page must not be controlled by the service worker of the physical URL.
EXPECT_EQ(false, EvalJs(shell(), "!!navigator.serviceWorker.controller"));
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
RegisterServiceWorkerFromSignedExchange) {
// SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
InstallUrlInterceptor(
GURL("https://test.example.org/test/publisher-service-worker.js"),
"content/test/data/sxg/publisher-service-worker.js");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
GURL expected_commit_url = GURL("https://test.example.org/test/");
{
std::u16string title = u"https://test.example.org/test/";
TitleWatcher title_watcher(shell()->web_contents(), title);
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
const std::string register_sw_script =
"(async function() {"
" try {"
" const registration = await navigator.serviceWorker.register("
" 'publisher-service-worker.js', {scope: './'});"
" return true;"
" } catch (e) {"
" return false;"
" }"
"})();";
// serviceWorker.register() fails because the document URL of
// ServiceWorkerHost is empty.
EXPECT_EQ(false, EvalJs(shell()->web_contents(), register_sw_script));
}
class SignedExchangeAcceptHeaderBrowserTest : public ContentBrowserTest {
public:
using self = SignedExchangeAcceptHeaderBrowserTest;
SignedExchangeAcceptHeaderBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
~SignedExchangeAcceptHeaderBrowserTest() override = default;
protected:
void SetUp() override {
https_server_.ServeFilesFromSourceDirectory("content/test/data");
https_server_.RegisterRequestHandler(
base::BindRepeating(&self::RedirectResponseHandler));
https_server_.RegisterRequestHandler(base::BindRepeating(
&self::FallbackSxgResponseHandler, base::Unretained(this)));
https_server_.RegisterRequestMonitor(
base::BindRepeating(&self::MonitorRequest, base::Unretained(this)));
ASSERT_TRUE(https_server_.Start());
ContentBrowserTest::SetUp();
}
void NavigateAndWaitForTitle(const GURL& url, const std::string& title) {
std::u16string expected_title = base::ASCIIToUTF16(title);
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
}
void NavigateWithRedirectAndWaitForTitle(const GURL& url,
const GURL& expected_commit_url,
const std::string& title) {
std::u16string expected_title = base::ASCIIToUTF16(title);
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
EXPECT_TRUE(NavigateToURL(shell(), url, expected_commit_url));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
}
void CheckAcceptHeader(const GURL& url,
bool is_navigation,
bool is_fallback) {
const auto accept_header = GetInterceptedAcceptHeader(url);
ASSERT_TRUE(accept_header);
EXPECT_EQ(*accept_header,
!is_fallback
? base::StrCat({kFrameAcceptHeaderValue,
kAcceptHeaderSignedExchangeSuffix})
: (is_navigation
? std::string(kFrameAcceptHeaderValue)
: std::string(network::kDefaultAcceptHeaderValue)));
}
void CheckNavigationAcceptHeader(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
SCOPED_TRACE(url);
CheckAcceptHeader(url, true /* is_navigation */, false /* is_fallback */);
}
}
void CheckPrefetchAcceptHeader(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
SCOPED_TRACE(url);
CheckAcceptHeader(url, false /* is_navigation */,
false /* is_fallback */);
}
}
void CheckFallbackAcceptHeader(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
SCOPED_TRACE(url);
CheckAcceptHeader(url, true /* is_navigation */, true /* is_fallback */);
}
}
std::optional<std::string> GetInterceptedAcceptHeader(const GURL& url) const {
base::AutoLock lock(url_accept_header_map_lock_);
const auto it = url_accept_header_map_.find(url);
if (it == url_accept_header_map_.end())
return std::nullopt;
return it->second;
}
void ClearInterceptedAcceptHeaders() {
base::AutoLock lock(url_accept_header_map_lock_);
url_accept_header_map_.clear();
}
net::EmbeddedTestServer https_server_;
private:
static std::unique_ptr<net::test_server::HttpResponse>
RedirectResponseHandler(const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url, "/r?",
base::CompareCase::SENSITIVE)) {
return nullptr;
}
std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
new net::test_server::BasicHttpResponse);
http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
http_response->AddCustomHeader("Location", request.relative_url.substr(3));
http_response->AddCustomHeader("Cache-Control", "no-cache");
return std::move(http_response);
}
// Responds with a prologue-only signed exchange that triggers a fallback
// redirect.
std::unique_ptr<net::test_server::HttpResponse> FallbackSxgResponseHandler(
const net::test_server::HttpRequest& request) {
const std::string prefix = "/fallback_sxg?";
if (!base::StartsWith(request.relative_url, prefix,
base::CompareCase::SENSITIVE)) {
return nullptr;
}
std::string fallback_url(request.relative_url.substr(prefix.length()));
if (fallback_url.empty()) {
// If fallback URL is not specified, fallback to itself.
fallback_url = https_server_.GetURL(prefix).spec();
}
std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
new net::test_server::BasicHttpResponse);
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("application/signed-exchange;v=b3");
std::string sxg("sxg1-b3", 8);
sxg.push_back(fallback_url.length() >> 8);
sxg.push_back(fallback_url.length() & 0xff);
sxg += fallback_url;
// FallbackUrlAndAfter() requires 6 more bytes for sizes of next fields.
sxg.resize(sxg.length() + 6);
http_response->set_content(sxg);
return std::move(http_response);
}
void MonitorRequest(const net::test_server::HttpRequest& request) {
const auto it = request.headers.find(net::HttpRequestHeaders::kAccept);
if (it == request.headers.end())
return;
// Note this method is called on the EmbeddedTestServer's background thread.
base::AutoLock lock(url_accept_header_map_lock_);
url_accept_header_map_[request.base_url.Resolve(request.relative_url)] =
it->second;
}
base::test::ScopedFeatureList feature_list_;
base::test::ScopedFeatureList feature_list_for_accept_header_;
// url_accept_header_map_ is accessed both on the main thread and on the
// EmbeddedTestServer's background thread via MonitorRequest(), so it must be
// locked.
mutable base::Lock url_accept_header_map_lock_;
std::map<GURL, std::string> url_accept_header_map_;
};
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest, Simple) {
const GURL test_url = https_server_.GetURL("/sxg/test.html");
NavigateAndWaitForTitle(test_url, test_url.spec());
CheckNavigationAcceptHeader({test_url});
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest, Redirect) {
const GURL test_url = https_server_.GetURL("/sxg/test.html");
const GURL redirect_url = https_server_.GetURL("/r?" + test_url.spec());
const GURL redirect_redirect_url =
https_server_.GetURL("/r?" + redirect_url.spec());
NavigateWithRedirectAndWaitForTitle(redirect_redirect_url, test_url,
test_url.spec());
CheckNavigationAcceptHeader({redirect_redirect_url, redirect_url, test_url});
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest,
FallbackRedirect) {
const GURL fallback_url = https_server_.GetURL("/sxg/test.html");
const GURL test_url =
https_server_.GetURL("/fallback_sxg?" + fallback_url.spec());
NavigateWithRedirectAndWaitForTitle(test_url, fallback_url,
fallback_url.spec());
CheckNavigationAcceptHeader({test_url});
CheckFallbackAcceptHeader({fallback_url});
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest,
FallbackRedirectLoop) {
const base::HistogramTester histogram_tester;
base::RunLoop run_loop;
FinishNavigationObserver finish_navigation_observer(shell()->web_contents(),
run_loop.QuitClosure());
const GURL test_url = https_server_.GetURL("/fallback_sxg?");
EXPECT_FALSE(NavigateToURL(shell()->web_contents(), test_url));
run_loop.Run();
ASSERT_TRUE(finish_navigation_observer.error_code())
<< "Unexpected navigation success: " << test_url;
EXPECT_EQ(net::ERR_TOO_MANY_REDIRECTS,
*finish_navigation_observer.error_code());
histogram_tester.ExpectUniqueSample(kRedirectLoopHistogram, true, 1);
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest,
PrefetchEnabledPageEnabledTarget) {
const GURL target = https_server_.GetURL("/sxg/hello.txt");
const GURL page_url =
https_server_.GetURL(std::string("/sxg/prefetch.html#") + target.spec());
NavigateAndWaitForTitle(page_url, "OK");
CheckPrefetchAcceptHeader({target});
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest,
PrefetchRedirect) {
const GURL target = https_server_.GetURL("/sxg/hello.txt");
const GURL redirect_url = https_server_.GetURL("/r?" + target.spec());
const GURL redirect_redirect_url =
https_server_.GetURL("/r?" + redirect_url.spec());
const GURL page_url = https_server_.GetURL(
std::string("/sxg/prefetch.html#") + redirect_redirect_url.spec());
NavigateAndWaitForTitle(page_url, "OK");
CheckPrefetchAcceptHeader({redirect_redirect_url, redirect_url, target});
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest, ServiceWorker) {
NavigateAndWaitForTitle(https_server_.GetURL("/sxg/service-worker.html"),
"Done");
const std::string frame_accept = kFrameAcceptHeaderValue;
const std::string frame_accept_with_sxg =
frame_accept + std::string(kAcceptHeaderSignedExchangeSuffix);
const std::vector<std::string> scopes = {"/sxg/sw-scope-generated/",
"/sxg/sw-scope-navigation-preload/",
"/sxg/sw-scope-no-respond-with/"};
for (const auto& scope : scopes) {
SCOPED_TRACE(scope);
const bool is_generated_scope =
scope == std::string("/sxg/sw-scope-generated/");
const GURL target_url = https_server_.GetURL(scope + "test.html");
const GURL redirect_target_url =
https_server_.GetURL("/r?" + target_url.spec());
const GURL redirect_redirect_target_url =
https_server_.GetURL("/r?" + redirect_target_url.spec());
const std::string expected_title =
is_generated_scope ? frame_accept_with_sxg : "Done";
const std::optional<std::string> expected_target_accept_header =
is_generated_scope ? std::nullopt
: std::optional<std::string>(frame_accept_with_sxg);
NavigateAndWaitForTitle(target_url, expected_title);
EXPECT_EQ(expected_target_accept_header,
GetInterceptedAcceptHeader(target_url));
ClearInterceptedAcceptHeaders();
NavigateWithRedirectAndWaitForTitle(redirect_target_url, target_url,
expected_title);
CheckNavigationAcceptHeader({redirect_target_url});
EXPECT_EQ(expected_target_accept_header,
GetInterceptedAcceptHeader(target_url));
ClearInterceptedAcceptHeaders();
NavigateWithRedirectAndWaitForTitle(redirect_redirect_target_url,
target_url, expected_title);
CheckNavigationAcceptHeader(
{redirect_redirect_target_url, redirect_target_url});
EXPECT_EQ(expected_target_accept_header,
GetInterceptedAcceptHeader(target_url));
ClearInterceptedAcceptHeaders();
}
}
IN_PROC_BROWSER_TEST_F(SignedExchangeAcceptHeaderBrowserTest,
ServiceWorkerPrefetch) {
NavigateAndWaitForTitle(
https_server_.GetURL("/sxg/service-worker-prefetch.html"), "Done");
const std::string scope = "/sxg/sw-prefetch-scope/";
const GURL target_url = https_server_.GetURL(scope + "test.html");
const GURL prefetch_target =
https_server_.GetURL(std::string("/sxg/hello.txt"));
const std::string load_prefetch_script = base::StringPrintf(
"(function loadPrefetch(urls) {"
" for (let url of urls) {"
" let link = document.createElement('link');"
" link.rel = 'prefetch';"
" link.href = url;"
" document.body.appendChild(link);"
" }"
" async function check() {"
" while (true) {"
" const entries = performance.getEntriesByType('resource');"
" const url_set = new Set(urls);"
" for (let entry of entries) {"
" url_set.delete(entry.name);"
" }"
" if (!url_set.size) {"
" return true;"
" } else {"
" await new Promise(resolve => setTimeout(resolve, 100));"
" }"
" }"
" }"
" return check();"
"})(['%s'])",
prefetch_target.spec().c_str());
NavigateAndWaitForTitle(target_url, "Done");
EXPECT_EQ(true, EvalJs(shell()->web_contents(), load_prefetch_script));
CheckPrefetchAcceptHeader({prefetch_target});
ClearInterceptedAcceptHeaders();
}
#if BUILDFLAG(ENABLE_REPORTING)
class SignedExchangeReportingBrowserTest
: public SignedExchangeRequestHandlerBrowserTest {
public:
SignedExchangeReportingBrowserTest() {
feature_list_.InitAndEnableFeature(
net::features::kPartitionConnectionsByNetworkIsolationKey);
}
~SignedExchangeReportingBrowserTest() override = default;
void SetUpOnMainThread() override {
SignedExchangeRequestHandlerBrowserTestBase::SetUpOnMainThread();
// Make all attempts to connect to the domain the SXG is for fail with a DNS
// error. Without this, they're fail with ERR_NOT_IMPLEMENTED. Making
// requests fail with ERR_NAME_NOT_RESOLVED instead better matches what
// happens in production.
host_resolver()->AddSimulatedFailure("test.example.org");
host_resolver()->AddRule("prefetch-origin.test", "127.0.0.1");
// Set up certificate for the report server.
net::CertVerifyResult ssl_server_result;
ssl_server_result.verified_cert = ssl_server_.GetCertificate();
ssl_server_result.is_issued_by_known_root = false;
ssl_server_result.policy_compliance =
net::ct::CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS;
mock_cert_verifier()->AddResultForCert(ssl_server_.GetCertificate(),
ssl_server_result, net::OK);
// Make the MockCertVerifier treat the signed exchange's certificate
// "prime256v1-sha256.public.pem" as invalid for "test.example.org", which
// should generate a report.
scoped_refptr<net::X509Certificate> original_cert =
SignedExchangeBrowserTestHelper::LoadCertificate();
net::CertVerifyResult dummy_result;
dummy_result.cert_status = net::CERT_STATUS_AUTHORITY_INVALID;
dummy_result.verified_cert = original_cert;
mock_cert_verifier()->AddResultForCertAndHost(
original_cert, "test.example.org", dummy_result, net::OK);
InstallMockCertChainInterceptor();
learn_report_to_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
&ssl_server_, kLearnReportToPath);
report_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
&ssl_server_, kReportPath);
// Set up server used to serve the signed exchange.
ssl_server_.ServeFilesFromSourceDirectory("content/test/data");
EXPECT_TRUE(ssl_server_.Start());
}
const GURL sxg_url() const {
return ssl_server_.GetURL("/sxg/test.example.org_test.sxg");
}
// The URL the SXG resource claims to be for.
static GURL sxg_validity_url() {
return GURL("https://test.example.org/test/");
}
protected:
const char* kLearnReportToPath = "/report-to";
const char* kReportPath = "/report";
base::test::ScopedFeatureList feature_list_;
// This server both serves the signed exchange and receives the report for the
// certificate error.
net::test_server::EmbeddedTestServer ssl_server_{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
std::unique_ptr<net::test_server::ControllableHttpResponse>
learn_report_to_response_;
std::unique_ptr<net::test_server::ControllableHttpResponse> report_response_;
};
IN_PROC_BROWSER_TEST_P(SignedExchangeReportingBrowserTest,
CertErrorSendsReport) {
GURL report_url = ssl_server_.GetURL(kReportPath);
// Get Report-To and NEL information for the server that serves the signed
// exchange. The same site is also used for the NetworkAnonymizationKey. This
// should result in sending a report to that server a request for that signed
// exchange fails with a certificate error.
{
GURL learn_report_to_url = ssl_server_.GetURL(kLearnReportToPath);
TestNavigationObserver same_tab_observer(shell()->web_contents(),
1 /* number_of_navigations */);
NavigationController::LoadURLParams params(learn_report_to_url);
params.transition_type = ui::PageTransitionFromInt(
ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
shell()->web_contents()->GetController().LoadURLWithParams(params);
learn_report_to_response_->WaitForRequest();
learn_report_to_response_->Send("HTTP/1.1 200 OK\r\n");
learn_report_to_response_->Send("Report-To: {\"endpoints\":[{\"url\":\"" +
report_url.spec() +
"\"}],\"max_age\":86400}\r\n");
learn_report_to_response_->Send(
"NEL: {\"report_to\":\"default\",\"max_age\":86400}\r\n");
learn_report_to_response_->Send("\r\n");
learn_report_to_response_->Done();
same_tab_observer.Wait();
}
if (UsePrefetch()) {
// Matches MaybeTriggerPrefetchSXG(), but uses |ssl_server_| instead of
// embedded_test_server().
const GURL prefetch_html_url = ssl_server_.GetURL(
std::string("/sxg/prefetch.html#") + sxg_url().spec());
std::u16string expected_title = u"FAIL";
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
EXPECT_TRUE(NavigateToURL(shell(), prefetch_html_url));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
} else {
// Try to navigate to the signed exchange. The signed exchange fails due
// to the bad certificate. That results in trying to load the
// resource the SXG refers to directly, which should fail with
// ERR_NAME_NOT_RESOLVED.
NavigationHandleObserver observer(shell()->web_contents(), sxg_url());
EXPECT_FALSE(NavigateToURL(shell(), sxg_url()));
EXPECT_EQ(sxg_validity_url(),
shell()->web_contents()->GetLastCommittedURL());
EXPECT_EQ(net::ERR_NAME_NOT_RESOLVED, observer.net_error_code());
}
// Check that a report was received.
report_response_->WaitForRequest();
EXPECT_EQ(report_url, report_response_->http_request()->GetURL());
EXPECT_EQ(net::test_server::METHOD_POST,
report_response_->http_request()->method);
}
INSTANTIATE_TEST_SUITE_P(All,
SignedExchangeReportingBrowserTest,
::testing::Bool());
#endif // BUILDFLAG(ENABLE_REPORTING)
class SignedExchangePKPBrowserTest
: public SignedExchangeRequestHandlerBrowserTest {
public:
SignedExchangePKPBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
net::features::kStaticKeyPinningEnforcement);
}
void SetUpOnMainThread() override {
SignedExchangeRequestHandlerBrowserTest::SetUpOnMainThread();
// Make all attempts to connect to the domain the SXG is for fail with a DNS
// error. Without this, they're fail with ERR_NOT_IMPLEMENTED. Making
// requests fail with ERR_NAME_NOT_RESOLVED instead better matches what
// happens in production.
host_resolver()->AddSimulatedFailure("test.example.org");
// Use a mock cert issued by a known root, so that PKP violation will not be
// bypassed. It also causes a CT failure, but PKP error takes precedence.
InstallMockCertByKnownRoot();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
EXPECT_TRUE(embedded_test_server()->Start());
EnableStaticPins(embedded_test_server()->port());
}
void TearDownOnMainThread() override {
if (IsOutOfProcessNetworkService()) {
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
mojo::Remote<network::mojom::NetworkServiceTest> network_service_test;
GetNetworkService()->BindTestInterfaceForTesting(
network_service_test.BindNewPipeAndPassReceiver());
network_service_test->SetTransportSecurityStateSource(0);
} else {
RunOnIOThreadBlocking(
base::BindOnce(&SignedExchangePKPBrowserTest::CleanUpOnIOThread,
base::Unretained(this)));
}
SignedExchangeRequestHandlerBrowserTest::TearDownOnMainThread();
}
void EnableStaticPins(int reporting_port) {
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
StoragePartition* partition = shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition();
partition->GetNetworkContext()->EnableStaticKeyPinningForTesting();
partition->FlushNetworkInterfaceForTesting();
if (IsOutOfProcessNetworkService()) {
mojo::Remote<network::mojom::NetworkServiceTest> network_service_test;
GetNetworkService()->BindTestInterfaceForTesting(
network_service_test.BindNewPipeAndPassReceiver());
network_service_test->SetTransportSecurityStateSource(reporting_port);
} else {
// TODO(crbug.com/40649862): This code is not threadsafe, as the
// network stack does not run on the IO thread. Ideally, the
// NetworkServiceTest object would be set up in-process on the network
// service's thread, and this path would be removed.
RunOnIOThreadBlocking(base::BindOnce(
&SignedExchangePKPBrowserTest::SetTransportSecurityStateSourceOnIO,
base::Unretained(this), reporting_port));
}
}
private:
void RunOnIOThreadBlocking(base::OnceClosure task) {
base::RunLoop run_loop;
GetIOThreadTaskRunner({})->PostTaskAndReply(FROM_HERE, std::move(task),
run_loop.QuitClosure());
run_loop.Run();
}
void SetTransportSecurityStateSourceOnIO(int reporting_port) {
transport_security_state_source_ =
std::make_unique<net::ScopedTransportSecurityStateSource>(
reporting_port);
}
void CleanUpOnIOThread() { transport_security_state_source_.reset(); }
// Only used when NetworkService is disabled. Accessed on IO thread.
std::unique_ptr<net::ScopedTransportSecurityStateSource>
transport_security_state_source_;
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_P(SignedExchangePKPBrowserTest, PKPViolation) {
GURL sxg_url =
embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
GURL fallback_url("https://test.example.org/test/");
MaybeTriggerPrefetchSXG(sxg_url, false);
NavigationHandleObserver observer(shell()->web_contents(), sxg_url);
EXPECT_FALSE(NavigateToURL(shell(), sxg_url));
EXPECT_EQ(fallback_url, shell()->web_contents()->GetLastCommittedURL());
EXPECT_EQ(net::ERR_NAME_NOT_RESOLVED, observer.net_error_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kPKPViolationError,
UsePrefetch() ? 2 : 1);
}
INSTANTIATE_TEST_SUITE_P(All, SignedExchangePKPBrowserTest, ::testing::Bool());
} // namespace content