// Copyright (c) 2009 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/strict_transport_security_state.h" #include "base/json_reader.h" #include "base/json_writer.h" #include "base/logging.h" #include "base/scoped_ptr.h" #include "base/sha2.h" #include "base/string_tokenizer.h" #include "base/string_util.h" #include "base/values.h" #include "googleurl/src/gurl.h" #include "net/base/base64.h" #include "net/base/dns_util.h" namespace net { StrictTransportSecurityState::StrictTransportSecurityState() : delegate_(NULL) { } void StrictTransportSecurityState::DidReceiveHeader(const GURL& url, const std::string& value) { int max_age; bool include_subdomains; if (!ParseHeader(value, &max_age, &include_subdomains)) return; base::Time current_time(base::Time::Now()); base::TimeDelta max_age_delta = base::TimeDelta::FromSeconds(max_age); base::Time expiry = current_time + max_age_delta; EnableHost(url.host(), expiry, include_subdomains); } void StrictTransportSecurityState::EnableHost(const std::string& host, base::Time expiry, bool include_subdomains) { const std::string canonicalised_host = CanonicaliseHost(host); if (canonicalised_host.empty()) return; char hashed[base::SHA256_LENGTH]; base::SHA256HashString(canonicalised_host, hashed, sizeof(hashed)); AutoLock lock(lock_); State state = {expiry, include_subdomains}; enabled_hosts_[std::string(hashed, sizeof(hashed))] = state; DirtyNotify(); } bool StrictTransportSecurityState::IsEnabledForHost(const std::string& host) { const std::string canonicalised_host = CanonicaliseHost(host); if (canonicalised_host.empty()) return false; base::Time current_time(base::Time::Now()); AutoLock lock(lock_); for (size_t i = 0; canonicalised_host[i]; i += canonicalised_host[i] + 1) { char hashed_domain[base::SHA256_LENGTH]; base::SHA256HashString(&canonicalised_host[i], &hashed_domain, sizeof(hashed_domain)); std::map::iterator j = enabled_hosts_.find(std::string(hashed_domain, sizeof(hashed_domain))); if (j == enabled_hosts_.end()) continue; if (current_time > j->second.expiry) { enabled_hosts_.erase(j); DirtyNotify(); continue; } // 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; } // "Strict-Transport-Security" ":" // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ] bool StrictTransportSecurityState::ParseHeader(const std::string& value, int* max_age, bool* include_subdomains) { DCHECK(max_age); DCHECK(include_subdomains); int max_age_candidate; 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 (!StringToInt(tokenizer.token(), &max_age_candidate)) return false; if (max_age_candidate < 0) 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 StrictTransportSecurityState::SetDelegate( StrictTransportSecurityState::Delegate* delegate) { AutoLock lock(lock_); 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::wstring HashedDomainToExternalString(const std::string& hashed) { std::string out; CHECK(Base64Encode(hashed, &out)); return ASCIIToWide(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::wstring& external) { std::string external_ascii = WideToASCII(external); std::string out; if (!Base64Decode(external_ascii, &out) || out.size() != base::SHA256_LENGTH) { return std::string(); } return out; } bool StrictTransportSecurityState::Serialise(std::string* output) { AutoLock lock(lock_); DictionaryValue toplevel; for (std::map::const_iterator i = enabled_hosts_.begin(); i != enabled_hosts_.end(); ++i) { DictionaryValue* state = new DictionaryValue; state->SetBoolean(L"include_subdomains", i->second.include_subdomains); state->SetReal(L"expiry", i->second.expiry.ToDoubleT()); toplevel.Set(HashedDomainToExternalString(i->first), state); } JSONWriter::Write(&toplevel, true /* pretty print */, output); return true; } bool StrictTransportSecurityState::Deserialise(const std::string& input) { AutoLock lock(lock_); enabled_hosts_.clear(); scoped_ptr value( JSONReader::Read(input, false /* do not allow trailing commas */)); if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) return false; DictionaryValue* dict_value = reinterpret_cast(value.get()); const base::Time current_time(base::Time::Now()); for (DictionaryValue::key_iterator i = dict_value->begin_keys(); i != dict_value->end_keys(); ++i) { DictionaryValue* state; if (!dict_value->GetDictionary(*i, &state)) continue; bool include_subdomains; double expiry; if (!state->GetBoolean(L"include_subdomains", &include_subdomains) || !state->GetReal(L"expiry", &expiry)) { continue; } base::Time expiry_time = base::Time::FromDoubleT(expiry); if (expiry_time <= current_time) continue; std::string hashed = ExternalStringToHashedDomain(*i); if (hashed.empty()) continue; State new_state = { expiry_time, include_subdomains }; enabled_hosts_[hashed] = new_state; } return true; } void StrictTransportSecurityState::DirtyNotify() { if (delegate_) delegate_->StateIsDirty(this); } // static std::string StrictTransportSecurityState::CanonicaliseHost( 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)) { NOTREACHED(); return std::string(); } for (size_t i = 0; new_host[i]; i += new_host[i] + 1) { const unsigned label_length = static_cast(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; } } // namespace