summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--net/http/transport_security_state.cc132
-rw-r--r--net/http/transport_security_state.h22
-rw-r--r--net/http/transport_security_state_unittest.cc107
3 files changed, 198 insertions, 63 deletions
diff --git a/net/http/transport_security_state.cc b/net/http/transport_security_state.cc
index a2dbd16..2404db0 100644
--- a/net/http/transport_security_state.cc
+++ b/net/http/transport_security_state.cc
@@ -29,7 +29,6 @@
#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/host_port_pair.h"
@@ -50,6 +49,10 @@ namespace {
#include "net/http/transport_security_state_static.h"
+const size_t kMaxHPKPReportCacheEntries = 50;
+const int kTimeToRememberHPKPReportsMins = 60;
+const size_t kReportCacheKeyLength = 16;
+
std::string TimeToISO8601(const base::Time& t) {
base::Time::Exploded exploded;
t.UTCExplode(&exploded);
@@ -73,23 +76,34 @@ scoped_ptr<base::ListValue> GetPEMEncodedChainAsList(
return result.Pass();
}
+bool HashReportForCache(const base::DictionaryValue& report,
+ const GURL& report_uri,
+ std::string* cache_key) {
+ char hashed[crypto::kSHA256Length];
+ std::string to_hash;
+ if (!base::JSONWriter::Write(report, &to_hash))
+ return false;
+ to_hash += "," + report_uri.spec();
+ crypto::SHA256HashString(to_hash, hashed, sizeof(hashed));
+ static_assert(kReportCacheKeyLength <= sizeof(hashed),
+ "HPKP report cache key size is larger than hash size.");
+ *cache_key = std::string(hashed, kReportCacheKeyLength);
+ return true;
+}
+
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
+ std::string* serialized_report,
+ std::string* cache_key) {
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);
@@ -127,6 +141,18 @@ bool GetHPKPReport(const HostPortPair& host_port_pair,
report.Set("known-pins", known_pin_list.Pass());
+ // For the sent reports cache, do not include the effective expiration
+ // date. The expiration date will likely change every time the user
+ // visits the site, so it would prevent reports from being effectively
+ // deduplicated.
+ if (!HashReportForCache(report, pkp_state.report_uri, cache_key)) {
+ LOG(ERROR) << "Failed to compute cache key for HPKP violation report.";
+ return false;
+ }
+
+ report.SetString("date-time", TimeToISO8601(now));
+ report.SetString("effective-expiration-date",
+ TimeToISO8601(pkp_state.expiry));
if (!base::JSONWriter::Write(report, serialized_report)) {
LOG(ERROR) << "Failed to serialize HPKP violation report.";
return false;
@@ -145,42 +171,6 @@ bool IsReportUriValidForHost(const GURL& report_uri, const std::string& host) {
!report_uri.SchemeIsCryptographic());
}
-bool CheckPinsAndMaybeSendReport(
- const HostPortPair& host_port_pair,
- const TransportSecurityState::PKPState& pkp_state,
- const HashValueVector& hashes,
- const X509Certificate* served_certificate_chain,
- const X509Certificate* validated_certificate_chain,
- const TransportSecurityState::PublicKeyPinReportStatus report_status,
- TransportSecurityState::ReportSender* report_sender,
- std::string* failure_log) {
- if (pkp_state.CheckPublicKeyPins(hashes, failure_log))
- return true;
-
- if (!report_sender ||
- report_status != TransportSecurityState::ENABLE_PIN_REPORTS ||
- pkp_state.report_uri.is_empty()) {
- return false;
- }
-
- DCHECK(pkp_state.report_uri.is_valid());
- // Report URIs should not be used if they are the same host as the pin
- // and are HTTPS, to avoid going into a report-sending loop.
- if (!IsReportUriValidForHost(pkp_state.report_uri, host_port_pair.host()))
- return false;
-
- 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;
-}
-
std::string HashesToBase64String(const HashValueVector& hashes) {
std::string str;
for (size_t i = 0; i != hashes.size(); ++i) {
@@ -606,7 +596,10 @@ bool DecodeHSTSPreload(const std::string& hostname, PreloadResult* out) {
} // namespace
TransportSecurityState::TransportSecurityState()
- : delegate_(nullptr), report_sender_(nullptr), enable_static_pins_(true) {
+ : delegate_(nullptr),
+ report_sender_(nullptr),
+ enable_static_pins_(true),
+ sent_reports_cache_(kMaxHPKPReportCacheEntries) {
// Static pinning is only enabled for official builds to make sure that
// others don't end up with pins that cannot be easily updated.
#if !defined(OFFICIAL_BUILD) || defined(OS_ANDROID) || defined(OS_IOS)
@@ -782,6 +775,53 @@ void TransportSecurityState::EnablePKPHost(const std::string& host,
DirtyNotify();
}
+bool TransportSecurityState::CheckPinsAndMaybeSendReport(
+ const HostPortPair& host_port_pair,
+ const TransportSecurityState::PKPState& pkp_state,
+ const HashValueVector& hashes,
+ const X509Certificate* served_certificate_chain,
+ const X509Certificate* validated_certificate_chain,
+ const TransportSecurityState::PublicKeyPinReportStatus report_status,
+ std::string* failure_log) {
+ if (pkp_state.CheckPublicKeyPins(hashes, failure_log))
+ return true;
+
+ if (!report_sender_ ||
+ report_status != TransportSecurityState::ENABLE_PIN_REPORTS ||
+ pkp_state.report_uri.is_empty()) {
+ return false;
+ }
+
+ DCHECK(pkp_state.report_uri.is_valid());
+ // Report URIs should not be used if they are the same host as the pin
+ // and are HTTPS, to avoid going into a report-sending loop.
+ if (!IsReportUriValidForHost(pkp_state.report_uri, host_port_pair.host()))
+ return false;
+
+ std::string serialized_report;
+ std::string report_cache_key;
+ if (!GetHPKPReport(host_port_pair, pkp_state, served_certificate_chain,
+ validated_certificate_chain, &serialized_report,
+ &report_cache_key)) {
+ return false;
+ }
+
+ // Limit the rate at which duplicate reports are sent to the same
+ // report URI. The same report will not be sent within
+ // |kTimeToRememberHPKPReportsMins|, which reduces load on servers and
+ // also prevents accidental loops (a.com triggers a report to b.com
+ // which triggers a report to a.com). See section 2.1.4 of RFC 7469.
+ if (sent_reports_cache_.Get(report_cache_key, base::TimeTicks::Now()))
+ return false;
+ sent_reports_cache_.Put(
+ report_cache_key, true, base::TimeTicks::Now(),
+ base::TimeTicks::Now() +
+ base::TimeDelta::FromMinutes(kTimeToRememberHPKPReportsMins));
+
+ report_sender_->Send(pkp_state.report_uri, serialized_report);
+ return false;
+}
+
bool TransportSecurityState::DeleteDynamicDataForHost(const std::string& host) {
DCHECK(CalledOnValidThread());
@@ -951,7 +991,7 @@ bool TransportSecurityState::ProcessHPKPReportOnlyHeader(
CheckPinsAndMaybeSendReport(
host_port_pair, pkp_state, ssl_info.public_key_hashes,
ssl_info.unverified_cert.get(), ssl_info.cert.get(), ENABLE_PIN_REPORTS,
- report_sender_, &unused_failure_log);
+ &unused_failure_log);
return true;
}
@@ -1004,7 +1044,7 @@ bool TransportSecurityState::CheckPublicKeyPinsImpl(
return CheckPinsAndMaybeSendReport(
host_port_pair, pkp_state, hashes, served_certificate_chain,
- validated_certificate_chain, report_status, report_sender_, failure_log);
+ validated_certificate_chain, report_status, failure_log);
}
bool TransportSecurityState::GetStaticDomainState(const std::string& host,
diff --git a/net/http/transport_security_state.h b/net/http/transport_security_state.h
index 554326a..d46993b 100644
--- a/net/http/transport_security_state.h
+++ b/net/http/transport_security_state.h
@@ -15,6 +15,7 @@
#include "base/gtest_prod_util.h"
#include "base/threading/non_thread_safe.h"
#include "base/time/time.h"
+#include "net/base/expiring_cache.h"
#include "net/base/net_export.h"
#include "net/cert/x509_cert_types.h"
#include "net/cert/x509_certificate.h"
@@ -368,6 +369,22 @@ class NET_EXPORT TransportSecurityState
void EnableSTSHost(const std::string& host, const STSState& state);
void EnablePKPHost(const std::string& host, const PKPState& state);
+ // Returns true if a request to |host_port_pair| with the given
+ // SubjectPublicKeyInfo |hashes| satisfies the pins in |pkp_state|,
+ // and false otherwise. If a violation is found and reporting is
+ // configured (i.e. there is a report URI in |pkp_state| and
+ // |report_status| says to), this method sends an HPKP violation
+ // report containing |served_certificate_chain| and
+ // |validated_certificate_chain|.
+ bool CheckPinsAndMaybeSendReport(
+ const HostPortPair& host_port_pair,
+ const TransportSecurityState::PKPState& pkp_state,
+ const HashValueVector& hashes,
+ const X509Certificate* served_certificate_chain,
+ const X509Certificate* validated_certificate_chain,
+ const TransportSecurityState::PublicKeyPinReportStatus report_status,
+ std::string* failure_log);
+
// The sets of hosts that have enabled TransportSecurity. |domain| will always
// be empty for a STSState or PKPState in these maps; the domain
// comes from the map keys instead. In addition, |upgrade_mode| in the
@@ -383,6 +400,11 @@ class NET_EXPORT TransportSecurityState
// True if static pins should be used.
bool enable_static_pins_;
+ // Keeps track of reports that have been sent recently for
+ // rate-limiting.
+ ExpiringCache<std::string, bool, base::TimeTicks, std::less<base::TimeTicks>>
+ sent_reports_cache_;
+
DISALLOW_COPY_AND_ASSIGN(TransportSecurityState);
};
diff --git a/net/http/transport_security_state_unittest.cc b/net/http/transport_security_state_unittest.cc
index 21160fb..afcc42c 100644
--- a/net/http/transport_security_state_unittest.cc
+++ b/net/http/transport_security_state_unittest.cc
@@ -1254,12 +1254,6 @@ TEST_F(TransportSecurityStateTest, HPKPReportOnly) {
MockCertificateReportSender mock_report_sender;
state.SetReportSender(&mock_report_sender);
- // Check that a report is not sent for a Report-Only header with no
- // violation.
- std::string header =
- "pin-sha256=\"" + std::string(kGoodPin1) + "\";pin-sha256=\"" +
- std::string(kGoodPin2) + "\";pin-sha256=\"" + std::string(kGoodPin3) +
- "\";report-uri=\"" + report_uri.spec() + "\";includeSubdomains";
SSLInfo ssl_info;
ssl_info.is_issued_by_known_root = true;
ssl_info.unverified_cert = cert1;
@@ -1267,6 +1261,25 @@ TEST_F(TransportSecurityStateTest, HPKPReportOnly) {
for (size_t i = 0; kGoodPath[i]; i++)
EXPECT_TRUE(AddHash(kGoodPath[i], &ssl_info.public_key_hashes));
+ // HTTPS report URIs on the same host as the pin violation should not
+ // be allowed, to avoid going into a report-sending loop.
+ std::string header = "pin-sha256=\"" + std::string(kGoodPin1) +
+ "\";pin-sha256=\"" + std::string(kGoodPin2) +
+ "\";pin-sha256=\"" + std::string(kGoodPin3) +
+ "\";report-uri=\"https://" + host_port_pair.host() +
+ "/report\";includeSubdomains";
+ EXPECT_TRUE(
+ state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info));
+ EXPECT_TRUE(mock_report_sender.latest_report_uri().is_empty());
+
+ // Check that a report is not sent for a Report-Only header with no
+ // violation.
+ mock_report_sender.Clear();
+ header = "pin-sha256=\"" + std::string(kGoodPin1) + "\";pin-sha256=\"" +
+ std::string(kGoodPin2) + "\";pin-sha256=\"" +
+ std::string(kGoodPin3) + "\";report-uri=\"" + report_uri.spec() +
+ "\";includeSubdomains";
+
EXPECT_TRUE(
state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info));
EXPECT_EQ(GURL(), mock_report_sender.latest_report_uri());
@@ -1286,17 +1299,6 @@ TEST_F(TransportSecurityStateTest, HPKPReportOnly) {
ASSERT_NO_FATAL_FAILURE(CheckHPKPReport(report, host_port_pair, true, kHost,
cert1.get(), cert2.get(),
ssl_info.public_key_hashes));
-
- // HTTPS report URIs on the same host as the pin violation should not
- // be allowed, to avoid going into a report-sending loop.
- mock_report_sender.Clear();
- header = "pin-sha256=\"" + std::string(kGoodPin1) + "\";pin-sha256=\"" +
- std::string(kGoodPin2) + "\";pin-sha256=\"" +
- std::string(kGoodPin3) + "\";report-uri=\"https://" +
- host_port_pair.host() + "/report\";includeSubdomains";
- EXPECT_TRUE(
- state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info));
- EXPECT_TRUE(mock_report_sender.latest_report_uri().is_empty());
}
// Tests that Report-Only reports are not sent on certs that chain to
@@ -1474,4 +1476,75 @@ TEST_F(TransportSecurityStateTest, HPKPReportUriToSameHost) {
EXPECT_EQ(http_report_uri, mock_report_sender.latest_report_uri());
}
+// Tests that redundant reports are rate-limited.
+TEST_F(TransportSecurityStateTest, HPKPReportRateLimiting) {
+ HostPortPair host_port_pair(kHost, kPort);
+ HostPortPair subdomain_host_port_pair(kSubdomain, kPort);
+ GURL report_uri(kReportUri);
+ // Two dummy certs to use as the server-sent and validated chains. The
+ // contents don't matter.
+ scoped_refptr<X509Certificate> cert1 =
+ ImportCertFromFile(GetTestCertsDirectory(), "test_mail_google_com.pem");
+ scoped_refptr<X509Certificate> cert2 =
+ ImportCertFromFile(GetTestCertsDirectory(), "expired_cert.pem");
+ ASSERT_TRUE(cert1);
+ ASSERT_TRUE(cert2);
+
+ HashValueVector good_hashes, bad_hashes;
+
+ for (size_t i = 0; kGoodPath[i]; i++)
+ EXPECT_TRUE(AddHash(kGoodPath[i], &good_hashes));
+ for (size_t i = 0; kBadPath[i]; i++)
+ EXPECT_TRUE(AddHash(kBadPath[i], &bad_hashes));
+
+ TransportSecurityState state;
+ MockCertificateReportSender mock_report_sender;
+ state.SetReportSender(&mock_report_sender);
+
+ const base::Time current_time = base::Time::Now();
+ const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000);
+ state.AddHPKP(kHost, expiry, true, good_hashes, report_uri);
+
+ EXPECT_EQ(GURL(), mock_report_sender.latest_report_uri());
+ EXPECT_EQ(std::string(), mock_report_sender.latest_report());
+
+ std::string failure_log;
+ EXPECT_FALSE(state.CheckPublicKeyPins(
+ host_port_pair, true, bad_hashes, cert1.get(), cert2.get(),
+ TransportSecurityState::ENABLE_PIN_REPORTS, &failure_log));
+
+ // A report should have been sent. Check that it contains the
+ // right information.
+ EXPECT_EQ(report_uri, mock_report_sender.latest_report_uri());
+ std::string report = mock_report_sender.latest_report();
+ ASSERT_FALSE(report.empty());
+ ASSERT_NO_FATAL_FAILURE(CheckHPKPReport(report, host_port_pair, true, kHost,
+ cert1.get(), cert2.get(),
+ good_hashes));
+ mock_report_sender.Clear();
+
+ // Now trigger the same violation; a duplicative report should not be
+ // sent.
+ EXPECT_FALSE(state.CheckPublicKeyPins(
+ host_port_pair, true, bad_hashes, cert1.get(), cert2.get(),
+ TransportSecurityState::ENABLE_PIN_REPORTS, &failure_log));
+ EXPECT_EQ(GURL(), mock_report_sender.latest_report_uri());
+ EXPECT_EQ(std::string(), mock_report_sender.latest_report());
+
+ // Trigger the same violation but with a different report-uri: it
+ // should be sent.
+ GURL report_uri2("http://report-example2.test/test");
+ state.AddHPKP(kHost, expiry, true, good_hashes, report_uri2);
+ EXPECT_FALSE(state.CheckPublicKeyPins(
+ host_port_pair, true, bad_hashes, cert1.get(), cert2.get(),
+ TransportSecurityState::ENABLE_PIN_REPORTS, &failure_log));
+ EXPECT_EQ(report_uri2, mock_report_sender.latest_report_uri());
+ report = mock_report_sender.latest_report();
+ ASSERT_FALSE(report.empty());
+ ASSERT_NO_FATAL_FAILURE(CheckHPKPReport(report, host_port_pair, true, kHost,
+ cert1.get(), cert2.get(),
+ good_hashes));
+ mock_report_sender.Clear();
+}
+
} // namespace net