blob: 29f892f55d1ff516a56f011c4e818be100200676 [file] [log] [blame]
// 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