| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/sct_auditing_cache.h" |
| |
| #include "base/feature_list.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "net/base/host_port_pair.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/test/cert_test_util.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/network_context.h" |
| #include "services/network/network_service.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/proto/sct_audit_report.pb.h" |
| #include "services/network/test/fake_test_cert_verifier_params_factory.h" |
| #include "services/network/test/test_network_context_client.h" |
| |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace network { |
| |
| namespace { |
| |
| class SCTAuditingCacheTest : public testing::Test { |
| public: |
| SCTAuditingCacheTest() |
| : network_service_(NetworkService::CreateForTesting()) {} |
| ~SCTAuditingCacheTest() override = default; |
| |
| SCTAuditingCacheTest(const SCTAuditingCacheTest&) = delete; |
| SCTAuditingCacheTest& operator=(const SCTAuditingCacheTest&) = delete; |
| |
| void SetUp() override { |
| InitNetworkContext(); |
| network_context_->SetIsSCTAuditingEnabledForTesting(true); |
| chain_ = |
| net::ImportCertFromFile(net::GetTestCertsDirectory(), "ok_cert.pem"); |
| ASSERT_TRUE(chain_.get()); |
| } |
| |
| protected: |
| void InitNetworkContext() { |
| mojom::NetworkContextParamsPtr params = mojom::NetworkContextParams::New(); |
| // Use a dummy CertVerifier that always passes cert verification, since |
| // these unittests don't need to test CertVerifier behavior. |
| params->cert_verifier_params = |
| FakeTestCertVerifierParamsFactory::GetCertVerifierParams(); |
| |
| network_context_ = std::make_unique<NetworkContext>( |
| network_service_.get(), |
| network_context_remote_.BindNewPipeAndPassReceiver(), |
| std::move(params)); |
| |
| // A NetworkContextClient is needed for embedder notifications to work. |
| mojo::PendingRemote<network::mojom::NetworkContextClient> |
| network_context_client_remote; |
| network_context_client_ = |
| std::make_unique<network::TestNetworkContextClient>( |
| network_context_client_remote.InitWithNewPipeAndPassReceiver()); |
| network_context_->SetClient(std::move(network_context_client_remote)); |
| } |
| |
| // Initializes the configuration for the SCTAuditingCache to defaults and |
| // sets up the URLLoaderFactory. Individual tests can directly call the set_* |
| // methods to tweak the configuration. |
| void InitSCTAuditing(SCTAuditingCache* cache) { |
| cache->set_enabled(true); |
| cache->set_sampling_rate(1.0); |
| cache->set_report_uri(GURL("https://example.test")); |
| cache->set_traffic_annotation( |
| net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); |
| |
| url_loader_factory_ = std::make_unique<TestURLLoaderFactory>(); |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> factory_client; |
| url_loader_factory_->Clone(factory_client.InitWithNewPipeAndPassReceiver()); |
| cache->set_url_loader_factory(std::move(factory_client)); |
| } |
| |
| // Getter for TestURLLoaderFactory to allow tests to specify responses. |
| TestURLLoaderFactory* url_loader_factory() { |
| return url_loader_factory_.get(); |
| } |
| |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::MainThreadType::IO}; |
| std::unique_ptr<NetworkService> network_service_; |
| std::unique_ptr<NetworkContext> network_context_; |
| std::unique_ptr<network::mojom::NetworkContextClient> network_context_client_; |
| std::unique_ptr<TestURLLoaderFactory> url_loader_factory_; |
| |
| scoped_refptr<net::X509Certificate> chain_; |
| |
| // Stores the mojo::Remote<mojom::NetworkContext> of the most recently created |
| // NetworkContext. |
| mojo::Remote<mojom::NetworkContext> network_context_remote_; |
| }; |
| |
| // 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, |
| net::ct::SCTVerifyStatus status, |
| net::SignedCertificateTimestampAndStatusList* sct_list) { |
| scoped_refptr<net::ct::SignedCertificateTimestamp> sct( |
| new net::ct::SignedCertificateTimestamp()); |
| sct->version = net::ct::SignedCertificateTimestamp::V1; |
| |
| // The particular value of the log ID doesn't matter; it just has to be the |
| // correct length. |
| const unsigned char kTestLogId[] = { |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, |
| 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; |
| const std::string log_id(reinterpret_cast<const char*>(kTestLogId), |
| sizeof(kTestLogId)); |
| 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)); |
| } |
| |
| } // namespace |
| |
| // Test that if auditing is disabled on the NetworkContext, no reports are |
| // cached. |
| TEST_F(SCTAuditingCacheTest, NoReportsCachedWhenAuditingDisabled) { |
| SCTAuditingCache cache(10); |
| InitSCTAuditing(&cache); |
| |
| network_context_->SetIsSCTAuditingEnabledForTesting(false); |
| |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature1", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| ASSERT_EQ(0u, cache.GetCacheForTesting()->size()); |
| } |
| |
| // Test that inserting and retrieving a report works. |
| TEST_F(SCTAuditingCacheTest, InsertAndRetrieveReport) { |
| SCTAuditingCache cache(10); |
| InitSCTAuditing(&cache); |
| |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature1", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| ASSERT_EQ(1u, cache.GetCacheForTesting()->size()); |
| } |
| |
| // Tests that old entries are evicted when the cache is full. |
| TEST_F(SCTAuditingCacheTest, EvictLRUAfterCacheFull) { |
| SCTAuditingCache cache(2); |
| InitSCTAuditing(&cache); |
| |
| const net::HostPortPair host_port_pair1("example1.com", 443); |
| const net::HostPortPair host_port_pair2("example2.com", 443); |
| const net::HostPortPair host_port_pair3("example3.com", 443); |
| |
| { |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature1", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair1, |
| chain_.get(), sct_list); |
| ASSERT_EQ(1u, cache.GetCacheForTesting()->size()); |
| } |
| |
| { |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature2", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair2, |
| chain_.get(), sct_list); |
| ASSERT_EQ(2u, cache.GetCacheForTesting()->size()); |
| } |
| |
| // Cache is now full, so the first entry (for "example1.com") should no longer |
| // be in the cache after inserting a third entry. |
| { |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature3", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair3, |
| chain_.get(), sct_list); |
| ASSERT_EQ(2u, cache.GetCacheForTesting()->size()); |
| for (const auto& entry : *cache.GetCacheForTesting()) { |
| ASSERT_NE("example1.com", entry.second->context().origin().hostname()); |
| } |
| } |
| } |
| |
| // Tests that a new report gets dropped if the same SCTs are already in the |
| // cache. |
| TEST_F(SCTAuditingCacheTest, ReportWithSameSCTsDeduplicated) { |
| SCTAuditingCache cache(10); |
| InitSCTAuditing(&cache); |
| |
| const net::HostPortPair host_port_pair1("example.com", 443); |
| const net::HostPortPair host_port_pair2("example.org", 443); |
| |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature1", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair1, |
| chain_.get(), sct_list); |
| |
| ASSERT_EQ(1u, cache.GetCacheForTesting()->size()); |
| |
| // Enqueuing the same SCTs won't cause a new report to be added to the queue |
| // (even if the connection origin is different). |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair2, |
| chain_.get(), sct_list); |
| ASSERT_EQ(1u, cache.GetCacheForTesting()->size()); |
| } |
| |
| // When a report gets deduplicated, the existing entry should have its last-seen |
| // time bumped up. |
| TEST_F(SCTAuditingCacheTest, DeduplicationUpdatesLastSeenTime) { |
| SCTAuditingCache cache(2); |
| InitSCTAuditing(&cache); |
| |
| const net::HostPortPair host_port_pair1("example1.com", 443); |
| const net::HostPortPair host_port_pair2("example2.com", 443); |
| const net::HostPortPair host_port_pair3("example3.com", 443); |
| |
| // Fill the cache with two reports. |
| net::SignedCertificateTimestampAndStatusList sct_list1; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature1", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list1); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair1, |
| chain_.get(), sct_list1); |
| |
| net::SignedCertificateTimestampAndStatusList sct_list2; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions2", "signature2", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list2); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair2, |
| chain_.get(), sct_list2); |
| |
| EXPECT_EQ(2u, cache.GetCacheForTesting()->size()); |
| |
| // Try to enqueue the report for "example1.com" again. It should be deduped. |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair1, |
| chain_.get(), sct_list1); |
| EXPECT_EQ(2u, cache.GetCacheForTesting()->size()); |
| |
| // If we enqueue a new report causing the cache size limit to be exceeded, |
| // "example1.com" should be the most-recent due to getting updated during |
| // deduping, and "example2.com" should get evicted instead. |
| net::SignedCertificateTimestampAndStatusList sct_list3; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions3", "signature3", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list3); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair3, |
| chain_.get(), sct_list3); |
| |
| EXPECT_EQ(2u, cache.GetCacheForTesting()->size()); |
| for (const auto& entry : *cache.GetCacheForTesting()) { |
| ASSERT_NE("example2.com", entry.second->context().origin().hostname()); |
| } |
| } |
| |
| TEST_F(SCTAuditingCacheTest, NoReportsCachedWhenCacheDisabled) { |
| SCTAuditingCache cache(2); |
| InitSCTAuditing(&cache); |
| cache.set_enabled(false); |
| |
| // Try to enqueue a report. |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions", "signature", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| // Check that there are no entries in the cache. |
| EXPECT_EQ(0u, cache.GetCacheForTesting()->size()); |
| } |
| |
| TEST_F(SCTAuditingCacheTest, ReportsCachedButNotSentWhenSamplingIsZero) { |
| SCTAuditingCache cache(2); |
| InitSCTAuditing(&cache); |
| cache.set_sampling_rate(0); |
| |
| // Enqueue a report. |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions", "signature", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| // Check that there is one entry in the cache. |
| EXPECT_EQ(1u, cache.GetCacheForTesting()->size()); |
| |
| // Check that there are no pending reports. |
| EXPECT_EQ(0, url_loader_factory()->NumPending()); |
| } |
| |
| // Tests that when a new report is sampled, it will be sent to the server. |
| // TODO(cthomp): Allow tracking success/failure of the report being sent. One |
| // way would be to have OnSuccess/OnError handlers be defined by an |
| // SCTAuditingReportingDelegate installed on the cache. |
| TEST_F(SCTAuditingCacheTest, ReportsSentWithServerOK) { |
| SCTAuditingCache cache(2); |
| InitSCTAuditing(&cache); |
| |
| // Enqueue a report which will trigger a send. |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions", "signature", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| // Check that there is one pending report. |
| EXPECT_EQ(1, url_loader_factory()->NumPending()); |
| |
| // Simulate the server returning 200 OK to the report request. |
| url_loader_factory()->AddResponse("https://example.test", |
| /*content=*/"", |
| /*status=*/net::HTTP_OK); |
| task_environment_.RunUntilIdle(); |
| |
| EXPECT_EQ(0, url_loader_factory()->NumPending()); |
| |
| // Check that the report has been cleared in the cache as it has been |
| // successfully sent. |
| for (const auto& entry : *cache.GetCacheForTesting()) { |
| EXPECT_FALSE(entry.second); |
| } |
| } |
| |
| // Tests when the report server returns an HTTP error code. |
| // TODO(cthomp): Check that the cache treats the send as a failure. |
| TEST_F(SCTAuditingCacheTest, ReportSentWithServerError) { |
| SCTAuditingCache cache(2); |
| InitSCTAuditing(&cache); |
| |
| // Enqueue a report which will trigger a send. |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions", "signature", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| // Check that there is one pending report. |
| EXPECT_EQ(1, url_loader_factory()->NumPending()); |
| |
| // Simulate the server returning 429 TOO MANY REQUEST to the report request. |
| url_loader_factory()->AddResponse("https://example.test", |
| /*content=*/"", |
| /*status=*/net::HTTP_TOO_MANY_REQUESTS); |
| task_environment_.RunUntilIdle(); |
| |
| EXPECT_EQ(0, url_loader_factory()->NumPending()); |
| |
| // Check that the report is still stored in the cache as it has not succeeded. |
| for (const auto& entry : *cache.GetCacheForTesting()) { |
| EXPECT_TRUE(entry.second); |
| } |
| } |
| |
| // Tests that cache size high water mark metrics are correctly logged. |
| TEST_F(SCTAuditingCacheTest, HighWaterMarkMetrics) { |
| base::HistogramTester histograms; |
| // Create a cache so we can trigger destruction when it goes out of scope, |
| // which is when HWM metrics are logged. |
| { |
| SCTAuditingCache cache(5); |
| InitSCTAuditing(&cache); |
| |
| const net::HostPortPair host_port_pair1("example1.com", 443); |
| const net::HostPortPair host_port_pair2("example2.com", 443); |
| |
| // Fill the cache with two reports. |
| net::SignedCertificateTimestampAndStatusList sct_list1; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions1", "signature1", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list1); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair1, |
| chain_.get(), sct_list1); |
| |
| net::SignedCertificateTimestampAndStatusList sct_list2; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions2", "signature2", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list2); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair2, |
| chain_.get(), sct_list2); |
| |
| EXPECT_EQ(2u, cache.GetCacheForTesting()->size()); |
| } |
| |
| // The bucket for a HWM of 2 should have a single sample as there were two |
| // items in the cache when it was destroyed. |
| histograms.ExpectUniqueSample("Security.SCTAuditing.OptIn.CacheHWM", 2, 1); |
| } |
| |
| // Tests that enqueueing a report causes its size to be logged. Trying to log |
| // the same SCTs a second time will cause the deduplication to be logged instead |
| // of logging the report size a second time. |
| TEST_F(SCTAuditingCacheTest, ReportSizeMetrics) { |
| SCTAuditingCache cache(10); |
| InitSCTAuditing(&cache); |
| |
| base::HistogramTester histograms; |
| |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions", "signature", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| // Get the size of the enqueued report and test that it is correctly logged. |
| size_t report_size = |
| cache.GetCacheForTesting()->begin()->second->ByteSizeLong(); |
| ASSERT_GT(report_size, 0u); |
| histograms.ExpectUniqueSample("Security.SCTAuditing.OptIn.ReportSize", |
| report_size, 1); |
| |
| // Retry enqueueing the same report which will be deduplicated. |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| histograms.ExpectTotalCount("Security.SCTAuditing.OptIn.ReportSampled", 1); |
| histograms.ExpectTotalCount("Security.SCTAuditing.OptIn.ReportSize", 1); |
| histograms.ExpectBucketCount("Security.SCTAuditing.OptIn.ReportDeduplicated", |
| true, 1); |
| } |
| |
| // Test that metrics for when reports are dropped due to sampling are correctly |
| // logged. |
| TEST_F(SCTAuditingCacheTest, ReportSampleDroppedMetrics) { |
| base::HistogramTester histograms; |
| |
| SCTAuditingCache cache(10); |
| InitSCTAuditing(&cache); |
| cache.set_sampling_rate(0); |
| |
| const net::HostPortPair host_port_pair("example.com", 443); |
| net::SignedCertificateTimestampAndStatusList sct_list; |
| MakeTestSCTAndStatus(net::ct::SignedCertificateTimestamp::SCT_EMBEDDED, |
| "extensions", "signature", base::Time::Now(), |
| net::ct::SCT_STATUS_LOG_UNKNOWN, &sct_list); |
| cache.MaybeEnqueueReport(network_context_.get(), host_port_pair, chain_.get(), |
| sct_list); |
| |
| histograms.ExpectUniqueSample("Security.SCTAuditing.OptIn.ReportSampled", |
| false, 1); |
| histograms.ExpectTotalCount("Security.SCTAuditing.OptIn.ReportSize", 0); |
| histograms.ExpectBucketCount("Security.SCTAuditing.OptIn.ReportDeduplicated", |
| false, 1); |
| } |
| |
| } // namespace network |