diff options
-rw-r--r-- | chrome_frame/test/net/fake_external_tab.cc | 2 | ||||
-rw-r--r-- | net/data/url_request_unittest/hpkp-headers.html | 1 | ||||
-rw-r--r-- | net/data/url_request_unittest/hpkp-headers.html.mock-http-headers | 6 | ||||
-rw-r--r-- | net/data/url_request_unittest/hsts-and-hpkp-headers2.html | 1 | ||||
-rw-r--r-- | net/data/url_request_unittest/hsts-and-hpkp-headers2.html.mock-http-headers | 8 | ||||
-rw-r--r-- | net/http/http_security_headers.cc | 5 | ||||
-rw-r--r-- | net/http/http_security_headers.h | 19 | ||||
-rw-r--r-- | net/http/http_security_headers_unittest.cc | 194 | ||||
-rw-r--r-- | net/http/transport_security_state.cc | 65 | ||||
-rw-r--r-- | net/http/transport_security_state.h | 19 | ||||
-rw-r--r-- | net/url_request/url_request_unittest.cc | 85 |
11 files changed, 336 insertions, 69 deletions
diff --git a/chrome_frame/test/net/fake_external_tab.cc b/chrome_frame/test/net/fake_external_tab.cc index 8ec91be..90ef166 100644 --- a/chrome_frame/test/net/fake_external_tab.cc +++ b/chrome_frame/test/net/fake_external_tab.cc @@ -257,9 +257,11 @@ void FilterDisabledTests() { // certs. So these tests time out waiting for user input. The // functionality they test (HTTP Strict Transport Security and // HTTP-based Public Key Pinning) does not work in Chrome Frame anyway. + "URLRequestTestHTTP.ProcessPKP", "URLRequestTestHTTP.ProcessSTS", "URLRequestTestHTTP.ProcessSTSOnce", "URLRequestTestHTTP.ProcessSTSAndPKP", + "URLRequestTestHTTP.ProcessSTSAndPKP2", // These tests have been disabled as the Chrome cookie policies don't make // sense or have not been implemented for the host network stack. diff --git a/net/data/url_request_unittest/hpkp-headers.html b/net/data/url_request_unittest/hpkp-headers.html new file mode 100644 index 0000000..364322d --- /dev/null +++ b/net/data/url_request_unittest/hpkp-headers.html @@ -0,0 +1 @@ +This file is boring; all the action's in the .mock-http-headers. diff --git a/net/data/url_request_unittest/hpkp-headers.html.mock-http-headers b/net/data/url_request_unittest/hpkp-headers.html.mock-http-headers new file mode 100644 index 0000000..f491840 --- /dev/null +++ b/net/data/url_request_unittest/hpkp-headers.html.mock-http-headers @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Cache-Control: private +Content-Type: text/html; charset=ISO-8859-1 +X-Multiple-Entries: a +X-Multiple-Entries: b +Public-Key-Pins: max-age=50000; pin-sha1="K9e3/nFL5j90GuVJOJBv6WXpvcs="; pin-sha256="kd16uBd5KFa9IJjF0X+8B+BXdAWkYYRZruNKDZ0M9Zw="; pin-sha1="Wws2/Z7YhKlX73v3rYHBBxO4OLE=" diff --git a/net/data/url_request_unittest/hsts-and-hpkp-headers2.html b/net/data/url_request_unittest/hsts-and-hpkp-headers2.html new file mode 100644 index 0000000..364322d --- /dev/null +++ b/net/data/url_request_unittest/hsts-and-hpkp-headers2.html @@ -0,0 +1 @@ +This file is boring; all the action's in the .mock-http-headers. diff --git a/net/data/url_request_unittest/hsts-and-hpkp-headers2.html.mock-http-headers b/net/data/url_request_unittest/hsts-and-hpkp-headers2.html.mock-http-headers new file mode 100644 index 0000000..9c0feda --- /dev/null +++ b/net/data/url_request_unittest/hsts-and-hpkp-headers2.html.mock-http-headers @@ -0,0 +1,8 @@ +HTTP/1.1 200 OK +Cache-Control: private +Content-Type: text/html; charset=ISO-8859-1 +X-Multiple-Entries: a +X-Multiple-Entries: b +Strict-Transport-Security: max-age=12300; includeSubdomains +Public-Key-Pins: max-age=50000; pin-sha1="K9e3/nFL5j90GuVJOJBv6WXpvcs="; pin-sha256="kd16uBd5KFa9IJjF0X+8B+BXdAWkYYRZruNKDZ0M9Zw="; pin-sha1="Wws2/Z7YhKlX73v3rYHBBxO4OLE=" +Public-Key-Pins: max-age=50000; pin-sha1="K9e3/nFL5j90GuVJOJBv6WXpvcs="; pin-sha256="kd16uBd5KFa9IJjF0X+8B+BXdAWkYYRZruNKDZ0M9Zw="; pin-sha1="Wws2/Z7YhKlX73v3rYHBBxO4OLE="; includeSubdomains diff --git a/net/http/http_security_headers.cc b/net/http/http_security_headers.cc index 0540bc7..9fc7627 100644 --- a/net/http/http_security_headers.cc +++ b/net/http/http_security_headers.cc @@ -276,8 +276,10 @@ bool ParseHSTSHeader(const std::string& value, bool ParseHPKPHeader(const std::string& value, const HashValueVector& chain_hashes, base::TimeDelta* max_age, + bool* include_subdomains, HashValueVector* hashes) { bool parsed_max_age = false; + bool include_subdomains_candidate = false; uint32 max_age_candidate = 0; HashValueVector pins; @@ -304,6 +306,8 @@ bool ParseHPKPHeader(const std::string& value, } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) { if (!ParseAndAppendPin(equals.second, HASH_VALUE_SHA256, &pins)) return false; + } else if (LowerCaseEqualsASCII(equals.first, "includesubdomains")) { + include_subdomains_candidate = true; } else { // Silently ignore unknown directives for forward compatibility. } @@ -318,6 +322,7 @@ bool ParseHPKPHeader(const std::string& value, return false; *max_age = base::TimeDelta::FromSeconds(max_age_candidate); + *include_subdomains = include_subdomains_candidate; for (HashValueVector::const_iterator i = pins.begin(); i != pins.end(); ++i) { hashes->push_back(*i); diff --git a/net/http/http_security_headers.h b/net/http/http_security_headers.h index b000971..12e6be9 100644 --- a/net/http/http_security_headers.h +++ b/net/http/http_security_headers.h @@ -30,25 +30,28 @@ bool NET_EXPORT_PRIVATE ParseHSTSHeader(const std::string& value, base::TimeDelta* max_age, bool* include_subdomains); -// Parses |value| as a Public-Key-Pins header value. If successful, -// returns true and populates the |*max_age| and hashes values. -// Otherwise returns false and leaves the output parameters unchanged. +// Parses |value| as a Public-Key-Pins header value. If successful, returns +// true and populates the |*max_age|, |*include_subdomains|, and |*hashes| +// values. Otherwise returns false and leaves the output parameters +// unchanged. // // value is the right-hand side of: // // "Public-Key-Pins" ":" // "max-age" "=" delta-seconds ";" // "pin-" algo "=" base64 [ ";" ... ] +// [ ";" "includeSubdomains" ] // // For this function to return true, the key hashes specified by the HPKP -// header must pass two additional checks. There MUST be at least one -// key hash which matches the SSL certificate chain of the current site -// (as specified by the chain_hashes) parameter. In addition, there MUST -// be at least one key hash which does NOT match the site's SSL certificate -// chain (this is the "backup pin"). +// header must pass two additional checks. There MUST be at least one key +// hash which matches the SSL certificate chain of the current site (as +// specified by the chain_hashes) parameter. In addition, there MUST be at +// least one key hash which does NOT match the site's SSL certificate chain +// (this is the "backup pin"). bool NET_EXPORT_PRIVATE ParseHPKPHeader(const std::string& value, const HashValueVector& chain_hashes, base::TimeDelta* max_age, + bool* include_subdomains, HashValueVector* hashes); } // namespace net diff --git a/net/http/http_security_headers_unittest.cc b/net/http/http_security_headers_unittest.cc index 0dd286b..0cc81b5 100644 --- a/net/http/http_security_headers_unittest.cc +++ b/net/http/http_security_headers_unittest.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <algorithm> + #include "base/base64.h" #include "base/sha1.h" #include "base/strings/string_piece.h" @@ -10,6 +12,8 @@ #include "net/base/test_completion_callback.h" #include "net/http/http_security_headers.h" #include "net/http/http_util.h" +#include "net/http/transport_security_state.h" +#include "net/ssl/ssl_info.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { @@ -119,6 +123,7 @@ TEST_F(HttpSecurityHeadersTest, BogusHeaders) { static void TestBogusPinsHeaders(HashValueTag tag) { base::TimeDelta max_age; + bool include_subdomains; HashValueVector hashes; HashValueVector chain_hashes; @@ -131,64 +136,75 @@ static void TestBogusPinsHeaders(HashValueTag tag) { std::string good_pin = GetTestPin(2, tag); std::string backup_pin = GetTestPin(4, tag); - EXPECT_FALSE( - ParseHPKPHeader(std::string(), chain_hashes, &max_age, &hashes)); - EXPECT_FALSE(ParseHPKPHeader(" ", chain_hashes, &max_age, &hashes)); - EXPECT_FALSE(ParseHPKPHeader("abc", chain_hashes, &max_age, &hashes)); - EXPECT_FALSE(ParseHPKPHeader(" abc", chain_hashes, &max_age, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(std::string(), chain_hashes, &max_age, + &include_subdomains, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(" ", chain_hashes, &max_age, + &include_subdomains, &hashes)); + EXPECT_FALSE(ParseHPKPHeader("abc", chain_hashes, &max_age, + &include_subdomains, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(" abc", chain_hashes, &max_age, + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" abc ", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age ", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age=", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age=", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age =", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age= ", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age = ", chain_hashes, - &max_age, &hashes)); + &max_age, &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age = xy", chain_hashes, - &max_age, &hashes)); + &max_age, &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader(" max-age = 3488a923", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, + &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age=3488a923 ", chain_hashes, - &max_age, &hashes)); + &max_age, &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-ag=3488923pins=" + good_pin + "," + backup_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, + &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-aged=3488923" + backup_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, + &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-aged=3488923; " + backup_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, + &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-aged=3488923; " + backup_pin + ";" + backup_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, + &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-aged=3488923; " + good_pin + ";" + good_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, + &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-aged=3488923; " + good_pin, - chain_hashes, &max_age, &hashes)); - EXPECT_FALSE(ParseHPKPHeader("max-age==3488923", chain_hashes, &max_age, + chain_hashes, &max_age, &include_subdomains, &hashes)); + EXPECT_FALSE(ParseHPKPHeader("max-age==3488923", chain_hashes, &max_age, + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("amax-age=3488923", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age=-3488923", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age=3488923;", chain_hashes, &max_age, - &hashes)); + &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age=3488923 e", chain_hashes, - &max_age, &hashes)); + &max_age, &include_subdomains, &hashes)); EXPECT_FALSE(ParseHPKPHeader("max-age=3488923 includesubdomain", - chain_hashes, &max_age, &hashes)); - EXPECT_FALSE(ParseHPKPHeader("max-age=34889.23", chain_hashes, &max_age, + chain_hashes, &max_age, &include_subdomains, &hashes)); + EXPECT_FALSE(ParseHPKPHeader("max-age=34889.23", chain_hashes, &max_age, + &include_subdomains, &hashes)); // Check the out args were not updated by checking the default // values for its predictable fields. @@ -310,9 +326,10 @@ TEST_F(HttpSecurityHeadersTest, ValidSTSHeaders) { EXPECT_TRUE(include_subdomains); } -static void TestValidPinsHeaders(HashValueTag tag) { +static void TestValidPKPHeaders(HashValueTag tag) { base::TimeDelta max_age; base::TimeDelta expect_max_age; + bool include_subdomains; HashValueVector hashes; HashValueVector chain_hashes; @@ -327,61 +344,78 @@ static void TestValidPinsHeaders(HashValueTag tag) { EXPECT_TRUE(ParseHPKPHeader( "max-age=243; " + good_pin + ";" + backup_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(243); EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( " " + good_pin + "; " + backup_pin + " ; Max-agE = 567", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(567); EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( - good_pin + ";" + backup_pin + " ; mAx-aGe = 890 ", - chain_hashes, &max_age, &hashes)); + "includeSubDOMAINS;" + good_pin + ";" + backup_pin + + " ; mAx-aGe = 890 ", + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(890); EXPECT_EQ(expect_max_age, max_age); + EXPECT_TRUE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( good_pin + ";" + backup_pin + "; max-age=123;IGNORED;", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(123); EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( "max-age=394082;" + backup_pin + ";" + good_pin + "; ", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(394082); EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( "max-age=39408299 ;" + backup_pin + ";" + good_pin + "; ", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds( std::min(kMaxHSTSAgeSecs, static_cast<int64>(GG_INT64_C(39408299)))); EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( - "max-age=39408038 ; cybers=39408038 ; " + + "max-age=39408038 ; cybers=39408038 ; includeSubdomains; " + good_pin + ";" + backup_pin + "; ", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds( std::min(kMaxHSTSAgeSecs, static_cast<int64>(GG_INT64_C(394082038)))); EXPECT_EQ(expect_max_age, max_age); + EXPECT_TRUE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( " max-age=0 ; " + good_pin + ";" + backup_pin, - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); + expect_max_age = base::TimeDelta::FromSeconds(0); + EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); + + EXPECT_TRUE(ParseHPKPHeader( + " max-age=0 ; includeSubdomains; " + good_pin + ";" + backup_pin, + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(0); EXPECT_EQ(expect_max_age, max_age); + EXPECT_TRUE(include_subdomains); EXPECT_TRUE(ParseHPKPHeader( " max-age=999999999999999999999999999999999999999999999 ; " + backup_pin + ";" + good_pin + "; ", - chain_hashes, &max_age, &hashes)); + chain_hashes, &max_age, &include_subdomains, &hashes)); expect_max_age = base::TimeDelta::FromSeconds(kMaxHSTSAgeSecs); EXPECT_EQ(expect_max_age, max_age); + EXPECT_FALSE(include_subdomains); } TEST_F(HttpSecurityHeadersTest, BogusPinsHeadersSHA1) { @@ -392,12 +426,80 @@ TEST_F(HttpSecurityHeadersTest, BogusPinsHeadersSHA256) { TestBogusPinsHeaders(HASH_VALUE_SHA256); } -TEST_F(HttpSecurityHeadersTest, ValidPinsHeadersSHA1) { - TestValidPinsHeaders(HASH_VALUE_SHA1); +TEST_F(HttpSecurityHeadersTest, ValidPKPHeadersSHA1) { + TestValidPKPHeaders(HASH_VALUE_SHA1); +} + +TEST_F(HttpSecurityHeadersTest, ValidPKPHeadersSHA256) { + TestValidPKPHeaders(HASH_VALUE_SHA256); } -TEST_F(HttpSecurityHeadersTest, ValidPinsHeadersSHA256) { - TestValidPinsHeaders(HASH_VALUE_SHA256); +TEST_F(HttpSecurityHeadersTest, UpdateDynamicPKPOnly) { + TransportSecurityState state; + TransportSecurityState::DomainState domain_state; + + // docs.google.com has preloaded pins. + std::string domain = "docs.google.com"; + EXPECT_TRUE(state.GetDomainState(domain, true, &domain_state)); + EXPECT_GT(domain_state.static_spki_hashes.size(), 1UL); + HashValueVector saved_hashes = domain_state.static_spki_hashes; + + // Add a header, which should only update the dynamic state. + HashValue good_hash = GetTestHashValue(1, HASH_VALUE_SHA1); + HashValue backup_hash = GetTestHashValue(2, HASH_VALUE_SHA1); + std::string good_pin = GetTestPin(1, HASH_VALUE_SHA1); + std::string backup_pin = GetTestPin(2, HASH_VALUE_SHA1); + std::string header = "max-age = 10000; " + good_pin + "; " + backup_pin; + + // Construct a fake SSLInfo that will pass AddHPKPHeader's checks. + SSLInfo ssl_info; + ssl_info.public_key_hashes.push_back(good_hash); + ssl_info.public_key_hashes.push_back(saved_hashes[0]); + EXPECT_TRUE(state.AddHPKPHeader(domain, header, ssl_info)); + + // Expect the preloaded state to remain unchanged. + std::string canonicalized_host = TransportSecurityState::CanonicalizeHost( + domain); + TransportSecurityState::DomainState static_domain_state; + EXPECT_TRUE(state.GetStaticDomainState(canonicalized_host, + true, + &static_domain_state)); + for (size_t i = 0; i < saved_hashes.size(); ++i) { + EXPECT_TRUE(HashValuesEqual( + saved_hashes[i])(static_domain_state.static_spki_hashes[i])); + } + + // Expect the dynamic state to reflect the header. + TransportSecurityState::DomainState dynamic_domain_state; + EXPECT_TRUE(state.GetDynamicDomainState(domain, &dynamic_domain_state)); + EXPECT_EQ(2UL, dynamic_domain_state.dynamic_spki_hashes.size()); + + HashValueVector::const_iterator hash = std::find_if( + dynamic_domain_state.dynamic_spki_hashes.begin(), + dynamic_domain_state.dynamic_spki_hashes.end(), + HashValuesEqual(good_hash)); + EXPECT_NE(dynamic_domain_state.dynamic_spki_hashes.end(), hash); + + hash = std::find_if( + dynamic_domain_state.dynamic_spki_hashes.begin(), + dynamic_domain_state.dynamic_spki_hashes.end(), + HashValuesEqual(backup_hash)); + EXPECT_NE(dynamic_domain_state.dynamic_spki_hashes.end(), hash); + + // Expect the overall state to reflect the header, too. + EXPECT_TRUE(state.GetDomainState(domain, true, &domain_state)); + EXPECT_EQ(2UL, domain_state.dynamic_spki_hashes.size()); + + hash = std::find_if(domain_state.dynamic_spki_hashes.begin(), + domain_state.dynamic_spki_hashes.end(), + HashValuesEqual(good_hash)); + EXPECT_NE(domain_state.dynamic_spki_hashes.end(), hash); + + hash = std::find_if( + domain_state.dynamic_spki_hashes.begin(), + domain_state.dynamic_spki_hashes.end(), + HashValuesEqual(backup_hash)); + EXPECT_NE(domain_state.dynamic_spki_hashes.end(), hash); } }; // namespace net diff --git a/net/http/transport_security_state.cc b/net/http/transport_security_state.cc index 92a223c..f2282ed 100644 --- a/net/http/transport_security_state.cc +++ b/net/http/transport_security_state.cc @@ -109,17 +109,7 @@ void TransportSecurityState::EnableHost(const std::string& host, if (canonicalized_host.empty()) return; - DomainState existing_state; - - // Use the original creation date if we already have this host. (But note - // that statically-defined states have no |created| date. Therefore, we do - // not bother to search the SNI-only static states.) DomainState state_copy(state); - if (GetDomainState(host, false /* sni_enabled */, &existing_state) && - !existing_state.created.is_null()) { - state_copy.created = existing_state.created; - } - // No need to store this value since it is redundant. (|canonicalized_host| // is the map key.) state_copy.domain.clear(); @@ -158,6 +148,7 @@ bool TransportSecurityState::GetDomainState(const std::string& host, bool has_preload = GetStaticDomainState(canonicalized_host, sni_enabled, &state); std::string canonicalized_preload = CanonicalizeHost(state.domain); + GetDynamicDomainState(host, &state); base::Time current_time(base::Time::Now()); @@ -625,6 +616,7 @@ bool TransportSecurityState::AddHSTSHeader(const std::string& host, base::Time now = base::Time::Now(); base::TimeDelta max_age; TransportSecurityState::DomainState domain_state; + GetDynamicDomainState(host, &domain_state); if (ParseHSTSHeader(value, &max_age, &domain_state.sts_include_subdomains)) { // Handle max-age == 0 if (max_age.InSeconds() == 0) @@ -647,10 +639,11 @@ bool TransportSecurityState::AddHPKPHeader(const std::string& host, base::Time now = base::Time::Now(); base::TimeDelta max_age; TransportSecurityState::DomainState domain_state; + GetDynamicDomainState(host, &domain_state); if (ParseHPKPHeader(value, ssl_info.public_key_hashes, - &max_age, &domain_state.dynamic_spki_hashes)) { - // TODO(palmer): http://crbug.com/243865 handle max-age == 0 - // and includeSubdomains. + &max_age, &domain_state.pkp_include_subdomains, + &domain_state.dynamic_spki_hashes)) { + // TODO(palmer): http://crbug.com/243865 handle max-age == 0. domain_state.created = now; domain_state.dynamic_spki_hashes_expiry = now + max_age; EnableHost(host, domain_state); @@ -789,6 +782,50 @@ bool TransportSecurityState::GetStaticDomainState( return false; } +bool TransportSecurityState::GetDynamicDomainState(const std::string& host, + DomainState* result) { + DCHECK(CalledOnValidThread()); + + DomainState state; + const std::string canonicalized_host = CanonicalizeHost(host); + if (canonicalized_host.empty()) + return false; + + base::Time current_time(base::Time::Now()); + + for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) { + std::string host_sub_chunk(&canonicalized_host[i], + canonicalized_host.size() - i); + DomainStateMap::iterator j = + enabled_hosts_.find(HashHost(host_sub_chunk)); + if (j == enabled_hosts_.end()) + continue; + + if (current_time > j->second.upgrade_expiry && + current_time > j->second.dynamic_spki_hashes_expiry) { + enabled_hosts_.erase(j); + DirtyNotify(); + continue; + } + + state = j->second; + state.domain = DNSDomainToString(host_sub_chunk); + + // Succeed if we matched the domain exactly or if subdomain matches are + // allowed. + if (i == 0 || j->second.sts_include_subdomains || + j->second.pkp_include_subdomains) { + *result = state; + return true; + } + + return false; + } + + return false; +} + + void TransportSecurityState::AddOrUpdateEnabledHosts( const std::string& hashed_host, const DomainState& state) { DCHECK(CalledOnValidThread()); @@ -796,7 +833,7 @@ void TransportSecurityState::AddOrUpdateEnabledHosts( } TransportSecurityState::DomainState::DomainState() - : upgrade_mode(MODE_FORCE_HTTPS), + : upgrade_mode(MODE_DEFAULT), created(base::Time::Now()), sts_include_subdomains(false), pkp_include_subdomains(false) { diff --git a/net/http/transport_security_state.h b/net/http/transport_security_state.h index 60df04a..ccbc53a 100644 --- a/net/http/transport_security_state.h +++ b/net/http/transport_security_state.h @@ -136,7 +136,8 @@ class NET_EXPORT TransportSecurityState // The following members are not valid when stored in |enabled_hosts_|: // The domain which matched during a search for this DomainState entry. - // Updated by |GetDomainState| and |GetStaticDomainState|. + // Updated by |GetDomainState|, |GetDynamicDomainState|, and + // |GetStaticDomainState|. std::string domain; }; @@ -261,6 +262,8 @@ class NET_EXPORT TransportSecurityState private: friend class TransportSecurityStateTest; + FRIEND_TEST_ALL_PREFIXES(HttpSecurityHeadersTest, + UpdateDynamicPKPOnly); typedef std::map<std::string, DomainState> DomainStateMap; @@ -298,6 +301,20 @@ class NET_EXPORT TransportSecurityState bool sni_enabled, DomainState* result); + // Returns true and updates |*result| iff there is a dynamic DomainState for + // |host|. + // + // |GetDynamicDomainState| is identical to |GetDomainState| except that it + // searches only the dynamically-added transport security state, ignoring + // all statically-defined DomainStates. + // + // If |host| matches both an exact entry and is a subdomain of another + // entry, the exact match determines the return value. + // + // Note that this method is not const because it opportunistically removes + // entries that have expired. + bool GetDynamicDomainState(const std::string& host, DomainState* result); + // The set of hosts that have enabled TransportSecurity. DomainStateMap enabled_hosts_; diff --git a/net/url_request/url_request_unittest.cc b/net/url_request/url_request_unittest.cc index ece4f61..099c5fc 100644 --- a/net/url_request/url_request_unittest.cc +++ b/net/url_request/url_request_unittest.cc @@ -3933,6 +3933,53 @@ TEST_F(URLRequestTestHTTP, ProcessSTS) { domain_state.upgrade_mode); EXPECT_TRUE(domain_state.sts_include_subdomains); EXPECT_FALSE(domain_state.pkp_include_subdomains); +#if defined(OS_ANDROID) + // Android's CertVerifyProc does not (yet) handle pins. +#else + EXPECT_FALSE(domain_state.HasPublicKeyPins()); +#endif +} + +// Android's CertVerifyProc does not (yet) handle pins. Therefore, it will +// reject HPKP headers, and a test setting only HPKP headers will fail (no +// DomainState present because header rejected). +#if defined(OS_ANDROID) +#define MAYBE_ProcessPKP DISABLED_ProcessPKP +#else +#define MAYBE_ProcessPKP ProcessPKP +#endif + +// Tests that enabling HPKP on a domain does not affect the HSTS +// validity/expiration. +TEST_F(URLRequestTestHTTP, MAYBE_ProcessPKP) { + SpawnedTestServer::SSLOptions ssl_options; + SpawnedTestServer https_test_server( + SpawnedTestServer::TYPE_HTTPS, + ssl_options, + base::FilePath(FILE_PATH_LITERAL("net/data/url_request_unittest"))); + ASSERT_TRUE(https_test_server.Start()); + + TestDelegate d; + URLRequest request( + https_test_server.GetURL("files/hpkp-headers.html"), + &d, + &default_context_); + request.Start(); + base::MessageLoop::current()->Run(); + + TransportSecurityState* security_state = + default_context_.transport_security_state(); + bool sni_available = true; + TransportSecurityState::DomainState domain_state; + EXPECT_TRUE(security_state->GetDomainState( + SpawnedTestServer::kLocalhost, sni_available, &domain_state)); + EXPECT_EQ(TransportSecurityState::DomainState::MODE_DEFAULT, + domain_state.upgrade_mode); + EXPECT_FALSE(domain_state.sts_include_subdomains); + EXPECT_FALSE(domain_state.pkp_include_subdomains); + EXPECT_TRUE(domain_state.HasPublicKeyPins()); + EXPECT_NE(domain_state.upgrade_expiry, + domain_state.dynamic_spki_hashes_expiry); } TEST_F(URLRequestTestHTTP, ProcessSTSOnce) { @@ -4004,6 +4051,44 @@ TEST_F(URLRequestTestHTTP, ProcessSTSAndPKP) { EXPECT_FALSE(domain_state.pkp_include_subdomains); } +// Tests that when multiple HPKP headers are present, asserting different +// policies, that only the first such policy is processed. +TEST_F(URLRequestTestHTTP, ProcessSTSAndPKP2) { + SpawnedTestServer::SSLOptions ssl_options; + SpawnedTestServer https_test_server( + SpawnedTestServer::TYPE_HTTPS, + ssl_options, + base::FilePath(FILE_PATH_LITERAL("net/data/url_request_unittest"))); + ASSERT_TRUE(https_test_server.Start()); + + TestDelegate d; + URLRequest request( + https_test_server.GetURL("files/hsts-and-hpkp-headers2.html"), + &d, + &default_context_); + request.Start(); + base::MessageLoop::current()->Run(); + + TransportSecurityState* security_state = + default_context_.transport_security_state(); + bool sni_available = true; + TransportSecurityState::DomainState domain_state; + EXPECT_TRUE(security_state->GetDomainState( + SpawnedTestServer::kLocalhost, sni_available, &domain_state)); + EXPECT_EQ(TransportSecurityState::DomainState::MODE_FORCE_HTTPS, + domain_state.upgrade_mode); +#if defined(OS_ANDROID) + // Android's CertVerifyProc does not (yet) handle pins. +#else + EXPECT_TRUE(domain_state.HasPublicKeyPins()); +#endif + EXPECT_NE(domain_state.upgrade_expiry, + domain_state.dynamic_spki_hashes_expiry); + + EXPECT_TRUE(domain_state.sts_include_subdomains); + EXPECT_FALSE(domain_state.pkp_include_subdomains); +} + TEST_F(URLRequestTestHTTP, ContentTypeNormalizationTest) { ASSERT_TRUE(test_server_.Start()); |