diff options
Diffstat (limited to 'chromium/net/http')
-rw-r--r-- | chromium/net/http/transport_security_state.cc | 78 | ||||
-rw-r--r-- | chromium/net/http/transport_security_state.h | 3 | ||||
-rw-r--r-- | chromium/net/http/transport_security_state_static.json.gz | bin | 1453307 -> 1453307 bytes | |||
-rw-r--r-- | chromium/net/http/transport_security_state_unittest.cc | 229 |
4 files changed, 284 insertions, 26 deletions
diff --git a/chromium/net/http/transport_security_state.cc b/chromium/net/http/transport_security_state.cc index deb1e3b944f..1133374c147 100644 --- a/chromium/net/http/transport_security_state.cc +++ b/chromium/net/http/transport_security_state.cc @@ -637,8 +637,7 @@ void TransportSecurityState::UpdatePinList( base::Time update_time) { pinsets_ = pinsets; key_pins_list_last_update_time_ = update_time; - host_pins_ = absl::make_optional( - std::map<std::string, std::pair<PinSet const*, bool>>()); + host_pins_.emplace(); std::map<std::string, PinSet const*> pinset_names_map; for (const auto& pinset : pinsets_) { pinset_names_map[pinset.name()] = &pinset; @@ -1182,32 +1181,61 @@ bool TransportSecurityState::GetStaticPKPState(const std::string& host, PreloadResult result; if (host_pins_.has_value()) { - auto iter = host_pins_->find(host); - if (iter != host_pins_->end()) { - pkp_result->domain = host; - pkp_result->last_observed = key_pins_list_last_update_time_; - pkp_result->include_subdomains = iter->second.second; - const PinSet* pinset = iter->second.first; - if (!pinset->report_uri().empty()) { - pkp_result->report_uri = GURL(pinset->report_uri()); - } - for (auto hash : pinset->static_spki_hashes()) { - // If the update is malformed, it's preferable to skip the hash than - // crash. - if (hash.size() == 32) { - AddHash(reinterpret_cast<const char*>(hash.data()), - &pkp_result->spki_hashes); + // Ensure that |host| is a valid hostname before processing. + if (CanonicalizeHost(host).empty()) { + return false; + } + // Normalize any trailing '.' used for DNS suffix searches. + std::string normalized_host = host; + size_t trailing_dot_found = normalized_host.find_last_not_of('.'); + if (trailing_dot_found == std::string::npos) { + // Hostname is either empty or all dots + return false; + } + normalized_host.erase(trailing_dot_found + 1); + normalized_host = base::ToLowerASCII(normalized_host); + + base::StringPiece search_hostname = normalized_host; + while (true) { + auto iter = host_pins_->find(search_hostname); + // Only consider this a match if either include_subdomains is set, or + // this is an exact match of the full hostname. + if (iter != host_pins_->end() && + (iter->second.second || search_hostname == normalized_host)) { + pkp_result->domain = std::string(search_hostname); + pkp_result->last_observed = key_pins_list_last_update_time_; + pkp_result->include_subdomains = iter->second.second; + const PinSet* pinset = iter->second.first; + if (!pinset->report_uri().empty()) { + pkp_result->report_uri = GURL(pinset->report_uri()); } - } - for (auto hash : pinset->bad_static_spki_hashes()) { - // If the update is malformed, it's preferable to skip the hash than - // crash. - if (hash.size() == 32) { - AddHash(reinterpret_cast<const char*>(hash.data()), - &pkp_result->bad_spki_hashes); + for (auto hash : pinset->static_spki_hashes()) { + // If the update is malformed, it's preferable to skip the hash than + // crash. + if (hash.size() == 32) { + AddHash(reinterpret_cast<const char*>(hash.data()), + &pkp_result->spki_hashes); + } } + for (auto hash : pinset->bad_static_spki_hashes()) { + // If the update is malformed, it's preferable to skip the hash than + // crash. + if (hash.size() == 32) { + AddHash(reinterpret_cast<const char*>(hash.data()), + &pkp_result->bad_spki_hashes); + } + } + return true; } - return true; + auto dot_pos = search_hostname.find("."); + if (dot_pos == std::string::npos) { + // If this was not a match, and there are no more dots in the string, + // there are no more domains to try. + return false; + } + // Try again in case this is a subdomain of a pinned domain that includes + // subdomains. + search_hostname = search_hostname.substr(dot_pos + 1); } } else if (DecodeHSTSPreload(host, &result) && result.has_pins) { if (result.pinset_id >= g_hsts_source->pinsets_count) diff --git a/chromium/net/http/transport_security_state.h b/chromium/net/http/transport_security_state.h index 464cbd182e1..c7a88437b8b 100644 --- a/chromium/net/http/transport_security_state.h +++ b/chromium/net/http/transport_security_state.h @@ -815,7 +815,8 @@ class NET_EXPORT TransportSecurityState { // The values in host_pins_ maps are references to PinSet objects in the // pinsets_ vector. - absl::optional<std::map<std::string, std::pair<const PinSet*, bool>>> + absl::optional< + std::map<std::string, std::pair<const PinSet*, bool>, std::less<>>> host_pins_; base::Time key_pins_list_last_update_time_; std::vector<PinSet> pinsets_; diff --git a/chromium/net/http/transport_security_state_static.json.gz b/chromium/net/http/transport_security_state_static.json.gz Binary files differindex 59c42c6a5aa..76c25ba5802 100644 --- a/chromium/net/http/transport_security_state_static.json.gz +++ b/chromium/net/http/transport_security_state_static.json.gz diff --git a/chromium/net/http/transport_security_state_unittest.cc b/chromium/net/http/transport_security_state_unittest.cc index 905bbb70ba5..a54ac648eb4 100644 --- a/chromium/net/http/transport_security_state_unittest.cc +++ b/chromium/net/http/transport_security_state_unittest.cc @@ -4053,6 +4053,23 @@ TEST_F(TransportSecurityStateTest, UpdateKeyPinsListNotValidPin) { host_port_pair, true, good_hashes, cert1.get(), cert2.get(), TransportSecurityState::ENABLE_PIN_REPORTS, network_anonymization_key, &unused_failure_log)); + + // Hashes should also be rejected if the hostname has a trailing dot. + host_port_pair = HostPortPair("example.test.", kPort); + EXPECT_EQ(TransportSecurityState::PKPStatus::VIOLATED, + state.CheckPublicKeyPins( + host_port_pair, true, good_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); + + // Hashes should also be rejected if the hostname has different + // capitalization. + host_port_pair = HostPortPair("ExAmpLe.tEsT", kPort); + EXPECT_EQ(TransportSecurityState::PKPStatus::VIOLATED, + state.CheckPublicKeyPins( + host_port_pair, true, good_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); } TEST_F(TransportSecurityStateTest, UpdateKeyPinsEmptyList) { @@ -4099,6 +4116,218 @@ TEST_F(TransportSecurityStateTest, UpdateKeyPinsEmptyList) { network_anonymization_key, &unused_failure_log)); } +TEST_F(TransportSecurityStateTest, UpdateKeyPinsIncludeSubdomains) { + base::test::ScopedFeatureList scoped_feature_list_; + scoped_feature_list_.InitAndEnableFeature( + net::features::kStaticKeyPinningEnforcement); + HostPortPair host_port_pair("example.sub.test", kPort); + GURL report_uri(kReportUri); + NetworkAnonymizationKey network_anonymization_key = + NetworkAnonymizationKey::CreateTransient(); + // Two dummy certs to use as the server-sent and validated chains. The + // contents don't matter. + scoped_refptr<X509Certificate> cert1 = + ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem"); + ASSERT_TRUE(cert1); + scoped_refptr<X509Certificate> cert2 = + ImportCertFromFile(GetTestCertsDirectory(), "expired_cert.pem"); + ASSERT_TRUE(cert2); + + // unpinned_hashes is a set of hashes that (after the update) won't match the + // expected hashes for the tld of this domain. kGoodPath is used here because + // it's a path that is accepted prior to any updates, and this test will + // validate it is rejected afterwards. + HashValueVector unpinned_hashes; + for (size_t i = 0; kGoodPath[i]; i++) { + EXPECT_TRUE(AddHash(kGoodPath[i], &unpinned_hashes)); + } + + TransportSecurityState state; + EnableStaticPins(&state); + std::string unused_failure_log; + + // Prior to updating the list, unpinned_hashes should be accepted + EXPECT_EQ(TransportSecurityState::PKPStatus::OK, + state.CheckPublicKeyPins( + host_port_pair, true, unpinned_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); + + // Update the pins list, adding kBadPath to the accepted hashes for this + // host, relying on include_subdomains for enforcement. The contents of the + // hashes don't matter as long as they are different from unpinned_hashes, + // kBadPath is used for convenience. + std::vector<std::vector<uint8_t>> accepted_hashes; + for (size_t i = 0; kBadPath[i]; i++) { + HashValue hash; + ASSERT_TRUE(hash.FromString(kBadPath[i])); + accepted_hashes.emplace_back(hash.data(), hash.data() + hash.size()); + } + TransportSecurityState::PinSet test_pinset( + /*name=*/"test", + /*static_spki_hashes=*/{accepted_hashes}, + /*bad_static_spki_hashes=*/{}, + /*report_uri=*/kReportUri); + // The host used in the test is "example.sub.test", so this pinset will only + // match due to include subdomains. + TransportSecurityState::PinSetInfo test_pinsetinfo( + /*hostname=*/"sub.test", /* pinset_name=*/"test", + /*include_subdomains=*/true); + state.UpdatePinList({test_pinset}, {test_pinsetinfo}, base::Time::Now()); + + // The path that was accepted before updating the pins should now be rejected. + EXPECT_EQ(TransportSecurityState::PKPStatus::VIOLATED, + state.CheckPublicKeyPins( + host_port_pair, true, unpinned_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); +} + +TEST_F(TransportSecurityStateTest, UpdateKeyPinsIncludeSubdomainsTLD) { + base::test::ScopedFeatureList scoped_feature_list_; + scoped_feature_list_.InitAndEnableFeature( + net::features::kStaticKeyPinningEnforcement); + HostPortPair host_port_pair(kHost, kPort); + GURL report_uri(kReportUri); + NetworkAnonymizationKey network_anonymization_key = + NetworkAnonymizationKey::CreateTransient(); + // Two dummy certs to use as the server-sent and validated chains. The + // contents don't matter. + scoped_refptr<X509Certificate> cert1 = + ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem"); + ASSERT_TRUE(cert1); + scoped_refptr<X509Certificate> cert2 = + ImportCertFromFile(GetTestCertsDirectory(), "expired_cert.pem"); + ASSERT_TRUE(cert2); + + // unpinned_hashes is a set of hashes that (after the update) won't match the + // expected hashes for the tld of this domain. kGoodPath is used here because + // it's a path that is accepted prior to any updates, and this test will + // validate it is rejected afterwards. + HashValueVector unpinned_hashes; + for (size_t i = 0; kGoodPath[i]; i++) { + EXPECT_TRUE(AddHash(kGoodPath[i], &unpinned_hashes)); + } + + TransportSecurityState state; + EnableStaticPins(&state); + std::string unused_failure_log; + + // Prior to updating the list, unpinned_hashes should be accepted + EXPECT_EQ(TransportSecurityState::PKPStatus::OK, + state.CheckPublicKeyPins( + host_port_pair, true, unpinned_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); + + // Update the pins list, adding kBadPath to the accepted hashes for this + // host, relying on include_subdomains for enforcement. The contents of the + // hashes don't matter as long as they are different from unpinned_hashes, + // kBadPath is used for convenience. + std::vector<std::vector<uint8_t>> accepted_hashes; + for (size_t i = 0; kBadPath[i]; i++) { + HashValue hash; + ASSERT_TRUE(hash.FromString(kBadPath[i])); + accepted_hashes.emplace_back(hash.data(), hash.data() + hash.size()); + } + TransportSecurityState::PinSet test_pinset( + /*name=*/"test", + /*static_spki_hashes=*/{accepted_hashes}, + /*bad_static_spki_hashes=*/{}, + /*report_uri=*/kReportUri); + // The host used in the test is "example.test", so this pinset will only match + // due to include subdomains. + TransportSecurityState::PinSetInfo test_pinsetinfo( + /*hostname=*/"test", /* pinset_name=*/"test", + /*include_subdomains=*/true); + state.UpdatePinList({test_pinset}, {test_pinsetinfo}, base::Time::Now()); + + // The path that was accepted before updating the pins should now be rejected. + EXPECT_EQ(TransportSecurityState::PKPStatus::VIOLATED, + state.CheckPublicKeyPins( + host_port_pair, true, unpinned_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); +} + +TEST_F(TransportSecurityStateTest, UpdateKeyPinsDontIncludeSubdomains) { + base::test::ScopedFeatureList scoped_feature_list_; + scoped_feature_list_.InitAndEnableFeature( + net::features::kStaticKeyPinningEnforcement); + HostPortPair host_port_pair(kHost, kPort); + GURL report_uri(kReportUri); + NetworkAnonymizationKey network_anonymization_key = + NetworkAnonymizationKey::CreateTransient(); + // Two dummy certs to use as the server-sent and validated chains. The + // contents don't matter. + scoped_refptr<X509Certificate> cert1 = + ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem"); + ASSERT_TRUE(cert1); + scoped_refptr<X509Certificate> cert2 = + ImportCertFromFile(GetTestCertsDirectory(), "expired_cert.pem"); + ASSERT_TRUE(cert2); + + // unpinned_hashes is a set of hashes that (after the update) won't match the + // expected hashes for the tld of this domain. kGoodPath is used here because + // it's a path that is accepted prior to any updates, and this test will + // validate it is accepted or rejected afterwards depending on whether the + // domain is an exact match. + HashValueVector unpinned_hashes; + for (size_t i = 0; kGoodPath[i]; i++) { + EXPECT_TRUE(AddHash(kGoodPath[i], &unpinned_hashes)); + } + + TransportSecurityState state; + EnableStaticPins(&state); + std::string unused_failure_log; + + // Prior to updating the list, unpinned_hashes should be accepted + EXPECT_EQ(TransportSecurityState::PKPStatus::OK, + state.CheckPublicKeyPins( + host_port_pair, true, unpinned_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); + + // Update the pins list, adding kBadPath to the accepted hashes for the + // tld of this host, but without include_subdomains set. The contents of the + // hashes don't matter as long as they are different from unpinned_hashes, + // kBadPath is used for convenience. + std::vector<std::vector<uint8_t>> accepted_hashes; + for (size_t i = 0; kBadPath[i]; i++) { + HashValue hash; + ASSERT_TRUE(hash.FromString(kBadPath[i])); + accepted_hashes.emplace_back(hash.data(), hash.data() + hash.size()); + } + TransportSecurityState::PinSet test_pinset( + /*name=*/"test", + /*static_spki_hashes=*/{accepted_hashes}, + /*bad_static_spki_hashes=*/{}, + /*report_uri=*/kReportUri); + // The host used in the test is "example.test", so this pinset will not match + // due to include subdomains not being set. + TransportSecurityState::PinSetInfo test_pinsetinfo( + /*hostname=*/"test", /* pinset_name=*/"test", + /*include_subdomains=*/false); + state.UpdatePinList({test_pinset}, {test_pinsetinfo}, base::Time::Now()); + + // Hashes that were accepted before the update should still be accepted since + // include subdomains is not set for the pinset, and this is not an exact + // match. + EXPECT_EQ(TransportSecurityState::PKPStatus::OK, + state.CheckPublicKeyPins( + host_port_pair, true, unpinned_hashes, cert1.get(), cert2.get(), + TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); + + // Hashes should be rejected for an exact match of the hostname. + HostPortPair exact_match_host("test", kPort); + EXPECT_EQ(TransportSecurityState::PKPStatus::VIOLATED, + state.CheckPublicKeyPins( + exact_match_host, true, unpinned_hashes, cert1.get(), + cert2.get(), TransportSecurityState::ENABLE_PIN_REPORTS, + network_anonymization_key, &unused_failure_log)); +} + TEST_F(TransportSecurityStateTest, UpdateKeyPinsListTimestamp) { base::test::ScopedFeatureList scoped_feature_list_; scoped_feature_list_.InitAndEnableFeature( |