blob: 5a22a72b893baea840b5a8b7fc056d9ff43db864 [file] [log] [blame]
// Copyright 2018 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/files/file_path.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "content/browser/web_package/signed_exchange_handler.h"
#include "content/public/browser/browser_thread.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/ssl_status.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.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_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_throttle.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/test_data_directory.h"
#include "net/test/url_request/url_request_mock_http_job.h"
#include "net/url_request/url_request_filter.h"
#include "net/url_request/url_request_interceptor.h"
#include "services/network/public/cpp/features.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
namespace content {
namespace {
const uint64_t kSignatureHeaderDate = 1520834000; // 2018-03-12T05:53:20Z
class NavigationFailureObserver : public WebContentsObserver {
public:
explicit NavigationFailureObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
~NavigationFailureObserver() override = default;
void DidStartNavigation(NavigationHandle* handle) override {
auto throttle = std::make_unique<TestNavigationThrottle>(handle);
throttle->SetCallback(
TestNavigationThrottle::WILL_FAIL_REQUEST,
base::BindRepeating(&NavigationFailureObserver::OnWillFailRequestCalled,
base::Unretained(this)));
handle->RegisterThrottleForTesting(
std::unique_ptr<TestNavigationThrottle>(std::move(throttle)));
}
bool did_fail() const { return did_fail_; }
private:
void OnWillFailRequestCalled() { did_fail_ = true; }
bool did_fail_ = false;
DISALLOW_COPY_AND_ASSIGN(NavigationFailureObserver);
};
} // namespace
class SignedExchangeRequestHandlerBrowserTest : public ContentBrowserTest {
public:
SignedExchangeRequestHandlerBrowserTest()
: mock_cert_verifier_(std::make_unique<net::MockCertVerifier>()) {
// This installs "root_ca_cert.pem" from which our test certificates are
// created. (Needed for the tests that use real certificate, i.e.
// RealCertVerifier)
net::EmbeddedTestServer::RegisterTestCerts();
}
void SetUp() override {
SignedExchangeHandler::SetCertVerifierForTesting(mock_cert_verifier_.get());
SignedExchangeHandler::SetVerificationTimeForTesting(
base::Time::UnixEpoch() +
base::TimeDelta::FromSeconds(kSignatureHeaderDate));
SetUpFeatures();
ContentBrowserTest::SetUp();
}
void TearDownOnMainThread() override {
interceptor_.reset();
SignedExchangeHandler::SetCertVerifierForTesting(nullptr);
SignedExchangeHandler::SetVerificationTimeForTesting(
base::Optional<base::Time>());
}
protected:
virtual void SetUpFeatures() {
feature_list_.InitWithFeatures({features::kSignedHTTPExchange},
{network::features::kNetworkService});
}
static scoped_refptr<net::X509Certificate> LoadCertificate(
const std::string& cert_file) {
base::ScopedAllowBlockingForTesting allow_io;
base::FilePath dir_path;
base::PathService::Get(content::DIR_TEST_DATA, &dir_path);
dir_path = dir_path.AppendASCII("sxg");
return net::CreateCertificateChainFromFile(
dir_path, cert_file, net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE);
}
void InstallUrlInterceptor(const GURL& url, const std::string& data_path) {
if (base::FeatureList::IsEnabled(network::features::kNetworkService)) {
if (!interceptor_) {
interceptor_ =
std::make_unique<URLLoaderInterceptor>(base::BindRepeating(
&SignedExchangeRequestHandlerBrowserTest::OnInterceptCallback,
base::Unretained(this)));
}
interceptor_data_path_map_[url] = data_path;
} else {
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::BindOnce(&InstallMockInterceptors, url, data_path));
}
}
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<net::MockCertVerifier> mock_cert_verifier_;
private:
static void InstallMockInterceptors(const GURL& url,
const std::string& data_path) {
DCHECK(!base::FeatureList::IsEnabled(network::features::kNetworkService));
base::FilePath root_path;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &root_path));
net::URLRequestFilter::GetInstance()->AddUrlInterceptor(
url, net::URLRequestMockHTTPJob::CreateInterceptorForSingleFile(
root_path.AppendASCII(data_path)));
}
bool OnInterceptCallback(URLLoaderInterceptor::RequestParams* params) {
DCHECK(base::FeatureList::IsEnabled(network::features::kNetworkService));
const auto it = interceptor_data_path_map_.find(params->url_request.url);
if (it == interceptor_data_path_map_.end())
return false;
URLLoaderInterceptor::WriteResponse(it->second, params->client.get());
return true;
}
std::unique_ptr<URLLoaderInterceptor> interceptor_;
std::map<GURL, std::string> interceptor_data_path_map_;
DISALLOW_COPY_AND_ASSIGN(SignedExchangeRequestHandlerBrowserTest);
};
class SignedExchangeRequestHandlerWithNetworkServiceBrowserTest
: public SignedExchangeRequestHandlerBrowserTest {
void SetUpFeatures() override {
feature_list_.InitWithFeatures(
{features::kSignedHTTPExchange, network::features::kNetworkService},
{});
}
};
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerBrowserTest, Simple) {
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
// Make the MockCertVerifier treat the certificate
// "prime256v1-sha256.public.pem" as valid for "test.example.org".
scoped_refptr<net::X509Certificate> original_cert =
LoadCertificate("prime256v1-sha256.public.pem");
net::CertVerifyResult dummy_result;
dummy_result.verified_cert = original_cert;
dummy_result.cert_status = net::OK;
dummy_result.ocsp_result.response_status = net::OCSPVerifyResult::PROVIDED;
dummy_result.ocsp_result.revocation_status = net::OCSPRevocationStatus::GOOD;
mock_cert_verifier_->AddResultForCertAndHost(
original_cert, "test.example.org", dummy_result, net::OK);
embedded_test_server()->RegisterRequestMonitor(
base::BindRepeating([](const net::test_server::HttpRequest& request) {
if (request.relative_url == "/sxg/test.example.org_test.sxg") {
const auto& accept_value = request.headers.find("accept")->second;
EXPECT_THAT(accept_value,
::testing::HasSubstr("application/signed-exchange;v=b1"));
}
}));
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");
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
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());
const net::SHA256HashValue original_fingerprint =
net::X509Certificate::CalculateFingerprint256(
original_cert->cert_buffer());
EXPECT_EQ(original_fingerprint, fingerprint);
}
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerBrowserTest,
InvalidContentType) {
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
// Make the MockCertVerifier treat the certificate
// "prime256v1-sha256.public.pem" as valid for "test.example.org".
scoped_refptr<net::X509Certificate> original_cert =
LoadCertificate("prime256v1-sha256.public.pem");
net::CertVerifyResult dummy_result;
dummy_result.verified_cert = original_cert;
dummy_result.cert_status = net::OK;
dummy_result.ocsp_result.response_status = net::OCSPVerifyResult::PROVIDED;
dummy_result.ocsp_result.revocation_status = net::OCSPRevocationStatus::GOOD;
mock_cert_verifier_->AddResultForCertAndHost(
original_cert, "test.example.org", dummy_result, net::OK);
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");
NavigationFailureObserver failure_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_TRUE(failure_observer.did_fail());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetVisibleEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
}
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerBrowserTest, CertNotFound) {
InstallUrlInterceptor(GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/404.msg");
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");
NavigationFailureObserver failure_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_TRUE(failure_observer.did_fail());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetVisibleEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
}
IN_PROC_BROWSER_TEST_F(
SignedExchangeRequestHandlerWithNetworkServiceBrowserTest,
NetworkServiceEnabled) {
InstallUrlInterceptor(
GURL("https://test.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
// Make the MockCertVerifier treat the certificate
// "prime256v1-sha256.public.pem" as valid for "test.example.org".
scoped_refptr<net::X509Certificate> original_cert =
LoadCertificate("prime256v1-sha256.public.pem");
net::CertVerifyResult dummy_result;
dummy_result.verified_cert = original_cert;
dummy_result.cert_status = net::OK;
dummy_result.ocsp_result.response_status = net::OCSPVerifyResult::PROVIDED;
dummy_result.ocsp_result.revocation_status = net::OCSPRevocationStatus::GOOD;
mock_cert_verifier_->AddResultForCertAndHost(
original_cert, "test.example.org", dummy_result, net::OK);
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");
NavigationFailureObserver failure_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_TRUE(failure_observer.did_fail());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetVisibleEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
}
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerBrowserTest,
RealCertVerifier) {
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
// Use "real" CertVerifier.
SignedExchangeHandler::SetCertVerifierForTesting(nullptr);
embedded_test_server()->RegisterRequestMonitor(
base::BindRepeating([](const net::test_server::HttpRequest& request) {
if (request.relative_url == "/sxg/test.example.org_test.sxg") {
const auto& accept_value = request.headers.find("accept")->second;
EXPECT_THAT(accept_value,
::testing::HasSubstr("application/signed-exchange;v=b1"));
}
}));
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");
// "test.example.org_test.sxg" should pass CertVerifier::Verify() and then
// fail at SignedExchangeHandler::CheckOCSPStatus() because of the dummy OCSP
// response.
// TODO(https://crbug.com/815024): 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.
content::ConsoleObserverDelegate console_observer(shell()->web_contents(),
"*OCSP*");
shell()->web_contents()->SetDelegate(&console_observer);
NavigationFailureObserver failure_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_TRUE(failure_observer.did_fail());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetVisibleEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
// Verify that it failed at the OCSP check step.
// TODO(https://crbug.com/803774): Find a better way than matching against the
// error message. We can probably make DevToolsProxy derive some context from
// StoragePartition so that we can record and extract the detailed error
// status for testing via that.
console_observer.Wait();
EXPECT_TRUE(base::StartsWith(console_observer.message(), "OCSP check failed.",
base::CompareCase::SENSITIVE));
}
} // namespace content