blob: 868b466c8574bdb325c83566046fb535db3f95e7 [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 <memory>
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/memory/ref_counted.h"
#include "base/stl_util.h"
#include "base/strings/string_piece.h"
#include "base/test/gtest_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_task_environment.h"
#include "components/certificate_transparency/features.h"
#include "components/certificate_transparency/single_tree_tracker.h"
#include "mojo/public/cpp/bindings/interface_request.h"
#include "net/base/address_list.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/ct_policy_status.h"
#include "net/cert/ct_serialization.h"
#include "net/cert/ct_verifier.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/cert/signed_certificate_timestamp.h"
#include "net/cert/signed_tree_head.h"
#include "net/cert/x509_certificate.h"
#include "net/dns/host_cache.h"
#include "net/dns/host_resolver.h"
#include "net/proxy_resolution/proxy_config_with_annotation.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/test_data_directory.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/url_request/url_request_context.h"
#include "services/network/network_context.h"
#include "services/network/network_service.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/mojom/proxy_config.mojom.h"
#include "services/network/test/test_url_loader_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace network {
namespace {
mojom::NetworkContextParamsPtr CreateContextParams() {
mojom::NetworkContextParamsPtr params = mojom::NetworkContextParams::New();
// Use a fixed proxy config, to avoid dependencies on local network
// configuration.
params->initial_proxy_config = net::ProxyConfigWithAnnotation::CreateDirect();
// Configure Certificate Transparency for the context.
// TODO(robpercival): https://crbug.com/839612 - Use test logs for
// integration tests rather than production logs.
const char kPilotKey[] =
"\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86"
"\x48\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x7d\xa8\x4b\x12\x29\x80\xa3"
"\x3d\xad\xd3\x5a\x77\xb8\xcc\xe2\x88\xb3\xa5\xfd\xf1\xd3\x0c\xcd\x18"
"\x0c\xe8\x41\x46\xe8\x81\x01\x1b\x15\xe1\x4b\xf1\x1b\x62\xdd\x36\x0a"
"\x08\x18\xba\xed\x0b\x35\x84\xd0\x9e\x40\x3c\x2d\x9e\x9b\x82\x65\xbd"
"\x1f\x04\x10\x41\x4c\xa0";
const char kAviatorKey[] =
"\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86"
"\x48\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xd7\xf4\xcc\x69\xb2\xe4\x0e"
"\x90\xa3\x8a\xea\x5a\x70\x09\x4f\xef\x13\x62\xd0\x8d\x49\x60\xff\x1b"
"\x40\x50\x07\x0c\x6d\x71\x86\xda\x25\x49\x8d\x65\xe1\x08\x0d\x47\x34"
"\x6b\xbd\x27\xbc\x96\x21\x3e\x34\xf5\x87\x76\x31\xb1\x7f\x1d\xc9\x85"
"\x3b\x0d\xf7\x1f\x3f\xe9";
const char kDigiCertKey[] =
"\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86"
"\x48\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x02\x46\xc5\xbe\x1b\xbb\x82"
"\x40\x16\xe8\xc1\xd2\xac\x19\x69\x13\x59\xf8\xf8\x70\x85\x46\x40\xb9"
"\x38\xb0\x23\x82\xa8\x64\x4c\x7f\xbf\xbb\x34\x9f\x4a\x5f\x28\x8a\xcf"
"\x19\xc4\x00\xf6\x36\x06\x93\x65\xed\x4c\xf5\xa9\x21\x62\x5a\xd8\x91"
"\xeb\x38\x24\x40\xac\xe8";
params->ct_logs.push_back(network::mojom::CTLogInfo::New(
std::string(kPilotKey, base::size(kPilotKey) - 1), "Google 'Pilot' Log",
"pilot.ct.invalid"));
params->ct_logs.push_back(network::mojom::CTLogInfo::New(
std::string(kAviatorKey, base::size(kAviatorKey) - 1),
"Google 'Aviator' Log", "aviator.ct.invalid"));
params->ct_logs.push_back(network::mojom::CTLogInfo::New(
std::string(kDigiCertKey, base::size(kDigiCertKey) - 1),
"DigiCert Log Server", "digicert.ct.invalid"));
return params;
}
// TODO(robpercival): https://crbug.com/839612 - Make it easier to use a test
// cert that is not so tightly-coupled to production logs and STHs.
scoped_refptr<net::X509Certificate> GetCTCertForTesting() {
base::ScopedAllowBlockingForTesting allow_blocking_for_loading_cert;
return net::CreateCertificateChainFromFile(
net::GetTestCertsDirectory(), "comodo-chain.pem",
net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE);
}
// The number of valid SCTs in |GetCTCertForTesting| from logs configured in
// |CreateContextParams()|.
const size_t kNumSCTs = 3;
// Decodes a base64-encoded "DigitallySigned" TLS struct into |*sig_out|.
// See https://tools.ietf.org/html/rfc5246#section-4.7.
// |sig_out| must not be null.
bool DecodeDigitallySigned(base::StringPiece base64_data,
net::ct::DigitallySigned* sig_out) {
std::string data;
if (!base::Base64Decode(base64_data, &data))
return false;
base::StringPiece data_ptr = data;
if (!net::ct::DecodeDigitallySigned(&data_ptr, sig_out))
return false;
return true;
}
// Populates |*sth_out| with the given information.
// |sth_out| must not be null.
bool BuildSignedTreeHead(base::Time timestamp,
uint64_t tree_size,
base::StringPiece root_hash_base64,
base::StringPiece signature_base64,
base::StringPiece log_id_base64,
net::ct::SignedTreeHead* sth_out) {
sth_out->version = net::ct::SignedTreeHead::V1;
sth_out->timestamp = timestamp;
sth_out->tree_size = tree_size;
std::string root_hash;
if (!base::Base64Decode(root_hash_base64, &root_hash)) {
return false;
}
root_hash.copy(sth_out->sha256_root_hash, net::ct::kSthRootHashLength);
return DecodeDigitallySigned(signature_base64, &sth_out->signature) &&
base::Base64Decode(log_id_base64, &sth_out->log_id);
}
TEST(NetworkContextCertTransparencyAuditingDisabledTest,
SCTsAreNotCheckedForInclusion) {
base::test::ScopedTaskEnvironment scoped_task_environment(
base::test::ScopedTaskEnvironment::MainThreadType::IO);
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
certificate_transparency::kCTLogAuditing);
std::unique_ptr<NetworkService> network_service =
NetworkService::CreateForTesting();
// Override the CertVerifier, so that a 'real' cert can be simulated being
// returned by the net::TestServer. This should be done before creating the
// context.
net::MockCertVerifier mock_cert_verifier;
NetworkContext::SetCertVerifierForTesting(&mock_cert_verifier);
base::ScopedClosureRunner cleanup(base::BindOnce(
[] { NetworkContext::SetCertVerifierForTesting(nullptr); }));
mojom::NetworkContextParamsPtr context_params = CreateContextParams();
mojom::NetworkContextPtr network_context_ptr;
std::unique_ptr<NetworkContext> network_context =
std::make_unique<NetworkContext>(network_service.get(),
mojo::MakeRequest(&network_context_ptr),
std::move(context_params));
// Certificate Transparency should be configured, but there should be
// nothing listening for SCTs (such as the NetworkContext's ct_tree_tracker_).
ASSERT_TRUE(
network_context->url_request_context()->cert_transparency_verifier());
EXPECT_FALSE(network_context->url_request_context()
->cert_transparency_verifier()
->GetObserver());
// Provide an STH from Google's Pilot log that can be used to prove
// inclusion for an SCT later in the test.
net::ct::SignedTreeHead pilot_sth;
ASSERT_TRUE(BuildSignedTreeHead(
base::Time::FromJsTime(1512419914170), 181871752,
"bvgljSy3Yg32Y6J8qL5WmUA3jn2WnOrEFDqxD0AxUvs=",
"BAMARjBEAiAwEXve2RBk3XkUR+6nACSETTgzKFaEeginxuj5U9BI/"
"wIgBPuQS5ACxsro6TtpY4bQyE6WlMdcSMiMd/SSGraOBOg=",
"pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA=", &pilot_sth));
network_service->UpdateSignedTreeHead(pilot_sth);
// Provide an STH from Google's Aviator log that is not recent enough to
// prove inclusion for an SCT later in the test.
net::ct::SignedTreeHead aviator_sth;
ASSERT_TRUE(BuildSignedTreeHead(
base::Time::FromJsTime(1442652106945), 8502329,
"bfG+gWZcHl9fqtNo0Z/uggs8E5YqGOtJQ0Z5zVZDRxI=",
"BAMARjBEAiA6elcNQoShmKLHj/"
"IA649UIbaQtWJEpj0Eot0q7G6fEgIgYChb7U6Reuvt0nO5PionH+3UciOxKV3Cy8/"
"eq59lSYY=",
"aPaY+B9kgr46jO65KB1M/HFRXWeT1ETRCmesu09P+8Q=", &aviator_sth));
network_service->UpdateSignedTreeHead(aviator_sth);
// Start a test server on "localhost" and configure connections to it to
// simulate using a real certificate.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
ASSERT_TRUE(https_server.Start());
// Configure "localhost" to be treated as if it went through DNS. This
// modifies the HostCache directly to simulate it being cached, rather than
// indirecting through a scoped HostResolverProc, as queries that use
// HostResolverProcs are treated as SOURCE_UNKNOWN, rather than SOURCE_DNS.
net::AddressList address_list;
ASSERT_TRUE(https_server.GetAddressList(&address_list));
net::HostCache* host_cache =
network_context->url_request_context()->host_resolver()->GetHostCache();
ASSERT_TRUE(host_cache);
host_cache->Set(
net::HostCache::Key("localhost", net::ADDRESS_FAMILY_UNSPECIFIED, 0),
net::HostCache::Entry(net::OK, address_list,
net::HostCache::Entry::SOURCE_DNS),
base::TimeTicks::Now(), base::TimeDelta());
// This certificate contains 3 SCTs and fulfills the Chrome CT policy.
// Simulate it being trusted by a known root, as otherwise CT is skipped for
// private roots.
net::CertVerifyResult verify_result;
verify_result.is_issued_by_known_root = true;
verify_result.cert_status = 0;
verify_result.verified_cert = GetCTCertForTesting();
ASSERT_TRUE(verify_result.verified_cert);
mock_cert_verifier.AddResultForCert(https_server.GetCertificate(),
verify_result, net::OK);
ResourceRequest request;
request.url = https_server.GetURL("localhost", "/");
request.method = "GET";
request.request_initiator = url::Origin();
mojom::URLLoaderFactoryPtr loader_factory;
auto url_loader_factory_params =
network::mojom::URLLoaderFactoryParams::New();
url_loader_factory_params->process_id = network::mojom::kBrowserProcessId;
url_loader_factory_params->is_corb_enabled = false;
network_context->CreateURLLoaderFactory(mojo::MakeRequest(&loader_factory),
std::move(url_loader_factory_params));
base::HistogramTester histograms;
mojom::URLLoaderPtr loader;
TestURLLoaderClient client;
int options = mojom::kURLLoadOptionSendSSLInfoWithResponse |
mojom::kURLLoadOptionSendSSLInfoForCertificateError;
loader_factory->CreateLoaderAndStart(
mojo::MakeRequest(&loader), 0 /* routing_id */, 0 /* request_id */,
options, request, client.CreateInterfacePtr(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_TRUE(client.has_received_response());
EXPECT_TRUE(client.has_received_completion());
// Expect a successful connection.
EXPECT_EQ(net::OK, client.completion_status().error_code);
// Expect 3 SCTs in this connection.
ASSERT_TRUE(client.ssl_info().has_value());
ASSERT_EQ(kNumSCTs, client.ssl_info()->signed_certificate_timestamps.size());
// Expect that all SCTs were embedded in the certificate.
size_t embedded_scts = 0;
for (const auto& sct : client.ssl_info()->signed_certificate_timestamps) {
if (sct.sct->origin == net::ct::SignedCertificateTimestamp::SCT_EMBEDDED)
++embedded_scts;
}
ASSERT_EQ(kNumSCTs, embedded_scts);
// No SCTs should be eligible for inclusion checking, as inclusion checking
// is disabled.
histograms.ExpectTotalCount(
"Net.CertificateTransparency.CanInclusionCheckSCT", 0);
}
TEST(NetworkContextCertTransparencyAuditingEnabledTest,
SCTsAreCheckedForInclusion) {
base::test::ScopedTaskEnvironment scoped_task_environment(
base::test::ScopedTaskEnvironment::MainThreadType::IO);
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
certificate_transparency::kCTLogAuditing);
std::unique_ptr<NetworkService> network_service =
NetworkService::CreateForTesting();
// Override the CertVerifier, so that a 'real' cert can be simulated being
// returned by the net::TestServer. This should be done before creating the
// context.
net::MockCertVerifier mock_cert_verifier;
NetworkContext::SetCertVerifierForTesting(&mock_cert_verifier);
base::ScopedClosureRunner cleanup(base::BindOnce(
[] { NetworkContext::SetCertVerifierForTesting(nullptr); }));
mojom::NetworkContextParamsPtr context_params = CreateContextParams();
mojom::NetworkContextPtr network_context_ptr;
std::unique_ptr<NetworkContext> network_context =
std::make_unique<NetworkContext>(network_service.get(),
mojo::MakeRequest(&network_context_ptr),
std::move(context_params));
// Certificate Transparency should be configured, but there should be
// nothing listening for SCTs (such as the NetworkContext's ct_tree_tracker_).
ASSERT_TRUE(
network_context->url_request_context()->cert_transparency_verifier());
EXPECT_TRUE(network_context->url_request_context()
->cert_transparency_verifier()
->GetObserver());
// Provide an STH from Google's Pilot log that can be used to prove
// inclusion for an SCT later in the test.
net::ct::SignedTreeHead pilot_sth;
ASSERT_TRUE(BuildSignedTreeHead(
base::Time::FromJsTime(1512419914170), 181871752,
"bvgljSy3Yg32Y6J8qL5WmUA3jn2WnOrEFDqxD0AxUvs=",
"BAMARjBEAiAwEXve2RBk3XkUR+6nACSETTgzKFaEeginxuj5U9BI/"
"wIgBPuQS5ACxsro6TtpY4bQyE6WlMdcSMiMd/SSGraOBOg=",
"pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA=", &pilot_sth));
network_service->UpdateSignedTreeHead(pilot_sth);
// Provide an STH from Google's Aviator log that is not recent enough to
// prove inclusion for an SCT later in the test.
net::ct::SignedTreeHead aviator_sth;
ASSERT_TRUE(BuildSignedTreeHead(
base::Time::FromJsTime(1442652106945), 8502329,
"bfG+gWZcHl9fqtNo0Z/uggs8E5YqGOtJQ0Z5zVZDRxI=",
"BAMARjBEAiA6elcNQoShmKLHj/"
"IA649UIbaQtWJEpj0Eot0q7G6fEgIgYChb7U6Reuvt0nO5PionH+3UciOxKV3Cy8/"
"eq59lSYY=",
"aPaY+B9kgr46jO65KB1M/HFRXWeT1ETRCmesu09P+8Q=", &aviator_sth));
network_service->UpdateSignedTreeHead(aviator_sth);
// Start a test server on "localhost" and configure connections to it to
// simulate using a real certificate.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
ASSERT_TRUE(https_server.Start());
// Configure "localhost" to be treated as if it went through DNS. This
// modifies the HostCache directly to simulate it being cached, rather than
// indirecting through a scoped HostResolverProc, as queries that use
// HostResolverProcs are treated as SOURCE_UNKNOWN, rather than SOURCE_DNS.
net::AddressList address_list;
ASSERT_TRUE(https_server.GetAddressList(&address_list));
net::HostCache* host_cache =
network_context->url_request_context()->host_resolver()->GetHostCache();
ASSERT_TRUE(host_cache);
host_cache->Set(
net::HostCache::Key("localhost", net::ADDRESS_FAMILY_UNSPECIFIED, 0),
net::HostCache::Entry(net::OK, address_list,
net::HostCache::Entry::SOURCE_DNS),
base::TimeTicks::Now(), base::TimeDelta());
// This certificate contains 3 SCTs and fulfills the Chrome CT policy.
// Simulate it being trusted by a known root, as otherwise CT is skipped for
// private roots.
net::CertVerifyResult verify_result;
verify_result.is_issued_by_known_root = true;
verify_result.cert_status = 0;
verify_result.verified_cert = GetCTCertForTesting();
ASSERT_TRUE(verify_result.verified_cert);
mock_cert_verifier.AddResultForCert(https_server.GetCertificate(),
verify_result, net::OK);
base::HistogramTester histograms;
ResourceRequest request;
request.url = https_server.GetURL("localhost", "/");
request.method = "GET";
request.request_initiator = url::Origin();
mojom::URLLoaderFactoryPtr loader_factory;
auto url_loader_factory_params =
network::mojom::URLLoaderFactoryParams::New();
url_loader_factory_params->process_id = network::mojom::kBrowserProcessId;
url_loader_factory_params->is_corb_enabled = false;
network_context->CreateURLLoaderFactory(mojo::MakeRequest(&loader_factory),
std::move(url_loader_factory_params));
mojom::URLLoaderPtr loader;
TestURLLoaderClient client;
int options = mojom::kURLLoadOptionSendSSLInfoWithResponse |
mojom::kURLLoadOptionSendSSLInfoForCertificateError;
loader_factory->CreateLoaderAndStart(
mojo::MakeRequest(&loader), 0 /* routing_id */, 0 /* request_id */,
options, request, client.CreateInterfacePtr(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_TRUE(client.has_received_response());
EXPECT_TRUE(client.has_received_completion());
// Expect a successful connection.
EXPECT_EQ(net::OK, client.completion_status().error_code);
// Expect 3 SCTs in this connection.
ASSERT_TRUE(client.ssl_info().has_value());
ASSERT_EQ(kNumSCTs, client.ssl_info()->signed_certificate_timestamps.size());
// Expect that all SCTs were embedded in the certificate.
size_t embedded_scts = 0;
for (const auto& sct : client.ssl_info()->signed_certificate_timestamps) {
if (sct.sct->origin == net::ct::SignedCertificateTimestamp::SCT_EMBEDDED)
++embedded_scts;
}
ASSERT_EQ(kNumSCTs, embedded_scts);
// The Pilot SCT should be eligible for inclusion checking, because a recent
// enough Pilot STH is available.
histograms.ExpectBucketCount(
"Net.CertificateTransparency.CanInclusionCheckSCT",
certificate_transparency::CAN_BE_CHECKED, 1);
// The Aviator SCT should not be eligible for inclusion checking, because
// there is not a recent enough Aviator STH available.
histograms.ExpectBucketCount(
"Net.CertificateTransparency.CanInclusionCheckSCT",
certificate_transparency::NEWER_STH_REQUIRED, 1);
// The DigiCert SCT should not be eligible for inclusion checking, because
// there is no DigiCert STH available.
histograms.ExpectBucketCount(
"Net.CertificateTransparency.CanInclusionCheckSCT",
certificate_transparency::VALID_STH_REQUIRED, 1);
}
} // namespace
} // namespace network