Skip to content

Commit

Permalink
Certificate Transparency DNS log client
Browse files Browse the repository at this point in the history
This can query CT logs over DNS, as defined by:
https://github.com/google/certificate-transparency-rfcs/blob/master/dns/draft-ct-over-dns.md

This is required for obtaining audit proofs, which will allow Chrome to verify
that SCTs it receives are trustworthy and that logs are behaving correctly.

BUG=612439

Committed: https://crrev.com/59b6ea2217dbc10400b6a9d433ad13c91bb6b7c2
Review-Url: https://codereview.chromium.org/2066553002
Cr-Original-Commit-Position: refs/heads/master@{#403798}
Cr-Commit-Position: refs/heads/master@{#404199}
  • Loading branch information
robpercival authored and Commit bot committed Jul 7, 2016
1 parent 8dfa7bc commit 2c7cd56
Show file tree
Hide file tree
Showing 10 changed files with 1,171 additions and 16 deletions.
3 changes: 3 additions & 0 deletions components/certificate_transparency.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
],
'dependencies': [
'../base/base.gyp:base',
'../components/components.gyp:base32',
'../components/components.gyp:safe_json',
'../components/components.gyp:url_matcher',
'../components/prefs/prefs.gyp:prefs',
Expand All @@ -23,6 +24,8 @@
'sources': [
'certificate_transparency/ct_policy_manager.cc',
'certificate_transparency/ct_policy_manager.h',
'certificate_transparency/log_dns_client.h',
'certificate_transparency/log_dns_client.cc',
'certificate_transparency/log_proof_fetcher.h',
'certificate_transparency/log_proof_fetcher.cc',
'certificate_transparency/pref_names.cc',
Expand Down
5 changes: 5 additions & 0 deletions components/certificate_transparency/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ source_set("certificate_transparency") {
sources = [
"ct_policy_manager.cc",
"ct_policy_manager.h",
"log_dns_client.cc",
"log_dns_client.h",
"log_proof_fetcher.cc",
"log_proof_fetcher.h",
"pref_names.cc",
Expand All @@ -18,6 +20,7 @@ source_set("certificate_transparency") {

deps = [
"//base",
"//components/base32",
"//components/prefs",
"//components/safe_json",
"//components/url_formatter",
Expand All @@ -31,6 +34,7 @@ source_set("unit_tests") {
testonly = true
sources = [
"ct_policy_manager_unittest.cc",
"log_dns_client_unittest.cc",
"log_proof_fetcher_unittest.cc",
"single_tree_tracker_unittest.cc",
]
Expand All @@ -41,6 +45,7 @@ source_set("unit_tests") {
"//components/prefs:test_support",
"//components/safe_json:test_support",
"//net:test_support",
"//testing/gmock",
"//testing/gtest",
]
}
2 changes: 2 additions & 0 deletions components/certificate_transparency/DEPS
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
include_rules = [
"+components/base32",
"+components/prefs",
"+components/safe_json",
"+components/url_formatter",
"+components/url_matcher",
"+crypto",
"+net",
]
294 changes: 294 additions & 0 deletions components/certificate_transparency/log_dns_client.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
// 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/log_dns_client.h"

#include <sstream>

#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/base32/base32.h"
#include "crypto/sha2.h"
#include "net/base/net_errors.h"
#include "net/cert/merkle_audit_proof.h"
#include "net/dns/dns_client.h"
#include "net/dns/dns_protocol.h"
#include "net/dns/dns_response.h"
#include "net/dns/dns_transaction.h"
#include "net/dns/record_parsed.h"
#include "net/dns/record_rdata.h"

namespace certificate_transparency {

namespace {

bool ParseTxtResponse(const net::DnsResponse& response, std::string* txt) {
DCHECK(txt);

net::DnsRecordParser parser = response.Parser();
// We don't care about the creation time, since we're going to throw
// |parsed_record| away as soon as we've extracted the payload, so provide
// the "null" time.
auto parsed_record = net::RecordParsed::CreateFrom(&parser, base::Time());
if (parsed_record == nullptr)
return false;

auto txt_record = parsed_record->rdata<net::TxtRecordRdata>();
if (txt_record == nullptr)
return false;

*txt = base::JoinString(txt_record->texts(), "");
return true;
}

bool ParseLeafIndex(const net::DnsResponse& response, uint64_t* index) {
DCHECK(index);

std::string index_str;
if (!ParseTxtResponse(response, &index_str))
return false;

return base::StringToUint64(index_str, index);
}

bool ParseAuditPath(const net::DnsResponse& response,
net::ct::MerkleAuditProof* proof) {
DCHECK(proof);

std::string audit_path;
if (!ParseTxtResponse(response, &audit_path))
return false;
// If empty or not a multiple of the node size, it is considered invalid.
// It's important to consider empty audit paths as invalid, as otherwise an
// infinite loop could occur if the server consistently returned empty
// responses.
if (audit_path.empty() || audit_path.size() % crypto::kSHA256Length != 0)
return false;

for (size_t i = 0; i < audit_path.size(); i += crypto::kSHA256Length) {
proof->nodes.push_back(audit_path.substr(i, crypto::kSHA256Length));
}

return true;
}

} // namespace

LogDnsClient::LogDnsClient(std::unique_ptr<net::DnsClient> dns_client,
const net::BoundNetLog& net_log)
: dns_client_(std::move(dns_client)),
net_log_(net_log),
weak_ptr_factory_(this) {
CHECK(dns_client_);
}

LogDnsClient::~LogDnsClient() {}

void LogDnsClient::QueryLeafIndex(base::StringPiece domain_for_log,
base::StringPiece leaf_hash,
const LeafIndexCallback& callback) {
if (domain_for_log.empty() || leaf_hash.size() != crypto::kSHA256Length) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(callback, net::Error::ERR_INVALID_ARGUMENT, 0));
return;
}

std::string encoded_leaf_hash =
base32::Base32Encode(leaf_hash, base32::Base32EncodePolicy::OMIT_PADDING);
DCHECK_EQ(encoded_leaf_hash.size(), 52u);

net::DnsTransactionFactory* factory = dns_client_->GetTransactionFactory();
if (factory == nullptr) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(callback, net::Error::ERR_NAME_RESOLUTION_FAILED, 0));
return;
}

std::ostringstream qname;
qname << encoded_leaf_hash << ".hash." << domain_for_log << ".";

net::DnsTransactionFactory::CallbackType transaction_callback = base::Bind(
&LogDnsClient::QueryLeafIndexComplete, weak_ptr_factory_.GetWeakPtr());

std::unique_ptr<net::DnsTransaction> dns_transaction =
factory->CreateTransaction(qname.str(), net::dns_protocol::kTypeTXT,
transaction_callback, net_log_);

dns_transaction->Start();
leaf_index_queries_.push_back({std::move(dns_transaction), callback});
}

// The performance of this could be improved by sending all of the expected
// queries up front. Each response can contain a maximum of 7 audit path nodes,
// so for an audit proof of size 20, it could send 3 queries (for nodes 0-6,
// 7-13 and 14-19) immediately. Currently, it sends only the first and then,
// based on the number of nodes received, sends the next query. The complexity
// of the code would increase though, as it would need to detect gaps in the
// audit proof caused by the server not responding with the anticipated number
// of nodes. Ownership of the proof would need to change, as it would be shared
// between simultaneous DNS transactions.
void LogDnsClient::QueryAuditProof(base::StringPiece domain_for_log,
uint64_t leaf_index,
uint64_t tree_size,
const AuditProofCallback& callback) {
if (domain_for_log.empty() || leaf_index >= tree_size) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(callback, net::Error::ERR_INVALID_ARGUMENT, nullptr));
return;
}

std::unique_ptr<net::ct::MerkleAuditProof> proof(
new net::ct::MerkleAuditProof);
proof->leaf_index = leaf_index;
// TODO(robpercival): Once a "tree_size" field is added to MerkleAuditProof,
// pass |tree_size| to QueryAuditProofNodes using that.

// Query for the first batch of audit proof nodes (i.e. starting from 0).
QueryAuditProofNodes(std::move(proof), domain_for_log, tree_size, 0,
callback);
}

void LogDnsClient::QueryLeafIndexComplete(net::DnsTransaction* transaction,
int net_error,
const net::DnsResponse* response) {
auto query_iterator =
std::find_if(leaf_index_queries_.begin(), leaf_index_queries_.end(),
[transaction](const Query<LeafIndexCallback>& query) {
return query.transaction.get() == transaction;
});
if (query_iterator == leaf_index_queries_.end()) {
NOTREACHED();
return;
}
const Query<LeafIndexCallback> query = std::move(*query_iterator);
leaf_index_queries_.erase(query_iterator);

// If we've received no response but no net::error either (shouldn't happen),
// report the response as invalid.
if (response == nullptr && net_error == net::OK) {
net_error = net::ERR_INVALID_RESPONSE;
}

if (net_error != net::OK) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(query.callback, net_error, 0));
return;
}

uint64_t leaf_index;
if (!ParseLeafIndex(*response, &leaf_index)) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(query.callback, net::ERR_DNS_MALFORMED_RESPONSE, 0));
return;
}

