// 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