// Copyright (c) 2011 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" #include "base/base64.h" #include "base/command_line.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/sha1.h" #include "base/string_number_conversions.h" #include "base/string_split.h" #include "base/string_tokenizer.h" #include "base/string_util.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/net_switches.h" namespace net { const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365; // 1 year TransportSecurityState::TransportSecurityState() : delegate_(NULL) { } static std::string HashHost(const std::string& canonicalized_host) { char hashed[crypto::SHA256_LENGTH]; crypto::SHA256HashString(canonicalized_host, hashed, sizeof(hashed)); return std::string(hashed, sizeof(hashed)); } void TransportSecurityState::EnableHost(const std::string& host, const DomainState& state) { const std::string canonicalized_host = CanonicalizeHost(host); if (canonicalized_host.empty()) return; // TODO(cevans) -- we likely want to permit a host to override a built-in, // for at least the case where the override is stricter (i.e. includes // subdomains, or includes certificate pinning). DomainState temp; if (IsPreloadedSTS(canonicalized_host, true, &temp)) return; // Use the original creation date if we already have this host. DomainState state_copy(state); DomainState existing_state; if (IsEnabledForHost(&existing_state, host, true)) state_copy.created = existing_state.created; // We don't store these values. state_copy.preloaded = false; state_copy.domain.clear(); enabled_hosts_[HashHost(canonicalized_host)] = state_copy; DirtyNotify(); } bool TransportSecurityState::DeleteHost(const std::string& host) { 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; } // IncludeNUL converts a char* to a std::string and includes the terminating // NUL in the result. static std::string IncludeNUL(const char* in) { return std::string(in, strlen(in) + 1); } bool TransportSecurityState::IsEnabledForHost(DomainState* result, const std::string& host, bool sni_available) { const std::string canonicalized_host = CanonicalizeHost(host); if (canonicalized_host.empty()) return false; if (IsPreloadedSTS(canonicalized_host, sni_available, result)) return result->mode != DomainState::MODE_NONE; *result = DomainState(); base::Time current_time(base::Time::Now()); for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) { std::string hashed_domain(HashHost(IncludeNUL(&canonicalized_host[i]))); std::map<std::string, DomainState>::iterator j = enabled_hosts_.find(hashed_domain); if (j == enabled_hosts_.end()) continue; if (current_time > j->second.expiry) { enabled_hosts_.erase(j); DirtyNotify(); continue; } *result = j->second; result->domain = DNSDomainToString( canonicalized_host.substr(i, canonicalized_host.size() - i)); // If we matched the domain exactly, it doesn't matter what the value of // include_subdomains is. if (i == 0) return true; return j->second.include_subdomains; } return false; } void TransportSecurityState::DeleteSince(const base::Time& time) { 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; } // "Strict-Transport-Security" ":" // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ] bool TransportSecurityState::ParseHeader(const std::string& value, int* max_age, bool* include_subdomains) { DCHECK(max_age); DCHECK(include_subdomains); int max_age_candidate = 0; enum ParserState { START, AFTER_MAX_AGE_LABEL, AFTER_MAX_AGE_EQUALS, AFTER_MAX_AGE, AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER, AFTER_INCLUDE_SUBDOMAINS, } state = START; StringTokenizer tokenizer(value, " \t=;"); tokenizer.set_options(StringTokenizer::RETURN_DELIMS); while (tokenizer.GetNext()) { DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1); switch (state) { case START: if (IsAsciiWhitespace(*tokenizer.token_begin())) continue; if (!LowerCaseEqualsASCII(tokenizer.token(), "max-age")) return false; state = AFTER_MAX_AGE_LABEL; break; case AFTER_MAX_AGE_LABEL: if (IsAsciiWhitespace(*tokenizer.token_begin())) continue; if (*tokenizer.token_begin() != '=') return false; DCHECK(tokenizer.token().length() == 1); state = AFTER_MAX_AGE_EQUALS; break; case AFTER_MAX_AGE_EQUALS: if (IsAsciiWhitespace(*tokenizer.token_begin())) continue; if (!MaxAgeToInt(tokenizer.token_begin(), tokenizer.token_end(), &max_age_candidate)) return false; state = AFTER_MAX_AGE; break; case AFTER_MAX_AGE: if (IsAsciiWhitespace(*tokenizer.token_begin())) continue; if (*tokenizer.token_begin() != ';') return false; state = AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER; break; case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER: if (IsAsciiWhitespace(*tokenizer.token_begin())) continue; if (!LowerCaseEqualsASCII(tokenizer.token(), "includesubdomains")) return false; state = AFTER_INCLUDE_SUBDOMAINS; break; case AFTER_INCLUDE_SUBDOMAINS: if (!IsAsciiWhitespace(*tokenizer.token_begin())) return false; break; default: NOTREACHED(); } } // We've consumed all the input. Let's see what state we ended up in. switch (state) { case START: case AFTER_MAX_AGE_LABEL: case AFTER_MAX_AGE_EQUALS: return false; case AFTER_MAX_AGE: *max_age = max_age_candidate; *include_subdomains = false; return true; case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER: return false; case AFTER_INCLUDE_SUBDOMAINS: *max_age = max_age_candidate; *include_subdomains = true; return true; default: NOTREACHED(); return false; } } void TransportSecurityState::SetDelegate( TransportSecurityState::Delegate* delegate) { delegate_ = delegate; } // This function converts the binary hashes, which we store in // |enabled_hosts_|, to a base64 string which we can include in a JSON file. static std::string HashedDomainToExternalString(const std::string& hashed) { std::string out; CHECK(base::Base64Encode(hashed, &out)); return out; } // This inverts |HashedDomainToExternalString|, above. It turns an external // string (from a JSON file) into an internal (binary) string. static std::string ExternalStringToHashedDomain(const std::string& external) { std::string out; if (!base::Base64Decode(external, &out) || out.size() != crypto::SHA256_LENGTH) { return std::string(); } return out; } bool TransportSecurityState::Serialise(std::string* output) { DictionaryValue toplevel; for (std::map<std::string, DomainState>::const_iterator i = enabled_hosts_.begin(); i != enabled_hosts_.end(); ++i) { DictionaryValue* state = new DictionaryValue; state->SetBoolean("include_subdomains", i->second.include_subdomains); state->SetDouble("created", i->second.created.ToDoubleT()); state->SetDouble("expiry", i->second.expiry.ToDoubleT()); switch (i->second.mode) { case DomainState::MODE_STRICT: state->SetString("mode", "strict"); break; case DomainState::MODE_OPPORTUNISTIC: state->SetString("mode", "opportunistic"); break; case DomainState::MODE_SPDY_ONLY: state->SetString("mode", "spdy-only"); break; default: NOTREACHED() << "DomainState with unknown mode"; delete state; continue; } ListValue* pins = new ListValue; for (std::vector<SHA1Fingerprint>::const_iterator j = i->second.public_key_hashes.begin(); j != i->second.public_key_hashes.end(); ++j) { std::string hash_str(reinterpret_cast<const char*>(j->data), sizeof(j->data)); std::string b64; base::Base64Encode(hash_str, &b64); pins->Append(new StringValue("sha1/" + b64)); } state->Set("public_key_hashes", pins); toplevel.Set(HashedDomainToExternalString(i->first), state); } base::JSONWriter::Write(&toplevel, true /* pretty print */, output); return true; } bool TransportSecurityState::LoadEntries(const std::string& input, bool* dirty) { enabled_hosts_.clear(); return Deserialise(input, dirty, &enabled_hosts_); } // static bool TransportSecurityState::Deserialise( const std::string& input, bool* dirty, std::map<std::string, DomainState>* out) { scoped_ptr<Value> value( base::JSONReader::Read(input, false /* do not allow trailing commas */)); if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) return false; DictionaryValue* dict_value = reinterpret_cast<DictionaryValue*>(value.get()); const base::Time current_time(base::Time::Now()); bool dirtied = false; for (DictionaryValue::key_iterator i = dict_value->begin_keys(); i != dict_value->end_keys(); ++i) { DictionaryValue* state; if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &state)) continue; bool include_subdomains; std::string mode_string; double created; double expiry; if (!state->GetBoolean("include_subdomains", &include_subdomains) || !state->GetString("mode", &mode_string) || !state->GetDouble("expiry", &expiry)) { continue; } ListValue* pins_list = NULL; std::vector<SHA1Fingerprint> public_key_hashes; if (state->GetList("public_key_hashes", &pins_list)) { size_t num_pins = pins_list->GetSize(); for (size_t i = 0; i < num_pins; ++i) { std::string type_and_base64; std::string hash_str; SHA1Fingerprint hash; if (pins_list->GetString(i, &type_and_base64) && type_and_base64.find("sha1/") == 0 && base::Base64Decode( type_and_base64.substr(5, type_and_base64.size() - 5), &hash_str) && hash_str.size() == base::SHA1_LENGTH) { memcpy(hash.data, hash_str.data(), sizeof(hash.data)); public_key_hashes.push_back(hash); } } } DomainState::Mode mode; if (mode_string == "strict") { mode = DomainState::MODE_STRICT; } else if (mode_string == "opportunistic") { mode = DomainState::MODE_OPPORTUNISTIC; } else if (mode_string == "spdy-only") { mode = DomainState::MODE_SPDY_ONLY; } else if (mode_string == "none") { mode = DomainState::MODE_NONE; } else { LOG(WARNING) << "Unknown TransportSecurityState mode string found: " << mode_string; continue; } base::Time expiry_time = base::Time::FromDoubleT(expiry); base::Time created_time; if (state->GetDouble("created", &created)) { created_time = base::Time::FromDoubleT(created); } else { // We're migrating an old entry with no creation date. Make sure we // write the new date back in a reasonable time frame. dirtied = true; created_time = base::Time::Now(); } if (expiry_time <= current_time) { // Make sure we dirty the state if we drop an entry. dirtied = true; continue; } std::string hashed = ExternalStringToHashedDomain(*i); if (hashed.empty()) { dirtied = true; continue; } DomainState new_state; new_state.mode = mode; new_state.created = created_time; new_state.expiry = expiry_time; new_state.include_subdomains = include_subdomains; new_state.public_key_hashes = public_key_hashes; (*out)[hashed] = new_state; } *dirty = dirtied; return true; } TransportSecurityState::~TransportSecurityState() { } void TransportSecurityState::DirtyNotify() { 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; } // IsPreloadedSTS returns true if the canonicalized hostname should always be // considered to have STS enabled. // static bool TransportSecurityState::IsPreloadedSTS( const std::string& canonicalized_host, bool sni_available, DomainState* out) { out->preloaded = true; out->mode = DomainState::MODE_STRICT; out->created = base::Time::FromTimeT(0); out->expiry = out->created; out->include_subdomains = false; std::map<std::string, DomainState> hosts; std::string cmd_line_hsts #ifdef ANDROID ; #else = CommandLine::ForCurrentProcess()->GetSwitchValueASCII( switches::kHstsHosts); #endif if (!cmd_line_hsts.empty()) { bool dirty; Deserialise(cmd_line_hsts, &dirty, &hosts); } // In the medium term this list is likely to just be hardcoded here. This, // slightly odd, form removes the need for additional relocations records. static const struct { uint8 length; bool include_subdomains; char dns_name[30]; } kPreloadedSTS[] = { {16, false, "\003www\006paypal\003com"}, {16, false, "\003www\006elanex\003biz"}, {12, true, "\006jottit\003com"}, {19, true, "\015sunshinepress\003org"}, {21, false, "\003www\013noisebridge\003net"}, {10, false, "\004neg9\003org"}, {12, true, "\006riseup\003net"}, {11, false, "\006factor\002cc"}, {22, false, "\007members\010mayfirst\003org"}, {22, false, "\007support\010mayfirst\003org"}, {17, false, "\002id\010mayfirst\003org"}, {20, false, "\005lists\010mayfirst\003org"}, {19, true, "\015splendidbacon\003com"}, {19, true, "\006health\006google\003com"}, {21, true, "\010checkout\006google\003com"}, {19, true, "\006chrome\006google\003com"}, {26, false, "\006latest\006chrome\006google\003com"}, {28, false, "\016aladdinschools\007appspot\003com"}, {14, true, "\011ottospora\002nl"}, {17, true, "\004docs\006google\003com"}, {18, true, "\005sites\006google\003com"}, {25, true, "\014spreadsheets\006google\003com"}, {22, false, "\011appengine\006google\003com"}, {25, false, "\003www\017paycheckrecords\003com"}, {20, true, "\006market\007android\003com"}, {14, false, "\010lastpass\003com"}, {18, false, "\003www\010lastpass\003com"}, {14, true, "\010keyerror\003com"}, {22, true, "\011encrypted\006google\003com"}, {13, false, "\010entropia\002de"}, {17, false, "\003www\010entropia\002de"}, {21, true, "\010accounts\006google\003com"}, #if defined(OS_CHROMEOS) {17, true, "\004mail\006google\003com"}, {13, false, "\007twitter\003com"}, {17, false, "\003www\007twitter\003com"}, {17, false, "\003api\007twitter\003com"}, {17, false, "\003dev\007twitter\003com"}, {22, false, "\010business\007twitter\003com"}, #endif }; static const size_t kNumPreloadedSTS = ARRAYSIZE_UNSAFE(kPreloadedSTS); static const struct { uint8 length; bool include_subdomains; char dns_name[30]; } kPreloadedSNISTS[] = { {11, false, "\005gmail\003com"}, {16, false, "\012googlemail\003com"}, {15, false, "\003www\005gmail\003com"}, {20, false, "\003www\012googlemail\003com"}, }; static const size_t kNumPreloadedSNISTS = ARRAYSIZE_UNSAFE(kPreloadedSNISTS); 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 (hosts.find(hashed_host) != hosts.end()) { *out = hosts[hashed_host]; out->domain = DNSDomainToString(host_sub_chunk); out->preloaded = true; return true; } for (size_t j = 0; j < kNumPreloadedSTS; j++) { if (kPreloadedSTS[j].length == canonicalized_host.size() - i && memcmp(kPreloadedSTS[j].dns_name, &canonicalized_host[i], kPreloadedSTS[j].length) == 0) { if (!kPreloadedSTS[j].include_subdomains && i != 0) return false; out->include_subdomains = kPreloadedSTS[j].include_subdomains; return true; } } if (sni_available) { for (size_t j = 0; j < kNumPreloadedSNISTS; j++) { if (kPreloadedSNISTS[j].length == canonicalized_host.size() - i && memcmp(kPreloadedSNISTS[j].dns_name, &canonicalized_host[i], kPreloadedSNISTS[j].length) == 0) { if (!kPreloadedSNISTS[j].include_subdomains && i != 0) return false; out->include_subdomains = kPreloadedSNISTS[j].include_subdomains; return true; } } } } return false; } static std::string HashesToBase64String( const std::vector<net::SHA1Fingerprint>& hashes) { std::vector<std::string> hashes_strs; for (std::vector<net::SHA1Fingerprint>::const_iterator i = hashes.begin(); i != hashes.end(); i++) { std::string s; const std::string hash_str(reinterpret_cast<const char*>(i->data), sizeof(i->data)); base::Base64Encode(hash_str, &s); hashes_strs.push_back(s); } return JoinString(hashes_strs, ','); } TransportSecurityState::DomainState::DomainState() : mode(MODE_STRICT), created(base::Time::Now()), include_subdomains(false), preloaded(false) { } TransportSecurityState::DomainState::~DomainState() { } bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted( const std::vector<net::SHA1Fingerprint>& hashes) { if (public_key_hashes.empty()) return true; for (std::vector<net::SHA1Fingerprint>::const_iterator i = hashes.begin(); i != hashes.end(); ++i) { for (std::vector<net::SHA1Fingerprint>::const_iterator j = public_key_hashes.begin(); j != public_key_hashes.end(); ++j) { if (i->Equals(*j)) return true; } } LOG(ERROR) << "Rejecting public key chain for domain " << domain << ". Validated chain: " << HashesToBase64String(hashes) << ", expected: " << HashesToBase64String(public_key_hashes); return false; } } // namespace