| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/base/transport_security_state.h" |
| |
| #if defined(USE_OPENSSL) |
| #include <openssl/ecdsa.h> |
| #include <openssl/ssl.h> |
| #else // !defined(USE_OPENSSL) |
| #include <cryptohi.h> |
| #include <hasht.h> |
| #include <keyhi.h> |
| #include <pk11pub.h> |
| #include <nspr.h> |
| #endif |
| |
| #include <algorithm> |
| |
| #include "base/base64.h" |
| #include "base/build_time.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/metrics/histogram.h" |
| #include "base/sha1.h" |
| #include "base/string_number_conversions.h" |
| #include "base/string_tokenizer.h" |
| #include "base/string_util.h" |
| #include "base/time.h" |
| #include "base/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "crypto/sha2.h" |
| #include "googleurl/src/gurl.h" |
| #include "net/base/dns_util.h" |
| #include "net/base/ssl_info.h" |
| #include "net/base/x509_cert_types.h" |
| #include "net/base/x509_certificate.h" |
| #include "net/http/http_util.h" |
| |
| #if defined(USE_OPENSSL) |
| #include "crypto/openssl_util.h" |
| #endif |
| |
| namespace net { |
| |
| const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365; // 1 year |
| |
| static std::string HashHost(const std::string& canonicalized_host) { |
| char hashed[crypto::kSHA256Length]; |
| crypto::SHA256HashString(canonicalized_host, hashed, sizeof(hashed)); |
| return std::string(hashed, sizeof(hashed)); |
| } |
| |
| TransportSecurityState::TransportSecurityState() |
| : delegate_(NULL) { |
| } |
| |
| TransportSecurityState::Iterator::Iterator(const TransportSecurityState& state) |
| : iterator_(state.enabled_hosts_.begin()), |
| end_(state.enabled_hosts_.end()) { |
| } |
| |
| TransportSecurityState::Iterator::~Iterator() {} |
| |
| void TransportSecurityState::SetDelegate( |
| TransportSecurityState::Delegate* delegate) { |
| delegate_ = delegate; |
| } |
| |
| void TransportSecurityState::EnableHost(const std::string& host, |
| const DomainState& state) { |
| DCHECK(CalledOnValidThread()); |
| |
| const std::string canonicalized_host = CanonicalizeHost(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(); |
| |
| enabled_hosts_[HashHost(canonicalized_host)] = state_copy; |
| DirtyNotify(); |
| } |
| |
| bool TransportSecurityState::DeleteHost(const std::string& host) { |
| DCHECK(CalledOnValidThread()); |
| |
| const std::string canonicalized_host = CanonicalizeHost(host); |
| if (canonicalized_host.empty()) |
| return false; |
| |
| std::map<std::string, DomainState>::iterator i = enabled_hosts_.find( |
| HashHost(canonicalized_host)); |
| if (i != enabled_hosts_.end()) { |
| enabled_hosts_.erase(i); |
| DirtyNotify(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool TransportSecurityState::GetDomainState(const std::string& host, |
| bool sni_enabled, |
| DomainState* result) { |
| DCHECK(CalledOnValidThread()); |
| |
| DomainState state; |
| const std::string canonicalized_host = CanonicalizeHost(host); |
| if (canonicalized_host.empty()) |
| return false; |
| |
| bool has_preload = GetStaticDomainState(canonicalized_host, sni_enabled, |
| &state); |
| std::string canonicalized_preload = CanonicalizeHost(state.domain); |
| |
| 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); |
| // Exact match of a preload always wins. |
| if (has_preload && host_sub_chunk == canonicalized_preload) { |
| *result = state; |
| return true; |
| } |
| |
| std::map<std::string, DomainState>::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.include_subdomains) { |
| *result = state; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| return false; |
| } |
| |
| void TransportSecurityState::DeleteSince(const base::Time& time) { |
| DCHECK(CalledOnValidThread()); |
| |
| bool dirtied = false; |
| |
| std::map<std::string, DomainState>::iterator i = enabled_hosts_.begin(); |
| while (i != enabled_hosts_.end()) { |
| if (i->second.created >= time) { |
| dirtied = true; |
| enabled_hosts_.erase(i++); |
| } else { |
| i++; |
| } |
| } |
| |
| if (dirtied) |
| DirtyNotify(); |
| } |
| |
| // MaxAgeToInt converts a string representation of a number of seconds into a |
| // int. We use strtol in order to handle overflow correctly. The string may |
| // contain an arbitary number which we should truncate correctly rather than |
| // throwing a parse failure. |
| static bool MaxAgeToInt(std::string::const_iterator begin, |
| std::string::const_iterator end, |
| int* result) { |
| const std::string s(begin, end); |
| char* endptr; |
| long int i = strtol(s.data(), &endptr, 10 /* base */); |
| if (*endptr || i < 0) |
| return false; |
| if (i > TransportSecurityState::kMaxHSTSAgeSecs) |
| i = TransportSecurityState::kMaxHSTSAgeSecs; |
| *result = i; |
| return true; |
| } |
| |
| // Strip, Split, StringPair, and ParsePins are private implementation details |
| // of ParsePinsHeader(std::string&, DomainState&). |
| static std::string Strip(const std::string& source) { |
| if (source.empty()) |
| return source; |
| |
| std::string::const_iterator start = source.begin(); |
| std::string::const_iterator end = source.end(); |
| HttpUtil::TrimLWS(&start, &end); |
| return std::string(start, end); |
| } |
| |
| typedef std::pair<std::string, std::string> StringPair; |
| |
| static StringPair Split(const std::string& source, char delimiter) { |
| StringPair pair; |
| size_t point = source.find(delimiter); |
| |
| pair.first = source.substr(0, point); |
| if (std::string::npos != point) |
| pair.second = source.substr(point + 1); |
| |
| return pair; |
| } |
| |
| // static |
| bool TransportSecurityState::ParsePin(const std::string& value, |
| HashValue* out) { |
| StringPair slash = Split(Strip(value), '/'); |
| |
| if (slash.first == "sha1") |
| out->tag = HASH_VALUE_SHA1; |
| else if (slash.first == "sha256") |
| out->tag = HASH_VALUE_SHA256; |
| else |
| return false; |
| |
| std::string decoded; |
| if (!base::Base64Decode(slash.second, &decoded) || |
| decoded.size() != out->size()) { |
| return false; |
| } |
| |
| memcpy(out->data(), decoded.data(), out->size()); |
| return true; |
| } |
| |
| static bool ParseAndAppendPin(const std::string& value, |
| HashValueTag tag, |
| HashValueVector* hashes) { |
| std::string unquoted = HttpUtil::Unquote(value); |
| std::string decoded; |
| |
| // This code has to assume that 32 bytes is SHA-256 and 20 bytes is SHA-1. |
| // Currently, those are the only two possibilities, so the assumption is |
| // valid. |
| if (!base::Base64Decode(unquoted, &decoded)) |
| return false; |
| |
| HashValue hash(tag); |
| if (decoded.size() != hash.size()) |
| return false; |
| |
| memcpy(hash.data(), decoded.data(), hash.size()); |
| hashes->push_back(hash); |
| return true; |
| } |
| |
| struct HashValuesEqualPredicate { |
| explicit HashValuesEqualPredicate(const HashValue& fingerprint) : |
| fingerprint_(fingerprint) {} |
| |
| bool operator()(const HashValue& other) const { |
| return fingerprint_.Equals(other); |
| } |
| |
| const HashValue& fingerprint_; |
| }; |
| |
| // Returns true iff there is an item in |pins| which is not present in |
| // |from_cert_chain|. Such an SPKI hash is called a "backup pin". |
| static bool IsBackupPinPresent(const HashValueVector& pins, |
| const HashValueVector& from_cert_chain) { |
| for (HashValueVector::const_iterator |
| i = pins.begin(); i != pins.end(); ++i) { |
| HashValueVector::const_iterator j = |
| std::find_if(from_cert_chain.begin(), from_cert_chain.end(), |
| HashValuesEqualPredicate(*i)); |
| if (j == from_cert_chain.end()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Returns true if the intersection of |a| and |b| is not empty. If either |
| // |a| or |b| is empty, returns false. |
| static bool HashesIntersect(const HashValueVector& a, |
| const HashValueVector& b) { |
| for (HashValueVector::const_iterator i = a.begin(); i != a.end(); ++i) { |
| HashValueVector::const_iterator j = |
| std::find_if(b.begin(), b.end(), HashValuesEqualPredicate(*i)); |
| if (j != b.end()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Returns true iff |pins| contains both a live and a backup pin. A live pin |
| // is a pin whose SPKI is present in the certificate chain in |ssl_info|. A |
| // backup pin is a pin intended for disaster recovery, not day-to-day use, and |
| // thus must be absent from the certificate chain. The Public-Key-Pins header |
| // specification requires both. |
| static bool IsPinListValid(const HashValueVector& pins, |
| const SSLInfo& ssl_info) { |
| // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual |
| // liveness and backupness below.) |
| if (pins.size() < 2) |
| return false; |
| |
| const HashValueVector& from_cert_chain = ssl_info.public_key_hashes; |
| if (from_cert_chain.empty()) |
| return false; |
| |
| return IsBackupPinPresent(pins, from_cert_chain) && |
| HashesIntersect(pins, from_cert_chain); |
| } |
| |
| // "Public-Key-Pins" ":" |
| // "max-age" "=" delta-seconds ";" |
| // "pin-" algo "=" base64 [ ";" ... ] |
| bool TransportSecurityState::DomainState::ParsePinsHeader( |
| const base::Time& now, |
| const std::string& value, |
| const SSLInfo& ssl_info) { |
| bool parsed_max_age = false; |
| int max_age_candidate = 0; |
| HashValueVector pins; |
| |
| std::string source = value; |
| |
| while (!source.empty()) { |
| StringPair semicolon = Split(source, ';'); |
| semicolon.first = Strip(semicolon.first); |
| semicolon.second = Strip(semicolon.second); |
| StringPair equals = Split(semicolon.first, '='); |
| equals.first = Strip(equals.first); |
| equals.second = Strip(equals.second); |
| |
| if (LowerCaseEqualsASCII(equals.first, "max-age")) { |
| if (equals.second.empty() || |
| !MaxAgeToInt(equals.second.begin(), equals.second.end(), |
| &max_age_candidate)) { |
| return false; |
| } |
| if (max_age_candidate > kMaxHSTSAgeSecs) |
| max_age_candidate = kMaxHSTSAgeSecs; |
| parsed_max_age = true; |
| } else if (StartsWithASCII(equals.first, "pin-", false)) { |
| HashValueTag tag; |
| if (LowerCaseEqualsASCII(equals.first, "pin-sha1")) { |
| tag = HASH_VALUE_SHA1; |
| } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) { |
| tag = HASH_VALUE_SHA256; |
| } else { |
| LOG(WARNING) << "Ignoring pin of unknown type: " << equals.first; |
| return false; |
| } |
| if (!ParseAndAppendPin(equals.second, tag, &pins)) |
| return false; |
| } else { |
| // Silently ignore unknown directives for forward compatibility. |
| } |
| |
| source = semicolon.second; |
| } |
| |
| if (!parsed_max_age || !IsPinListValid(pins, ssl_info)) |
| return false; |
| |
| dynamic_spki_hashes_expiry = |
| now + base::TimeDelta::FromSeconds(max_age_candidate); |
| |
| dynamic_spki_hashes.clear(); |
| if (max_age_candidate > 0) { |
| for (HashValueVector::const_iterator i = pins.begin(); |
| i != pins.end(); ++i) { |
| dynamic_spki_hashes.push_back(*i); |
| } |
| } |
| |
| return true; |
| } |
| |
| // Parse the Strict-Transport-Security header, as currently defined in |
| // http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14: |
| // |
| // Strict-Transport-Security = "Strict-Transport-Security" ":" |
| // [ directive ] *( ";" [ directive ] ) |
| // |
| // directive = directive-name [ "=" directive-value ] |
| // directive-name = token |
| // directive-value = token | quoted-string |
| // |
| // 1. The order of appearance of directives is not significant. |
| // |
| // 2. All directives MUST appear only once in an STS header field. |
| // Directives are either optional or required, as stipulated in |
| // their definitions. |
| // |
| // 3. Directive names are case-insensitive. |
| // |
| // 4. UAs MUST ignore any STS header fields containing directives, or |
| // other header field value data, that does not conform to the |
| // syntax defined in this specification. |
| // |
| // 5. If an STS header field contains directive(s) not recognized by |
| // the UA, the UA MUST ignore the unrecognized directives and if the |
| // STS header field otherwise satisfies the above requirements (1 |
| // through 4), the UA MUST process the recognized directives. |
| bool TransportSecurityState::DomainState::ParseSTSHeader( |
| const base::Time& now, |
| const std::string& value) { |
| int max_age_candidate = 0; |
| bool include_subdomains_candidate = false; |
| |
| // We must see max-age exactly once. |
| int max_age_observed = 0; |
| // We must see includeSubdomains exactly 0 or 1 times. |
| int include_subdomains_observed = 0; |
| |
| enum ParserState { |
| START, |
| AFTER_MAX_AGE_LABEL, |
| AFTER_MAX_AGE_EQUALS, |
| AFTER_MAX_AGE, |
| AFTER_INCLUDE_SUBDOMAINS, |
| AFTER_UNKNOWN_LABEL, |
| DIRECTIVE_END |
| } state = START; |
| |
| StringTokenizer tokenizer(value, " \t=;"); |
| tokenizer.set_options(StringTokenizer::RETURN_DELIMS); |
| tokenizer.set_quote_chars("\""); |
| std::string unquoted; |
| while (tokenizer.GetNext()) { |
| DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1); |
| switch (state) { |
| case START: |
| case DIRECTIVE_END: |
| if (IsAsciiWhitespace(*tokenizer.token_begin())) |
| continue; |
| if (LowerCaseEqualsASCII(tokenizer.token(), "max-age")) { |
| state = AFTER_MAX_AGE_LABEL; |
| max_age_observed++; |
| } else if (LowerCaseEqualsASCII(tokenizer.token(), |
| "includesubdomains")) { |
| state = AFTER_INCLUDE_SUBDOMAINS; |
| include_subdomains_observed++; |
| include_subdomains_candidate = true; |
| } else { |
| state = AFTER_UNKNOWN_LABEL; |
| } |
| break; |
| |
| case AFTER_MAX_AGE_LABEL: |
| if (IsAsciiWhitespace(*tokenizer.token_begin())) |
| continue; |
| if (*tokenizer.token_begin() != '=') |
| return false; |
| DCHECK_EQ(tokenizer.token().length(), 1U); |
| state = AFTER_MAX_AGE_EQUALS; |
| break; |
| |
| case AFTER_MAX_AGE_EQUALS: |
| if (IsAsciiWhitespace(*tokenizer.token_begin())) |
| continue; |
| unquoted = HttpUtil::Unquote(tokenizer.token()); |
| if (!MaxAgeToInt(unquoted.begin(), |
| unquoted.end(), |
| &max_age_candidate)) |
| return false; |
| state = AFTER_MAX_AGE; |
| break; |
| |
| case AFTER_MAX_AGE: |
| case AFTER_INCLUDE_SUBDOMAINS: |
| if (IsAsciiWhitespace(*tokenizer.token_begin())) |
| continue; |
| else if (*tokenizer.token_begin() == ';') |
| state = DIRECTIVE_END; |
| else |
| return false; |
| break; |
| |
| case AFTER_UNKNOWN_LABEL: |
| // Consume and ignore the post-label contents (if any). |
| if (*tokenizer.token_begin() != ';') |
| continue; |
| state = DIRECTIVE_END; |
| break; |
| } |
| } |
| |
| // We've consumed all the input. Let's see what state we ended up in. |
| if (max_age_observed != 1 || |
| (include_subdomains_observed != 0 && include_subdomains_observed != 1)) { |
| return false; |
| } |
| |
| switch (state) { |
| case AFTER_MAX_AGE: |
| case AFTER_INCLUDE_SUBDOMAINS: |
| case AFTER_UNKNOWN_LABEL: |
| if (max_age_candidate > 0) { |
| upgrade_expiry = now + base::TimeDelta::FromSeconds(max_age_candidate); |
| upgrade_mode = MODE_FORCE_HTTPS; |
| } else { |
| upgrade_expiry = now; |
| upgrade_mode = MODE_DEFAULT; |
| } |
| include_subdomains = include_subdomains_candidate; |
| return true; |
| case START: |
| case DIRECTIVE_END: |
| case AFTER_MAX_AGE_LABEL: |
| case AFTER_MAX_AGE_EQUALS: |
| return false; |
| default: |
| NOTREACHED(); |
| return false; |
| } |
| } |
| |
| static bool AddHash(const std::string& type_and_base64, |
| HashValueVector* out) { |
| HashValue hash; |
| |
| if (!TransportSecurityState::ParsePin(type_and_base64, &hash)) |
| return false; |
| |
| out->push_back(hash); |
| return true; |
| } |
| |
| TransportSecurityState::~TransportSecurityState() {} |
| |
| void TransportSecurityState::DirtyNotify() { |
| DCHECK(CalledOnValidThread()); |
| |
| if (delegate_) |
| delegate_->StateIsDirty(this); |
| } |
| |
| // static |
| std::string TransportSecurityState::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 check |
| // that there are no invalid characters in the host and lowercase the result. |
| |
| std::string new_host; |
| if (!DNSDomainFromDot(host, &new_host)) { |
| // 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::string(); |
| } |
| |
| for (size_t i = 0; new_host[i]; i += new_host[i] + 1) { |
| const unsigned label_length = static_cast<unsigned>(new_host[i]); |
| if (!label_length) |
| break; |
| |
| for (size_t j = 0; j < label_length; ++j) { |
| // RFC 3490, 4.1, step 3 |
| if (!IsSTD3ASCIIValidCharacter(new_host[i + 1 + j])) |
| return std::string(); |
| |
| new_host[i + 1 + j] = tolower(new_host[i + 1 + j]); |
| } |
| |
| // step 3(b) |
| if (new_host[i + 1] == '-' || |
| new_host[i + label_length] == '-') { |
| return std::string(); |
| } |
| } |
| |
| return new_host; |
| } |
| |
| // |ReportUMAOnPinFailure| uses these to report which domain was associated |
| // with the public key pinning failure. |
| // |
| // DO NOT CHANGE THE ORDERING OF THESE NAMES OR REMOVE ANY OF THEM. Add new |
| // domains at the END of the listing (but before DOMAIN_NUM_EVENTS). |
| enum SecondLevelDomainName { |
| DOMAIN_NOT_PINNED, |
| |
| DOMAIN_GOOGLE_COM, |
| DOMAIN_ANDROID_COM, |
| DOMAIN_GOOGLE_ANALYTICS_COM, |
| DOMAIN_GOOGLEPLEX_COM, |
| DOMAIN_YTIMG_COM, |
| DOMAIN_GOOGLEUSERCONTENT_COM, |
| DOMAIN_YOUTUBE_COM, |
| DOMAIN_GOOGLEAPIS_COM, |
| DOMAIN_GOOGLEADSERVICES_COM, |
| DOMAIN_GOOGLECODE_COM, |
| DOMAIN_APPSPOT_COM, |
| DOMAIN_GOOGLESYNDICATION_COM, |
| DOMAIN_DOUBLECLICK_NET, |
| DOMAIN_GSTATIC_COM, |
| DOMAIN_GMAIL_COM, |
| DOMAIN_GOOGLEMAIL_COM, |
| DOMAIN_GOOGLEGROUPS_COM, |
| |
| DOMAIN_TORPROJECT_ORG, |
| |
| DOMAIN_TWITTER_COM, |
| DOMAIN_TWIMG_COM, |
| |
| DOMAIN_AKAMAIHD_NET, |
| |
| DOMAIN_TOR2WEB_ORG, |
| |
| DOMAIN_YOUTU_BE, |
| DOMAIN_GOOGLECOMMERCE_COM, |
| DOMAIN_URCHIN_COM, |
| DOMAIN_GOO_GL, |
| DOMAIN_G_CO, |
| DOMAIN_GOOGLE_AC, |
| DOMAIN_GOOGLE_AD, |
| DOMAIN_GOOGLE_AE, |
| DOMAIN_GOOGLE_AF, |
| DOMAIN_GOOGLE_AG, |
| DOMAIN_GOOGLE_AM, |
| DOMAIN_GOOGLE_AS, |
| DOMAIN_GOOGLE_AT, |
| DOMAIN_GOOGLE_AZ, |
| DOMAIN_GOOGLE_BA, |
| DOMAIN_GOOGLE_BE, |
| DOMAIN_GOOGLE_BF, |
| DOMAIN_GOOGLE_BG, |
| DOMAIN_GOOGLE_BI, |
| DOMAIN_GOOGLE_BJ, |
| DOMAIN_GOOGLE_BS, |
| DOMAIN_GOOGLE_BY, |
| DOMAIN_GOOGLE_CA, |
| DOMAIN_GOOGLE_CAT, |
| DOMAIN_GOOGLE_CC, |
| DOMAIN_GOOGLE_CD, |
| DOMAIN_GOOGLE_CF, |
| DOMAIN_GOOGLE_CG, |
| DOMAIN_GOOGLE_CH, |
| DOMAIN_GOOGLE_CI, |
| DOMAIN_GOOGLE_CL, |
| DOMAIN_GOOGLE_CM, |
| DOMAIN_GOOGLE_CN, |
| DOMAIN_CO_AO, |
| DOMAIN_CO_BW, |
| DOMAIN_CO_CK, |
| DOMAIN_CO_CR, |
| DOMAIN_CO_HU, |
| DOMAIN_CO_ID, |
| DOMAIN_CO_IL, |
| DOMAIN_CO_IM, |
| DOMAIN_CO_IN, |
| DOMAIN_CO_JE, |
| DOMAIN_CO_JP, |
| DOMAIN_CO_KE, |
| DOMAIN_CO_KR, |
| DOMAIN_CO_LS, |
| DOMAIN_CO_MA, |
| DOMAIN_CO_MZ, |
| DOMAIN_CO_NZ, |
| DOMAIN_CO_TH, |
| DOMAIN_CO_TZ, |
| DOMAIN_CO_UG, |
| DOMAIN_CO_UK, |
| DOMAIN_CO_UZ, |
| DOMAIN_CO_VE, |
| DOMAIN_CO_VI, |
| DOMAIN_CO_ZA, |
| DOMAIN_CO_ZM, |
| DOMAIN_CO_ZW, |
| DOMAIN_COM_AF, |
| DOMAIN_COM_AG, |
| DOMAIN_COM_AI, |
| DOMAIN_COM_AR, |
| DOMAIN_COM_AU, |
| DOMAIN_COM_BD, |
| DOMAIN_COM_BH, |
| DOMAIN_COM_BN, |
| DOMAIN_COM_BO, |
| DOMAIN_COM_BR, |
| DOMAIN_COM_BY, |
| DOMAIN_COM_BZ, |
| DOMAIN_COM_CN, |
| DOMAIN_COM_CO, |
| DOMAIN_COM_CU, |
| DOMAIN_COM_CY, |
| DOMAIN_COM_DO, |
| DOMAIN_COM_EC, |
| DOMAIN_COM_EG, |
| DOMAIN_COM_ET, |
| DOMAIN_COM_FJ, |
| DOMAIN_COM_GE, |
| DOMAIN_COM_GH, |
| DOMAIN_COM_GI, |
| DOMAIN_COM_GR, |
| DOMAIN_COM_GT, |
| DOMAIN_COM_HK, |
| DOMAIN_COM_IQ, |
| DOMAIN_COM_JM, |
| DOMAIN_COM_JO, |
| DOMAIN_COM_KH, |
| DOMAIN_COM_KW, |
| DOMAIN_COM_LB, |
| DOMAIN_COM_LY, |
| DOMAIN_COM_MT, |
| DOMAIN_COM_MX, |
| DOMAIN_COM_MY, |
| DOMAIN_COM_NA, |
| DOMAIN_COM_NF, |
| DOMAIN_COM_NG, |
| DOMAIN_COM_NI, |
| DOMAIN_COM_NP, |
| DOMAIN_COM_NR, |
| DOMAIN_COM_OM, |
| DOMAIN_COM_PA, |
| DOMAIN_COM_PE, |
| DOMAIN_COM_PH, |
| DOMAIN_COM_PK, |
| DOMAIN_COM_PL, |
| DOMAIN_COM_PR, |
| DOMAIN_COM_PY, |
| DOMAIN_COM_QA, |
| DOMAIN_COM_RU, |
| DOMAIN_COM_SA, |
| DOMAIN_COM_SB, |
| DOMAIN_COM_SG, |
| DOMAIN_COM_SL, |
| DOMAIN_COM_SV, |
| DOMAIN_COM_TJ, |
| DOMAIN_COM_TN, |
| DOMAIN_COM_TR, |
| DOMAIN_COM_TW, |
| DOMAIN_COM_UA, |
| DOMAIN_COM_UY, |
| DOMAIN_COM_VC, |
| DOMAIN_COM_VE, |
| DOMAIN_COM_VN, |
| DOMAIN_GOOGLE_CV, |
| DOMAIN_GOOGLE_CZ, |
| DOMAIN_GOOGLE_DE, |
| DOMAIN_GOOGLE_DJ, |
| DOMAIN_GOOGLE_DK, |
| DOMAIN_GOOGLE_DM, |
| DOMAIN_GOOGLE_DZ, |
| DOMAIN_GOOGLE_EE, |
| DOMAIN_GOOGLE_ES, |
| DOMAIN_GOOGLE_FI, |
| DOMAIN_GOOGLE_FM, |
| DOMAIN_GOOGLE_FR, |
| DOMAIN_GOOGLE_GA, |
| DOMAIN_GOOGLE_GE, |
| DOMAIN_GOOGLE_GG, |
| DOMAIN_GOOGLE_GL, |
| DOMAIN_GOOGLE_GM, |
| DOMAIN_GOOGLE_GP, |
| DOMAIN_GOOGLE_GR, |
| DOMAIN_GOOGLE_GY, |
| DOMAIN_GOOGLE_HK, |
| DOMAIN_GOOGLE_HN, |
| DOMAIN_GOOGLE_HR, |
| DOMAIN_GOOGLE_HT, |
| DOMAIN_GOOGLE_HU, |
| DOMAIN_GOOGLE_IE, |
| DOMAIN_GOOGLE_IM, |
| DOMAIN_GOOGLE_INFO, |
| DOMAIN_GOOGLE_IQ, |
| DOMAIN_GOOGLE_IS, |
| DOMAIN_GOOGLE_IT, |
| DOMAIN_IT_AO, |
| DOMAIN_GOOGLE_JE, |
| DOMAIN_GOOGLE_JO, |
| DOMAIN_GOOGLE_JOBS, |
| DOMAIN_GOOGLE_JP, |
| DOMAIN_GOOGLE_KG, |
| DOMAIN_GOOGLE_KI, |
| DOMAIN_GOOGLE_KZ, |
| DOMAIN_GOOGLE_LA, |
| DOMAIN_GOOGLE_LI, |
| DOMAIN_GOOGLE_LK, |
| DOMAIN_GOOGLE_LT, |
| DOMAIN_GOOGLE_LU, |
| DOMAIN_GOOGLE_LV, |
| DOMAIN_GOOGLE_MD, |
| DOMAIN_GOOGLE_ME, |
| DOMAIN_GOOGLE_MG, |
| DOMAIN_GOOGLE_MK, |
| DOMAIN_GOOGLE_ML, |
| DOMAIN_GOOGLE_MN, |
| DOMAIN_GOOGLE_MS, |
| DOMAIN_GOOGLE_MU, |
| DOMAIN_GOOGLE_MV, |
| DOMAIN_GOOGLE_MW, |
| DOMAIN_GOOGLE_NE, |
| DOMAIN_NE_JP, |
| DOMAIN_GOOGLE_NET, |
| DOMAIN_GOOGLE_NL, |
| DOMAIN_GOOGLE_NO, |
| DOMAIN_GOOGLE_NR, |
| DOMAIN_GOOGLE_NU, |
| DOMAIN_OFF_AI, |
| DOMAIN_GOOGLE_PK, |
| DOMAIN_GOOGLE_PL, |
| DOMAIN_GOOGLE_PN, |
| DOMAIN_GOOGLE_PS, |
| DOMAIN_GOOGLE_PT, |
| DOMAIN_GOOGLE_RO, |
| DOMAIN_GOOGLE_RS, |
| DOMAIN_GOOGLE_RU, |
| DOMAIN_GOOGLE_RW, |
| DOMAIN_GOOGLE_SC, |
| DOMAIN_GOOGLE_SE, |
| DOMAIN_GOOGLE_SH, |
| DOMAIN_GOOGLE_SI, |
| DOMAIN_GOOGLE_SK, |
| DOMAIN_GOOGLE_SM, |
| DOMAIN_GOOGLE_SN, |
| DOMAIN_GOOGLE_SO, |
| DOMAIN_GOOGLE_ST, |
| DOMAIN_GOOGLE_TD, |
| DOMAIN_GOOGLE_TG, |
| DOMAIN_GOOGLE_TK, |
| DOMAIN_GOOGLE_TL, |
| DOMAIN_GOOGLE_TM, |
| DOMAIN_GOOGLE_TN, |
| DOMAIN_GOOGLE_TO, |
| DOMAIN_GOOGLE_TP, |
| DOMAIN_GOOGLE_TT, |
| DOMAIN_GOOGLE_US, |
| DOMAIN_GOOGLE_UZ, |
| DOMAIN_GOOGLE_VG, |
| DOMAIN_GOOGLE_VU, |
| DOMAIN_GOOGLE_WS, |
| |
| DOMAIN_CHROMIUM_ORG, |
| |
| DOMAIN_CRYPTO_CAT, |
| |
| // Boundary value for UMA_HISTOGRAM_ENUMERATION: |
| DOMAIN_NUM_EVENTS |
| }; |
| |
| // PublicKeyPins contains a number of SubjectPublicKeyInfo hashes for a site. |
| // The validated certificate chain for the site must not include any of |
| // |excluded_hashes| and must include one or more of |required_hashes|. |
| struct PublicKeyPins { |
| const char* const* required_hashes; |
| const char* const* excluded_hashes; |
| }; |
| |
| struct HSTSPreload { |
| uint8 length; |
| bool include_subdomains; |
| char dns_name[34]; |
| bool https_required; |
| PublicKeyPins pins; |
| SecondLevelDomainName second_level_domain_name; |
| }; |
| |
| static bool HasPreload(const struct HSTSPreload* entries, size_t num_entries, |
| const std::string& canonicalized_host, size_t i, |
| TransportSecurityState::DomainState* out, bool* ret) { |
| for (size_t j = 0; j < num_entries; j++) { |
| if (entries[j].length == canonicalized_host.size() - i && |
| memcmp(entries[j].dns_name, &canonicalized_host[i], |
| entries[j].length) == 0) { |
| if (!entries[j].include_subdomains && i != 0) { |
| *ret = false; |
| } else { |
| out->include_subdomains = entries[j].include_subdomains; |
| *ret = true; |
| if (!entries[j].https_required) |
| out->upgrade_mode = TransportSecurityState::DomainState::MODE_DEFAULT; |
| if (entries[j].pins.required_hashes) { |
| const char* const* hash = entries[j].pins.required_hashes; |
| while (*hash) { |
| bool ok = AddHash(*hash, &out->static_spki_hashes); |
| DCHECK(ok) << " failed to parse " << *hash; |
| hash++; |
| } |
| } |
| if (entries[j].pins.excluded_hashes) { |
| const char* const* hash = entries[j].pins.excluded_hashes; |
| while (*hash) { |
| bool ok = AddHash(*hash, &out->bad_static_spki_hashes); |
| DCHECK(ok) << " failed to parse " << *hash; |
| hash++; |
| } |
| } |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| #include "net/base/transport_security_state_static.h" |
| |
| // Returns the HSTSPreload entry for the |canonicalized_host| in |entries|, |
| // or NULL if there is none. Prefers exact hostname matches to those that |
| // match only because HSTSPreload.include_subdomains is true. |
| // |
| // |canonicalized_host| should be the hostname as canonicalized by |
| // CanonicalizeHost. |
| static const struct HSTSPreload* GetHSTSPreload( |
| const std::string& canonicalized_host, |
| const struct HSTSPreload* entries, |
| size_t num_entries) { |
| for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) { |
| for (size_t j = 0; j < num_entries; j++) { |
| const struct HSTSPreload* entry = entries + j; |
| |
| if (i != 0 && !entry->include_subdomains) |
| continue; |
| |
| if (entry->length == canonicalized_host.size() - i && |
| memcmp(entry->dns_name, &canonicalized_host[i], entry->length) == 0) { |
| return entry; |
| } |
| } |
| } |
| |
| return NULL; |
| } |
| |
| // static |
| bool TransportSecurityState::IsGooglePinnedProperty(const std::string& host, |
| bool sni_enabled) { |
| #if !defined(__LB_SHELL__) |
| std::string canonicalized_host = CanonicalizeHost(host); |
| const struct HSTSPreload* entry = |
| GetHSTSPreload(canonicalized_host, kPreloadedSTS, kNumPreloadedSTS); |
| |
| if (entry && entry->pins.required_hashes == kGoogleAcceptableCerts) |
| return true; |
| |
| if (sni_enabled) { |
| entry = GetHSTSPreload(canonicalized_host, kPreloadedSNISTS, |
| kNumPreloadedSNISTS); |
| if (entry && entry->pins.required_hashes == kGoogleAcceptableCerts) |
| return true; |
| } |
| #endif // !defined(__LB_SHELL__) |
| return false; |
| } |
| |
| // static |
| void TransportSecurityState::ReportUMAOnPinFailure(const std::string& host) { |
| #if !defined(__LB_SHELL__) |
| std::string canonicalized_host = CanonicalizeHost(host); |
| |
| const struct HSTSPreload* entry = |
| GetHSTSPreload(canonicalized_host, kPreloadedSTS, kNumPreloadedSTS); |
| |
| if (!entry) { |
| entry = GetHSTSPreload(canonicalized_host, kPreloadedSNISTS, |
| kNumPreloadedSNISTS); |
| } |
| |
| if (!entry) { |
| // We don't care to report pin failures for dynamic pins. |
| return; |
| } |
| |
| DCHECK(entry); |
| DCHECK(entry->pins.required_hashes); |
| DCHECK(entry->second_level_domain_name != DOMAIN_NOT_PINNED); |
| |
| UMA_HISTOGRAM_ENUMERATION("Net.PublicKeyPinFailureDomain", |
| entry->second_level_domain_name, DOMAIN_NUM_EVENTS); |
| #endif // !defined(__LB_SHELL__) |
| } |
| |
| // static |
| const char* TransportSecurityState::HashValueLabel( |
| const HashValue& hash_value) { |
| switch (hash_value.tag) { |
| case HASH_VALUE_SHA1: |
| return "sha1/"; |
| case HASH_VALUE_SHA256: |
| return "sha256/"; |
| default: |
| NOTREACHED(); |
| LOG(WARNING) << "Invalid fingerprint of unknown type " << hash_value.tag; |
| return "unknown/"; |
| } |
| } |
| |
| // 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 */; |
| } |
| |
| bool TransportSecurityState::GetStaticDomainState( |
| const std::string& canonicalized_host, |
| bool sni_enabled, |
| DomainState* out) { |
| DCHECK(CalledOnValidThread()); |
| #if !defined(__LB_SHELL__) |
| out->upgrade_mode = DomainState::MODE_FORCE_HTTPS; |
| out->include_subdomains = false; |
| |
| const bool is_build_timely = IsBuildTimely(); |
| |
| 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); |
| out->domain = DNSDomainToString(host_sub_chunk); |
| std::string hashed_host(HashHost(host_sub_chunk)); |
| if (forced_hosts_.find(hashed_host) != forced_hosts_.end()) { |
| *out = forced_hosts_[hashed_host]; |
| out->domain = DNSDomainToString(host_sub_chunk); |
| return true; |
| } |
| bool ret; |
| if (is_build_timely && |
| HasPreload(kPreloadedSTS, kNumPreloadedSTS, canonicalized_host, i, out, |
| &ret)) { |
| return ret; |
| } |
| if (sni_enabled && |
| is_build_timely && |
| HasPreload(kPreloadedSNISTS, kNumPreloadedSNISTS, canonicalized_host, i, |
| out, &ret)) { |
| return ret; |
| } |
| } |
| #endif // !defined(__LB_SHELL__) |
| return false; |
| } |
| |
| void TransportSecurityState::AddOrUpdateEnabledHosts( |
| const std::string& hashed_host, const DomainState& state) { |
| enabled_hosts_[hashed_host] = state; |
| } |
| |
| void TransportSecurityState::AddOrUpdateForcedHosts( |
| const std::string& hashed_host, const DomainState& state) { |
| forced_hosts_[hashed_host] = state; |
| } |
| |
| static std::string HashesToBase64String( |
| const HashValueVector& hashes) { |
| std::vector<std::string> hashes_strs; |
| for (HashValueVector::const_iterator |
| i = hashes.begin(); i != hashes.end(); i++) { |
| std::string s; |
| const std::string hash_str(reinterpret_cast<const char*>(i->data()), |
| i->size()); |
| base::Base64Encode(hash_str, &s); |
| hashes_strs.push_back(s); |
| } |
| |
| return JoinString(hashes_strs, ','); |
| } |
| |
| TransportSecurityState::DomainState::DomainState() |
| : upgrade_mode(MODE_FORCE_HTTPS), |
| created(base::Time::Now()), |
| include_subdomains(false) { |
| } |
| |
| TransportSecurityState::DomainState::~DomainState() { |
| } |
| |
| bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted( |
| const HashValueVector& hashes) 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()) { |
| LOG(ERROR) << "Rejecting empty public key chain for public-key-pinned " |
| "domain " << domain; |
| return false; |
| } |
| |
| if (HashesIntersect(bad_static_spki_hashes, hashes)) { |
| LOG(ERROR) << "Rejecting public key chain for domain " << domain |
| << ". Validated chain: " << HashesToBase64String(hashes) |
| << ", matches one or more bad hashes: " |
| << HashesToBase64String(bad_static_spki_hashes); |
| return false; |
| } |
| |
| // If there are no pins, then any valid chain is acceptable. |
| if (dynamic_spki_hashes.empty() && static_spki_hashes.empty()) |
| return true; |
| |
| if (HashesIntersect(dynamic_spki_hashes, hashes) || |
| HashesIntersect(static_spki_hashes, hashes)) { |
| return true; |
| } |
| |
| LOG(ERROR) << "Rejecting public key chain for domain " << domain |
| << ". Validated chain: " << HashesToBase64String(hashes) |
| << ", expected: " << HashesToBase64String(dynamic_spki_hashes) |
| << " or: " << HashesToBase64String(static_spki_hashes); |
| return false; |
| } |
| |
| bool TransportSecurityState::DomainState::ShouldRedirectHTTPToHTTPS() const { |
| return upgrade_mode == MODE_FORCE_HTTPS; |
| } |
| |
| bool TransportSecurityState::DomainState::Equals( |
| const DomainState& other) const { |
| // TODO(palmer): Implement this |
| (void) other; |
| return true; |
| } |
| |
| bool TransportSecurityState::DomainState::HasPins() const { |
| return static_spki_hashes.size() > 0 || |
| bad_static_spki_hashes.size() > 0 || |
| dynamic_spki_hashes.size() > 0; |
| } |
| |
| } // namespace |