base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(query.callback, net::OK, leaf_index));
}

void LogDnsClient::QueryAuditProofNodes(
std::unique_ptr<net::ct::MerkleAuditProof> proof,
base::StringPiece domain_for_log,
uint64_t tree_size,
uint64_t node_index,
const AuditProofCallback& callback) {
// Preconditions that should be guaranteed internally by this class.
DCHECK(proof);
DCHECK(!domain_for_log.empty());
DCHECK_LT(proof->leaf_index, tree_size);
DCHECK_LT(node_index,
net::ct::CalculateAuditPathLength(proof->leaf_index, tree_size));

net::DnsTransactionFactory* factory = dns_client_->GetTransactionFactory();
if (factory == nullptr) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(callback, net::Error::ERR_NAME_RESOLUTION_FAILED, nullptr));
return;
}

std::ostringstream qname;
qname << node_index << "." << proof->leaf_index << "." << tree_size
<< ".tree." << domain_for_log << ".";

net::DnsTransactionFactory::CallbackType transaction_callback =
base::Bind(&LogDnsClient::QueryAuditProofNodesComplete,
weak_ptr_factory_.GetWeakPtr(), base::Passed(std::move(proof)),
domain_for_log, tree_size);

std::unique_ptr<net::DnsTransaction> dns_transaction =
factory->CreateTransaction(qname.str(), net::dns_protocol::kTypeTXT,
transaction_callback, net_log_);
dns_transaction->Start();
audit_proof_queries_.push_back({std::move(dns_transaction), callback});
}

