| // 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/base64.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/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/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<std::string, State>::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(base::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 (!base::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<std::string, State>::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); |
| } |
| |
| base::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> 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()); |
| |
| 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; |
| 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<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; |
| } |
| |
| } // namespace |