diff options
-rw-r--r-- | net/http/transport_security_state.cc | 132 | ||||
-rw-r--r-- | net/http/transport_security_state.h | 22 | ||||
-rw-r--r-- | net/http/transport_security_state_unittest.cc | 107 |
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 |