void LogDnsClient::QueryAuditProofNodesComplete(
std::unique_ptr<net::ct::MerkleAuditProof> proof,
base::StringPiece domain_for_log,
uint64_t tree_size,
net::DnsTransaction* transaction,
int net_error,
const net::DnsResponse* response) {
// Preconditions that should be guaranteed internally by this class.
DCHECK(proof);
DCHECK(!domain_for_log.empty());

auto query_iterator =
std::find_if(audit_proof_queries_.begin(), audit_proof_queries_.end(),
[transaction](const Query<AuditProofCallback>& query) {
return query.transaction.get() == transaction;
});

if (query_iterator == audit_proof_queries_.end()) {
NOTREACHED();
return;
}
const Query<AuditProofCallback> query = std::move(*query_iterator);
audit_proof_queries_.erase(query_iterator);

// If we've received no response but no net::error either (shouldn't happen),
// report the response as invalid.
if (response == nullptr && net_error == net::OK) {
net_error = net::ERR_INVALID_RESPONSE;
}

if (net_error != net::OK) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(query.callback, net_error, nullptr));
return;
}

const uint64_t audit_path_length =
net::ct::CalculateAuditPathLength(proof->leaf_index, tree_size);
proof->nodes.reserve(audit_path_length);

if (!ParseAuditPath(*response, proof.get())) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(query.callback, net::ERR_DNS_MALFORMED_RESPONSE, nullptr));
return;
}

const uint64_t audit_path_nodes_received = proof->nodes.size();
if (audit_path_nodes_received < audit_path_length) {
QueryAuditProofNodes(std::move(proof), domain_for_log, tree_size,
audit_path_nodes_received, query.callback);
return;
}

base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(query.callback, net::OK, base::Passed(std::move(proof))));
}

} // namespace certificate_transparency
Loading

0 comments on commit 2c7cd56

Please sign in to comment.