blob: 178d5ef33e239dde441d243f2be8e89470cbc031 [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 "chrome/browser/ssl/sct_reporting_service.h"
#include <memory>
#include <tuple>
#include "base/base64.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/i18n/time_formatting.h"
#include "base/json/json_writer.h"
#include "base/memory/scoped_refptr.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/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/cert_verifier_browser_test.h"
#include "chrome/browser/ssl/sct_reporting_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/network_service_util.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/browsing_data_remover_test_util.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/network_service_test_helper.h"
#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/sct_status_flags.h"
#include "net/cert/signed_certificate_timestamp.h"
#include "net/cert/signed_certificate_timestamp_and_status.h"
#include "net/cert/x509_certificate.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/cert_test_util.h"
#include "net/test/ct_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/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/simple_connection_listener.h"
#include "net/test/test_data_directory.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/ct_log_info.mojom.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "services/network/public/proto/sct_audit_report.pb.h"
#include "services/network/test/test_url_loader_factory.h"
namespace {
// These LogId constants allow test cases to specify SCTs from both Google and
// non-Google logs, allowing tests to vary how they meet (or don't meet) the
// Chrome CT policy. To be compliant, the cert used by the embedded test server
// currently requires three embedded SCTs, including at least one from a Google
// log and one from a non-Google log.
//
// Google's "Argon2023" log ("6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4="):
const uint8_t kTestGoogleLogId[] = {
0xe8, 0x3e, 0xd0, 0xda, 0x3e, 0xf5, 0x06, 0x35, 0x32, 0xe7, 0x57,
0x28, 0xbc, 0x89, 0x6b, 0xc9, 0x03, 0xd3, 0xcb, 0xd1, 0x11, 0x6b,
0xec, 0xeb, 0x69, 0xe1, 0x77, 0x7d, 0x6d, 0x06, 0xbd, 0x6e};
// Cloudflare's "Nimbus2023" log
// ("ejKMVNi3LbYg6jjgUh7phBZwMhOFTTvSK8E6V6NS61I="):
const uint8_t kTestNonGoogleLogId1[] = {
0x7a, 0x32, 0x8c, 0x54, 0xd8, 0xb7, 0x2d, 0xb6, 0x20, 0xea, 0x38,
0xe0, 0x52, 0x1e, 0xe9, 0x84, 0x16, 0x70, 0x32, 0x13, 0x85, 0x4d,
0x3b, 0xd2, 0x2b, 0xc1, 0x3a, 0x57, 0xa3, 0x52, 0xeb, 0x52};
// DigiCert's "Yeti2023" log ("Nc8ZG7+xbFe/D61MbULLu7YnICZR6j/hKu+oA8M71kw="):
const uint8_t kTestNonGoogleLogId2[] = {
0x35, 0xcf, 0x19, 0x1b, 0xbf, 0xb1, 0x6c, 0x57, 0xbf, 0x0f, 0xad,
0x4c, 0x6d, 0x42, 0xcb, 0xbb, 0xb6, 0x27, 0x20, 0x26, 0x51, 0xea,
0x3f, 0xe1, 0x2a, 0xef, 0xa8, 0x03, 0xc3, 0x3b, 0xd6, 0x4c};
// Constructs a net::SignedCertificateTimestampAndStatus with the given
// information and appends it to |sct_list|.
void MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::Origin origin,
const std::string& extensions,
const std::string& signature_data,
const base::Time& timestamp,
const std::string& log_id,
net::ct::SCTVerifyStatus status,
net::SignedCertificateTimestampAndStatusList* sct_list) {
scoped_refptr<net::ct::SignedCertificateTimestamp> sct(
new net::ct::SignedCertificateTimestamp());
sct->version = net::ct::SignedCertificateTimestamp::V1;
sct->log_id = log_id;
sct->extensions = extensions;
sct->timestamp = timestamp;
sct->signature.signature_data = signature_data;
sct->origin = origin;
sct_list->push_back(net::SignedCertificateTimestampAndStatus(sct, status));
}
std::string ExtractRESTURLParameter(std::string url, std::string param) {
size_t length_start = url.find(param) + param.size() + 1;
size_t length_end = url.find('/', length_start);
return url.substr(length_start, length_end - length_start);
}
std::string HexToString(const char* hex) {
std::string result;
bool ok = base::HexStringToString(hex, &result);
DCHECK(ok);
return result;
}
} // namespace
class SCTReportingServiceBrowserTest : public CertVerifierBrowserTest {
public:
SCTReportingServiceBrowserTest() {
// Set sampling rate to 1.0 to ensure deterministic behavior.
scoped_feature_list_.InitWithFeaturesAndParameters(
{{features::kSCTAuditing,
{{features::kSCTAuditingSamplingRate.name, "1.0"}}}},
{});
SystemNetworkContextManager::SetEnableCertificateTransparencyForTesting(
true);
// The report server must be initialized here so the reporting URL can be
// set before the network service is initialized.
std::ignore = report_server()->InitializeAndListen();
SCTReportingService::GetReportURLInstance() = report_server()->GetURL("/");
SCTReportingService::GetHashdanceLookupQueryURLInstance() =
report_server()->GetURL("/hashdance/length/$1/prefix/$2");
}
~SCTReportingServiceBrowserTest() override {
SystemNetworkContextManager::SetEnableCertificateTransparencyForTesting(
std::nullopt);
}
SCTReportingServiceBrowserTest(const SCTReportingServiceBrowserTest&) =
delete;
const SCTReportingServiceBrowserTest& operator=(
const SCTReportingServiceBrowserTest&) = delete;
void SetUpOnMainThread() override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// ConnectionListener must be set before the report server is started. Lets
// tests wait for one connection to be made to the report server (e.g. a
// failed connection due to the cert error that won't trigger the
// WaitForRequests() helper from the parent class).
report_connection_listener_ =
std::make_unique<net::test_server::SimpleConnectionListener>(
1, net::test_server::SimpleConnectionListener::
ALLOW_ADDITIONAL_CONNECTIONS);
report_server()->SetConnectionListener(report_connection_listener());
host_resolver()->AddRule("*", "127.0.0.1");
https_server()->AddDefaultHandlers(GetChromeTestDataDir());
report_server()->RegisterRequestHandler(base::BindRepeating(
&SCTReportingServiceBrowserTest::HandleReportRequest,
base::Unretained(this)));
report_server()->StartAcceptingConnections();
ASSERT_TRUE(https_server()->Start());
mock_cert_verifier()->set_default_result(net::OK);
// Mock the cert verify results so that it has valid CT verification
// results.
cert_with_precert_ = CreateCertificateChainFromFile(
net::GetTestCertsDirectory(), "ct-test-embedded-cert.pem",
net::X509Certificate::FORMAT_AUTO);
ASSERT_TRUE(cert_with_precert_);
ASSERT_EQ(1u, cert_with_precert_->intermediate_buffers().size());
net::CertVerifyResult verify_result;
verify_result.verified_cert = cert_with_precert_;
verify_result.is_issued_by_known_root = true;
// Add three "valid" SCTs and mark the certificate as compliant.
// The default test set up is embedded SCTs where one SCT is from a Google
// log and two are from non-Google logs (to meet the Chrome CT policy).
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions1",
"signature1", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestGoogleLogId),
std::size(kTestGoogleLogId)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions2",
"signature2", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
std::size(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions3",
"signature3", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId2),
std::size(kTestNonGoogleLogId2)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
// Set up two test hosts as using publicly-issued certificates for testing.
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "a.test", verify_result,
net::OK);
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "b.test", verify_result,
net::OK);
// Set up a third (internal) test host for FlushAndCheckZeroReports().
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(),
"flush-and-check-zero-reports.test", verify_result, net::OK);
CertVerifierBrowserTest::SetUpOnMainThread();
// Set up NetworkServiceTest once.
content::GetNetworkService()->BindTestInterfaceForTesting(
network_service_test_.BindNewPipeAndPassReceiver());
// Override the retry delay to 0 so that retries happen immediately.
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
network_service_test_->SetSCTAuditingRetryDelay(base::TimeDelta());
}
void TearDownOnMainThread() override {
// Reset the retry delay override.
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
network_service_test_->SetSCTAuditingRetryDelay(std::nullopt);
CertVerifierBrowserTest::TearDownOnMainThread();
}
net::test_server::SimpleConnectionListener* report_connection_listener() {
return report_connection_listener_.get();
}
mojo::Remote<network::mojom::NetworkServiceTest>& network_service_test() {
return network_service_test_;
}
protected:
void SetExtendedReportingEnabled(bool enabled) {
browser()->profile()->GetPrefs()->SetBoolean(
prefs::kSafeBrowsingScoutReportingEnabled, enabled);
}
void SetSafeBrowsingEnabled(bool enabled) {
browser()->profile()->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled,
enabled);
}
// |suffix_list| must be sorted lexicographically.
void SetHashdanceSuffixList(std::vector<std::string> suffix_list) {
suffix_list_ = std::move(suffix_list);
}
net::EmbeddedTestServer* https_server() { return &https_server_; }
net::EmbeddedTestServer* report_server() { return &report_server_; }
void WaitForRequests(size_t num_requests) {
// Each loop iteration will account for one request being processed. (This
// simplifies the request handler code below, and reduces the state that
// must be tracked and handled under locks.)
while (true) {
base::RunLoop run_loop;
{
base::AutoLock auto_lock(requests_lock_);
if (requests_seen_ >= num_requests)
return;
requests_closure_ = run_loop.QuitClosure();
}
run_loop.Run();
}
}
size_t requests_seen() {
base::AutoLock auto_lock(requests_lock_);
return requests_seen_;
}
sct_auditing::SCTClientReport GetLastSeenReport() {
base::AutoLock auto_lock(requests_lock_);
sct_auditing::SCTClientReport auditing_report;
if (last_seen_request_.has_content)
auditing_report.ParseFromString(last_seen_request_.content);
return auditing_report;
}
// Checks that no reports have been sent. To do this, opt-in the profile,
// make a new navigation, and check that there is only a single report and it
// was for this new navigation specifically. This should be used at the end of
// any negative tests to reduce the chance of false successes.
bool FlushAndCheckZeroReports(size_t requests_so_far = 0) {
SetSafeBrowsingEnabled(true);
SetExtendedReportingEnabled(true);
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(),
https_server()->GetURL("flush-and-check-zero-reports.test", "/")));
WaitForRequests(1);
return (requests_so_far + 1 == requests_seen() &&
"flush-and-check-zero-reports.test" == GetLastSeenReport()
.certificate_report(0)
.context()
.origin()
.hostname());
}
void set_error_count(int error_count) { error_count_ = error_count; }
const std::string& last_seen_length() { return last_seen_length_; }
const std::string& last_seen_prefix() { return last_seen_prefix_; }
const scoped_refptr<net::X509Certificate> certificate() {
return cert_with_precert_;
}
private:
std::unique_ptr<net::test_server::HttpResponse> HandleReportRequest(
const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(requests_lock_);
last_seen_request_ = request;
++requests_seen_;
if (requests_closure_)
std::move(requests_closure_).Run();
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
if (request.relative_url.find("hashdance") == std::string::npos) {
// Request is a report.
// Check if the server should just return an error for the full report
// request, otherwise just return OK.
if (error_count_ > 0) {
http_response->set_code(net::HTTP_TOO_MANY_REQUESTS);
--error_count_;
} else {
http_response->set_code(net::HTTP_OK);
}
return http_response;
}
// Request is a hashdance lookup query.
// Parse the URL.
DCHECK(!request.has_content);
last_seen_length_ = ExtractRESTURLParameter(request.relative_url, "length");
last_seen_prefix_ = ExtractRESTURLParameter(request.relative_url, "prefix");
// Create a response.
// 2022-01-01 00:00:00 GMT.
base::Time server_time =
base::Time::UnixEpoch() + base::Seconds(1640995200);
base::Value::Dict response;
response.Set("responseStatus", "OK");
response.Set("now", base::TimeFormatAsIso8601(server_time));
base::Value::List suffixes;
for (const auto& suffix : suffix_list_) {
suffixes.Append(
base::Base64Encode(base::as_bytes(base::make_span(suffix))));
}
response.Set("hashSuffix", std::move(suffixes));
base::Value::List log_list;
{
base::Value::Dict log_status;
log_status.Set("logId", base::Base64Encode(kTestGoogleLogId));
log_status.Set("ingestedUntil", base::TimeFormatAsIso8601(server_time));
log_list.Append(std::move(log_status));
}
{
base::Value::Dict log_status;
log_status.Set("logId", base::Base64Encode(kTestNonGoogleLogId1));
log_status.Set("ingestedUntil", base::TimeFormatAsIso8601(server_time));
log_list.Append(std::move(log_status));
}
{
base::Value::Dict log_status;
log_status.Set("logId", base::Base64Encode(kTestNonGoogleLogId2));
log_status.Set("ingestedUntil", base::TimeFormatAsIso8601(server_time));
log_list.Append(std::move(log_status));
}
response.Set("logStatus", std::move(log_list));
std::string json;
bool ok = base::JSONWriter::Write(response, &json);
DCHECK(ok);
http_response->set_content(std::move(json));
http_response->set_content_type("application/octet-stream");
return http_response;
}
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
net::EmbeddedTestServer report_server_{net::EmbeddedTestServer::TYPE_HTTPS};
base::test::ScopedFeatureList scoped_feature_list_;
scoped_refptr<net::X509Certificate> cert_with_precert_;
std::unique_ptr<net::test_server::SimpleConnectionListener>
report_connection_listener_;
mojo::Remote<network::mojom::NetworkServiceTest> network_service_test_;
// `requests_lock_` is used to force sequential access to these variables to
// avoid races that can cause test flakes.
base::Lock requests_lock_;
net::test_server::HttpRequest last_seen_request_;
size_t requests_seen_ = 0;
// Lookup query received parameters.
std::string last_seen_length_;
std::string last_seen_prefix_;
// Lookup query settings.
std::vector<std::string> suffix_list_;
base::OnceClosure requests_closure_;
// How many times the report server should return an error before succeeding,
// specific to full report requests.
size_t error_count_ = 0;
};
// Tests that reports should be sent when extended reporting is opted in.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
OptedIn_ShouldEnqueueReport) {
SetExtendedReportingEnabled(true);
// Visit an HTTPS page and wait for the report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
WaitForRequests(1);
// Check that one report was sent and contains the expected details.
EXPECT_EQ(1u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
// Tests that disabling Safe Browsing entirely should cause reports to not get
// sent.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest, DisableSafebrowsing) {
SetSafeBrowsingEnabled(false);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
EXPECT_EQ(0u, requests_seen());
EXPECT_TRUE(FlushAndCheckZeroReports());
}
// Tests that we don't send a report for a navigation with a cert error.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
CertErrorDoesNotEnqueueReport) {
SetExtendedReportingEnabled(true);
// Visit a page with an invalid cert.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("invalid.test", "/")));
EXPECT_EQ(0u, requests_seen());
EXPECT_TRUE(FlushAndCheckZeroReports());
}
// Tests that reports aren't sent for Incognito windows.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
IncognitoWindow_ShouldNotEnqueueReport) {
// Enable SBER in the main profile.
SetExtendedReportingEnabled(true);
// Create a new Incognito window.
auto* incognito = CreateIncognitoBrowser();
ASSERT_TRUE(
ui_test_utils::NavigateToURL(incognito, https_server()->GetURL("/")));
EXPECT_EQ(0u, requests_seen());
EXPECT_TRUE(FlushAndCheckZeroReports());
}
// Tests that disabling Extended Reporting causes the cache to be cleared.
// TODO(crbug.com/40749747): Reenable. Flakes heavily on all platforms.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
DISABLED_OptingOutClearsSCTAuditingCache) {
// Enable SCT auditing and enqueue a report.
SetExtendedReportingEnabled(true);
// Visit an HTTPS page and wait for a report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
WaitForRequests(1);
// Check that one report was sent.
EXPECT_EQ(1u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
// Disable Extended Reporting which should clear the underlying cache.
SetExtendedReportingEnabled(false);
// We can check that the same report gets cached again instead of being
// deduplicated (i.e., another report should be sent).
SetExtendedReportingEnabled(true);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
WaitForRequests(2);
EXPECT_EQ(2u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
// Tests that reports are still sent for opted-in profiles after the network
// service crashes and is restarted.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
ReportsSentAfterNetworkServiceRestart) {
// This test is only applicable to out-of-process network service because it
// tests what happens when the network service crashes and restarts.
if (content::IsInProcessNetworkService()) {
return;
}
SetExtendedReportingEnabled(true);
// Crash the NetworkService to force it to restart.
SimulateNetworkServiceCrash();
// Flush the network interface to make sure it notices the crash.
browser()
->profile()
->GetDefaultStoragePartition()
->FlushNetworkInterfaceForTesting();
g_browser_process->system_network_context_manager()
->FlushNetworkInterfaceForTesting();
// The mock cert verify result will be lost when the network service restarts,
// so set back up the necessary rules.
mock_cert_verifier()->set_default_result(net::OK);
// The retry delay override will be reset when the network service restarts,
// so set back up a retry delay of zero to avoid test timeouts.
{
network_service_test().reset();
content::GetNetworkService()->BindTestInterfaceForTesting(
network_service_test().BindNewPipeAndPassReceiver());
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
network_service_test()->SetSCTAuditingRetryDelay(base::TimeDelta());
// Default test fixture teardown will reset the delay back to the default.
}
net::CertVerifyResult verify_result;
verify_result.verified_cert = https_server()->GetCertificate().get();
verify_result.is_issued_by_known_root = true;
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions1",
"signature1", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestGoogleLogId),
std::size(kTestGoogleLogId)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions2",
"signature2", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
std::size(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions3",
"signature3", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId2),
std::size(kTestNonGoogleLogId2)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "a.test", verify_result, net::OK);
// Visit an HTTPS page and wait for the report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
WaitForRequests(1);
// Check that one report was enqueued.
EXPECT_EQ(1u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
// Tests that invalid SCTs don't get reported when the overall result is
// compliant with CT policy.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
CTCompliantInvalidSCTsNotReported) {
// Set up a mocked CertVerifyResult that includes both valid and invalid SCTs.
net::CertVerifyResult verify_result;
verify_result.verified_cert = https_server()->GetCertificate().get();
verify_result.is_issued_by_known_root = true;
// Add three valid SCTs and one invalid SCT. The three valid SCTs meet the
// Chrome CT policy.
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions1",
"signature1", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestGoogleLogId),
sizeof(kTestGoogleLogId)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions2",
"signature2", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
sizeof(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions3",
"signature3", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId2),
sizeof(kTestNonGoogleLogId2)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions4",
"signature4", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId2),
sizeof(kTestNonGoogleLogId2)),
net::ct::SCT_STATUS_INVALID_SIGNATURE, &verify_result.scts);
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "mixed-scts.test", verify_result,
net::OK);
SetExtendedReportingEnabled(true);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("mixed-scts.test", "/")));
WaitForRequests(1);
EXPECT_EQ(1u, requests_seen());
auto report = GetLastSeenReport();
EXPECT_EQ(3, report.certificate_report(0).included_sct_size());
}
// Tests that invalid SCTs don't get included when the overall result is
// non-compliant with CT policy. Valid SCTs should still be reported.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
CTNonCompliantInvalidSCTsNotReported) {
// Set up a mocked CertVerifyResult that includes both valid and invalid SCTs.
net::CertVerifyResult verify_result;
verify_result.verified_cert = https_server()->GetCertificate().get();
verify_result.is_issued_by_known_root = true;
// Add one valid SCT and two invalid SCTs. These SCTs will not meet the Chrome
// CT policy requirements.
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions1",
"signature1", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
sizeof(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions2",
"signature2", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
sizeof(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_INVALID_SIGNATURE, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions3",
"signature3", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId2),
sizeof(kTestNonGoogleLogId2)),
net::ct::SCT_STATUS_INVALID_SIGNATURE, &verify_result.scts);
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "mixed-scts.test", verify_result,
net::OK);
SetExtendedReportingEnabled(true);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("mixed-scts.test", "/")));
WaitForRequests(1);
EXPECT_EQ(1u, requests_seen());
auto report = GetLastSeenReport();
EXPECT_EQ(1, report.certificate_report(0).included_sct_size());
}
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest, NoValidSCTsNoReport) {
// Set up a mocked CertVerifyResult with only invalid SCTs.
net::CertVerifyResult verify_result;
verify_result.verified_cert = https_server()->GetCertificate().get();
verify_result.is_issued_by_known_root = true;
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions1",
"signature1", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
sizeof(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_INVALID_TIMESTAMP, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions2",
"signature2", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
sizeof(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_INVALID_SIGNATURE, &verify_result.scts);
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions3",
"signature3", base::Time::Now(),
std::string(reinterpret_cast<const char*>(kTestNonGoogleLogId1),
sizeof(kTestNonGoogleLogId1)),
net::ct::SCT_STATUS_INVALID_SIGNATURE, &verify_result.scts);
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "invalid-scts.test",
verify_result, net::OK);
SetExtendedReportingEnabled(true);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("invalid-scts.test", "/")));
EXPECT_EQ(0u, requests_seen());
EXPECT_TRUE(FlushAndCheckZeroReports());
}
class SCTReportingServiceZeroSamplingRateBrowserTest
: public SCTReportingServiceBrowserTest {
public:
SCTReportingServiceZeroSamplingRateBrowserTest() {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{features::kSCTAuditing,
{{features::kSCTAuditingSamplingRate.name, "0.0"}}}},
{});
}
SCTReportingServiceZeroSamplingRateBrowserTest(
const SCTReportingServiceZeroSamplingRateBrowserTest&) = delete;
const SCTReportingServiceZeroSamplingRateBrowserTest& operator=(
const SCTReportingServiceZeroSamplingRateBrowserTest&) = delete;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that the embedder is not notified when the sampling rate is zero.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceZeroSamplingRateBrowserTest,
EmbedderNotNotified) {
SetExtendedReportingEnabled(true);
// Visit an HTTPS page.
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), https_server()->GetURL("/")));
// Check that no reports are observed.
EXPECT_EQ(0u, requests_seen());
}
// Tests the simple case where a report succeeds on the first try.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest, SucceedOnFirstTry) {
// Succeed on the first try.
set_error_count(0);
SetExtendedReportingEnabled(true);
// Visit an HTTPS page and wait for the report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
WaitForRequests(1);
// Check that one report was sent and contains the expected details.
EXPECT_EQ(1u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest, RetryOnceAndSucceed) {
// Succeed on the second try.
set_error_count(1);
SetExtendedReportingEnabled(true);
// Visit an HTTPS page and wait for the report to be sent twice.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
WaitForRequests(2);
// Check that the report was sent twice and contains the expected details.
EXPECT_EQ(2u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest, FailAfterMaxRetries) {
// Don't succeed for max_retries+1.
set_error_count(16);
SetExtendedReportingEnabled(true);
// Visit an HTTPS page and wait for the report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
// Wait until the reporter completes 16 requests.
WaitForRequests(16);
// Check that the report was sent 16x and contains the expected details.
EXPECT_EQ(16u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
// Test that a cert error on the first attempt to send a report will trigger
// retries that succeed if the server starts using a good cert.
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
CertificateErrorTriggersRetry) {
{
// Override the retry delay to 1s so that the retries don't all happen
// immediately and the test can reset the default verifier result in
// between retry attempts.
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
network_service_test()->SetSCTAuditingRetryDelay(base::Seconds(1));
// Default test fixture teardown will reset the delay back to the default.
}
// The first request to the report server will trigger a certificate error via
// the mock cert verifier.
mock_cert_verifier()->set_default_result(net::ERR_CERT_COMMON_NAME_INVALID);
SetExtendedReportingEnabled(true);
// Visit an HTTPS page, which will trigger a report being sent to the report
// server but that report request will result in a cert error.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
report_connection_listener()->WaitForConnections();
// After seeing one connection, replace the mock cert verifier result with a
// successful result.
mock_cert_verifier()->set_default_result(net::OK);
WaitForRequests(1);
// The second try should have resulted in the first successful report being
// seen by the HandleRequest() handler.
EXPECT_EQ(1u, requests_seen());
EXPECT_EQ(
"a.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
class SCTHashdanceBrowserTest : public SCTReportingServiceBrowserTest {
protected:
void SetUpOnMainThread() override {
SCTReportingServiceBrowserTest::SetUpOnMainThread();
SetExtendedReportingEnabled(false);
// Add a valid SCT that was issued at the beginning of time (and should
// therefore be assumed to have been ingested by the server already). We
// need to use a stable timestamp to get the same leaf hash in every run.
// This SCT results in the leaf hash
// 157F5BD43E660E1A87C45797CE524B4171A231CC10FE912A51A14ABA17EAB6B2.
net::CertVerifyResult verify_result;
verify_result.verified_cert = certificate();
verify_result.is_issued_by_known_root = true;
MakeTestSCTAndStatus(
net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, "extensions1",
"signature1", base::Time::UnixEpoch(),
std::string(reinterpret_cast<const char*>(kTestGoogleLogId),
std::size(kTestGoogleLogId)),
net::ct::SCT_STATUS_OK, &verify_result.scts);
mock_cert_verifier()->AddResultForCertAndHost(
https_server()->GetCertificate().get(), "hashdance.test", verify_result,
net::OK);
network::mojom::CTLogInfoPtr log(std::in_place);
std::string googleLogIdAsString(
reinterpret_cast<const char*>(kTestGoogleLogId),
sizeof(kTestGoogleLogId));
log->id = googleLogIdAsString;
log->mmd = base::Seconds(86400);
std::vector<network::mojom::CTLogInfoPtr> log_list;
log_list.emplace_back(std::move(log));
base::RunLoop run_loop;
content::GetNetworkService()->UpdateCtLogList(std::move(log_list),
run_loop.QuitClosure());
run_loop.Run();
}
private:
base::test::ScopedFeatureList scoped_feature_list_{
features::kSCTAuditingHashdance};
};
IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest, ReportSCTNotFound) {
SetHashdanceSuffixList(
{base::HexEncode(base::as_bytes(base::make_span(
"000000000000000000000000000000000000000000000000000000000000"))),
base::HexEncode(base::as_bytes(base::make_span(
"0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")))});
// Visit an HTTPS page and wait for the lookup query to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("hashdance.test", "/")));
WaitForRequests(2);
// Check that the lookup query was sent with the expected details.
EXPECT_EQ(requests_seen(), 2u);
EXPECT_EQ(last_seen_length(), "20");
EXPECT_EQ(last_seen_prefix(), "157F50");
EXPECT_EQ(
"hashdance.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
}
IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest, DoNotReportSCTFound) {
SetHashdanceSuffixList(
{HexToString(
"000000000000000000000000000000000000000000000000000000000000"),
HexToString(
"5BD43E660E1A87C45797CE524B4171A231CC10FE912A51A14ABA17EAB6B2"),
HexToString(
"0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")});
// Visit an HTTPS page and wait for the lookup query to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("hashdance.test", "/")));
WaitForRequests(1);
// Check that the lookup query was sent with the expected details.
EXPECT_EQ(requests_seen(), 1u);
EXPECT_EQ(last_seen_length(), "20");
EXPECT_EQ(last_seen_prefix(), "157F50");
// No requests should have been sent.
EXPECT_TRUE(FlushAndCheckZeroReports(/*requests_so_far=*/1));
}
IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest,
HashdanceReportCountIncremented) {
base::HistogramTester histograms;
// Visit an HTTPS page and wait for the full report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("hashdance.test", "/")));
WaitForRequests(2);
// Check that two requests (lookup and full report) were sent and the report
// contains the expected details.
EXPECT_EQ(2u, requests_seen());
EXPECT_EQ(
"hashdance.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
// Check that the report count got incremented.
int report_count = g_browser_process->local_state()->GetInteger(
prefs::kSCTAuditingHashdanceReportCount);
EXPECT_EQ(report_count, 1);
// The histogram is logged *before* the report count is incremented, so the
// histogram will only log a report count of zero, once.
histograms.ExpectUniqueSample("Security.SCTAuditing.OptOut.ReportCount", 0,
1);
}
// Test that report count isn't incremented when retrying a single audit report.
// Regression test for crbug.com/1348313.
IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest,
HashdanceReportCountNotIncrementedOnRetry) {
base::HistogramTester histograms;
// Don't succeed for max_retries+1, for the *full report sending*, but the
// hashdance lookup query will always succeed.
set_error_count(16);
// Visit an HTTPS page and wait for the report to be sent.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("hashdance.test", "/")));
// Wait until the reporter completes 32 requests (16 lookup queries which
// succeed, and 16 full report requests which fail).
WaitForRequests(32);
// Check that 32 requests were seen and contains the expected details.
EXPECT_EQ(32u, requests_seen());
EXPECT_EQ(
"hashdance.test",
GetLastSeenReport().certificate_report(0).context().origin().hostname());
// Check that the report was only counted once towards the max-reports limit.
int report_count = g_browser_process->local_state()->GetInteger(
prefs::kSCTAuditingHashdanceReportCount);
EXPECT_EQ(report_count, 1);
// Retrying sending the same report will only check the report count once the
// first time, so the histogram will only log a report count of zero, once.
histograms.ExpectUniqueSample("Security.SCTAuditing.OptOut.ReportCount", 0,
1);
}
IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest, HashdanceReportLimitReached) {
base::HistogramTester histograms;
// Override the report count to be the maximum.
g_browser_process->local_state()->SetInteger(
prefs::kSCTAuditingHashdanceReportCount, 3);
// Visit an HTTPS page.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("hashdance.test", "/")));
// Check that no reports are sent.
EXPECT_EQ(0u, requests_seen());
SetSafeBrowsingEnabled(false); // Clears the deduplication cache.
EXPECT_TRUE(FlushAndCheckZeroReports());
histograms.ExpectUniqueSample("Security.SCTAuditing.OptOut.ReportCount", 3,
1);
}
// Wrapper around FilePathWatcher to help tests wait for an auditing report to
// be persisted to disk. This is also robust to the persistence file being
// written to before the test initiates the wait, helping avoid race conditions
// that can cause hard-to-debug flakes.
//
// This currently monitors *two* file paths, because depending on the platform
// (and the state of the network service sandbox rollout) the persisted data
// file path may have an extra "Network/" subdirectory component, but it is
// difficult to determine this from test data. WaitUntilPersisted() will wait
// until *either* of the two paths have been written to.
//
// ReportPersistenceWaiter also takes a `filesize_threshold` as the "empty"
// persistence file still has some structure/data in it. For the current
// persistence format (list of JSON dicts), the "empty" persistence file is 2
// bytes (the empty list `[]`).
class ReportPersistenceWaiter {
public:
ReportPersistenceWaiter(const base::FilePath& watched_file_path,
const base::FilePath& alternative_file_path,
int64_t filesize_threshold)
: watched_file_path1_(watched_file_path),
watched_file_path2_(alternative_file_path),
filesize_threshold_(filesize_threshold) {}
ReportPersistenceWaiter(const ReportPersistenceWaiter&) = delete;
ReportPersistenceWaiter& operator=(const ReportPersistenceWaiter&) = delete;
void WaitUntilPersisted() {
DCHECK(!watcher1_);
DCHECK(!watcher2_);
{
// Check if either file was already written and if so return early.
base::ScopedAllowBlockingForTesting allow_blocking;
int64_t file_size;
// GetFileSize() will return `false` if the file does not yet exist.
if (base::GetFileSize(watched_file_path1_, &file_size) &&
file_size > filesize_threshold_) {
return;
}
if (base::GetFileSize(watched_file_path2_, &file_size) &&
file_size > filesize_threshold_) {
return;
}
}
watcher1_ = std::make_unique<base::FilePathWatcher>();
watcher2_ = std::make_unique<base::FilePathWatcher>();
EXPECT_TRUE(watcher1_->Watch(
watched_file_path1_, base::FilePathWatcher::Type::kNonRecursive,
base::BindRepeating(&ReportPersistenceWaiter::OnPathChanged,
base::Unretained(this))));
EXPECT_TRUE(watcher2_->Watch(
watched_file_path2_, base::FilePathWatcher::Type::kNonRecursive,
base::BindRepeating(&ReportPersistenceWaiter::OnPathChanged,
base::Unretained(this))));
run_loop_.Run();
// The watchers should be destroyed before quitting the run loop.
DCHECK(!watcher1_);
DCHECK(!watcher2_);
}
private:
void OnPathChanged(const base::FilePath& path, bool error) {
EXPECT_TRUE(path == watched_file_path1_ || path == watched_file_path2_);
EXPECT_FALSE(error);
watcher1_.reset();
watcher2_.reset();
run_loop_.Quit();
}
base::RunLoop run_loop_;
const base::FilePath watched_file_path1_;
const base::FilePath watched_file_path2_;
const int64_t filesize_threshold_;
std::unique_ptr<base::FilePathWatcher> watcher1_;
std::unique_ptr<base::FilePathWatcher> watcher2_;
};
IN_PROC_BROWSER_TEST_F(SCTReportingServiceBrowserTest,
PersistedReportClearedOnClearBrowsingHistory) {
// Set a long retry delay so that retries don't occur immediately.
{
mojo::ScopedAllowSyncCallForTesting allow_sync_call;
network_service_test()->SetSCTAuditingRetryDelay(base::Minutes(1));
}
// Don't immediately succeed, so report stays persisted to disk.
set_error_count(10);
SetExtendedReportingEnabled(true);
// The empty/cleared persistence file will be 2 bytes (the empty JSON list).
constexpr int64_t kEmptyPersistenceFileSize = 2;
base::FilePath persistence_path1 = browser()->profile()->GetPath();
// If the network service sandbox is enabled, then the network service data
// dir path has an additional "Network" subdirectory in it. This means that
// different platforms will have different persistence paths depending on the
// current state of the network service sandbox rollout.
// TODO(crbug.com/41315406): Simplify this once the paths are consistent
// (i.e., after the network service sandbox is fully rolled out.)
base::FilePath persistence_path2 =
persistence_path1.Append(chrome::kNetworkDataDirname);
persistence_path1 =
persistence_path1.Append(chrome::kSCTAuditingPendingReportsFileName);
persistence_path2 =
persistence_path2.Append(chrome::kSCTAuditingPendingReportsFileName);
// Visit an HTTPS page to generate an SCT auditing report. Sending the report
// will result in an error, so the pending report will remain.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server()->GetURL("a.test", "/")));
// Check that the report got persisted to disk.
{
base::ScopedAllowBlockingForTesting allow_blocking;
ReportPersistenceWaiter waiter(persistence_path1, persistence_path2,
kEmptyPersistenceFileSize);
waiter.WaitUntilPersisted();
int64_t file_size1;
int64_t file_size2;
bool one_file_is_written =
base::GetFileSize(persistence_path1, &file_size1) ||
base::GetFileSize(persistence_path2, &file_size2);
EXPECT_TRUE(one_file_is_written);
EXPECT_TRUE(file_size1 > kEmptyPersistenceFileSize ||
file_size2 > kEmptyPersistenceFileSize);
}
// Trigger removal and wait for completion.
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
content::BrowsingDataRemover* remover =
contents->GetBrowserContext()->GetBrowsingDataRemover();
content::BrowsingDataRemoverCompletionObserver completion_observer(remover);
remover->RemoveAndReply(
base::Time(), base::Time::Max(),
content::BrowsingDataRemover::DATA_TYPE_CACHE,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB,
&completion_observer);
completion_observer.BlockUntilCompletion();
// Check that the persistence file is cleared.
{
base::ScopedAllowBlockingForTesting allow_blocking;
int64_t file_size;
if (base::GetFileSize(persistence_path1, &file_size)) {
EXPECT_EQ(file_size, kEmptyPersistenceFileSize);
} else if (base::GetFileSize(persistence_path2, &file_size)) {
EXPECT_EQ(file_size, kEmptyPersistenceFileSize);
} else {
FAIL() << "Neither persistence file was ever written";
}
}
}