| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/http/transport_security_state.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <memory> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/build_time.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/time/time_to_iso8601.h" |
| #include "base/values.h" |
| #include "build/branding_buildflags.h" |
| #include "build/build_config.h" |
| #include "crypto/sha2.h" |
| #include "net/base/features.h" |
| #include "net/base/hash_value.h" |
| #include "net/base/host_port_pair.h" |
| #include "net/cert/ct_policy_status.h" |
| #include "net/cert/symantec_certs.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/dns/dns_names_util.h" |
| #include "net/extras/preload_data/decoder.h" |
| #include "net/http/http_security_headers.h" |
| #include "net/net_buildflags.h" |
| #include "net/ssl/ssl_info.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| #include "net/http/transport_security_state_ct_policies.inc" |
| |
| #if BUILDFLAG(INCLUDE_TRANSPORT_SECURITY_STATE_PRELOAD_LIST) |
| #include "net/http/transport_security_state_static.h" // nogncheck |
| // Points to the active transport security state source. |
| const TransportSecurityStateSource* const kDefaultHSTSSource = &kHSTSSource; |
| #else |
| const TransportSecurityStateSource* const kDefaultHSTSSource = nullptr; |
| #endif |
| |
| const TransportSecurityStateSource* g_hsts_source = kDefaultHSTSSource; |
| |
| // Parameters for remembering sent HPKP reports. |
| const size_t kMaxReportCacheEntries = 50; |
| const int kTimeToRememberReportsMins = 60; |
| const size_t kReportCacheKeyLength = 16; |
| |
| // Override for CheckCTRequirements() for unit tests. Possible values: |
| // false: Use the default implementation (e.g. production) |
| // true: Unless a delegate says otherwise, require CT. |
| bool g_ct_required_for_testing = false; |
| |
| base::Value GetPEMEncodedChainAsList(const net::X509Certificate* cert_chain) { |
| if (!cert_chain) |
| return base::Value(base::Value::Type::LIST); |
| |
| base::Value::List result; |
| std::vector<std::string> pem_encoded_chain; |
| cert_chain->GetPEMEncodedChain(&pem_encoded_chain); |
| for (const std::string& cert : pem_encoded_chain) |
| result.Append(cert); |
| |
| return base::Value(std::move(result)); |
| } |
| |
| bool HashReportForCache(const base::Value::Dict& 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, |
| std::string* cache_key) { |
| if (pkp_state.report_uri.is_empty()) |
| return false; |
| |
| base::Value::Dict report; |
| base::Time now = base::Time::Now(); |
| report.Set("hostname", host_port_pair.host()); |
| report.Set("port", host_port_pair.port()); |
| report.Set("include-subdomains", pkp_state.include_subdomains); |
| report.Set("noted-hostname", pkp_state.domain); |
| |
| auto served_certificate_chain_list = |
| GetPEMEncodedChainAsList(served_certificate_chain); |
| auto validated_certificate_chain_list = |
| GetPEMEncodedChainAsList(validated_certificate_chain); |
| report.Set("served-certificate-chain", |
| std::move(served_certificate_chain_list)); |
| report.Set("validated-certificate-chain", |
| std::move(validated_certificate_chain_list)); |
| |
| base::Value::List known_pin_list; |
| for (const auto& hash_value : pkp_state.spki_hashes) { |
| std::string known_pin; |
| |
| switch (hash_value.tag()) { |
| case HASH_VALUE_SHA256: |
| known_pin += "pin-sha256="; |
| break; |
| default: |
| // Don't bother reporting about hash types we don't support. SHA-256 is |
| // the only standardized hash function for HPKP anyway. |
| continue; |
| } |
| |
| std::string base64_value; |
| base::Base64Encode( |
| base::StringPiece(reinterpret_cast<const char*>(hash_value.data()), |
| hash_value.size()), |
| &base64_value); |
| known_pin += "\"" + base64_value + "\""; |
| |
| known_pin_list.Append(known_pin); |
| } |
| |
| report.Set("known-pins", std::move(known_pin_list)); |
| |
| // 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.Set("date-time", base::TimeToISO8601(now)); |
| report.Set("effective-expiration-date", |
| base::TimeToISO8601(pkp_state.expiry)); |
| if (!base::JSONWriter::Write(report, serialized_report)) { |
| LOG(ERROR) << "Failed to serialize HPKP violation report."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Do not send a report over HTTPS to the same host that set the |
| // pin. Such report URIs will result in loops. (A.com has a pinning |
| // violation which results in a report being sent to A.com, which |
| // results in a pinning violation which results in a report being sent |
| // to A.com, etc.) |
| bool IsReportUriValidForHost(const GURL& report_uri, const std::string& host) { |
| return (report_uri.host_piece() != host || |
| !report_uri.SchemeIsCryptographic()); |
| } |
| |
| std::string HashesToBase64String(const HashValueVector& hashes) { |
| std::string str; |
| for (size_t i = 0; i != hashes.size(); ++i) { |
| if (i != 0) |
| str += ","; |
| str += hashes[i].ToString(); |
| } |
| return str; |
| } |
| |
| TransportSecurityState::HashedHost HashHost( |
| base::span<const uint8_t> canonicalized_host) { |
| return crypto::SHA256Hash(canonicalized_host); |
| } |
| |
| // Returns true if the intersection of |a| and |b| is not empty. If either |
| // |a| or |b| is empty, returns false. |
| bool HashesIntersect(const HashValueVector& a, const HashValueVector& b) { |
| for (const auto& hash : a) { |
| if (base::Contains(b, hash)) |
| return true; |
| } |
| return false; |
| } |
| |
| bool AddHash(const char* sha256_hash, HashValueVector* out) { |
| HashValue hash(HASH_VALUE_SHA256); |
| memcpy(hash.data(), sha256_hash, hash.size()); |
| out->push_back(hash); |
| return true; |
| } |
| |
| // Converts |hostname| from dotted form ("www.google.com") to the form |
| // used in DNS: "\x03www\x06google\x03com", lowercases that, and returns |
| // the result. |
| std::vector<uint8_t> CanonicalizeHost(const std::string& host) { |
| // We cannot perform the operations as detailed in the spec here as `host` |
| // has already undergone IDN processing before it reached us. Thus, we |
| // lowercase the input (probably redudnant since most input here has been |
| // lowercased through URL canonicalization) and check that there are no |
| // invalid characters in the host (via DNSDomainFromDot()). |
| std::string lowered_host = base::ToLowerASCII(host); |
| |
| absl::optional<std::vector<uint8_t>> new_host = |
| dns_names_util::DottedNameToNetwork( |
| lowered_host, |
| /*require_valid_internet_hostname=*/true); |
| if (!new_host.has_value()) { |
| // DNSDomainFromDot can fail if any label is > 63 bytes or if the whole |
| // name is >255 bytes. However, search terms can have those properties. |
| return std::vector<uint8_t>(); |
| } |
| |
| return new_host.value(); |
| } |
| |
| // PreloadResult is the result of resolving a specific name in the preloaded |
| // data. |
| struct PreloadResult { |
| uint32_t pinset_id = 0; |
| // hostname_offset contains the number of bytes from the start of the given |
| // hostname where the name of the matching entry starts. |
| size_t hostname_offset = 0; |
| bool sts_include_subdomains = false; |
| bool pkp_include_subdomains = false; |
| bool force_https = false; |
| bool has_pins = false; |
| }; |
| |
| using net::extras::PreloadDecoder; |
| |
| // Extracts the current PreloadResult entry from the given Huffman encoded trie. |
| // If an "end of string" matches a period in the hostname then the information |
| // is remembered because, if no more specific node is found, then that |
| // information applies to the hostname. |
| class HSTSPreloadDecoder : public net::extras::PreloadDecoder { |
| public: |
| using net::extras::PreloadDecoder::PreloadDecoder; |
| |
| // net::extras::PreloadDecoder: |
| bool ReadEntry(net::extras::PreloadDecoder::BitReader* reader, |
| const std::string& search, |
| size_t current_search_offset, |
| bool* out_found) override { |
| bool is_simple_entry; |
| if (!reader->Next(&is_simple_entry)) { |
| return false; |
| } |
| PreloadResult tmp; |
| // Simple entries only configure HSTS with IncludeSubdomains and use a |
| // compact serialization format where the other policy flags are |
| // omitted. The omitted flags are assumed to be 0 and the associated |
| // policies are disabled. |
| if (is_simple_entry) { |
| tmp.force_https = true; |
| tmp.sts_include_subdomains = true; |
| } else { |
| if (!reader->Next(&tmp.sts_include_subdomains) || |
| !reader->Next(&tmp.force_https) || !reader->Next(&tmp.has_pins)) { |
| return false; |
| } |
| |
| tmp.pkp_include_subdomains = tmp.sts_include_subdomains; |
| |
| if (tmp.has_pins) { |
| if (!reader->Read(4, &tmp.pinset_id) || |
| (!tmp.sts_include_subdomains && |
| !reader->Next(&tmp.pkp_include_subdomains))) { |
| return false; |
| } |
| } |
| } |
| |
| tmp.hostname_offset = current_search_offset; |
| |
| if (current_search_offset == 0 || |
| search[current_search_offset - 1] == '.') { |
| *out_found = tmp.sts_include_subdomains || tmp.pkp_include_subdomains; |
| |
| result_ = tmp; |
| |
| if (current_search_offset > 0) { |
| result_.force_https &= tmp.sts_include_subdomains; |
| } else { |
| *out_found = true; |
| return true; |
| } |
| } |
| return true; |
| } |
| |
| PreloadResult result() const { return result_; } |
| |
| private: |
| PreloadResult result_; |
| }; |
| |
| bool DecodeHSTSPreload(const std::string& search_hostname, PreloadResult* out) { |
| #if !BUILDFLAG(INCLUDE_TRANSPORT_SECURITY_STATE_PRELOAD_LIST) |
| if (g_hsts_source == nullptr) |
| return false; |
| #endif |
| bool found = false; |
| |
| // Ensure that |search_hostname| is a valid hostname before |
| // processing. |
| if (CanonicalizeHost(search_hostname).empty()) { |
| return false; |
| } |
| // Normalize any trailing '.' used for DNS suffix searches. |
| std::string hostname = search_hostname; |
| size_t trailing_dot_found = hostname.find_last_not_of('.'); |
| if (trailing_dot_found != std::string::npos) { |
| hostname.erase(trailing_dot_found + 1); |
| } else { |
| hostname.clear(); |
| } |
| |
| // |hostname| has already undergone IDN conversion, so should be |
| // entirely A-Labels. The preload data is entirely normalized to |
| // lower case. |
| hostname = base::ToLowerASCII(hostname); |
| if (hostname.empty()) { |
| return false; |
| } |
| |
| HSTSPreloadDecoder decoder( |
| g_hsts_source->huffman_tree, g_hsts_source->huffman_tree_size, |
| g_hsts_source->preloaded_data, g_hsts_source->preloaded_bits, |
| g_hsts_source->root_position); |
| if (!decoder.Decode(hostname, &found)) { |
| DCHECK(false) << "Internal error in DecodeHSTSPreload for hostname " |
| << hostname; |
| return false; |
| } |
| if (found) |
| *out = decoder.result(); |
| return found; |
| } |
| |
| } // namespace |
| |
| // static |
| BASE_FEATURE(kCertificateTransparencyEnforcement, |
| "CertificateTransparencyEnforcement", |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| void SetTransportSecurityStateSourceForTesting( |
| const TransportSecurityStateSource* source) { |
| g_hsts_source = source ? source : kDefaultHSTSSource; |
| } |
| |
| TransportSecurityState::TransportSecurityState() |
| : TransportSecurityState(std::vector<std::string>()) {} |
| |
| TransportSecurityState::TransportSecurityState( |
| std::vector<std::string> hsts_host_bypass_list) |
| : sent_hpkp_reports_cache_(kMaxReportCacheEntries) { |
| // 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 !BUILDFLAG(GOOGLE_CHROME_BRANDING) || BUILDFLAG(IS_IOS) |
| enable_static_pins_ = false; |
| #endif |
| // Check that there no invalid entries in the static HSTS bypass list. |
| for (auto& host : hsts_host_bypass_list) { |
| DCHECK(host.find('.') == std::string::npos); |
| hsts_host_bypass_list_.insert(host); |
| } |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| // Both HSTS and HPKP cause fatal SSL errors, so return true if a |
| // host has either. |
| bool TransportSecurityState::ShouldSSLErrorsBeFatal(const std::string& host) { |
| STSState unused_sts; |
| PKPState unused_pkp; |
| return GetSTSState(host, &unused_sts) || GetPKPState(host, &unused_pkp); |
| } |
| |
| base::Value::Dict TransportSecurityState::NetLogUpgradeToSSLParam( |
| const std::string& host) { |
| STSState sts_state; |
| base::Value::Dict dict; |
| dict.Set("host", host); |
| dict.Set("get_sts_state_result", GetSTSState(host, &sts_state)); |
| dict.Set("should_upgrade_to_ssl", sts_state.ShouldUpgradeToSSL()); |
| dict.Set("host_found_in_hsts_bypass_list", |
| hsts_host_bypass_list_.find(host) != hsts_host_bypass_list_.end()); |
| return dict; |
| } |
| |
| bool TransportSecurityState::ShouldUpgradeToSSL( |
| const std::string& host, |
| const NetLogWithSource& net_log) { |
| STSState sts_state; |
| net_log.AddEvent( |
| NetLogEventType::TRANSPORT_SECURITY_STATE_SHOULD_UPGRADE_TO_SSL, |
| [&] { return NetLogUpgradeToSSLParam(host); }); |
| return GetSTSState(host, &sts_state) && sts_state.ShouldUpgradeToSSL(); |
| } |
| |
| TransportSecurityState::PKPStatus TransportSecurityState::CheckPublicKeyPins( |
| const HostPortPair& host_port_pair, |
| bool is_issued_by_known_root, |
| const HashValueVector& public_key_hashes, |
| const X509Certificate* served_certificate_chain, |
| const X509Certificate* validated_certificate_chain, |
| const PublicKeyPinReportStatus report_status, |
| const NetworkAnonymizationKey& network_anonymization_key, |
| std::string* pinning_failure_log) { |
| // Perform pin validation only if the server actually has public key pins. |
| if (!HasPublicKeyPins(host_port_pair.host())) { |
| return PKPStatus::OK; |
| } |
| |
| PKPStatus pin_validity = CheckPublicKeyPinsImpl( |
| host_port_pair, is_issued_by_known_root, public_key_hashes, |
| served_certificate_chain, validated_certificate_chain, report_status, |
| network_anonymization_key, pinning_failure_log); |
| |
| // Don't track statistics when a local trust anchor would override the pinning |
| // anyway. |
| if (!is_issued_by_known_root) |
| return pin_validity; |
| |
| UMA_HISTOGRAM_BOOLEAN("Net.PublicKeyPinSuccess", |
| pin_validity == PKPStatus::OK); |
| return pin_validity; |
| } |
| |
| bool TransportSecurityState::HasPublicKeyPins(const std::string& host) { |
| PKPState pkp_state; |
| return GetPKPState(host, &pkp_state) && pkp_state.HasPublicKeyPins(); |
| } |
| |
| TransportSecurityState::CTRequirementsStatus |
| TransportSecurityState::CheckCTRequirements( |
| const net::HostPortPair& host_port_pair, |
| bool is_issued_by_known_root, |
| const HashValueVector& public_key_hashes, |
| const X509Certificate* validated_certificate_chain, |
| const X509Certificate* served_certificate_chain, |
| const SignedCertificateTimestampAndStatusList& |
| signed_certificate_timestamps, |
| ct::CTPolicyCompliance policy_compliance) { |
| using CTRequirementLevel = RequireCTDelegate::CTRequirementLevel; |
| std::string hostname = host_port_pair.host(); |
| |
| // If CT is emergency disabled, either through a component updater set flag or |
| // through the feature flag, we don't require CT for any host. |
| if (ct_emergency_disable_ || |
| !base::FeatureList::IsEnabled(kCertificateTransparencyEnforcement)) { |
| return CT_NOT_REQUIRED; |
| } |
| |
| // CT is not required if the certificate does not chain to a publicly |
| // trusted root certificate. Testing can override this, as certain tests |
| // rely on using a non-publicly-trusted root. |
| if (!is_issued_by_known_root && !g_ct_required_for_testing) |
| return CT_NOT_REQUIRED; |
| |
| // A connection is considered compliant if it has sufficient SCTs or if the |
| // build is outdated. Other statuses are not considered compliant; this |
| // includes COMPLIANCE_DETAILS_NOT_AVAILABLE because compliance must have been |
| // evaluated in order to determine that the connection is compliant. |
| bool complies = |
| (policy_compliance == |
| ct::CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS || |
| policy_compliance == ct::CTPolicyCompliance::CT_POLICY_BUILD_NOT_TIMELY); |
| |
| CTRequirementLevel ct_required = CTRequirementLevel::DEFAULT; |
| if (require_ct_delegate_) { |
| // Allow the delegate to override the CT requirement state. |
| ct_required = require_ct_delegate_->IsCTRequiredForHost( |
| hostname, validated_certificate_chain, public_key_hashes); |
| } |
| switch (ct_required) { |
| case CTRequirementLevel::REQUIRED: |
| return complies ? CT_REQUIREMENTS_MET : CT_REQUIREMENTS_NOT_MET; |
| case CTRequirementLevel::NOT_REQUIRED: |
| return CT_NOT_REQUIRED; |
| case CTRequirementLevel::DEFAULT: |
| break; |
| } |
| |
| const base::Time epoch = base::Time::UnixEpoch(); |
| const CTRequiredPolicies& ct_required_policies = GetCTRequiredPolicies(); |
| |
| bool found = false; |
| for (const auto& restricted_ca : ct_required_policies) { |
| if (!restricted_ca.effective_date.is_zero() && |
| (epoch + restricted_ca.effective_date > |
| validated_certificate_chain->valid_start())) { |
| // The candidate cert is not subject to the CT policy, because it |
| // was issued before the effective CT date. |
| continue; |
| } |
| |
| if (!IsAnySHA256HashInSortedArray( |
| public_key_hashes, |
| base::make_span(restricted_ca.roots, restricted_ca.roots_length))) { |
| // No match for this set of restricted roots. |
| continue; |
| } |
| |
| // Found a match, indicating this certificate is potentially |
| // restricted. Determine if any of the hashes are on the exclusion |
| // list as exempt from the CT requirement. |
| if (restricted_ca.exceptions && |
| IsAnySHA256HashInSortedArray( |
| public_key_hashes, |
| base::make_span(restricted_ca.exceptions, |
| restricted_ca.exceptions_length))) { |
| // Found an excluded sub-CA; CT is not required. |
| continue; |
| } |
| |
| // No exception found. This certificate must conform to the CT policy. The |
| // compliance state is treated as additive - it must comply with all |
| // stated policies. |
| found = true; |
| } |
| if (found || g_ct_required_for_testing) |
| return complies ? CT_REQUIREMENTS_MET : CT_REQUIREMENTS_NOT_MET; |
| |
| return CT_NOT_REQUIRED; |
| } |
| |
| void TransportSecurityState::SetDelegate( |
| TransportSecurityState::Delegate* delegate) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| delegate_ = delegate; |
| } |
| |
| void TransportSecurityState::SetReportSender( |
| TransportSecurityState::ReportSenderInterface* report_sender) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| report_sender_ = report_sender; |
| } |
| |
| void TransportSecurityState::SetRequireCTDelegate(RequireCTDelegate* delegate) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| require_ct_delegate_ = delegate; |
| } |
| |
| void TransportSecurityState::UpdatePinList( |
| const std::vector<PinSet>& pinsets, |
| const std::vector<PinSetInfo>& host_pins, |
| base::Time update_time) { |
| pinsets_ = pinsets; |
| key_pins_list_last_update_time_ = update_time; |
| host_pins_.emplace(); |
| std::map<std::string, PinSet const*> pinset_names_map; |
| for (const auto& pinset : pinsets_) { |
| pinset_names_map[pinset.name()] = &pinset; |
| } |
| for (const auto& pin : host_pins) { |
| if (!base::Contains(pinset_names_map, pin.pinset_name_)) { |
| // This should never happen, but if the component is bad and missing an |
| // entry, we will ignore that particular pin. |
| continue; |
| } |
| host_pins_.value()[pin.hostname_] = std::make_pair( |
| pinset_names_map[pin.pinset_name_], pin.include_subdomains_); |
| } |
| } |
| |
| void TransportSecurityState::AddHSTSInternal( |
| const std::string& host, |
| TransportSecurityState::STSState::UpgradeMode upgrade_mode, |
| const base::Time& expiry, |
| bool include_subdomains) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const std::vector<uint8_t> canonicalized_host = CanonicalizeHost(host); |
| if (canonicalized_host.empty()) |
| return; |
| |
| STSState sts_state; |
| // No need to store |sts_state.domain| since it is redundant. |
| // (|canonicalized_host| is the map key.) |
| sts_state.last_observed = base::Time::Now(); |
| sts_state.include_subdomains = include_subdomains; |
| sts_state.expiry = expiry; |
| sts_state.upgrade_mode = upgrade_mode; |
| |
| // Only store new state when HSTS is explicitly enabled. If it is |
| // disabled, remove the state from the enabled hosts. |
| if (sts_state.ShouldUpgradeToSSL()) { |
| enabled_sts_hosts_[HashHost(canonicalized_host)] = sts_state; |
| } else { |
| const HashedHost hashed_host = HashHost(canonicalized_host); |
| enabled_sts_hosts_.erase(hashed_host); |
| } |
| |
| DirtyNotify(); |
| } |
| |
| void TransportSecurityState::AddHPKPInternal(const std::string& host, |
| const base::Time& last_observed, |
| const base::Time& expiry, |
| bool include_subdomains, |
| const HashValueVector& hashes, |
| const GURL& report_uri) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const std::vector<uint8_t> canonicalized_host = CanonicalizeHost(host); |
| if (canonicalized_host.empty()) |
| return; |
| |
| PKPState pkp_state; |
| // No need to store |pkp_state.domain| since it is redundant. |
| // (|canonicalized_host| is the map key.) |
| pkp_state.last_observed = last_observed; |
| pkp_state.expiry = expiry; |
| pkp_state.include_subdomains = include_subdomains; |
| pkp_state.spki_hashes = hashes; |
| pkp_state.report_uri = report_uri; |
| |
| // Only store new state when HPKP is explicitly enabled. If it is |
| // disabled, remove the state from the enabled hosts. |
| if (pkp_state.HasPublicKeyPins()) { |
| enabled_pkp_hosts_[HashHost(canonicalized_host)] = pkp_state; |
| } else { |
| const HashedHost hashed_host = HashHost(canonicalized_host); |
| enabled_pkp_hosts_.erase(hashed_host); |
| } |
| |
| DirtyNotify(); |
| } |
| |
| void TransportSecurityState:: |
| SetEnablePublicKeyPinningBypassForLocalTrustAnchors(bool value) { |
| enable_pkp_bypass_for_local_trust_anchors_ = value; |
| } |
| |
| TransportSecurityState::PKPStatus |
| TransportSecurityState::CheckPinsAndMaybeSendReport( |
| const HostPortPair& host_port_pair, |
| bool is_issued_by_known_root, |
| const TransportSecurityState::PKPState& pkp_state, |
| const HashValueVector& hashes, |
| const X509Certificate* served_certificate_chain, |
| const X509Certificate* validated_certificate_chain, |
| const TransportSecurityState::PublicKeyPinReportStatus report_status, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| std::string* failure_log) { |
| if (pkp_state.CheckPublicKeyPins(hashes, failure_log)) |
| return PKPStatus::OK; |
| |
| // Don't report violations for certificates that chain to local roots. |
| if (!is_issued_by_known_root && enable_pkp_bypass_for_local_trust_anchors_) |
| return PKPStatus::BYPASSED; |
| |
| if (!report_sender_ || |
| report_status != TransportSecurityState::ENABLE_PIN_REPORTS || |
| pkp_state.report_uri.is_empty()) { |
| return PKPStatus::VIOLATED; |
| } |
| |
| 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 PKPStatus::VIOLATED; |
| |
| 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 PKPStatus::VIOLATED; |
| } |
| |
| // Limit the rate at which duplicate reports are sent to the same |
| // report URI. The same report will not be sent within |
| // |kTimeToRememberReportsMins|, 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_hpkp_reports_cache_.Get(report_cache_key, base::TimeTicks::Now())) |
| return PKPStatus::VIOLATED; |
| sent_hpkp_reports_cache_.Put( |
| report_cache_key, true, base::TimeTicks::Now(), |
| base::TimeTicks::Now() + base::Minutes(kTimeToRememberReportsMins)); |
| |
| report_sender_->Send(pkp_state.report_uri, "application/json; charset=utf-8", |
| serialized_report, network_anonymization_key, |
| base::OnceCallback<void()>(), |
| base::OnceCallback<void(const GURL&, int, int)>()); |
| return PKPStatus::VIOLATED; |
| } |
| |
| bool TransportSecurityState::DeleteDynamicDataForHost(const std::string& host) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| const std::vector<uint8_t> canonicalized_host = CanonicalizeHost(host); |
| if (canonicalized_host.empty()) |
| return false; |
| |
| const HashedHost hashed_host = HashHost(canonicalized_host); |
| bool deleted = false; |
| auto sts_interator = enabled_sts_hosts_.find(hashed_host); |
| if (sts_interator != enabled_sts_hosts_.end()) { |
| enabled_sts_hosts_.erase(sts_interator); |
| deleted = true; |
| } |
| |
| auto pkp_iterator = enabled_pkp_hosts_.find(hashed_host); |
| if (pkp_iterator != enabled_pkp_hosts_.end()) { |
| enabled_pkp_hosts_.erase(pkp_iterator); |
| deleted = true; |
| } |
| |
| if (deleted) |
| DirtyNotify(); |
| return deleted; |
| } |
| |
| void TransportSecurityState::ClearDynamicData() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| enabled_sts_hosts_.clear(); |
| enabled_pkp_hosts_.clear(); |
| } |
| |
| void TransportSecurityState::DeleteAllDynamicDataBetween( |
| base::Time start_time, |
| base::Time end_time, |
| base::OnceClosure callback) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| bool dirtied = false; |
| auto sts_iterator = enabled_sts_hosts_.begin(); |
| while (sts_iterator != enabled_sts_hosts_.end()) { |
| if (sts_iterator->second.last_observed >= start_time && |
| sts_iterator->second.last_observed < end_time) { |
| dirtied = true; |
| enabled_sts_hosts_.erase(sts_iterator++); |
| continue; |
| } |
| |
| ++sts_iterator; |
| } |
| |
| auto pkp_iterator = enabled_pkp_hosts_.begin(); |
| while (pkp_iterator != enabled_pkp_hosts_.end()) { |
| if (pkp_iterator->second.last_observed >= start_time && |
| pkp_iterator->second.last_observed < end_time) { |
| dirtied = true; |
| enabled_pkp_hosts_.erase(pkp_iterator++); |
| continue; |
| } |
| |
| ++pkp_iterator; |
| } |
| |
| if (dirtied && delegate_) |
| delegate_->WriteNow(this, std::move(callback)); |
| else |
| std::move(callback).Run(); |
| } |
| |
| TransportSecurityState::~TransportSecurityState() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| void TransportSecurityState::DirtyNotify() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| if (delegate_) |
| delegate_->StateIsDirty(this); |
| } |
| |
| bool TransportSecurityState::AddHSTSHeader(const std::string& host, |
| const std::string& value) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| base::Time now = base::Time::Now(); |
| base::TimeDelta max_age; |
| bool include_subdomains; |
| if (!ParseHSTSHeader(value, &max_age, &include_subdomains)) { |
| return false; |
| } |
| |
| // Handle max-age == 0. |
| STSState::UpgradeMode upgrade_mode; |
| if (max_age.InSeconds() == 0) { |
| upgrade_mode = STSState::MODE_DEFAULT; |
| } else { |
| upgrade_mode = STSState::MODE_FORCE_HTTPS; |
| } |
| |
| AddHSTSInternal(host, upgrade_mode, now + max_age, include_subdomains); |
| return true; |
| } |
| |
| void TransportSecurityState::AddHSTS(const std::string& host, |
| const base::Time& expiry, |
| bool include_subdomains) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| AddHSTSInternal(host, STSState::MODE_FORCE_HTTPS, expiry, include_subdomains); |
| } |
| |
| void TransportSecurityState::AddHPKP(const std::string& host, |
| const base::Time& expiry, |
| bool include_subdomains, |
| const HashValueVector& hashes, |
| const GURL& report_uri) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| AddHPKPInternal(host, base::Time::Now(), expiry, include_subdomains, hashes, |
| report_uri); |
| } |
| |
| // static |
| void TransportSecurityState::SetRequireCTForTesting(bool required) { |
| g_ct_required_for_testing = required; |
| } |
| |
| void TransportSecurityState::ClearReportCachesForTesting() { |
| sent_hpkp_reports_cache_.Clear(); |
| } |
| |
| size_t TransportSecurityState::num_sts_entries() const { |
| return enabled_sts_hosts_.size(); |
| } |
| |
| // static |
| bool TransportSecurityState::IsBuildTimely() { |
| const base::Time build_time = base::GetBuildTime(); |
| // We consider built-in information to be timely for 10 weeks. |
| return (base::Time::Now() - build_time).InDays() < 70 /* 10 weeks */; |
| } |
| |
| TransportSecurityState::PKPStatus |
| TransportSecurityState::CheckPublicKeyPinsImpl( |
| const HostPortPair& host_port_pair, |
| bool is_issued_by_known_root, |
| const HashValueVector& hashes, |
| const X509Certificate* served_certificate_chain, |
| const X509Certificate* validated_certificate_chain, |
| const PublicKeyPinReportStatus report_status, |
| const NetworkAnonymizationKey& network_anonymization_key, |
| std::string* failure_log) { |
| PKPState pkp_state; |
| bool found_state = GetPKPState(host_port_pair.host(), &pkp_state); |
| |
| // HasPublicKeyPins should have returned true in order for this method to have |
| // been called. |
| DCHECK(found_state); |
| return CheckPinsAndMaybeSendReport( |
| host_port_pair, is_issued_by_known_root, pkp_state, hashes, |
| served_certificate_chain, validated_certificate_chain, report_status, |
| network_anonymization_key, failure_log); |
| } |
| |
| bool TransportSecurityState::GetStaticSTSState(const std::string& host, |
| STSState* sts_result) const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| if (!IsBuildTimely()) |
| return false; |
| |
| PreloadResult result; |
| if (DecodeHSTSPreload(host, &result) && |
| hsts_host_bypass_list_.find(host) == hsts_host_bypass_list_.end() && |
| result.force_https) { |
| sts_result->domain = host.substr(result.hostname_offset); |
| sts_result->include_subdomains = result.sts_include_subdomains; |
| sts_result->last_observed = base::GetBuildTime(); |
| sts_result->upgrade_mode = STSState::MODE_FORCE_HTTPS; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool TransportSecurityState::GetStaticPKPState(const std::string& host, |
| PKPState* pkp_result) const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| if (!enable_static_pins_ || !IsStaticPKPListTimely() || |
| !base::FeatureList::IsEnabled(features::kStaticKeyPinningEnforcement)) { |
| return false; |
| } |
| |
| PreloadResult result; |
| if (host_pins_.has_value()) { |
| // 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->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; |
| } |
| 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) |
| return false; |
| |
| pkp_result->domain = host.substr(result.hostname_offset); |
| pkp_result->include_subdomains = result.pkp_include_subdomains; |
| pkp_result->last_observed = base::GetBuildTime(); |
| |
| const TransportSecurityStateSource::Pinset* pinset = |
| &g_hsts_source->pinsets[result.pinset_id]; |
| if (pinset->report_uri != kNoReportURI) |
| pkp_result->report_uri = GURL(pinset->report_uri); |
| |
| if (pinset->accepted_pins) { |
| const char* const* sha256_hash = pinset->accepted_pins; |
| while (*sha256_hash) { |
| AddHash(*sha256_hash, &pkp_result->spki_hashes); |
| sha256_hash++; |
| } |
| } |
| if (pinset->rejected_pins) { |
| const char* const* sha256_hash = pinset->rejected_pins; |
| while (*sha256_hash) { |
| AddHash(*sha256_hash, &pkp_result->bad_spki_hashes); |
| sha256_hash++; |
| } |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool TransportSecurityState::GetSTSState(const std::string& host, |
| STSState* result) { |
| return GetDynamicSTSState(host, result) || GetStaticSTSState(host, result); |
| } |
| |
| bool TransportSecurityState::GetPKPState(const std::string& host, |
| PKPState* result) { |
| return GetDynamicPKPState(host, result) || GetStaticPKPState(host, result); |
| } |
| |
| bool TransportSecurityState::GetDynamicSTSState(const std::string& host, |
| STSState* result) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| const std::vector<uint8_t> 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) { |
| base::span<const uint8_t> host_sub_chunk = |
| base::make_span(canonicalized_host).subspan(i); |
| auto j = enabled_sts_hosts_.find(HashHost(host_sub_chunk)); |
| if (j == enabled_sts_hosts_.end()) |
| continue; |
| |
| // If the entry is invalid, drop it. |
| if (current_time > j->second.expiry) { |
| enabled_sts_hosts_.erase(j); |
| DirtyNotify(); |
| continue; |
| } |
| |
| // An entry matches if it is either an exact match, or if it is a prefix |
| // match and the includeSubDomains directive was included. |
| if (i == 0 || j->second.include_subdomains) { |
| absl::optional<std::string> dotted_name = |
| dns_names_util::NetworkToDottedName(host_sub_chunk); |
| if (!dotted_name) |
| return false; |
| |
| *result = j->second; |
| result->domain = std::move(dotted_name).value(); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| bool TransportSecurityState::GetDynamicPKPState(const std::string& host, |
| PKPState* result) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| const std::vector<uint8_t> 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) { |
| base::span<const uint8_t> host_sub_chunk = |
| base::make_span(canonicalized_host).subspan(i); |
| auto j = enabled_pkp_hosts_.find(HashHost(host_sub_chunk)); |
| if (j == enabled_pkp_hosts_.end()) |
| continue; |
| |
| // If the entry is invalid, drop it. |
| if (current_time > j->second.expiry) { |
| enabled_pkp_hosts_.erase(j); |
| DirtyNotify(); |
| continue; |
| } |
| |
| // If this is the most specific PKP match, add it to the result. Note: a PKP |
| // entry at a more specific domain overrides a less specific domain whether |
| // or not |include_subdomains| is set. |
| // |
| // TODO(davidben): This does not match the HSTS behavior. We no longer |
| // implement HPKP, so this logic is only used via AddHPKP(), reachable from |
| // Cronet. |
| if (i == 0 || j->second.include_subdomains) { |
| absl::optional<std::string> dotted_name = |
| dns_names_util::NetworkToDottedName(host_sub_chunk); |
| if (!dotted_name) |
| return false; |
| |
| *result = j->second; |
| result->domain = std::move(dotted_name).value(); |
| return true; |
| } |
| |
| break; |
| } |
| |
| return false; |
| } |
| |
| void TransportSecurityState::AddOrUpdateEnabledSTSHosts( |
| const HashedHost& hashed_host, |
| const STSState& state) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK(state.ShouldUpgradeToSSL()); |
| enabled_sts_hosts_[hashed_host] = state; |
| } |
| |
| TransportSecurityState::STSState::STSState() = default; |
| |
| TransportSecurityState::STSState::~STSState() = default; |
| |
| bool TransportSecurityState::STSState::ShouldUpgradeToSSL() const { |
| return upgrade_mode == MODE_FORCE_HTTPS; |
| } |
| |
| TransportSecurityState::STSStateIterator::STSStateIterator( |
| const TransportSecurityState& state) |
| : iterator_(state.enabled_sts_hosts_.begin()), |
| end_(state.enabled_sts_hosts_.end()) {} |
| |
| TransportSecurityState::STSStateIterator::~STSStateIterator() = default; |
| |
| TransportSecurityState::PKPState::PKPState() = default; |
| |
| TransportSecurityState::PKPState::PKPState(const PKPState& other) = default; |
| |
| TransportSecurityState::PKPState::~PKPState() = default; |
| |
| TransportSecurityState::PinSet::PinSet( |
| std::string name, |
| std::vector<std::vector<uint8_t>> static_spki_hashes, |
| std::vector<std::vector<uint8_t>> bad_static_spki_hashes, |
| std::string report_uri) |
| : name_(std::move(name)), |
| static_spki_hashes_(std::move(static_spki_hashes)), |
| bad_static_spki_hashes_(std::move(bad_static_spki_hashes)), |
| report_uri_(std::move(report_uri)) {} |
| |
| TransportSecurityState::PinSet::PinSet(const PinSet& other) = default; |
| TransportSecurityState::PinSet::~PinSet() = default; |
| |
| TransportSecurityState::PinSetInfo::PinSetInfo(std::string hostname, |
| std::string pinset_name, |
| bool include_subdomains) |
| : hostname_(std::move(hostname)), |
| pinset_name_(std::move(pinset_name)), |
| include_subdomains_(std::move(include_subdomains)) {} |
| |
| bool TransportSecurityState::PKPState::CheckPublicKeyPins( |
| const HashValueVector& hashes, |
| std::string* failure_log) const { |
| // Validate that hashes is not empty. By the time this code is called (in |
| // production), that should never happen, but it's good to be defensive. |
| // And, hashes *can* be empty in some test scenarios. |
| if (hashes.empty()) { |
| failure_log->append( |
| "Rejecting empty public key chain for public-key-pinned domains: " + |
| domain); |
| return false; |
| } |
| |
| if (HashesIntersect(bad_spki_hashes, hashes)) { |
| failure_log->append("Rejecting public key chain for domain " + domain + |
| ". Validated chain: " + HashesToBase64String(hashes) + |
| ", matches one or more bad hashes: " + |
| HashesToBase64String(bad_spki_hashes)); |
| return false; |
| } |
| |
| // If there are no pins, then any valid chain is acceptable. |
| if (spki_hashes.empty()) |
| return true; |
| |
| if (HashesIntersect(spki_hashes, hashes)) { |
| return true; |
| } |
| |
| failure_log->append("Rejecting public key chain for domain " + domain + |
| ". Validated chain: " + HashesToBase64String(hashes) + |
| ", expected: " + HashesToBase64String(spki_hashes)); |
| return false; |
| } |
| |
| bool TransportSecurityState::PKPState::HasPublicKeyPins() const { |
| return spki_hashes.size() > 0 || bad_spki_hashes.size() > 0; |
| } |
| |
| bool TransportSecurityState::IsStaticPKPListTimely() const { |
| if (pins_list_always_timely_for_testing_) { |
| return true; |
| } |
| |
| // If the list has not been updated via component updater, freshness depends |
| // on the compiled-in list freshness. |
| if (!host_pins_.has_value()) { |
| #if BUILDFLAG(INCLUDE_TRANSPORT_SECURITY_STATE_PRELOAD_LIST) |
| return (base::Time::Now() - kPinsListTimestamp).InDays() < 70; |
| #else |
| return false; |
| #endif |
| } |
| DCHECK(!key_pins_list_last_update_time_.is_null()); |
| // Else, we use the last update time. |
| return (base::Time::Now() - key_pins_list_last_update_time_).InDays() < |
| 70 /* 10 weeks */; |
| } |
| |
| } // namespace net |