Skip to content

Commit

Permalink
Build and send HPKP violation reports
Browse files Browse the repository at this point in the history
This CL adds code to TransportSecurityState to build HPKP reports, and
sends them with a CertificateReportSender constructed by
ProfileIOData. Calls to CheckPublicKeyPins() indicate whether a report
should be sent and pass necessary reporting information as arguments.

CL #1: crrev.com/1211363005 (parse report-uri)
CL #2: crrev.com/1212973002 (add net::CertificateReportSender)
This is CL #3.

BUG=445793

Review URL: https://codereview.chromium.org/1212613004

Cr-Commit-Position: refs/heads/master@{#340687}
  • Loading branch information
estark authored and Anton Obzhirov committed Aug 7, 2015
1 parent 1c7fb04 commit f43a498
Show file tree
Hide file tree
Showing 11 changed files with 415 additions and 71 deletions.
11 changes: 11 additions & 0 deletions chrome/browser/profiles/profile_io_data.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
#include "net/proxy/proxy_service.h"
#include "net/ssl/channel_id_service.h"
#include "net/ssl/client_cert_store.h"
#include "net/url_request/certificate_report_sender.h"
#include "net/url_request/data_protocol_handler.h"
#include "net/url_request/file_protocol_handler.h"
#include "net/url_request/ftp_protocol_handler.h"
Expand Down Expand Up @@ -640,6 +641,11 @@ ProfileIOData::~ProfileIOData() {
static_cast<void*>(it->second), sizeof(void*));
}

// Destroy certificate_report_sender_ before main_request_context_,
// since the former has a reference to the latter.
transport_security_state_->SetReportSender(nullptr);
certificate_report_sender_.reset();

// TODO(ajwong): These AssertNoURLRequests() calls are unnecessary since they
// are already done in the URLRequestContext destructor.
if (main_request_context_)
Expand Down Expand Up @@ -1059,6 +1065,11 @@ void ProfileIOData::Init(
base::SequencedWorkerPool::BLOCK_SHUTDOWN),
IsOffTheRecord()));

certificate_report_sender_.reset(new net::CertificateReportSender(
main_request_context_.get(),
net::CertificateReportSender::DO_NOT_SEND_COOKIES));
transport_security_state_->SetReportSender(certificate_report_sender_.get());

// Take ownership over these parameters.
cookie_settings_ = profile_params_->cookie_settings;
host_content_settings_map_ = profile_params_->host_content_settings_map;
Expand Down
2 changes: 2 additions & 0 deletions chrome/browser/profiles/profile_io_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class InfoMap;
}

