diff options
author | estark <estark@chromium.org> | 2015-08-03 11:31:51 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-08-03 18:32:37 +0000 |
commit | 53fee7cfec8bf6426d2ed3d8117ddc08f42e9d87 (patch) | |
tree | ee2fc289d558ededff5df60eac27362ab37eaab1 | |
parent | a90e6c95e36bc873fb2c276938e88b16611ce1d9 (diff) | |
download | chromium_src-53fee7cfec8bf6426d2ed3d8117ddc08f42e9d87.zip chromium_src-53fee7cfec8bf6426d2ed3d8117ddc08f42e9d87.tar.gz chromium_src-53fee7cfec8bf6426d2ed3d8117ddc08f42e9d87.tar.bz2 |
Process Public-Key-Pin-Report-Only headers
This CL adds a call to parse and process HPKP-RO headers in
URLRequestHTTPJob.
This is based on top of crrev.com/1267513003
BUG=445793
Review URL: https://codereview.chromium.org/1266723003
Cr-Commit-Position: refs/heads/master@{#341563}
-rw-r--r-- | net/http/transport_security_state.cc | 91 | ||||
-rw-r--r-- | net/http/transport_security_state.h | 8 | ||||
-rw-r--r-- | net/http/transport_security_state_unittest.cc | 207 | ||||
-rw-r--r-- | net/url_request/url_request_http_job.cc | 9 |
4 files changed, 251 insertions, 64 deletions
diff --git a/net/http/transport_security_state.cc b/net/http/transport_security_state.cc index 50e9c39..b343223 100644 --- a/net/http/transport_security_state.cc +++ b/net/http/transport_security_state.cc @@ -135,6 +135,38 @@ bool GetHPKPReport(const HostPortPair& host_port_pair, return true; } +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()); + + 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) { @@ -873,6 +905,42 @@ void TransportSecurityState::AddHPKP(const std::string& host, report_uri); } +bool TransportSecurityState::ProcessHPKPReportOnlyHeader( + const std::string& value, + const HostPortPair& host_port_pair, + const SSLInfo& ssl_info) { + DCHECK(CalledOnValidThread()); + + base::Time now = base::Time::Now(); + bool include_subdomains; + HashValueVector spki_hashes; + GURL report_uri; + std::string unused_failure_log; + + if (!ParseHPKPReportOnlyHeader(value, &include_subdomains, &spki_hashes, + &report_uri) || + !report_uri.is_valid() || report_uri.is_empty()) + return false; + + PKPState pkp_state; + pkp_state.last_observed = now; + pkp_state.expiry = now; + pkp_state.include_subdomains = include_subdomains; + pkp_state.spki_hashes = spki_hashes; + pkp_state.report_uri = report_uri; + pkp_state.domain = DNSDomainToString(CanonicalizeHost(host_port_pair.host())); + + // Only perform pin validation if the cert chains up to a known root. + if (!ssl_info.is_issued_by_known_root) + return true; + + 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); + return true; +} + // static bool TransportSecurityState::IsGooglePinnedProperty(const std::string& host) { PreloadResult result; @@ -927,26 +995,9 @@ bool TransportSecurityState::CheckPublicKeyPinsImpl( 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; + return CheckPinsAndMaybeSendReport( + host_port_pair, pkp_state, hashes, served_certificate_chain, + validated_certificate_chain, report_status, report_sender_, 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 1c5658a..d7befb7 100644 --- a/net/http/transport_security_state.h +++ b/net/http/transport_security_state.h @@ -299,6 +299,14 @@ class NET_EXPORT TransportSecurityState const HashValueVector& hashes, const GURL& report_uri); + // Parses |value| as a Public-Key-Pins-Report-Only header value and + // sends a HPKP report for |host_port_pair| if |ssl_info| violates the + // pin. Returns true if |value| parses and includes a valid + // report-uri, and false otherwise. + bool ProcessHPKPReportOnlyHeader(const std::string& value, + const HostPortPair& host_port_pair, + const SSLInfo& ssl_info); + // Returns true iff we have any static public key pins for the |host| and // iff its set of required pins is the set we expect for Google // properties. diff --git a/net/http/transport_security_state_unittest.cc b/net/http/transport_security_state_unittest.cc index 31c4062..4c63c06 100644 --- a/net/http/transport_security_state_unittest.cc +++ b/net/http/transport_security_state_unittest.cc @@ -42,8 +42,28 @@ namespace net { namespace { +const char kHost[] = "example.test"; +const char kSubdomain[] = "foo.example.test"; +const uint16_t kPort = 443; const char kReportUri[] = "http://example.test/test"; +// kGoodPath is blog.torproject.org. +const char* const kGoodPath[] = { + "sha1/m9lHYJYke9k0GtVZ+bXSQYE8nDI=", "sha1/o5OZxATDsgmwgcIfIWIneMJ0jkw=", + "sha1/wHqYaI2J+6sFZAwRfap9ZbjKzE4=", NULL, +}; + +const char kGoodPin1[] = "m9lHYJYke9k0GtVZ+bXSQYE8nDI="; +const char kGoodPin2[] = "o5OZxATDsgmwgcIfIWIneMJ0jkw="; +const char kGoodPin3[] = "wHqYaI2J+6sFZAwRfap9ZbjKzE4="; + +// kBadPath is plus.google.com via Trustcenter, which is utterly wrong for +// torproject.org. +const char* const kBadPath[] = { + "sha1/4BjDjn8v2lWeUFQnqSs0BgbIcrU=", "sha1/gzuEEAB/bkqdQS3EIjk2by7lW+k=", + "sha1/SOZo+SvSspXXR9gjIBBPM5iQn9Q=", NULL, +}; + // A mock ReportSender that just remembers the latest report // URI and report to be sent. class MockCertificateReportSender @@ -57,6 +77,11 @@ class MockCertificateReportSender latest_report_ = report; } + void Clear() { + latest_report_uri_ = GURL(); + latest_report_ = std::string(); + } + const GURL& latest_report_uri() { return latest_report_uri_; } const std::string& latest_report() { return latest_report_; } @@ -83,14 +108,11 @@ void CompareCertificateChainWithList( void CheckHPKPReport( const std::string& report, const HostPortPair& host_port_pair, - const base::Time& expiry, bool include_subdomains, const std::string& noted_hostname, const scoped_refptr<X509Certificate>& served_certificate_chain, const scoped_refptr<X509Certificate>& validated_certificate_chain, const HashValueVector& known_pins) { - // TODO(estark): check time in RFC3339 format. - scoped_ptr<base::Value> value(base::JSONReader::Read(report)); ASSERT_TRUE(value); ASSERT_TRUE(value->IsType(base::Value::TYPE_DICTIONARY)); @@ -115,6 +137,17 @@ void CheckHPKPReport( EXPECT_TRUE(report_dict->GetString("noted-hostname", &report_noted_hostname)); EXPECT_EQ(noted_hostname, report_noted_hostname); + // TODO(estark): check times in RFC3339 format. + + std::string report_expiration; + EXPECT_TRUE( + report_dict->GetString("effective-expiration-date", &report_expiration)); + EXPECT_FALSE(report_expiration.empty()); + + std::string report_date; + EXPECT_TRUE(report_dict->GetString("date-time", &report_date)); + EXPECT_FALSE(report_date.empty()); + base::ListValue* report_served_certificate_chain; EXPECT_TRUE(report_dict->GetList("served-certificate-chain", &report_served_certificate_chain)); @@ -1057,23 +1090,6 @@ static bool AddHash(const std::string& type_and_base64, } TEST_F(TransportSecurityStateTest, PinValidationWithoutRejectedCerts) { - // kGoodPath is blog.torproject.org. - static const char* const kGoodPath[] = { - "sha1/m9lHYJYke9k0GtVZ+bXSQYE8nDI=", - "sha1/o5OZxATDsgmwgcIfIWIneMJ0jkw=", - "sha1/wHqYaI2J+6sFZAwRfap9ZbjKzE4=", - NULL, - }; - - // kBadPath is plus.google.com via Trustcenter, which is utterly wrong for - // torproject.org. - static const char* const kBadPath[] = { - "sha1/4BjDjn8v2lWeUFQnqSs0BgbIcrU=", - "sha1/gzuEEAB/bkqdQS3EIjk2by7lW+k=", - "sha1/SOZo+SvSspXXR9gjIBBPM5iQn9Q=", - NULL, - }; - HashValueVector good_hashes, bad_hashes; for (size_t i = 0; kGoodPath[i]; i++) { @@ -1200,12 +1216,9 @@ TEST_F(TransportSecurityStateTest, GooglePinnedProperties) { } TEST_F(TransportSecurityStateTest, HPKPReporting) { - const char kHost[] = "example.test"; - const char kSubdomain[] = "foo.example.test"; - static const uint16_t kPort = 443; HostPortPair host_port_pair(kHost, kPort); HostPortPair subdomain_host_port_pair(kSubdomain, kPort); - GURL report_uri("http://www.example.test/report"); + 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 = @@ -1215,19 +1228,6 @@ TEST_F(TransportSecurityStateTest, HPKPReporting) { ASSERT_TRUE(cert1); ASSERT_TRUE(cert2); - // kGoodPath is blog.torproject.org. - static const char* const kGoodPath[] = { - "sha1/m9lHYJYke9k0GtVZ+bXSQYE8nDI=", "sha1/o5OZxATDsgmwgcIfIWIneMJ0jkw=", - "sha1/wHqYaI2J+6sFZAwRfap9ZbjKzE4=", NULL, - }; - - // kBadPath is plus.google.com via Trustcenter, which is utterly wrong for - // torproject.org. - static const char* const kBadPath[] = { - "sha1/4BjDjn8v2lWeUFQnqSs0BgbIcrU=", "sha1/gzuEEAB/bkqdQS3EIjk2by7lW+k=", - "sha1/SOZo+SvSspXXR9gjIBBPM5iQn9Q=", NULL, - }; - HashValueVector good_hashes, bad_hashes; for (size_t i = 0; kGoodPath[i]; i++) @@ -1273,10 +1273,10 @@ TEST_F(TransportSecurityStateTest, HPKPReporting) { 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, expiry, true, - kHost, cert1.get(), cert2.get(), + ASSERT_NO_FATAL_FAILURE(CheckHPKPReport(report, host_port_pair, true, kHost, + cert1.get(), cert2.get(), good_hashes)); - + mock_report_sender.Clear(); EXPECT_FALSE(state.CheckPublicKeyPins( subdomain_host_port_pair, true, bad_hashes, cert1.get(), cert2.get(), TransportSecurityState::ENABLE_PIN_REPORTS, &failure_log)); @@ -1287,8 +1287,131 @@ TEST_F(TransportSecurityStateTest, HPKPReporting) { report = mock_report_sender.latest_report(); ASSERT_FALSE(report.empty()); ASSERT_NO_FATAL_FAILURE(CheckHPKPReport(report, subdomain_host_port_pair, - expiry, true, kHost, cert1.get(), - cert2.get(), good_hashes)); + true, kHost, cert1.get(), cert2.get(), + good_hashes)); +} + +TEST_F(TransportSecurityStateTest, HPKPReportOnly) { + HostPortPair host_port_pair(kHost, 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); + + TransportSecurityState state; + 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-sha1=\"" + std::string(kGoodPin1) + "\";pin-sha1=\"" + + std::string(kGoodPin2) + "\";pin-sha1=\"" + 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; + ssl_info.cert = cert2; + for (size_t i = 0; kGoodPath[i]; i++) + EXPECT_TRUE(AddHash(kGoodPath[i], &ssl_info.public_key_hashes)); + + EXPECT_TRUE( + state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info)); + EXPECT_EQ(GURL(), mock_report_sender.latest_report_uri()); + EXPECT_EQ(std::string(), mock_report_sender.latest_report()); + + // Check that a report is sent for a Report-Only header with a + // violation. + ssl_info.public_key_hashes.clear(); + for (size_t i = 0; kBadPath[i]; i++) + EXPECT_TRUE(AddHash(kBadPath[i], &ssl_info.public_key_hashes)); + + EXPECT_TRUE( + state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info)); + 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(), + ssl_info.public_key_hashes)); +} + +// Test that Report-Only reports are not sent on certs that chain to +// local roots. +TEST_F(TransportSecurityStateTest, HPKPReportOnlyOnLocalRoot) { + HostPortPair host_port_pair(kHost, 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); + + std::string header = + "pin-sha1=\"" + std::string(kGoodPin1) + "\";pin-sha1=\"" + + std::string(kGoodPin2) + "\";pin-sha1=\"" + std::string(kGoodPin3) + + "\";report-uri=\"" + report_uri.spec() + "\";includeSubdomains"; + + TransportSecurityState state; + MockCertificateReportSender mock_report_sender; + state.SetReportSender(&mock_report_sender); + + SSLInfo ssl_info; + ssl_info.is_issued_by_known_root = true; + ssl_info.unverified_cert = cert1; + ssl_info.cert = cert2; + for (size_t i = 0; kGoodPath[i]; i++) + EXPECT_TRUE(AddHash(kGoodPath[i], &ssl_info.public_key_hashes)); + ssl_info.is_issued_by_known_root = false; + + EXPECT_TRUE( + state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info)); + EXPECT_EQ(GURL(), mock_report_sender.latest_report_uri()); + EXPECT_EQ(std::string(), mock_report_sender.latest_report()); +} + +// Test that ProcessHPKPReportOnlyHeader() returns false if a report-uri +// wasn't specified or if the header fails to parse. +TEST_F(TransportSecurityStateTest, HPKPReportOnlyParseErrors) { + HostPortPair host_port_pair(kHost, 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); + + std::string header = "pin-sha1=\"" + std::string(kGoodPin1) + + "\";pin-sha1=\"" + std::string(kGoodPin2) + + "\";pin-sha1=\"" + std::string(kGoodPin3) + "\""; + + TransportSecurityState state; + MockCertificateReportSender mock_report_sender; + state.SetReportSender(&mock_report_sender); + + SSLInfo ssl_info; + ssl_info.is_issued_by_known_root = true; + ssl_info.unverified_cert = cert1; + ssl_info.cert = cert2; + for (size_t i = 0; kGoodPath[i]; i++) + EXPECT_TRUE(AddHash(kGoodPath[i], &ssl_info.public_key_hashes)); + + EXPECT_FALSE( + state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info)); + header += ";report-uri=\""; + EXPECT_FALSE( + state.ProcessHPKPReportOnlyHeader(header, host_port_pair, ssl_info)); } } // namespace net diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc index 019a0a7..318aa4f 100644 --- a/net/url_request/url_request_http_job.cc +++ b/net/url_request/url_request_http_job.cc @@ -883,15 +883,20 @@ void URLRequestHttpJob::ProcessPublicKeyPinsHeader() { if (request_info_.url.HostIsIPAddress()) return; - // http://tools.ietf.org/html/draft-ietf-websec-key-pinning: + // http://tools.ietf.org/html/rfc7469: // // If a UA receives more than one PKP header field in an HTTP // response message over secure transport, then the UA MUST process // only the first such header field. HttpResponseHeaders* headers = GetResponseHeaders(); std::string value; - if (headers->EnumerateHeader(NULL, "Public-Key-Pins", &value)) + if (headers->EnumerateHeader(nullptr, "Public-Key-Pins", &value)) security_state->AddHPKPHeader(request_info_.url.host(), value, ssl_info); + if (headers->EnumerateHeader(nullptr, "Public-Key-Pins-Report-Only", + &value)) { + security_state->ProcessHPKPReportOnlyHeader( + value, HostPortPair::FromURL(request_info_.url), ssl_info); + } } void URLRequestHttpJob::OnStartCompleted(int result) { |