Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(225)

Unified Diff: components/certificate_transparency/single_tree_tracker.cc

Issue 2017563002: Add Certificate Transparency logs auditing (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressing Ryan's two final comments Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: components/certificate_transparency/single_tree_tracker.cc
diff --git a/components/certificate_transparency/single_tree_tracker.cc b/components/certificate_transparency/single_tree_tracker.cc
index adaf04d4e1df486c9bfc5d0b9d56e3f92fa59634..281231690f6a3257b61ed084a75ed8d45635d57b 100644
--- a/components/certificate_transparency/single_tree_tracker.cc
+++ b/components/certificate_transparency/single_tree_tracker.cc
@@ -4,15 +4,56 @@
#include "components/certificate_transparency/single_tree_tracker.h"
+#include <algorithm>
+#include <iterator>
+#include <list>
#include <utility>
+#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
+#include "components/certificate_transparency/log_dns_client.h"
+#include "crypto/sha2.h"
+#include "net/base/hash_value.h"
+#include "net/base/net_errors.h"
#include "net/cert/ct_log_verifier.h"
+#include "net/cert/merkle_audit_proof.h"
+#include "net/cert/merkle_tree_leaf.h"
#include "net/cert/signed_certificate_timestamp.h"
#include "net/cert/x509_certificate.h"
+using net::SHA256HashValue;
+using net::ct::LogEntry;
+using net::ct::MerkleAuditProof;
+using net::ct::MerkleTreeLeaf;
+using net::ct::SignedCertificateTimestamp;
using net::ct::SignedTreeHead;
+// Overview of the process for auditing CT log entries
+//
+// A CT log entry, represented by net::ct::LogEntry, is made up of the
+// end-entity certificate and the Signed Certificate Timestamp associated with
+// it.
+//
+// In this file, obsered CT log entries are audited for inclusion in the CT log.
+// A pre-requirement for auditing a log entry is having a Signed Tree Head (STH)
+// from that log that is 24 hours (MMD period) after the timestamp in the SCT.
+// Log entries observed while the client has no STH from that log or an STH that
+// is too old start in the PENDING_NEWER_STH state.
+//
+// Once a fresh-enough STH is obtained, all entries that can be audited using
+// this STH move to the PENDING_INCLUSION_PROOF_REQUEST state.
+//
+// Requests for the entry index and inclusion proof are obtained using a
+// LogDnsClient instance - when an inclusion proof for an entry has been
+// successfully requested (e.g. it has not been throttled), it moves to the
+// INCLUSION_PROOF_REQUESTED state.
+//
+// Once the inclusion check is done, the entry is removed from
+// |pending_entries_|. If the inclusion check has been successful, the entry
+// is added to |checked_entries_|.
+
+namespace certificate_transparency {
+
namespace {
// Enum indicating whether an SCT can be checked for inclusion and if not,
@@ -25,13 +66,20 @@ enum SCTCanBeCheckedForInclusion {
// first required to evaluate whether the SCT can be checked for inclusion
// or not.
VALID_STH_REQUIRED = 0,
+
// If the STH does not cover the SCT (the timestamp in the SCT is greater than
// MMD + timestamp in the STH), then a newer STH is needed.
NEWER_STH_REQUIRED = 1,
+
// When an SCT is observed, if the SingleTreeTracker instance has a valid STH
// and the STH covers the SCT (the timestamp in the SCT is less than MMD +
// timestamp in the STH), then it can be checked for inclusion.
CAN_BE_CHECKED = 2,
+
+ // This SCT was not audited because the queue of pending entries was
+ // full.
+ NOT_AUDITED_QUEUE_FULL = 3,
+
SCT_CAN_BE_CHECKED_MAX
};
@@ -43,46 +91,179 @@ void LogCanBeCheckedForInclusionToUMA(
can_be_checked, SCT_CAN_BE_CHECKED_MAX);
}
+// Enum indicating the outcome of an inclusion check for a particular log
+// entry.
+//
+// Note: The numeric values are used within a histogram and should not change
+// or be re-assigned.
+enum LogEntryInclusionCheckResult {
+ // Inclusion check succeeded: Proof obtained and validated successfully.
+ GOT_VALID_INCLUSION_PROOF = 0,
+
+ // Could not get an inclusion proof.
+ FAILED_GETTING_INCLUSION_PROOF = 1,
+
+ // An inclusion proof was obtained but it is invalid.
+ GOT_INVALID_INCLUSION_PROOF = 2,
+
+ // The SCT could not be audited because the client's DNS configuration
+ // is faulty.
+ DNS_QUERY_NOT_POSSIBLE = 3,
+
+ LOG_ENTRY_INCLUSION_CHECK_RESULT_MAX
+};
+
+void LogInclusionCheckResult(LogEntryInclusionCheckResult result) {
+ UMA_HISTOGRAM_ENUMERATION("Net.CertificateTransparency.InclusionCheckResult",
+ result, LOG_ENTRY_INCLUSION_CHECK_RESULT_MAX);
+}
+
+// Calculate the leaf hash of the the entry in the log represented by
+// the given |cert| and |sct|. If leaf hash calculation succeeds returns
+// true, false otherwise.
+bool GetLogEntryLeafHash(const net::X509Certificate* cert,
+ const SignedCertificateTimestamp* sct,
+ SHA256HashValue* leaf_hash) {
+ MerkleTreeLeaf leaf;
+ if (!GetMerkleTreeLeaf(cert, sct, &leaf))
+ return false;
+
+ std::string leaf_hash_str;
+ if (!HashMerkleTreeLeaf(leaf, &leaf_hash_str))
+ return false;
+
+ memcpy(leaf_hash->data, leaf_hash_str.data(), crypto::kSHA256Length);
+ return true;
+}
+
+// Audit state of a log entry.
+enum AuditState {
+ // Entry cannot be audited because a newer STH is needed.
+ PENDING_NEWER_STH,
+ // A leaf index has been obtained and the entry is now pending request
+ // of an inclusion proof.
+ PENDING_INCLUSION_PROOF_REQUEST,
+ // An inclusion proof for this entry has been requested from the log.
+ INCLUSION_PROOF_REQUESTED
+};
+
+// Maximal size of the checked entries cache.
+size_t kCheckedEntriesCacheSize = 100;
+
+// Maximal size of the pending entries queue.
+size_t kPendingEntriesQueueSize = 100;
+
+// Maximum Merge Delay - logs can have individual MMD, but all known logs
+// currently have 24 hours MMD and Chrome's CT policy requires an MMD
+// that's no greater than that. For simplicity, use 24 hours for all logs.
+constexpr base::TimeDelta kMaximumMergeDelay = base::TimeDelta::FromHours(24);
+
+// The log MUST incorporate the a certificate in the tree within the Maximum
+// Merge Delay, so an entry can be audited once the timestamp from the SCT +
+// MMD has passed.
+// Returns true if the timestamp from the STH is newer than SCT timestamp + MMD.
+bool IsSCTReadyForAudit(base::Time sth_timestamp, base::Time sct_timestamp) {
+ return sct_timestamp + kMaximumMergeDelay < sth_timestamp;
+}
+
} // namespace
-namespace certificate_transparency {
+// The entry that is being audited.
+struct SingleTreeTracker::EntryToAudit {
+ base::Time sct_timestamp;
+ SHA256HashValue leaf_hash;
+
+ explicit EntryToAudit(base::Time timestamp) : sct_timestamp(timestamp) {}
+};
+
+// State of a log entry: its audit state and information necessary to
+// validate an inclusion proof. Gets updated as the entry transitions
+// between the different audit states.
+struct SingleTreeTracker::EntryAuditState {
+ // Current phase of inclusion check.
+ AuditState state;
+
+ // The proof to be filled in by the LogDnsClient
+ MerkleAuditProof proof;
+
+ // The root hash of the tree for which an inclusion proof was requested.
+ // The root hash is needed after the inclusion proof is fetched for validating
+ // the inclusion proof (each inclusion proof is valid for one particular leaf,
+ // denoted by the leaf index, in exactly one particular tree, denoted by the
+ // tree size in the proof).
+ // To avoid having to re-fetch the inclusion proof if a newer STH is provided
+ // to the SingleTreeTracker, the size of the original tree for which the
+ // inclusion proof was requested is stored in |proof| and the root hash
+ // in |root_hash|.
+ std::string root_hash;
+
+ explicit EntryAuditState(AuditState state) : state(state) {}
+};
+
+// Orders entries by the SCT timestamp. In case of tie, which is very unlikely
+// as it requires two SCTs issued from a log at exactly the same time, order
+// by leaf hash.
+bool SingleTreeTracker::OrderByTimestamp::operator()(
+ const EntryToAudit& lhs,
+ const EntryToAudit& rhs) const {
+ if (lhs.sct_timestamp != rhs.sct_timestamp)
+ return lhs.sct_timestamp < rhs.sct_timestamp;
+
+ return net::SHA256HashValueLessThan()(lhs.leaf_hash, rhs.leaf_hash);
+}
SingleTreeTracker::SingleTreeTracker(
- scoped_refptr<const net::CTLogVerifier> ct_log)
- : ct_log_(std::move(ct_log)) {}
+ scoped_refptr<const net::CTLogVerifier> ct_log,
+ LogDnsClient* dns_client)
+ : ct_log_(std::move(ct_log)),
+ checked_entries_(kCheckedEntriesCacheSize),
+ dns_client_(dns_client),
+ weak_factory_(this) {
+ memory_pressure_listener_.reset(new base::MemoryPressureListener(base::Bind(
+ &SingleTreeTracker::OnMemoryPressure, base::Unretained(this))));
+}
SingleTreeTracker::~SingleTreeTracker() {}
-void SingleTreeTracker::OnSCTVerified(
- net::X509Certificate* cert,
- const net::ct::SignedCertificateTimestamp* sct) {
+void SingleTreeTracker::OnSCTVerified(net::X509Certificate* cert,
+ const SignedCertificateTimestamp* sct) {
DCHECK_EQ(ct_log_->key_id(), sct->log_id);
- // SCT was previously observed, so its status should not be changed.
- if (entries_status_.find(sct->timestamp) != entries_status_.end())
+ EntryToAudit entry(sct->timestamp);
+ if (!GetLogEntryLeafHash(cert, sct, &entry.leaf_hash))
return;
+ // Avoid queueing multiple instances of the same entry.
+ if (GetAuditedEntryInclusionStatus(entry) != SCT_NOT_OBSERVED)
+ return;
+
+ if (pending_entries_.size() >= kPendingEntriesQueueSize) {
+ // Queue is full - cannot audit SCT.
+ LogCanBeCheckedForInclusionToUMA(NOT_AUDITED_QUEUE_FULL);
+ return;
+ }
+
// If there isn't a valid STH or the STH is not fresh enough to check
// inclusion against, store the SCT for future checking and return.
if (verified_sth_.timestamp.is_null() ||
- (verified_sth_.timestamp <
- (sct->timestamp + base::TimeDelta::FromHours(24)))) {
- entries_status_.insert(
- std::make_pair(sct->timestamp, SCT_PENDING_NEWER_STH));
+ !IsSCTReadyForAudit(verified_sth_.timestamp, entry.sct_timestamp)) {
+ pending_entries_.insert(
+ std::make_pair(std::move(entry), EntryAuditState(PENDING_NEWER_STH)));
- if (!verified_sth_.timestamp.is_null()) {
- LogCanBeCheckedForInclusionToUMA(NEWER_STH_REQUIRED);
- } else {
+ if (verified_sth_.timestamp.is_null()) {
LogCanBeCheckedForInclusionToUMA(VALID_STH_REQUIRED);
+ } else {
+ LogCanBeCheckedForInclusionToUMA(NEWER_STH_REQUIRED);
}
return;
}
LogCanBeCheckedForInclusionToUMA(CAN_BE_CHECKED);
- // TODO(eranm): Check inclusion here.
- entries_status_.insert(
- std::make_pair(sct->timestamp, SCT_PENDING_INCLUSION_CHECK));
+ pending_entries_.insert(std::make_pair(
+ std::move(entry), EntryAuditState(PENDING_INCLUSION_PROOF_REQUEST)));
+
+ ProcessPendingEntries();
}
void SingleTreeTracker::NewSTHObserved(const SignedTreeHead& sth) {
@@ -103,36 +284,175 @@ void SingleTreeTracker::NewSTHObserved(const SignedTreeHead& sth) {
// a newer timestamp.
const bool sths_for_same_tree = verified_sth_.tree_size == sth.tree_size;
const bool received_sth_is_for_larger_tree =
- (verified_sth_.tree_size > sth.tree_size);
+ (verified_sth_.tree_size < sth.tree_size);
const bool received_sth_is_newer = (sth.timestamp > verified_sth_.timestamp);
- if (verified_sth_.timestamp.is_null() || received_sth_is_for_larger_tree ||
- (sths_for_same_tree && received_sth_is_newer)) {
- verified_sth_ = sth;
+ if (!verified_sth_.timestamp.is_null() && !received_sth_is_for_larger_tree &&
+ !(sths_for_same_tree && received_sth_is_newer)) {
+ // Observed an old STH - do nothing.
+ return;
}
- // Find out which SCTs can now be checked for inclusion.
- // TODO(eranm): Keep two maps of MerkleTreeLeaf instances, one for leaves
- // pending inclusion checks and one for leaves pending a new STH.
- // The comparison function between MerkleTreeLeaf instances should use the
- // timestamp to determine sorting order, so that bulk moving from one
- // map to the other can happen.
- auto entry = entries_status_.begin();
- while (entry != entries_status_.end() &&
- entry->first < verified_sth_.timestamp) {
- entry->second = SCT_PENDING_INCLUSION_CHECK;
- ++entry;
- // TODO(eranm): Check inclusion here.
+ verified_sth_ = sth;
+
+ // Find the first entry in the PENDING_NEWER_STH state - entries
+ // before that should be pending leaf index / inclusion proof, no
+ // reason to inspect them.
+ auto auditable_entries_begin = std::find_if(
+ pending_entries_.begin(), pending_entries_.end(),
+ [](std::pair<const EntryToAudit&, const EntryAuditState&> value) {
+ return value.second.state == PENDING_NEWER_STH;
+ });
+
+ // Find where to stop - this is the first entry whose timestamp + MMD
+ // is greater than the STH's timestamp.
+ auto auditable_entries_end = std::lower_bound(
+ auditable_entries_begin, pending_entries_.end(), sth.timestamp,
+ [](std::pair<const EntryToAudit&, const EntryAuditState&> value,
+ base::Time sth_timestamp) {
+ return IsSCTReadyForAudit(sth_timestamp, value.first.sct_timestamp);
+ });
+
+ // Update the state of all entries that can now be checked for inclusion.
+ for (auto curr_entry = auditable_entries_begin;
+ curr_entry != auditable_entries_end; ++curr_entry) {
+ DCHECK_EQ(curr_entry->second.state, PENDING_NEWER_STH);
+ curr_entry->second.state = PENDING_INCLUSION_PROOF_REQUEST;
}
+
+ if (auditable_entries_begin == auditable_entries_end)
+ return;
+
+ ProcessPendingEntries();
}
SingleTreeTracker::SCTInclusionStatus
SingleTreeTracker::GetLogEntryInclusionStatus(
net::X509Certificate* cert,
- const net::ct::SignedCertificateTimestamp* sct) {
- auto it = entries_status_.find(sct->timestamp);
+ const SignedCertificateTimestamp* sct) {
+ EntryToAudit entry(sct->timestamp);
+ if (!GetLogEntryLeafHash(cert, sct, &entry.leaf_hash))
+ return SCT_NOT_OBSERVED;
+ return GetAuditedEntryInclusionStatus(entry);
+}
+
+void SingleTreeTracker::ProcessPendingEntries() {
+ for (auto it = pending_entries_.begin(); it != pending_entries_.end(); ++it) {
+ if (it->second.state != PENDING_INCLUSION_PROOF_REQUEST) {
+ continue;
+ }
+
+ it->second.root_hash =
+ std::string(verified_sth_.sha256_root_hash, crypto::kSHA256Length);
- return it == entries_status_.end() ? SCT_NOT_OBSERVED : it->second;
+ std::string leaf_hash(
+ reinterpret_cast<const char*>(it->first.leaf_hash.data),
+ crypto::kSHA256Length);
+ net::Error result = dns_client_->QueryAuditProof(
+ ct_log_->dns_domain(), leaf_hash, verified_sth_.tree_size,
+ &(it->second.proof),
+ base::Bind(&SingleTreeTracker::OnAuditProofObtained,
+ weak_factory_.GetWeakPtr(), it->first));
+ // Handling proofs returned synchronously is not implemeted.
+ DCHECK_NE(result, net::OK);
+ if (result == net::ERR_IO_PENDING) {
+ // Successfully requested an inclusion proof - change entry state
+ // and continue to the next one.
+ it->second.state = INCLUSION_PROOF_REQUESTED;
+ } else if (result == net::ERR_TEMPORARILY_THROTTLED) {
+ dns_client_->NotifyWhenNotThrottled(
+ base::Bind(&SingleTreeTracker::ProcessPendingEntries,
+ weak_factory_.GetWeakPtr()));
+ // Exit the loop since all subsequent calls to QueryAuditProof
+ // will be throttled.
+ break;
+ } else if (result == net::ERR_NAME_RESOLUTION_FAILED) {
+ LogInclusionCheckResult(DNS_QUERY_NOT_POSSIBLE);
+ // Lookup failed due to bad DNS configuration, erase the entry and
+ // continue to the next one.
+ it = pending_entries_.erase(it);
+ // Break here if it's the last entry to avoid |it| being incremented
+ // by the for loop.
+ if (it == pending_entries_.end())
+ break;
+ } else {
+ // BUG: an invalid argument was provided or an unexpected error
+ // was returned from LogDnsClient.
+ DCHECK_EQ(result, net::ERR_INVALID_ARGUMENT);
+ NOTREACHED();
+ }
+ }
+}
+
+SingleTreeTracker::SCTInclusionStatus
+SingleTreeTracker::GetAuditedEntryInclusionStatus(const EntryToAudit& entry) {
+ const auto checked_entries_iterator = checked_entries_.Get(entry.leaf_hash);
+ if (checked_entries_iterator != checked_entries_.end()) {
+ return SCT_INCLUDED_IN_LOG;
+ }
+
+ auto pending_iterator = pending_entries_.find(entry);
+ if (pending_iterator == pending_entries_.end()) {
+ return SCT_NOT_OBSERVED;
+ }
+
+ switch (pending_iterator->second.state) {
+ case PENDING_NEWER_STH:
+ return SCT_PENDING_NEWER_STH;
+ case PENDING_INCLUSION_PROOF_REQUEST:
+ case INCLUSION_PROOF_REQUESTED:
+ return SCT_PENDING_INCLUSION_CHECK;
+ }
+
+ NOTREACHED();
+ return SCT_NOT_OBSERVED;
+}
+
+void SingleTreeTracker::OnAuditProofObtained(const EntryToAudit& entry,
+ int net_error) {
+ auto it = pending_entries_.find(entry);
+ // The entry may not be present if it was evacuated due to low memory
+ // pressure.
+ if (it == pending_entries_.end())
+ return;
+
+ DCHECK_EQ(it->second.state, INCLUSION_PROOF_REQUESTED);
+
+ if (net_error != net::OK) {
+ // XXX(eranm): Should failures be cached? For now, they are not.
+ LogInclusionCheckResult(FAILED_GETTING_INCLUSION_PROOF);
+ pending_entries_.erase(it);
+ return;
+ }
+
+ std::string leaf_hash(reinterpret_cast<const char*>(entry.leaf_hash.data),
+ crypto::kSHA256Length);
+
+ bool verified = ct_log_->VerifyAuditProof(it->second.proof,
+ it->second.root_hash, leaf_hash);
+
+ if (!verified) {
+ LogInclusionCheckResult(GOT_INVALID_INCLUSION_PROOF);
+ } else {
+ LogInclusionCheckResult(GOT_VALID_INCLUSION_PROOF);
+ checked_entries_.Put(entry.leaf_hash, EntryAuditResult());
+ }
+
+ pending_entries_.erase(it);
+}
+
+void SingleTreeTracker::OnMemoryPressure(
+ base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) {
+ switch (memory_pressure_level) {
+ case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE:
+ break;
+ case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL:
+ pending_entries_.clear();
+ // Fall through to clearing the other cache.
+ case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE:
+ checked_entries_.Clear();
+ break;
+ }
}
} // namespace certificate_transparency

Powered by Google App Engine
This is Rietveld 408576698