Index: net/http/transport_security_state_unittest.cc |
diff --git a/net/http/transport_security_state_unittest.cc b/net/http/transport_security_state_unittest.cc |
index 771f3c6c6a736b3176888a483674d0e159a12df5..ece0a67985db1b9e01be861d34c4d9382920ff0f 100644 |
--- a/net/http/transport_security_state_unittest.cc |
+++ b/net/http/transport_security_state_unittest.cc |
@@ -10,9 +10,11 @@ |
#include "base/base64.h" |
#include "base/files/file_path.h" |
+#include "base/json/json_reader.h" |
#include "base/rand_util.h" |
#include "base/sha1.h" |
#include "base/strings/string_piece.h" |
+#include "base/values.h" |
#include "crypto/sha2.h" |
#include "net/base/net_errors.h" |
#include "net/base/test_completion_callback.h" |
@@ -23,7 +25,9 @@ |
#include "net/cert/test_root_certs.h" |
#include "net/cert/x509_cert_types.h" |
#include "net/cert/x509_certificate.h" |
+#include "net/http/certificate_report_sender.h" |
#include "net/http/http_util.h" |
+#include "net/http/transport_security_reporter.h" |
#include "net/log/net_log.h" |
#include "net/ssl/ssl_info.h" |
#include "net/test/cert_test_util.h" |
@@ -35,6 +39,94 @@ |
#include "crypto/nss_util.h" |
#endif |
+namespace { |
+ |
+// A mock CertificateReportSender that just remembers the latest report |
+// URI and report to be sent. |
+class MockCertificateReportSender : public net::CertificateReportSender { |
+ public: |
+ MockCertificateReportSender() {} |
+ ~MockCertificateReportSender() override {} |
+ |
+ void Send(const GURL& report_uri, const std::string& report) override { |
+ latest_report_uri_ = report_uri; |
+ latest_report_ = report; |
+ } |
+ |
+ const GURL& latest_report_uri() { return latest_report_uri_; } |
+ const std::string& latest_report() { return latest_report_; } |
+ |
+ private: |
+ GURL latest_report_uri_; |
+ std::string latest_report_; |
+}; |
+ |
+void CompareCertificateChainWithList( |
+ const scoped_refptr<net::X509Certificate>& cert_chain, |
+ const base::ListValue* cert_list) { |
+ ASSERT_TRUE(cert_chain); |
+ std::vector<std::string> pem_encoded_chain; |
+ cert_chain->GetPEMEncodedChain(&pem_encoded_chain); |
+ EXPECT_EQ(pem_encoded_chain.size(), cert_list->GetSize()); |
+ |
+ for (size_t i = 0; i < pem_encoded_chain.size(); i++) { |
+ std::string list_cert; |
+ ASSERT_TRUE(cert_list->GetString(i, &list_cert)); |
+ EXPECT_EQ(pem_encoded_chain[i], list_cert); |
+ } |
+} |
+ |
+void CheckHPKPReport( |
+ const std::string& report, |
+ const std::string& hostname, |
+ uint16_t port, |
+ const base::Time& expiry, |
+ bool include_subdomains, |
+ const std::string& noted_hostname, |
+ const scoped_refptr<net::X509Certificate>& served_certificate_chain, |
+ const scoped_refptr<net::X509Certificate>& validated_certificate_chain, |
+ const net::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)); |
+ |
+ scoped_ptr<base::DictionaryValue> report_dict( |
+ static_cast<base::DictionaryValue*>(value.release())); |
+ |
+ std::string report_hostname; |
+ EXPECT_TRUE(report_dict->GetString("hostname", &report_hostname)); |
+ EXPECT_EQ(hostname, report_hostname); |
+ |
+ int report_port; |
+ EXPECT_TRUE(report_dict->GetInteger("port", &report_port)); |
+ EXPECT_EQ(port, report_port); |
+ |
+ bool report_include_subdomains; |
+ EXPECT_TRUE(report_dict->GetBoolean("include-subdomains", |
+ &report_include_subdomains)); |
+ EXPECT_EQ(include_subdomains, report_include_subdomains); |
+ |
+ std::string report_noted_hostname; |
+ EXPECT_TRUE(report_dict->GetString("hostname", &report_noted_hostname)); |
+ EXPECT_EQ(hostname, report_noted_hostname); |
+ |
+ base::ListValue* report_served_certificate_chain; |
+ EXPECT_TRUE(report_dict->GetList("served-certificate-chain", |
+ &report_served_certificate_chain)); |
+ CompareCertificateChainWithList(served_certificate_chain, |
+ report_served_certificate_chain); |
+ |
+ base::ListValue* report_validated_certificate_chain; |
+ EXPECT_TRUE(report_dict->GetList("validated-certificate-chain", |
+ &report_validated_certificate_chain)); |
+ CompareCertificateChainWithList(validated_certificate_chain, |
+ report_validated_certificate_chain); |
+} |
+ |
+} // namespace |
+ |
namespace net { |
class TransportSecurityStateTest : public testing::Test { |
@@ -212,6 +304,9 @@ TEST_F(TransportSecurityStateTest, SubdomainMatches) { |
// with it, regardless of the includeSubDomains bit. This is a regression test |
// for https://crbug.com/469957. |
TEST_F(TransportSecurityStateTest, SubdomainCarveout) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
const base::Time current_time(base::Time::Now()); |
const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); |
@@ -220,8 +315,10 @@ TEST_F(TransportSecurityStateTest, SubdomainCarveout) { |
state.AddHSTS("example1.test", expiry, true); |
state.AddHSTS("foo.example1.test", expiry, false); |
- state.AddHPKP("example2.test", expiry, true, GetSampleSPKIHashes()); |
- state.AddHPKP("foo.example2.test", expiry, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example2.test", expiry, true, GetSampleSPKIHashes(), |
+ report_uri); |
+ state.AddHPKP("foo.example2.test", expiry, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("example1.test")); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("foo.example1.test")); |
@@ -241,7 +338,8 @@ TEST_F(TransportSecurityStateTest, SubdomainCarveout) { |
// Expire the foo.example*.test rules. |
state.AddHSTS("foo.example1.test", older, false); |
- state.AddHPKP("foo.example2.test", older, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("foo.example2.test", older, false, GetSampleSPKIHashes(), |
+ report_uri); |
// Now the base example*.test rules apply to bar.foo.example*.test. |
EXPECT_TRUE(state.ShouldUpgradeToSSL("bar.foo.example1.test")); |
@@ -251,12 +349,16 @@ TEST_F(TransportSecurityStateTest, SubdomainCarveout) { |
} |
TEST_F(TransportSecurityStateTest, FatalSSLErrors) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
const base::Time current_time(base::Time::Now()); |
const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); |
state.AddHSTS("example1.test", expiry, false); |
- state.AddHPKP("example2.test", expiry, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example2.test", expiry, false, GetSampleSPKIHashes(), |
+ report_uri); |
// The presense of either HSTS or HPKP is enough to make SSL errors fatal. |
EXPECT_TRUE(state.ShouldSSLErrorsBeFatal("example1.test")); |
@@ -266,6 +368,9 @@ TEST_F(TransportSecurityStateTest, FatalSSLErrors) { |
// Tests that HPKP and HSTS state both expire. Also tests that expired entries |
// are pruned. |
TEST_F(TransportSecurityStateTest, Expiration) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
const base::Time current_time(base::Time::Now()); |
const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); |
@@ -279,14 +384,16 @@ TEST_F(TransportSecurityStateTest, Expiration) { |
// Querying |state| for a domain should flush out expired entries. |
EXPECT_FALSE(TransportSecurityState::Iterator(state).HasNext()); |
- state.AddHPKP("example1.test", older, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example1.test", older, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(TransportSecurityState::Iterator(state).HasNext()); |
EXPECT_FALSE(state.HasPublicKeyPins("example1.test")); |
// Querying |state| for a domain should flush out expired entries. |
EXPECT_FALSE(TransportSecurityState::Iterator(state).HasNext()); |
state.AddHSTS("example1.test", older, false); |
- state.AddHPKP("example1.test", older, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example1.test", older, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(TransportSecurityState::Iterator(state).HasNext()); |
EXPECT_FALSE(state.ShouldSSLErrorsBeFatal("example1.test")); |
// Querying |state| for a domain should flush out expired entries. |
@@ -294,13 +401,15 @@ TEST_F(TransportSecurityStateTest, Expiration) { |
// Test that HSTS can outlive HPKP. |
state.AddHSTS("example1.test", expiry, false); |
- state.AddHPKP("example1.test", older, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example1.test", older, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("example1.test")); |
EXPECT_FALSE(state.HasPublicKeyPins("example1.test")); |
// Test that HPKP can outlive HSTS. |
state.AddHSTS("example2.test", older, false); |
- state.AddHPKP("example2.test", expiry, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example2.test", expiry, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_FALSE(state.ShouldUpgradeToSSL("example2.test")); |
EXPECT_TRUE(state.HasPublicKeyPins("example2.test")); |
} |
@@ -320,15 +429,20 @@ TEST_F(TransportSecurityStateTest, InvalidDomains) { |
// Tests that HPKP and HSTS state are queried independently for subdomain |
// matches. |
TEST_F(TransportSecurityStateTest, IndependentSubdomain) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
const base::Time current_time(base::Time::Now()); |
const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); |
state.AddHSTS("example1.test", expiry, true); |
- state.AddHPKP("example1.test", expiry, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example1.test", expiry, false, GetSampleSPKIHashes(), |
+ report_uri); |
state.AddHSTS("example2.test", expiry, false); |
- state.AddHPKP("example2.test", expiry, true, GetSampleSPKIHashes()); |
+ state.AddHPKP("example2.test", expiry, true, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("foo.example1.test")); |
EXPECT_FALSE(state.HasPublicKeyPins("foo.example1.test")); |
@@ -338,13 +452,17 @@ TEST_F(TransportSecurityStateTest, IndependentSubdomain) { |
// Tests that HPKP and HSTS state are inserted and overridden independently. |
TEST_F(TransportSecurityStateTest, IndependentInsertion) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
const base::Time current_time(base::Time::Now()); |
const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); |
// Place an includeSubdomains HSTS entry below a normal HPKP entry. |
state.AddHSTS("example1.test", expiry, true); |
- state.AddHPKP("foo.example1.test", expiry, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("foo.example1.test", expiry, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("foo.example1.test")); |
EXPECT_TRUE(state.HasPublicKeyPins("foo.example1.test")); |
@@ -359,13 +477,15 @@ TEST_F(TransportSecurityStateTest, IndependentInsertion) { |
// Place an includeSubdomains HPKP entry below a normal HSTS entry. |
state.AddHSTS("foo.example2.test", expiry, false); |
- state.AddHPKP("example2.test", expiry, true, GetSampleSPKIHashes()); |
+ state.AddHPKP("example2.test", expiry, true, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("foo.example2.test")); |
EXPECT_TRUE(state.HasPublicKeyPins("foo.example2.test")); |
// Drop the includeSubdomains from the HSTS entry. |
- state.AddHPKP("example2.test", expiry, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("example2.test", expiry, false, GetSampleSPKIHashes(), |
+ report_uri); |
EXPECT_TRUE(state.ShouldUpgradeToSSL("foo.example2.test")); |
EXPECT_FALSE(state.HasPublicKeyPins("foo.example2.test")); |
@@ -374,13 +494,17 @@ TEST_F(TransportSecurityStateTest, IndependentInsertion) { |
// Tests that GetDynamicDomainState appropriately stitches together the results |
// of HSTS and HPKP. |
TEST_F(TransportSecurityStateTest, DynamicDomainState) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
const base::Time current_time(base::Time::Now()); |
const base::Time expiry1 = current_time + base::TimeDelta::FromSeconds(1000); |
const base::Time expiry2 = current_time + base::TimeDelta::FromSeconds(2000); |
state.AddHSTS("example.com", expiry1, true); |
- state.AddHPKP("foo.example.com", expiry2, false, GetSampleSPKIHashes()); |
+ state.AddHPKP("foo.example.com", expiry2, false, GetSampleSPKIHashes(), |
+ report_uri); |
TransportSecurityState::DomainState domain_state; |
ASSERT_TRUE(state.GetDynamicDomainState("foo.example.com", &domain_state)); |
@@ -397,6 +521,9 @@ TEST_F(TransportSecurityStateTest, DynamicDomainState) { |
// Tests that new pins always override previous pins. This should be true for |
// both pins at the same domain or includeSubdomains pins at a parent domain. |
TEST_F(TransportSecurityStateTest, NewPinsOverride) { |
+ static const char kReportUri[] = "http://example.com/test"; |
+ std::string report_uri(kReportUri); |
+ |
TransportSecurityState state; |
TransportSecurityState::DomainState domain_state; |
const base::Time current_time(base::Time::Now()); |
@@ -408,19 +535,22 @@ TEST_F(TransportSecurityStateTest, NewPinsOverride) { |
HashValue hash3(HASH_VALUE_SHA1); |
memset(hash3.data(), 0x03, hash1.size()); |
- state.AddHPKP("example.com", expiry, true, HashValueVector(1, hash1)); |
+ state.AddHPKP("example.com", expiry, true, HashValueVector(1, hash1), |
+ report_uri); |
ASSERT_TRUE(state.GetDynamicDomainState("foo.example.com", &domain_state)); |
ASSERT_EQ(1u, domain_state.pkp.spki_hashes.size()); |
EXPECT_TRUE(domain_state.pkp.spki_hashes[0].Equals(hash1)); |
- state.AddHPKP("foo.example.com", expiry, false, HashValueVector(1, hash2)); |
+ state.AddHPKP("foo.example.com", expiry, false, HashValueVector(1, hash2), |
+ report_uri); |
ASSERT_TRUE(state.GetDynamicDomainState("foo.example.com", &domain_state)); |
ASSERT_EQ(1u, domain_state.pkp.spki_hashes.size()); |
EXPECT_TRUE(domain_state.pkp.spki_hashes[0].Equals(hash2)); |
- state.AddHPKP("foo.example.com", expiry, false, HashValueVector(1, hash3)); |
+ state.AddHPKP("foo.example.com", expiry, false, HashValueVector(1, hash3), |
+ report_uri); |
ASSERT_TRUE(state.GetDynamicDomainState("foo.example.com", &domain_state)); |
ASSERT_EQ(1u, domain_state.pkp.spki_hashes.size()); |
@@ -1035,4 +1165,101 @@ TEST_F(TransportSecurityStateTest, GooglePinnedProperties) { |
"www.googlegroups.com")); |
} |
+TEST_F(TransportSecurityStateTest, HPKPReporting) { |
+ const char kHost[] = "example.com"; |
+ const char kSubdomain[] = "foo.example.com"; |
+ const uint16_t kPort = 443; |
+ GURL report_uri("http://www.example.com/report"); |
+ // 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); |
+ |
+ // 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++) { |
+ 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 = |
+ new MockCertificateReportSender(); |
+ TransportSecurityReporter reporter( |
+ &state, scoped_ptr<CertificateReportSender>(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.spec()); |
+ |
+ 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( |
+ kHost, true, bad_hashes, kPort, cert1, cert2, |
+ TransportSecurityState::DO_NOT_SEND_PUBLIC_KEY_PIN_REPORT, &failure_log)); |
+ |
+ // No report should have been sent because of the DO_NOT_SEND_REPORT |
+ // argument. |
+ EXPECT_EQ(GURL(), mock_report_sender->latest_report_uri()); |
+ EXPECT_EQ(std::string(), mock_report_sender->latest_report()); |
+ |
+ EXPECT_TRUE(state.CheckPublicKeyPins( |
+ kHost, true, good_hashes, kPort, cert1, cert2, |
+ TransportSecurityState::SEND_PUBLIC_KEY_PIN_REPORT, &failure_log)); |
+ |
+ // No report should have been sent because there was no violation. |
+ EXPECT_EQ(GURL(), mock_report_sender->latest_report_uri()); |
+ EXPECT_EQ(std::string(), mock_report_sender->latest_report()); |
+ |
+ EXPECT_FALSE(state.CheckPublicKeyPins( |
+ kHost, true, bad_hashes, kPort, cert1, cert2, |
+ TransportSecurityState::SEND_PUBLIC_KEY_PIN_REPORT, &failure_log)); |
+ |
+ // Now 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()); |
+ CheckHPKPReport(report, kHost, kPort, expiry, true, kHost, cert1, cert2, |
+ good_hashes); |
+ |
+ EXPECT_FALSE(state.CheckPublicKeyPins( |
+ kSubdomain, true, bad_hashes, kPort, cert1, cert2, |
+ TransportSecurityState::SEND_PUBLIC_KEY_PIN_REPORT, &failure_log)); |
+ |
+ // Now a report should have been sent for the subdomain. Check that it |
+ // contains the |
+ // right information. |
+ EXPECT_EQ(report_uri, mock_report_sender->latest_report_uri()); |
+ report = mock_report_sender->latest_report(); |
+ ASSERT_FALSE(report.empty()); |
+ CheckHPKPReport(report, kSubdomain, kPort, expiry, true, kHost, cert1, cert2, |
+ good_hashes); |
+} |
+ |
} // namespace net |