namespace net {
class CertificateReportSender;
class CertVerifier;
class ChannelIDService;
class CookieStore;
Expand Down Expand Up @@ -557,6 +558,7 @@ class ProfileIOData {

mutable scoped_ptr<net::TransportSecurityPersister>
transport_security_persister_;
mutable scoped_ptr<net::CertificateReportSender> certificate_report_sender_;

// These are only valid in between LazyInitialize() and their accessor being
// called.
Expand Down
3 changes: 0 additions & 3 deletions net/base/hash_value.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ struct NET_EXPORT SHA256HashValue {
enum HashValueTag {
HASH_VALUE_SHA1,
HASH_VALUE_SHA256,

// This must always be last.
HASH_VALUE_TAGS_COUNT
};

class NET_EXPORT HashValue {
Expand Down
41 changes: 23 additions & 18 deletions net/http/http_security_headers_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "base/sha1.h"
#include "base/strings/string_piece.h"
#include "crypto/sha2.h"
#include "net/base/host_port_pair.h"
#include "net/base/test_completion_callback.h"
#include "net/http/http_security_headers.h"
#include "net/http/http_util.h"
Expand Down Expand Up @@ -678,8 +679,10 @@ TEST_F(HttpSecurityHeadersTest, UpdateDynamicPKPOnly) {
hashes.push_back(good_hash);
std::string failure_log;
const bool is_issued_by_known_root = true;
HostPortPair domain_port(domain, 443);
EXPECT_TRUE(state.CheckPublicKeyPins(
domain, is_issued_by_known_root, hashes, &failure_log));
domain_port, is_issued_by_known_root, hashes, nullptr, nullptr,
TransportSecurityState::DISABLE_PIN_REPORTS, &failure_log));

TransportSecurityState::PKPState new_dynamic_pkp_state;
EXPECT_TRUE(state.GetDynamicPKPState(domain, &new_dynamic_pkp_state));
Expand Down Expand Up @@ -771,9 +774,11 @@ TEST_F(HttpSecurityHeadersTest, UpdateDynamicPKPMaxAge0) {
new_static_pkp_state2.spki_hashes[2].data()[0] ^= 0x80;

const bool is_issued_by_known_root = true;
EXPECT_FALSE(state.CheckPublicKeyPins(domain, is_issued_by_known_root,
new_static_pkp_state2.spki_hashes,
&failure_log));
HostPortPair domain_port(domain, 443);
EXPECT_FALSE(state.CheckPublicKeyPins(
domain_port, is_issued_by_known_root, new_static_pkp_state2.spki_hashes,
nullptr, nullptr, TransportSecurityState::DISABLE_PIN_REPORTS,
&failure_log));
EXPECT_NE(0UL, failure_log.length());
}

Expand Down Expand Up @@ -805,10 +810,10 @@ TEST_F(HttpSecurityHeadersTest, NoClobberPins) {
EXPECT_TRUE(state.ShouldUpgradeToSSL(domain));
std::string failure_log;
const bool is_issued_by_known_root = true;
EXPECT_TRUE(state.CheckPublicKeyPins(domain,
is_issued_by_known_root,
saved_hashes,
&failure_log));
HostPortPair domain_port(domain, 443);
EXPECT_TRUE(state.CheckPublicKeyPins(
domain_port, is_issued_by_known_root, saved_hashes, nullptr, nullptr,
TransportSecurityState::DISABLE_PIN_REPORTS, &failure_log));

// Add an HPKP header, which should only update the dynamic state.
HashValue good_hash = GetTestHashValue(1, HASH_VALUE_SHA1);
Expand All @@ -828,10 +833,9 @@ TEST_F(HttpSecurityHeadersTest, NoClobberPins) {
EXPECT_TRUE(state.ShouldUpgradeToSSL(domain));
// The dynamic pins, which do not match |saved_hashes|, should take
// precedence over the static pins and cause the check to fail.
EXPECT_FALSE(state.CheckPublicKeyPins(domain,
is_issued_by_known_root,
saved_hashes,
&failure_log));
EXPECT_FALSE(state.CheckPublicKeyPins(
domain_port, is_issued_by_known_root, saved_hashes, nullptr, nullptr,
TransportSecurityState::DISABLE_PIN_REPORTS, &failure_log));
}

// Tests that seeing an invalid HPKP header leaves the existing one alone.
Expand All @@ -855,9 +859,10 @@ TEST_F(HttpSecurityHeadersTest, IgnoreInvalidHeaders) {
EXPECT_TRUE(state.HasPublicKeyPins("example.com"));
std::string failure_log;
bool is_issued_by_known_root = true;
EXPECT_TRUE(state.CheckPublicKeyPins("example.com", is_issued_by_known_root,
ssl_info.public_key_hashes,
&failure_log));
HostPortPair domain_port("example.com", 443);
EXPECT_TRUE(state.CheckPublicKeyPins(
domain_port, is_issued_by_known_root, ssl_info.public_key_hashes, nullptr,
nullptr, TransportSecurityState::DISABLE_PIN_REPORTS, &failure_log));

// Now assert an invalid one. This should fail.
EXPECT_FALSE(state.AddHPKPHeader(
Expand All @@ -866,9 +871,9 @@ TEST_F(HttpSecurityHeadersTest, IgnoreInvalidHeaders) {

// The old pins must still exist.
EXPECT_TRUE(state.HasPublicKeyPins("example.com"));
EXPECT_TRUE(state.CheckPublicKeyPins("example.com", is_issued_by_known_root,
ssl_info.public_key_hashes,
&failure_log));
EXPECT_TRUE(state.CheckPublicKeyPins(
domain_port, is_issued_by_known_root, ssl_info.public_key_hashes, nullptr,
nullptr, TransportSecurityState::DISABLE_PIN_REPORTS, &failure_log));
}

}; // namespace net
149 changes: 134 additions & 15 deletions net/http/transport_security_state.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@

#include "base/base64.h"
#include "base/build_time.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/sparse_histogram.h"
#include "base/sha1.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "crypto/sha2.h"
#include "net/base/dns_util.h"
#include "net/base/host_port_pair.h"
#include "net/cert/x509_cert_types.h"
#include "net/cert/x509_certificate.h"
#include "net/http/http_security_headers.h"
Expand All @@ -47,6 +50,91 @@ namespace {

#include "net/http/transport_security_state_static.h"

std::string TimeToISO8601(const base::Time& t) {
base::Time::Exploded exploded;
t.UTCExplode(&exploded);
return base::StringPrintf(
"%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", exploded.year, exploded.month,
exploded.day_of_month, exploded.hour, exploded.minute, exploded.second,
exploded.millisecond);
}

scoped_ptr<base::ListValue> GetPEMEncodedChainAsList(
const net::X509Certificate* cert_chain) {
if (!cert_chain)
return make_scoped_ptr(new base::ListValue());

scoped_ptr<base::ListValue> result(new base::ListValue());
std::vector<std::string> pem_encoded_chain;
cert_chain->GetPEMEncodedChain(&pem_encoded_chain);
for (const std::string& cert : pem_encoded_chain)
result->Append(make_scoped_ptr(new base::StringValue(cert)));

return result.Pass();
}

bool GetHPKPReport(const HostPortPair& host_port_pair,
const TransportSecurityState::PKPState& pkp_state,
const X509Certificate* served_certificate_chain,
const X509Certificate* validated_certificate_chain,
std::string* serialized_report) {
// TODO(estark): keep track of reports already sent and rate-limit,
// break loops
if (pkp_state.report_uri.is_empty())
return false;

base::DictionaryValue report;
base::Time now = base::Time::Now();
report.SetString("date-time", TimeToISO8601(now));
report.SetString("hostname", host_port_pair.host());
report.SetInteger("port", host_port_pair.port());
report.SetString("effective-expiration-date",
TimeToISO8601(pkp_state.expiry));
report.SetBoolean("include-subdomains", pkp_state.include_subdomains);
report.SetString("noted-hostname", pkp_state.domain);

scoped_ptr<base::ListValue> served_certificate_chain_list =
GetPEMEncodedChainAsList(served_certificate_chain);
scoped_ptr<base::ListValue> validated_certificate_chain_list =
GetPEMEncodedChainAsList(validated_certificate_chain);
report.Set("served-certificate-chain", served_certificate_chain_list.Pass());
report.Set("validated-certificate-chain",
validated_certificate_chain_list.Pass());

scoped_ptr<base::ListValue> known_pin_list(new base::ListValue());
for (const auto& hash_value : pkp_state.spki_hashes) {
std::string known_pin;

switch (hash_value.tag) {
case HASH_VALUE_SHA1:
known_pin += "pin-sha1=";
break;
case HASH_VALUE_SHA256:
known_pin += "pin-sha256=";
break;
}

std::string base64_value;
base::Base64Encode(
base::StringPiece(reinterpret_cast<const char*>(hash_value.data()),
hash_value.size()),
&base64_value);
known_pin += "\"" + base64_value + "\"";

known_pin_list->Append(
scoped_ptr<base::Value>(new base::StringValue(known_pin)));
}

report.Set("known-pins", known_pin_list.Pass());

if (!base::JSONWriter::Write(report, serialized_report)) {
LOG(ERROR) << "Failed to serialize HPKP violation report.";
return false;
}

return true;
}

std::string HashesToBase64String(const HashValueVector& hashes) {
std::string str;
for (size_t i = 0; i != hashes.size(); ++i) {
Expand Down Expand Up @@ -510,24 +598,28 @@ bool TransportSecurityState::ShouldUpgradeToSSL(const std::string& host) {
}

bool TransportSecurityState::CheckPublicKeyPins(
const std::string& host,
const HostPortPair& host_port_pair,
bool is_issued_by_known_root,
const HashValueVector& public_key_hashes,
const X509Certificate* served_certificate_chain,
const X509Certificate* validated_certificate_chain,
const PublicKeyPinReportStatus report_status,
std::string* pinning_failure_log) {
// Perform pin validation if, and only if, all these conditions obtain:
//
// * the server's certificate chain chains up to a known root (i.e. not a
// user-installed trust anchor); and
// * the server actually has public key pins.
if (!is_issued_by_known_root || !HasPublicKeyPins(host)) {
if (!is_issued_by_known_root || !HasPublicKeyPins(host_port_pair.host())) {
return true;
}

bool pins_are_valid =
CheckPublicKeyPinsImpl(host, public_key_hashes, pinning_failure_log);
bool pins_are_valid = CheckPublicKeyPinsImpl(
host_port_pair, public_key_hashes, served_certificate_chain,
validated_certificate_chain, report_status, pinning_failure_log);
if (!pins_are_valid) {
LOG(ERROR) << *pinning_failure_log;
ReportUMAOnPinFailure(host);
ReportUMAOnPinFailure(host_port_pair.host());
}

UMA_HISTOGRAM_BOOLEAN("Net.PublicKeyPinSuccess", pins_are_valid);
Expand Down Expand Up @@ -555,6 +647,12 @@ void TransportSecurityState::SetDelegate(
delegate_ = delegate;
}

void TransportSecurityState::SetReportSender(
TransportSecurityState::ReportSender* report_sender) {
DCHECK(CalledOnValidThread());
report_sender_ = report_sender;
}

void TransportSecurityState::AddHSTSInternal(
const std::string& host,
TransportSecurityState::STSState::UpgradeMode upgrade_mode,
Expand Down Expand Up @@ -813,20 +911,41 @@ bool TransportSecurityState::IsBuildTimely() {
}

bool TransportSecurityState::CheckPublicKeyPinsImpl(
const std::string& host,
const HostPortPair& host_port_pair,
const HashValueVector& hashes,
const X509Certificate* served_certificate_chain,
const X509Certificate* validated_certificate_chain,
const PublicKeyPinReportStatus report_status,
std::string* failure_log) {
PKPState dynamic_state;
if (GetDynamicPKPState(host, &dynamic_state))
return dynamic_state.CheckPublicKeyPins(hashes, failure_log);

PKPState static_pkp_state;
PKPState pkp_state;
STSState unused;
if (GetStaticDomainState(host, &unused, &static_pkp_state))
return static_pkp_state.CheckPublicKeyPins(hashes, failure_log);

// HasPublicKeyPins should have returned true in order for this method
// to have been called, so if we fall through to here, it's an error.
if (!GetDynamicPKPState(host_port_pair.host(), &pkp_state) &&
!GetStaticDomainState(host_port_pair.host(), &unused, &pkp_state)) {
// HasPublicKeyPins should have returned true in order for this method
// to have been called, so if we fall through to here, it's an error.
return false;
}

if (pkp_state.CheckPublicKeyPins(hashes, failure_log))
return true;

if (!report_sender_ || report_status != ENABLE_PIN_REPORTS ||
pkp_state.report_uri.is_empty()) {
return false;
}

DCHECK(pkp_state.report_uri.is_valid());

std::string serialized_report;

if (!GetHPKPReport(host_port_pair, pkp_state, served_certificate_chain,
validated_certificate_chain, &serialized_report)) {
return false;
}

report_sender_->Send(pkp_state.report_uri, serialized_report);

return false;
}

Expand Down
Loading

0 comments on commit f43a498

Please sign in to comment.