blob: 247d456b219db1d04918f883047fb1890be557bb [file] [log] [blame]
// Copyright 2016 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 "components/certificate_transparency/single_tree_tracker.h"
#include <string>
#include <utility>
#include <memory>
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_task_environment.h"
#include "components/base32/base32.h"
#include "components/certificate_transparency/log_dns_client.h"
#include "components/certificate_transparency/mock_log_dns_traffic.h"
#include "crypto/sha2.h"
#include "net/base/network_change_notifier.h"
#include "net/cert/ct_log_verifier.h"
#include "net/cert/ct_serialization.h"
#include "net/cert/merkle_tree_leaf.h"
#include "net/cert/signed_certificate_timestamp.h"
#include "net/cert/signed_tree_head.h"
#include "net/cert/x509_certificate.h"
#include "net/dns/dns_client.h"
#include "net/dns/mock_host_resolver.h"
#include "net/log/net_log.h"
#include "net/log/test_net_log.h"
#include "net/log/test_net_log_util.h"
#include "net/test/ct_test_util.h"
#include "net/url_request/url_request_filter.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
using net::ct::SignedCertificateTimestamp;
using net::ct::SignedTreeHead;
using net::ct::GetSampleSignedTreeHead;
using net::ct::GetTestPublicKeyId;
using net::ct::GetTestPublicKey;
using net::ct::kSthRootHashLength;
using net::ct::GetX509CertSCT;
namespace certificate_transparency {
namespace {
const char kHostname[] = "example.test";
const char kCanCheckForInclusionHistogramName[] =
"Net.CertificateTransparency.CanInclusionCheckSCT";
const char kInclusionCheckResultHistogramName[] =
"Net.CertificateTransparency.InclusionCheckResult";
const char kDNSRequestSuffix[] = "dns.example.com";
// These tests use a 0 time-to-live for HostCache entries, so all entries will
// be stale. This is fine because SingleTreeTracker considers stale entries to
// still be evidence that a DNS lookup was performed for a given hostname.
// Ignoring stale entries could be exploited, as an attacker could set their
// website's DNS record to have a very short TTL in order to avoid having
// inclusion checks performed on the SCTs they provide.
constexpr base::TimeDelta kZeroTTL;
constexpr base::TimeDelta kMoreThanMMD = base::TimeDelta::FromHours(25);
bool GetOldSignedTreeHead(SignedTreeHead* sth) {
sth->version = SignedTreeHead::V1;
sth->timestamp = base::Time::UnixEpoch() +
base::TimeDelta::FromMilliseconds(INT64_C(1348589665525));
sth->tree_size = 12u;
const uint8_t kOldSTHRootHash[] = {
0x18, 0x04, 0x1b, 0xd4, 0x66, 0x50, 0x83, 0x00, 0x1f, 0xba, 0x8c,
0x54, 0x11, 0xd2, 0xd7, 0x48, 0xe8, 0xab, 0xbf, 0xdc, 0xdf, 0xd9,
0x21, 0x8c, 0xb0, 0x2b, 0x68, 0xa7, 0x8e, 0x7d, 0x4c, 0x23};
memcpy(sth->sha256_root_hash, kOldSTHRootHash, kSthRootHashLength);
sth->log_id = GetTestPublicKeyId();
const uint8_t kOldSTHSignatureData[] = {
0x04, 0x03, 0x00, 0x47, 0x30, 0x45, 0x02, 0x20, 0x15, 0x7b, 0x23,
0x42, 0xa2, 0x5f, 0x88, 0xc9, 0x0b, 0x30, 0xa6, 0xb4, 0x49, 0x50,
0xb3, 0xab, 0xf5, 0x25, 0xfe, 0x27, 0xf0, 0x3f, 0x9a, 0xbf, 0xc1,
0x16, 0x5a, 0x7a, 0xc0, 0x62, 0x2b, 0xbb, 0x02, 0x21, 0x00, 0xe6,
0x57, 0xa3, 0xfe, 0xfc, 0x5a, 0x82, 0x9b, 0x29, 0x46, 0x15, 0x1d,
0xbc, 0xfd, 0x9e, 0x87, 0x7f, 0xd0, 0x00, 0x5d, 0x62, 0x4f, 0x9a,
0x1a, 0x9f, 0x20, 0x79, 0xd0, 0xc1, 0x34, 0x2e, 0x08};
base::StringPiece sp(reinterpret_cast<const char*>(kOldSTHSignatureData),
sizeof(kOldSTHSignatureData));
return DecodeDigitallySigned(&sp, &(sth->signature)) && sp.empty();
}
scoped_refptr<SignedCertificateTimestamp> GetSCT() {
scoped_refptr<SignedCertificateTimestamp> sct;
// TODO(eranm): Move setting of the origin field to ct_test_util.cc
GetX509CertSCT(&sct);
sct->origin = SignedCertificateTimestamp::SCT_FROM_OCSP_RESPONSE;
return sct;
}
std::string LeafHash(const net::X509Certificate* cert,
const SignedCertificateTimestamp* sct) {
net::ct::MerkleTreeLeaf leaf;
if (!GetMerkleTreeLeaf(cert, sct, &leaf))
return std::string();
std::string leaf_hash;
if (!HashMerkleTreeLeaf(leaf, &leaf_hash))
return std::string();
return leaf_hash;
}
std::string Base32LeafHash(const net::X509Certificate* cert,
const SignedCertificateTimestamp* sct) {
std::string leaf_hash = LeafHash(cert, sct);
if (leaf_hash.empty())
return std::string();
return base32::Base32Encode(leaf_hash,
base32::Base32EncodePolicy::OMIT_PADDING);
}
// Fills in |sth| for a tree of size 2, where the root hash is a hash of
// the test SCT (from GetX509CertSCT) and another entry,
// whose hash is '0a' 32 times.
bool GetSignedTreeHeadForTreeOfSize2(SignedTreeHead* sth) {
sth->version = SignedTreeHead::V1;
// Timestamp is after the timestamp of the test SCT (GetX509CertSCT)
// to indicate it can be audited using this STH.
sth->timestamp = base::Time::UnixEpoch() +
base::TimeDelta::FromMilliseconds(INT64_C(1365354256089));
sth->tree_size = 2;
// Root hash is:
// HASH (0x01 || HASH(log entry made of test SCT) || HASH(0x0a * 32))
// The proof provided by FillVectorWithValidAuditProofForTreeOfSize2 would
// validate with this root hash for the log entry made of the test SCT +
// cert.
const uint8_t kRootHash[] = {0x16, 0x80, 0xbd, 0x5a, 0x1b, 0xc1, 0xb6, 0xcf,
0x1b, 0x7e, 0x77, 0x41, 0xeb, 0xed, 0x86, 0x8b,
0x73, 0x81, 0x87, 0xf5, 0xab, 0x93, 0x6d, 0xb2,
0x0a, 0x79, 0x0d, 0x9e, 0x40, 0x55, 0xc3, 0xe6};
memcpy(sth->sha256_root_hash, reinterpret_cast<const char*>(kRootHash),
kSthRootHashLength);
sth->log_id = GetTestPublicKeyId();
// valid signature over the STH, using the test log key at:
// https://github.com/google/certificate-transparency/blob/master/test/testdata/ct-server-key.pem
const uint8_t kTreeHeadSignatureData[] = {
0x04, 0x03, 0x00, 0x46, 0x30, 0x44, 0x02, 0x20, 0x25, 0xa1, 0x9d,
0x7b, 0xf6, 0xe6, 0xfc, 0x47, 0xa7, 0x2d, 0xef, 0x6b, 0xf4, 0x84,
0x71, 0xb7, 0x7b, 0x7e, 0xd4, 0x4c, 0x7a, 0x5c, 0x4f, 0x9a, 0xb7,
0x04, 0x71, 0x6e, 0xd0, 0xa8, 0x0f, 0x53, 0x02, 0x20, 0x27, 0xe5,
0xed, 0x7d, 0xc3, 0x5d, 0x4c, 0xf0, 0x67, 0x35, 0x5d, 0x8a, 0x10,
0xae, 0x25, 0x87, 0x1a, 0xef, 0xea, 0xd2, 0xf7, 0xe3, 0x73, 0x2f,
0x07, 0xb3, 0x4b, 0xea, 0x5b, 0xdd, 0x81, 0x2d};
base::StringPiece sp(reinterpret_cast<const char*>(kTreeHeadSignatureData),
sizeof(kTreeHeadSignatureData));
return DecodeDigitallySigned(&sp, &sth->signature);
}
void FillVectorWithValidAuditProofForTreeOfSize2(
std::vector<std::string>* out_proof) {
std::string node(crypto::kSHA256Length, '\0');
for (size_t i = 0; i < crypto::kSHA256Length; ++i) {
node[i] = static_cast<char>(0x0a);
}
out_proof->push_back(node);
}
void AddCacheEntry(net::HostCache* cache,
const std::string& hostname,
bool secure,
net::HostCache::Entry::Source source,
base::TimeDelta ttl) {
auto key = net::HostCache::Key(hostname, net::ADDRESS_FAMILY_UNSPECIFIED, 0);
key.secure = secure;
cache->Set(key, net::HostCache::Entry(net::OK, net::AddressList(), source),
base::TimeTicks::Now(), ttl);
}
} // namespace
class SingleTreeTrackerTest : public ::testing::Test {
void SetUp() override {
log_ = net::CTLogVerifier::Create(GetTestPublicKey(), "testlog",
kDNSRequestSuffix);
ASSERT_TRUE(log_);
ASSERT_EQ(log_->key_id(), GetTestPublicKeyId());
const std::string der_test_cert(net::ct::GetDerEncodedX509Cert());
chain_ = net::X509Certificate::CreateFromBytes(der_test_cert.data(),
der_test_cert.length());
ASSERT_TRUE(chain_.get());
GetX509CertSCT(&cert_sct_);
cert_sct_->origin = SignedCertificateTimestamp::SCT_FROM_OCSP_RESPONSE;
net_change_notifier_ =
base::WrapUnique(net::NetworkChangeNotifier::CreateMock());
mock_dns_.InitializeDnsConfig();
net::URLRequestFilter* filter = net::URLRequestFilter::GetInstance();
filter->AddHostnameInterceptor(
"https", "mock.http",
std::make_unique<MockLogDnsTraffic::DohJobInterceptor>());
}
void TearDown() override {
net::URLRequestFilter* filter = net::URLRequestFilter::GetInstance();
filter->ClearHandlers();
}
protected:
void CreateTreeTracker() {
log_dns_client_ = std::make_unique<LogDnsClient>(
mock_dns_.CreateDnsClient(), new net::TestURLRequestContext(),
net_log_with_source_, 1);
tree_tracker_ = std::make_unique<SingleTreeTracker>(
log_, log_dns_client_.get(), &host_resolver_, &net_log_);
}
void CreateTreeTrackerWithDefaultDnsExpectation() {
// Default to throttling requests as it means observed log entries will
// be frozen in a pending state, simplifying testing of the
// SingleTreeTracker.
ASSERT_TRUE(ExpectLeafIndexRequestAndThrottle(chain_, cert_sct_));
CreateTreeTracker();
}
// Configured the |mock_dns_| to expect a request for the leaf index
// and have th mock DNS client throttle it.
bool ExpectLeafIndexRequestAndThrottle(
const scoped_refptr<net::X509Certificate>& chain,
const scoped_refptr<SignedCertificateTimestamp>& sct) {
return mock_dns_.ExpectRequestAndSocketError(
Base32LeafHash(chain.get(), sct.get()) + ".hash." + kDNSRequestSuffix,
net::Error::ERR_TEMPORARILY_THROTTLED);
}
bool MatchAuditingResultInNetLog(net::TestNetLog& net_log,
std::string expected_leaf_hash,
bool expected_success) {
net::TestNetLogEntry::List entries;
net_log.GetEntries(&entries);
if (entries.size() == 0)
return false;
size_t pos = net::ExpectLogContainsSomewhere(
entries, 0, net::NetLogEventType::CT_LOG_ENTRY_AUDITED,
net::NetLogEventPhase::NONE);
const net::TestNetLogEntry& logged_entry = entries[pos];
std::string logged_log_id, logged_leaf_hash;
if (!logged_entry.GetStringValue("log_id", &logged_log_id) ||
!logged_entry.GetStringValue("log_entry", &logged_leaf_hash))
return false;
if (base::HexEncode(GetTestPublicKeyId().data(),
GetTestPublicKeyId().size()) != logged_log_id)
return false;
if (base::HexEncode(expected_leaf_hash.data(), expected_leaf_hash.size()) !=
logged_leaf_hash)
return false;
bool logged_success;
if (!logged_entry.GetBooleanValue("success", &logged_success))
return false;
return logged_success == expected_success;
}
base::test::ScopedTaskEnvironment task_environment_{
base::test::ScopedTaskEnvironment::MainThreadType::IO};
MockLogDnsTraffic mock_dns_;
scoped_refptr<const net::CTLogVerifier> log_;
std::unique_ptr<net::NetworkChangeNotifier> net_change_notifier_;
std::unique_ptr<LogDnsClient> log_dns_client_;
net::MockCachingHostResolver host_resolver_;
std::unique_ptr<SingleTreeTracker> tree_tracker_;
scoped_refptr<net::X509Certificate> chain_;
scoped_refptr<SignedCertificateTimestamp> cert_sct_;
net::TestNetLog net_log_;
net::NetLogWithSource net_log_with_source_;
};
// Test that an SCT is discarded if the HostResolver cache does not indicate
// that the hostname lookup was done using DNS. To perform an inclusion check
// in this case could compromise privacy, as the DNS resolver would learn that
// the user had visited that host.
TEST_F(SingleTreeTrackerTest, DiscardsSCTWhenHostnameNotLookedUpUsingDNS) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_UNKNOWN, kZeroTTL);
base::HistogramTester histograms;
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Make sure the SCT status is the same as if there's no STH for this log.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// The status for this SCT should still be 'not observed'.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Exactly one value should be logged, indicating that the SCT could not be
// checked for inclusion because of no prior DNS lookup for this hostname.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 4, 1);
// Nothing should be logged in the result histogram or net log since an
// inclusion check wasn't performed.
histograms.ExpectTotalCount(kInclusionCheckResultHistogramName, 0);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that an SCT is discarded if the hostname it was obtained from is an IP
// literal. To perform an inclusion check in this case could compromise privacy,
// as the DNS resolver would learn that the user had visited that host.
TEST_F(SingleTreeTrackerTest, DiscardsSCTWhenHostnameIsIPLiteral) {
CreateTreeTrackerWithDefaultDnsExpectation();
base::HistogramTester histograms;
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Make sure the SCT status is the same as if there's no STH for this log.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
tree_tracker_->OnSCTVerified("::1", chain_.get(), cert_sct_.get());
// The status for this SCT should still be 'not observed'.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Exactly one value should be logged, indicating that the SCT could not be
// checked for inclusion because of no prior DNS lookup for this hostname
// (because it's an IP literal).
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 4, 1);
// Nothing should be logged in the result histogram or net log since an
// inclusion check wasn't performed.
histograms.ExpectTotalCount(kInclusionCheckResultHistogramName, 0);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that an SCT is discarded if the network has changed since the hostname
// lookup was performed. To perform an inclusion check in this case could
// compromise privacy, as the current DNS resolver would learn that the user had
// visited that host (it would not already know this already because the
// hostname lookup was performed on a different network, using a different DNS
// resolver).
TEST_F(SingleTreeTrackerTest,
DiscardsSCTWhenHostnameLookedUpUsingDNSOnDiffNetwork) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
// Simulate network change.
host_resolver_.GetHostCache()->Invalidate();
base::HistogramTester histograms;
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Make sure the SCT status is the same as if there's no STH for this log.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// The status for this SCT should still be 'not observed'.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Exactly one value should be logged, indicating that the SCT could not be
// checked for inclusion because of no prior DNS lookup for this hostname on
// the current network.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 4, 1);
// Nothing should be logged in the result histogram or net log since an
// inclusion check wasn't performed.
histograms.ExpectTotalCount(kInclusionCheckResultHistogramName, 0);
EXPECT_EQ(0u, net_log_.GetSize());
}
TEST_F(SingleTreeTrackerTest, EntriesIndistinguishedBySecurity) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, true /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
base::HistogramTester histograms;
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Make sure the SCT status is the same as if there's no STH for this log.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// The status for this SCT should be 'pending inclusion check' with
// |pending_lookup_securely| set to true since the cache check will return the
// secure key.
bool pending_lookup_securely;
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get(), &pending_lookup_securely));
EXPECT_TRUE(pending_lookup_securely);
// Exactly one value should be logged, indicating the SCT can be checked for
// inclusion, as |tree_tracker_| did have a valid STH when it was notified
// of a new SCT.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 2, 1);
// Simulate network change.
host_resolver_.GetHostCache()->Invalidate();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// The status for this SCT should still be 'pending inclusion check' with
// |pending_lookup_securely| set to true.
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get(), &pending_lookup_securely));
EXPECT_TRUE(pending_lookup_securely);
// Another value should be logged, indicating that there is already a
// pending audit check for this SCT.
histograms.ExpectBucketCount(kCanCheckForInclusionHistogramName, 6, 1);
// Nothing should be logged in the result histogram or net log since an
// inclusion check wasn't performed.
histograms.ExpectTotalCount(kInclusionCheckResultHistogramName, 0);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that an SCT is classified as pending for a newer STH if the
// SingleTreeTracker has not seen any STHs so far.
TEST_F(SingleTreeTrackerTest, CorrectlyClassifiesUnobservedSCTNoSTH) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
base::HistogramTester histograms;
// First make sure the SCT has not been observed at all.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// Since no STH was provided to the tree_tracker_ the status should be that
// the SCT is pending a newer STH.
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Expect logging of a value indicating a valid STH is required.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 0, 1);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that an SCT is classified as pending an inclusion check if the
// SingleTreeTracker has a fresh-enough STH to check inclusion against.
TEST_F(SingleTreeTrackerTest, CorrectlyClassifiesUnobservedSCTWithRecentSTH) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
base::HistogramTester histograms;
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Make sure the SCT status is the same as if there's no STH for
// this log.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// The status for this SCT should be 'pending inclusion check' since the STH
// provided at the beginning of the test is newer than the SCT.
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Exactly one value should be logged, indicating the SCT can be checked for
// inclusion, as |tree_tracker_| did have a valid STH when it was notified
// of a new SCT.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 2, 1);
// Nothing should be logged in the result histogram since inclusion check
// didn't finish.
histograms.ExpectTotalCount(kInclusionCheckResultHistogramName, 0);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that the SingleTreeTracker correctly queues verified SCTs for inclusion
// checking such that, upon receiving a fresh STH, it changes the SCT's status
// from pending newer STH to pending inclusion check.
TEST_F(SingleTreeTrackerTest, CorrectlyUpdatesSCTStatusOnNewSTH) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
base::HistogramTester histograms;
// Report an observed SCT and make sure it's in the pending newer STH
// state.
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
histograms.ExpectTotalCount(kCanCheckForInclusionHistogramName, 1);
// Provide with a fresh STH
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Test that its status has changed.
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Check that no additional UMA was logged for this case as the histogram is
// only supposed to measure the state of newly-observed SCTs, not pending
// ones.
histograms.ExpectTotalCount(kCanCheckForInclusionHistogramName, 1);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that the SingleTreeTracker does not change an SCT's status if an STH
// from the log it was issued by is observed, but that STH is too old to check
// inclusion against.
TEST_F(SingleTreeTrackerTest, DoesNotUpdatesSCTStatusOnOldSTH) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
// Notify of an SCT and make sure it's in the 'pending newer STH' state.
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Provide an old STH for the same log.
SignedTreeHead sth;
GetOldSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
// Make sure the SCT's state hasn't changed.
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that the SingleTreeTracker correctly logs that an SCT is pending a new
// STH, when it has a valid STH, but the observed SCT is not covered by the
// STH.
TEST_F(SingleTreeTrackerTest, LogsUMAForNewSCTAndOldSTH) {
CreateTreeTrackerWithDefaultDnsExpectation();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
base::HistogramTester histograms;
// Provide an old STH for the same log.
SignedTreeHead sth;
GetOldSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
histograms.ExpectTotalCount(kCanCheckForInclusionHistogramName, 0);
// Notify of an SCT and make sure it's in the 'pending newer STH' state.
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// Exactly one value should be logged, indicating the SCT cannot be checked
// for inclusion as the STH is too old.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 1, 1);
EXPECT_EQ(0u, net_log_.GetSize());
}
// Test that an entry transitions to the "not found" state if the LogDnsClient
// fails to get a leaf index.
TEST_F(SingleTreeTrackerTest, TestEntryNotPendingAfterLeafIndexFetchFailure) {
ASSERT_TRUE(mock_dns_.ExpectRequestAndSocketError(
Base32LeafHash(chain_.get(), cert_sct_.get()) + ".hash." +
kDNSRequestSuffix,
net::Error::ERR_FAILED));
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Provide with a fresh STH
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// There should have been one NetLog event, logged with failure.
EXPECT_TRUE(MatchAuditingResultInNetLog(
net_log_, LeafHash(chain_.get(), cert_sct_.get()), false));
}
// Test that an entry transitions to the "not found" state if the LogDnsClient
// succeeds to get a leaf index but fails to get an inclusion proof.
TEST_F(SingleTreeTrackerTest, TestEntryNotPendingAfterInclusionCheckFailure) {
// Return 12 as the index of this leaf.
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), cert_sct_.get()) + ".hash." +
kDNSRequestSuffix,
12));
// Expect a request for an inclusion proof for leaf #12 in a tree of size
// 21, which is the size of the tree in the STH returned by
// GetSampleSignedTreeHead.
ASSERT_TRUE(mock_dns_.ExpectRequestAndSocketError(
std::string("0.12.21.tree.") + kDNSRequestSuffix,
net::Error::ERR_FAILED));
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Provide with a fresh STH
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
tree_tracker_->NewSTHObserved(sth);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// There should have been one NetLog event, logged with failure.
EXPECT_TRUE(MatchAuditingResultInNetLog(
net_log_, LeafHash(chain_.get(), cert_sct_.get()), false));
}
// Test that an entry transitions to the "included" state if the LogDnsClient
// succeeds to get a leaf index and an inclusion proof.
TEST_F(SingleTreeTrackerTest, TestEntryIncludedAfterInclusionCheckSuccess) {
std::vector<std::string> audit_proof;
FillVectorWithValidAuditProofForTreeOfSize2(&audit_proof);
// Return 0 as the index for this leaf, so the proof provided
// later on would verify.
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), cert_sct_.get()) + ".hash." +
kDNSRequestSuffix,
0));
// The STH (later on) is for a tree of size 2 and the entry has index 0
// in the tree, so expect an inclusion proof for entry 0 in a tree
// of size 2 (0.0.2).
ASSERT_TRUE(mock_dns_.ExpectAuditProofRequestAndResponse(
std::string("0.0.2.tree.") + kDNSRequestSuffix, audit_proof.begin(),
audit_proof.begin() + 1));
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Provide with a fresh STH, which is for a tree of size 2.
SignedTreeHead sth;
ASSERT_TRUE(GetSignedTreeHeadForTreeOfSize2(&sth));
ASSERT_TRUE(log_->VerifySignedTreeHead(sth));
tree_tracker_->NewSTHObserved(sth);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(SingleTreeTracker::SCT_INCLUDED_IN_LOG,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// There should have been one NetLog event, with success logged.
EXPECT_TRUE(MatchAuditingResultInNetLog(
net_log_, LeafHash(chain_.get(), cert_sct_.get()), true));
}
// Tests that inclusion checks are aborted and SCTs discarded if under critical
// memory pressure.
TEST_F(SingleTreeTrackerTest,
TestInclusionCheckCancelledIfUnderMemoryPressure) {
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Provide with a fresh STH, which is for a tree of size 2.
SignedTreeHead sth;
ASSERT_TRUE(GetSignedTreeHeadForTreeOfSize2(&sth));
ASSERT_TRUE(log_->VerifySignedTreeHead(sth));
// Make the first event that is processed a critical memory pressure
// notification. This should be handled before the response to the first DNS
// request, so no requests after the first one should be sent (the leaf index
// request).
base::MemoryPressureListener::NotifyMemoryPressure(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL);
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), cert_sct_.get()) + ".hash." +
kDNSRequestSuffix,
0));
tree_tracker_->NewSTHObserved(sth);
base::RunLoop().RunUntilIdle();
// Expect the SCT to have been discarded.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
}
// Test that pending entries transition states correctly according to the
// STHs provided:
// * Start without an STH.
// * Add a collection of entries with mixed timestamps (i.e. SCTs not added
// in the order of their timestamps).
// * Provide an STH that covers some of the entries, test these are audited.
// * Provide another STH that covers more of the entries, test that the entries
// already audited are not audited again and that those that need to be
// audited are audited, while those that are not covered by that STH are
// not audited.
TEST_F(SingleTreeTrackerTest, TestMultipleEntriesTransitionStateCorrectly) {
SignedTreeHead old_sth;
GetOldSignedTreeHead(&old_sth);
SignedTreeHead new_sth;
GetSampleSignedTreeHead(&new_sth);
base::TimeDelta kLessThanMMD = base::TimeDelta::FromHours(23);
// Assert the gap between the two timestamps is big enough so that
// all assumptions below on which SCT can be audited with the
// new STH are true.
ASSERT_LT(old_sth.timestamp + (kMoreThanMMD * 2), new_sth.timestamp);
// Oldest SCT - auditable by the old and new STHs.
scoped_refptr<SignedCertificateTimestamp> oldest_sct(GetSCT());
oldest_sct->timestamp = old_sth.timestamp - kMoreThanMMD;
// SCT that's older than the old STH's timestamp but by less than the MMD,
// so not auditable by old STH.
scoped_refptr<SignedCertificateTimestamp> not_auditable_by_old_sth_sct(
GetSCT());
not_auditable_by_old_sth_sct->timestamp = old_sth.timestamp - kLessThanMMD;
// SCT that's newer than the old STH's timestamp so is only auditable by
// the new STH.
scoped_refptr<SignedCertificateTimestamp> newer_than_old_sth_sct(GetSCT());
newer_than_old_sth_sct->timestamp = old_sth.timestamp + kLessThanMMD;
// SCT that's older than the new STH's timestamp but by less than the MMD,
// so isn't auditable by the new STH.
scoped_refptr<SignedCertificateTimestamp> not_auditable_by_new_sth_sct(
GetSCT());
not_auditable_by_new_sth_sct->timestamp = new_sth.timestamp - kLessThanMMD;
// SCT that's newer than the new STH's timestamp so isn't auditable by the
// the new STH.
scoped_refptr<SignedCertificateTimestamp> newer_than_new_sth_sct(GetSCT());
newer_than_new_sth_sct->timestamp = new_sth.timestamp + kLessThanMMD;
// Set up DNS expectations based on inclusion proof request order.
ASSERT_TRUE(ExpectLeafIndexRequestAndThrottle(chain_, oldest_sct));
ASSERT_TRUE(
ExpectLeafIndexRequestAndThrottle(chain_, not_auditable_by_old_sth_sct));
ASSERT_TRUE(
ExpectLeafIndexRequestAndThrottle(chain_, newer_than_old_sth_sct));
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
// Add SCTs in mixed order.
tree_tracker_->OnSCTVerified(kHostname, chain_.get(),
newer_than_new_sth_sct.get());
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), oldest_sct.get());
tree_tracker_->OnSCTVerified(kHostname, chain_.get(),
not_auditable_by_new_sth_sct.get());
tree_tracker_->OnSCTVerified(kHostname, chain_.get(),
newer_than_old_sth_sct.get());
tree_tracker_->OnSCTVerified(kHostname, chain_.get(),
not_auditable_by_old_sth_sct.get());
// Ensure all are in the PENDING_NEWER_STH state.
for (const auto& sct :
{oldest_sct, not_auditable_by_old_sth_sct, newer_than_old_sth_sct,
not_auditable_by_new_sth_sct, newer_than_new_sth_sct}) {
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(chain_.get(),
sct.get()))
<< "SCT age: " << sct->timestamp;
}
// Provide the old STH, ensure only the oldest one is auditable.
tree_tracker_->NewSTHObserved(old_sth);
// Ensure all but the oldest are in the PENDING_NEWER_STH state.
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), oldest_sct.get()));
for (const auto& sct :
{not_auditable_by_old_sth_sct, newer_than_old_sth_sct,
not_auditable_by_new_sth_sct, newer_than_new_sth_sct}) {
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(chain_.get(),
sct.get()))
<< "SCT age: " << sct->timestamp;
}
// Provide the newer one, ensure two more are auditable but the
// rest aren't.
tree_tracker_->NewSTHObserved(new_sth);
for (const auto& sct :
{not_auditable_by_old_sth_sct, newer_than_old_sth_sct}) {
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(chain_.get(),
sct.get()))
<< "SCT age: " << sct->timestamp;
}
for (const auto& sct :
{not_auditable_by_new_sth_sct, newer_than_new_sth_sct}) {
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_NEWER_STH,
tree_tracker_->GetLogEntryInclusionStatusForTesting(chain_.get(),
sct.get()))
<< "SCT age: " << sct->timestamp;
}
}
// Test that if a request for an entry is throttled, it remains in a
// pending state.
// Test that if several entries are throttled, when the LogDnsClient notifies
// of un-throttling all entries are handled.
TEST_F(SingleTreeTrackerTest, TestThrottledEntryGetsHandledAfterUnthrottling) {
std::vector<std::string> audit_proof;
FillVectorWithValidAuditProofForTreeOfSize2(&audit_proof);
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), cert_sct_.get()) + ".hash." +
kDNSRequestSuffix,
0));
ASSERT_TRUE(mock_dns_.ExpectAuditProofRequestAndResponse(
std::string("0.0.2.tree.") + kDNSRequestSuffix, audit_proof.begin(),
audit_proof.begin() + 1));
scoped_refptr<SignedCertificateTimestamp> second_sct(GetSCT());
second_sct->timestamp -= base::TimeDelta::FromHours(1);
// Process request for |second_sct|
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), second_sct.get()) + ".hash." +
kDNSRequestSuffix,
1));
ASSERT_TRUE(mock_dns_.ExpectAuditProofRequestAndResponse(
std::string("0.1.2.tree.") + kDNSRequestSuffix, audit_proof.begin(),
audit_proof.begin() + 1));
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
SignedTreeHead sth;
ASSERT_TRUE(GetSignedTreeHeadForTreeOfSize2(&sth));
tree_tracker_->NewSTHObserved(sth);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), second_sct.get());
// Both entries should be in the pending state, the first because the
// LogDnsClient did not invoke the callback yet, the second one because
// the LogDnsClient is "busy" with the first entry and so would throttle.
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
ASSERT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), second_sct.get()));
// Process pending DNS queries so later assertions are on handling
// of the entries based on replies received.
base::RunLoop().RunUntilIdle();
// Check that the first sct is included in the log.
ASSERT_EQ(SingleTreeTracker::SCT_INCLUDED_IN_LOG,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Check that the second SCT got an invalid proof and is not included, rather
// than being in the pending state.
ASSERT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), second_sct.get()));
}
// Test that proof fetching failure due to DNS config errors is handled
// correctly:
// (1) Entry removed from pending queue.
// (2) UMA logged
TEST_F(SingleTreeTrackerTest,
TestProofLookupDueToBadDNSConfigHandledCorrectly) {
base::HistogramTester histograms;
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSampleSignedTreeHead(&sth);
// Clear existing DNS configuration, so that the DnsClient created
// by the MockLogDnsTraffic has no valid DnsConfig.
net_change_notifier_.reset();
net_change_notifier_ =
base::WrapUnique(net::NetworkChangeNotifier::CreateMock());
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
tree_tracker_->NewSTHObserved(sth);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
// Make sure the SCT status indicates the entry has been removed from
// the SingleTreeTracker's internal queue as the DNS lookup failed
// synchronously.
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(
chain_.get(), cert_sct_.get()));
// Exactly one value should be logged, indicating the SCT can be checked for
// inclusion, as |tree_tracker_| did have a valid STH when it was notified
// of a new SCT.
histograms.ExpectUniqueSample(kCanCheckForInclusionHistogramName, 2, 1);
// Failure due to DNS configuration should be logged in the result histogram.
histograms.ExpectUniqueSample(kInclusionCheckResultHistogramName, 3, 1);
}
// Test that entries are no longer pending after a network state
// change.
TEST_F(SingleTreeTrackerTest, DiscardsPendingEntriesAfterNetworkChange) {
// Setup expectations for 2 SCTs to pass inclusion checking.
// However, the first should be cancelled half way through (when the network
// change occurs) and the second should be throttled (and then cancelled) so,
// by the end of test, neither should actually have passed the checks.
std::vector<std::string> audit_proof;
FillVectorWithValidAuditProofForTreeOfSize2(&audit_proof);
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), cert_sct_.get()) + ".hash." +
kDNSRequestSuffix,
0));
ASSERT_TRUE(mock_dns_.ExpectAuditProofRequestAndResponse(
std::string("0.0.2.tree.") + kDNSRequestSuffix, audit_proof.begin(),
audit_proof.begin() + 1));
scoped_refptr<SignedCertificateTimestamp> second_sct(GetSCT());
second_sct->timestamp -= base::TimeDelta::FromHours(1);
ASSERT_TRUE(mock_dns_.ExpectLeafIndexRequestAndResponse(
Base32LeafHash(chain_.get(), second_sct.get()) + ".hash." +
kDNSRequestSuffix,
1));
ASSERT_TRUE(mock_dns_.ExpectAuditProofRequestAndResponse(
std::string("0.1.2.tree.") + kDNSRequestSuffix, audit_proof.begin(),
audit_proof.begin() + 1));
CreateTreeTracker();
AddCacheEntry(host_resolver_.GetHostCache(), kHostname, false /* secure */,
net::HostCache::Entry::SOURCE_DNS, kZeroTTL);
// Provide an STH to the tree_tracker_.
SignedTreeHead sth;
GetSignedTreeHeadForTreeOfSize2(&sth);
tree_tracker_->NewSTHObserved(sth);
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), cert_sct_.get());
tree_tracker_->OnSCTVerified(kHostname, chain_.get(), second_sct.get());
for (auto sct : {cert_sct_, second_sct}) {
EXPECT_EQ(SingleTreeTracker::SCT_PENDING_INCLUSION_CHECK,
tree_tracker_->GetLogEntryInclusionStatusForTesting(chain_.get(),
sct.get()));
}
net_change_notifier_->NotifyObserversOfNetworkChangeForTests(
net::NetworkChangeNotifier::CONNECTION_UNKNOWN);
base::RunLoop().RunUntilIdle();
for (auto sct : {cert_sct_, second_sct}) {
EXPECT_EQ(SingleTreeTracker::SCT_NOT_OBSERVED,
tree_tracker_->GetLogEntryInclusionStatusForTesting(chain_.get(),
sct.get()));
}
}
} // namespace certificate_transparency