diff options
author | palmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-01-05 02:26:15 +0000 |
---|---|---|
committer | palmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-01-05 02:26:15 +0000 |
commit | fecef22a0e8711a8f6c3309540f27098eeeabc57 (patch) | |
tree | c728ffcbb7f8bec5843e53eeee3aa53915d84d4f | |
parent | ddc9c9e6708ceeb6428455546b0550715e5fb49f (diff) | |
download | chromium_src-fecef22a0e8711a8f6c3309540f27098eeeabc57.zip chromium_src-fecef22a0e8711a8f6c3309540f27098eeeabc57.tar.gz chromium_src-fecef22a0e8711a8f6c3309540f27098eeeabc57.tar.bz2 |
Implement HTTP header-based public key pinning.
Upon receipt of the Public-Key-Pins header, check the syntax and the pins, and
set the pins in the TransportSecurityState. From then on, use these new
dynamic pins to validate TLS connections: as with preloaded pins, refuse to
connect to TLS servers that fail the pin check.
The Public-Key-Pins header is defined in an IETF Internet-Draft, and
discussed on the websec@ietf.org mailing list.
Clarified TransportSecurityState member function and field documentation.
Also: Minor "gcl lint" repairs, and a new typedef
std::vector<net::SHA1Fingerprint> FingerprintVector used everywhere relevant.
BUG=78369
TEST=net_unittests TransportSecurityStateTest.*, browser_tests NetInternalsTest.*
Review URL: http://codereview.chromium.org/8082016
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@116443 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/resources/net_internals/hsts_view.js | 25 | ||||
-rw-r--r-- | chrome/browser/ui/webui/net_internals_ui.cc | 54 | ||||
-rw-r--r-- | chrome/test/data/webui/net_internals/hsts_view.js | 23 | ||||
-rw-r--r-- | net/base/transport_security_state.cc | 403 | ||||
-rw-r--r-- | net/base/transport_security_state.h | 112 | ||||
-rw-r--r-- | net/base/transport_security_state_unittest.cc | 264 | ||||
-rw-r--r-- | net/url_request/url_request_http_job.cc | 41 | ||||
-rw-r--r-- | net/url_request/url_request_http_job.h | 7 |
8 files changed, 759 insertions, 170 deletions
diff --git a/chrome/browser/resources/net_internals/hsts_view.js b/chrome/browser/resources/net_internals/hsts_view.js index 9e69e33..f6420de 100644 --- a/chrome/browser/resources/net_internals/hsts_view.js +++ b/chrome/browser/resources/net_internals/hsts_view.js @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -7,6 +7,8 @@ * use HTTPS. See http://dev.chromium.org/sts * * This UI allows a user to query and update the browser's list of HSTS domains. + * It also allows users to query and update the browser's list of public key + * pins. */ var HSTSView = (function() { @@ -132,8 +134,25 @@ var HSTSView = (function() { addTextNode(this.queryOutputDiv_, ' pubkey_hashes:'); t = addNode(this.queryOutputDiv_, 'tt'); - t.textContent = result.public_key_hashes; - + // |public_key_hashes| is an old synonym for what is now + // |preloaded_spki_hashes|. Look for both, and also for + // |dynamic_spki_hashes|. + if (typeof result.public_key_hashes === 'undefined') + result.public_key_hashes = ''; + if (typeof result.preloaded_spki_hashes === 'undefined') + result.preloaded_spki_hashes = ''; + if (typeof result.dynamic_spki_hashes === 'undefined') + result.dynamic_spki_hashes = ''; + + var hashes = []; + if (result.public_key_hashes) + hashes.push(result.public_key_hashes); + if (result.preloaded_spki_hashes) + hashes.push(result.preloaded_spki_hashes); + if (result.dynamic_spki_hashes) + hashes.push(result.dynamic_spki_hashes); + + t.textContent = hashes.join(","); yellowFade(this.queryOutputDiv_); } }; diff --git a/chrome/browser/ui/webui/net_internals_ui.cc b/chrome/browser/ui/webui/net_internals_ui.cc index 4f9ede9..7a118a0 100644 --- a/chrome/browser/ui/webui/net_internals_ui.cc +++ b/chrome/browser/ui/webui/net_internals_ui.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -53,6 +53,7 @@ #include "net/base/net_errors.h" #include "net/base/net_util.h" #include "net/base/sys_addrinfo.h" +#include "net/base/transport_security_state.h" #include "net/base/x509_cert_types.h" #include "net/disk_cache/disk_cache.h" #include "net/http/http_cache.h" @@ -1005,6 +1006,21 @@ void NetInternalsMessageHandler::IOThreadImpl::OnStartConnectionTests( connection_tester_->RunAllTests(url); } +void SPKIHashesToString(const net::FingerprintVector& hashes, + std::string* string) { + for (net::FingerprintVector::const_iterator + i = hashes.begin(); i != hashes.end(); ++i) { + base::StringPiece hash_str(reinterpret_cast<const char*>(i->data), + arraysize(i->data)); + std::string encoded; + base::Base64Encode(hash_str, &encoded); + + if (i != hashes.begin()) + *string += ","; + *string += "sha1/" + encoded; + } +} + void NetInternalsMessageHandler::IOThreadImpl::OnHSTSQuery( const ListValue* list) { // |list| should be: [<domain to query>]. @@ -1030,20 +1046,17 @@ void NetInternalsMessageHandler::IOThreadImpl::OnHSTSQuery( result->SetBoolean("subdomains", state.include_subdomains); result->SetBoolean("preloaded", state.preloaded); result->SetString("domain", state.domain); + result->SetDouble("expiry", state.expiry.ToDoubleT()); + result->SetDouble("dynamic_spki_hashes_expiry", + state.dynamic_spki_hashes_expiry.ToDoubleT()); + + std::string hashes; + SPKIHashesToString(state.preloaded_spki_hashes, &hashes); + result->SetString("preloaded_spki_hashes", hashes); - std::vector<std::string> parts; - for (std::vector<net::SHA1Fingerprint>::const_iterator - i = state.public_key_hashes.begin(); - i != state.public_key_hashes.end(); i++) { - std::string part = "sha1/"; - std::string hash_str(reinterpret_cast<const char*>(i->data), - sizeof(i->data)); - std::string b64; - base::Base64Encode(hash_str, &b64); - part += b64; - parts.push_back(part); - } - result->SetString("public_key_hashes", JoinString(parts, ',')); + hashes.clear(); + SPKIHashesToString(state.dynamic_spki_hashes, &hashes); + result->SetString("dynamic_spki_hashes", hashes); } } } @@ -1074,7 +1087,6 @@ void NetInternalsMessageHandler::IOThreadImpl::OnHSTSAdd( net::TransportSecurityState::DomainState state; state.expiry = state.created + base::TimeDelta::FromDays(1000); state.include_subdomains = include_subdomains; - state.public_key_hashes.clear(); if (!hashes_str.empty()) { std::vector<std::string> type_and_b64s; base::SplitString(hashes_str, ',', &type_and_b64s); @@ -1082,17 +1094,11 @@ void NetInternalsMessageHandler::IOThreadImpl::OnHSTSAdd( i = type_and_b64s.begin(); i != type_and_b64s.end(); i++) { std::string type_and_b64; RemoveChars(*i, " \t\r\n", &type_and_b64); - if (type_and_b64.find("sha1/") != 0) - continue; - std::string b64 = type_and_b64.substr(5, type_and_b64.size() - 5); - std::string hash_str; - if (!base::Base64Decode(b64, &hash_str)) - continue; net::SHA1Fingerprint hash; - if (hash_str.size() != sizeof(hash.data)) + if (!net::TransportSecurityState::ParsePin(type_and_b64, &hash)) continue; - memcpy(hash.data, hash_str.data(), sizeof(hash.data)); - state.public_key_hashes.push_back(hash); + + state.dynamic_spki_hashes.push_back(hash); } } diff --git a/chrome/test/data/webui/net_internals/hsts_view.js b/chrome/test/data/webui/net_internals/hsts_view.js index be9f973..c47b4ca 100644 --- a/chrome/test/data/webui/net_internals/hsts_view.js +++ b/chrome/test/data/webui/net_internals/hsts_view.js @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -110,7 +110,26 @@ CheckQueryResultTask.prototype = { checkSuccess_: function(result) { expectEquals(QueryResultType.SUCCESS, this.queryResultType_); expectEquals(this.subdomains_, result.subdomains); - expectEquals(this.publicKeyHashes_, result.public_key_hashes); + + // |public_key_hashes| is an old synonym for what is now + // |preloaded_spki_hashes|. Look for both, and also for + // |dynamic_spki_hashes|. + if (typeof result.public_key_hashes === 'undefined') + result.public_key_hashes = ''; + if (typeof result.preloaded_spki_hashes === 'undefined') + result.preloaded_spki_hashes = ''; + if (typeof result.dynamic_spki_hashes === 'undefined') + result.dynamic_spki_hashes = ''; + + var hashes = []; + if (result.public_key_hashes) + hashes.push(result.public_key_hashes); + if (result.preloaded_spki_hashes) + hashes.push(result.preloaded_spki_hashes); + if (result.dynamic_spki_hashes) + hashes.push(result.dynamic_spki_hashes); + + expectEquals(this.publicKeyHashes_, hashes.join(",")); // Verify that the domain appears somewhere in the displayed text. outputText = $(HSTSView.QUERY_OUTPUT_DIV_ID).innerText; diff --git a/net/base/transport_security_state.cc b/net/base/transport_security_state.cc index ecafe47..ddd50f5 100644 --- a/net/base/transport_security_state.cc +++ b/net/base/transport_security_state.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -8,14 +8,16 @@ #include <openssl/ecdsa.h> #include <openssl/ssl.h> #else // !defined(USE_OPENSSL) -#include <nspr.h> - #include <cryptohi.h> #include <hasht.h> #include <keyhi.h> #include <pk11pub.h> +#include <nspr.h> #endif +#include <algorithm> +#include <utility> + #include "base/base64.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" @@ -26,12 +28,17 @@ #include "base/string_number_conversions.h" #include "base/string_tokenizer.h" #include "base/string_util.h" +#include "base/time.h" #include "base/utf_string_conversions.h" #include "base/values.h" #include "crypto/sha2.h" #include "googleurl/src/gurl.h" +#include "net/base/asn1_util.h" #include "net/base/dns_util.h" #include "net/base/public_key_hashes.h" +#include "net/base/ssl_info.h" +#include "net/base/x509_certificate.h" +#include "net/http/http_util.h" #if defined(USE_OPENSSL) #include "crypto/openssl_util.h" @@ -68,18 +75,17 @@ void TransportSecurityState::EnableHost(const std::string& 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 out; - if (IsPreloadedSTS(canonicalized_host, true, &out) && - canonicalized_host == CanonicalizeHost(out.domain)) { + // Only override a preloaded state if the new state describes a more strict + // policy. TODO(palmer): Reconsider this? + DomainState existing_state; + if (IsPreloadedSTS(canonicalized_host, true, &existing_state) && + canonicalized_host == CanonicalizeHost(existing_state.domain) && + existing_state.IsMoreStrict(state)) { return; } // Use the original creation date if we already have this host. DomainState state_copy(state); - DomainState existing_state; if (GetDomainState(&existing_state, host, true) && !existing_state.created.is_null()) { state_copy.created = existing_state.created; @@ -116,7 +122,8 @@ bool TransportSecurityState::HasPinsForHost(DomainState* result, DCHECK(CalledOnValidThread()); return HasMetadata(result, host, sni_available) && - !result->public_key_hashes.empty(); + (!result->dynamic_spki_hashes.empty() || + !result->preloaded_spki_hashes.empty()); } bool TransportSecurityState::GetDomainState(DomainState* result, @@ -133,7 +140,6 @@ bool TransportSecurityState::HasMetadata(DomainState* result, DCHECK(CalledOnValidThread()); *result = DomainState(); - const std::string canonicalized_host = CanonicalizeHost(host); if (canonicalized_host.empty()) return false; @@ -155,7 +161,8 @@ bool TransportSecurityState::HasMetadata(DomainState* result, if (j == enabled_hosts_.end()) continue; - if (current_time > j->second.expiry) { + if (current_time > j->second.expiry && + current_time > j->second.dynamic_spki_hashes_expiry) { enabled_hosts_.erase(j); DirtyNotify(); continue; @@ -212,6 +219,209 @@ static bool MaxAgeToInt(std::string::const_iterator begin, return true; } +// Strip, Split, StringPair, and ParsePins are private implementation details +// of ParsePinsHeader(std::string&, DomainState&). +static std::string Strip(const std::string& source) { + if (source.empty()) + return source; + + std::string::const_iterator start = source.begin(); + std::string::const_iterator end = source.end(); + HttpUtil::TrimLWS(&start, &end); + return std::string(start, end); +} + +typedef std::pair<std::string, std::string> StringPair; + +static StringPair Split(const std::string& source, char delimiter) { + StringPair pair; + size_t point = source.find(delimiter); + + pair.first = source.substr(0, point); + if (std::string::npos != point) + pair.second = source.substr(point + 1); + + return pair; +} + +// TODO(palmer): Support both sha256 and sha1. This will require additional +// infrastructure code changes and can come in a later patch. +// +// static +bool TransportSecurityState::ParsePin(const std::string& value, + SHA1Fingerprint* out) { + StringPair slash = Split(Strip(value), '/'); + if (slash.first != "sha1") + return false; + + std::string decoded; + if (!base::Base64Decode(slash.second, &decoded) || + decoded.size() != arraysize(out->data)) { + return false; + } + + memcpy(out->data, decoded.data(), arraysize(out->data)); + return true; +} + +static bool ParseAndAppendPin(const std::string& value, + FingerprintVector* fingerprints) { + // The base64'd fingerprint MUST be a quoted-string. 20 bytes base64'd is 28 + // characters; 32 bytes base64'd is 44 characters. TODO(palmer): Support + // SHA256. + size_t size = value.size(); + if (size != 30 || value[0] != '"' || value[size - 1] != '"') + return false; + + std::string unquoted = HttpUtil::Unquote(value); + std::string decoded; + SHA1Fingerprint fp; + + if (!base::Base64Decode(unquoted, &decoded) || + decoded.size() != arraysize(fp.data)) { + return false; + } + + memcpy(fp.data, decoded.data(), arraysize(fp.data)); + fingerprints->push_back(fp); + return true; +} + +// static +bool TransportSecurityState::GetPublicKeyHash( + const X509Certificate& cert, SHA1Fingerprint* spki_hash) { + std::string der_bytes; + if (!X509Certificate::GetDEREncoded(cert.os_cert_handle(), &der_bytes)) + return false; + + base::StringPiece spki; + if (!asn1::ExtractSPKIFromDERCert(der_bytes, &spki)) + return false; + + base::SHA1HashBytes(reinterpret_cast<const unsigned char*>(spki.data()), + spki.size(), spki_hash->data); + + return true; +} + +struct FingerprintsEqualPredicate { + explicit FingerprintsEqualPredicate(const SHA1Fingerprint& fingerprint) : + fingerprint_(fingerprint) {} + + bool operator()(const SHA1Fingerprint& other) const { + return fingerprint_.Equals(other); + } + + const SHA1Fingerprint& fingerprint_; +}; + +// Returns true iff there is an item in |pins| which is not present in +// |from_cert_chain|. Such an SPKI hash is called a "backup pin". +static bool IsBackupPinPresent(const FingerprintVector& pins, + const FingerprintVector& from_cert_chain) { + for (FingerprintVector::const_iterator + i = pins.begin(); i != pins.end(); ++i) { + FingerprintVector::const_iterator j = + std::find_if(from_cert_chain.begin(), from_cert_chain.end(), + FingerprintsEqualPredicate(*i)); + if (j == from_cert_chain.end()) + return true; + } + + return false; +} + +static bool HashesIntersect(const FingerprintVector& a, + const FingerprintVector& b) { + for (FingerprintVector::const_iterator + i = a.begin(); i != a.end(); ++i) { + FingerprintVector::const_iterator j = + std::find_if(b.begin(), b.end(), FingerprintsEqualPredicate(*i)); + if (j != b.end()) + return true; + } + + return false; +} + +// Returns true iff |pins| contains both a live and a backup pin. A live pin +// is a pin whose SPKI is present in the certificate chain in |ssl_info|. A +// backup pin is a pin intended for disaster recovery, not day-to-day use, and +// thus must be absent from the certificate chain. The Public-Key-Pins header +// specification requires both. +static bool IsPinListValid(const FingerprintVector& pins, + const SSLInfo& ssl_info) { + if (pins.size() < 2) + return false; + + const FingerprintVector& from_cert_chain = ssl_info.public_key_hashes; + if (from_cert_chain.empty()) + return false; + + return IsBackupPinPresent(pins, from_cert_chain) && + HashesIntersect(pins, from_cert_chain); +} + +// "Public-Key-Pins" ":" +// "max-age" "=" delta-seconds ";" +// "pin-" algo "=" base64 [ ";" ... ] +// +// static +bool TransportSecurityState::ParsePinsHeader(const std::string& value, + const SSLInfo& ssl_info, + DomainState* state) { + bool parsed_max_age = false; + int max_age = 0; + FingerprintVector pins; + + std::string source = value; + + while (!source.empty()) { + StringPair semicolon = Split(source, ';'); + semicolon.first = Strip(semicolon.first); + semicolon.second = Strip(semicolon.second); + StringPair equals = Split(semicolon.first, '='); + equals.first = Strip(equals.first); + equals.second = Strip(equals.second); + + if (LowerCaseEqualsASCII(equals.first, "max-age")) { + if (equals.second.empty() || + !MaxAgeToInt(equals.second.begin(), equals.second.end(), &max_age)) { + return false; + } + if (max_age > kMaxHSTSAgeSecs) + max_age = kMaxHSTSAgeSecs; + parsed_max_age = true; + } else if (LowerCaseEqualsASCII(equals.first, "pin-sha1")) { + if (!ParseAndAppendPin(equals.second, &pins)) + return false; + } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) { + // TODO(palmer) + } else { + // Silently ignore unknown directives for forward compatibility. + } + + source = semicolon.second; + } + + if (!parsed_max_age || !IsPinListValid(pins, ssl_info)) + return false; + + state->max_age = max_age; + state->dynamic_spki_hashes_expiry = + base::Time::Now() + base::TimeDelta::FromSeconds(max_age); + + state->dynamic_spki_hashes.clear(); + if (max_age > 0) { + for (FingerprintVector::const_iterator i = pins.begin(); + i != pins.end(); i++) { + state->dynamic_spki_hashes.push_back(*i); + } + } + + return true; +} + // "Strict-Transport-Security" ":" // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ] // @@ -346,12 +556,12 @@ static bool ParseTags(base::StringPiece* in, TagMap *out) { #error assumes little endian #endif - if (in->size() < sizeof(uint16)) + uint16 num_tags_16; + if (in->size() < sizeof(num_tags_16)) return false; - uint16 num_tags_16; - memcpy(&num_tags_16, in->data(), sizeof(uint16)); - in->remove_prefix(sizeof(uint16)); + memcpy(&num_tags_16, in->data(), sizeof(num_tags_16)); + in->remove_prefix(sizeof(num_tags_16)); unsigned num_tags = num_tags_16; if (in->size() < 6 * num_tags) @@ -529,7 +739,7 @@ static const uint32 kTagSPIN = 0x4e495053; bool TransportSecurityState::ParseSidePin( const base::StringPiece& leaf_spki, const base::StringPiece& in_side_info, - std::vector<SHA1Fingerprint> *out_pub_key_hash) { + FingerprintVector* out_pub_key_hash) { base::StringPiece side_info(in_side_info); TagMap outer; @@ -607,16 +817,34 @@ static std::string ExternalStringToHashedDomain(const std::string& external) { return out; } +static ListValue* SPKIHashesToListValue(const FingerprintVector& hashes) { + ListValue* pins = new ListValue; + + for (FingerprintVector::const_iterator i = hashes.begin(); + i != hashes.end(); ++i) { + std::string hash_str(reinterpret_cast<const char*>(i->data), + sizeof(i->data)); + std::string b64; + base::Base64Encode(hash_str, &b64); + pins->Append(new StringValue("sha1/" + b64)); + } + + return pins; +} + bool TransportSecurityState::Serialise(std::string* output) { DCHECK(CalledOnValidThread()); DictionaryValue toplevel; + base::Time now = base::Time::Now(); 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()); + state->SetDouble("dynamic_spki_hashes_expiry", + i->second.dynamic_spki_hashes_expiry.ToDoubleT()); switch (i->second.mode) { case DomainState::MODE_STRICT: @@ -625,23 +853,22 @@ bool TransportSecurityState::Serialise(std::string* output) { case DomainState::MODE_SPDY_ONLY: state->SetString("mode", "spdy-only"); break; + case DomainState::MODE_PINNING_ONLY: + state->SetString("mode", "pinning-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("preloaded_spki_hashes", + SPKIHashesToListValue(i->second.preloaded_spki_hashes)); + + if (now < i->second.dynamic_spki_hashes_expiry) { + state->Set("dynamic_spki_hashes", + SPKIHashesToListValue(i->second.dynamic_spki_hashes)); } - state->Set("public_key_hashes", pins); toplevel.Set(HashedDomainToExternalString(i->first), state); } @@ -659,18 +886,24 @@ bool TransportSecurityState::LoadEntries(const std::string& input, } static bool AddHash(const std::string& type_and_base64, - std::vector<SHA1Fingerprint>* out) { - std::string hash_str; - if (type_and_base64.find("sha1/") == 0 && - base::Base64Decode(type_and_base64.substr(5, type_and_base64.size() - 5), - &hash_str) && - hash_str.size() == base::kSHA1Length) { - SHA1Fingerprint hash; - memcpy(hash.data, hash_str.data(), sizeof(hash.data)); - out->push_back(hash); - return true; + FingerprintVector* out) { + SHA1Fingerprint hash; + + if (!TransportSecurityState::ParsePin(type_and_base64, &hash)) + return false; + + out->push_back(hash); + return true; +} + +static void SPKIHashesFromListValue(FingerprintVector* hashes, + const ListValue& pins) { + size_t num_pins = pins.GetSize(); + for (size_t i = 0; i < num_pins; ++i) { + std::string type_and_base64; + if (pins.GetString(i, &type_and_base64)) + AddHash(type_and_base64, hashes); } - return false; } // static @@ -697,6 +930,7 @@ bool TransportSecurityState::Deserialise( std::string mode_string; double created; double expiry; + double dynamic_spki_hashes_expiry = 0.0; if (!state->GetBoolean("include_subdomains", &include_subdomains) || !state->GetString("mode", &mode_string) || @@ -704,16 +938,18 @@ bool TransportSecurityState::Deserialise( continue; } + // Don't fail if this key is not present. + (void) state->GetDouble("dynamic_spki_hashes_expiry", + &dynamic_spki_hashes_expiry); + 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; - if (pins_list->GetString(i, &type_and_base64)) - AddHash(type_and_base64, &public_key_hashes); - } - } + FingerprintVector preloaded_spki_hashes; + if (state->GetList("preloaded_spki_hashes", &pins_list)) + SPKIHashesFromListValue(&preloaded_spki_hashes, *pins_list); + + FingerprintVector dynamic_spki_hashes; + if (state->GetList("dynamic_spki_hashes", &pins_list)) + SPKIHashesFromListValue(&dynamic_spki_hashes, *pins_list); DomainState::Mode mode; if (mode_string == "strict") { @@ -729,6 +965,8 @@ bool TransportSecurityState::Deserialise( } base::Time expiry_time = base::Time::FromDoubleT(expiry); + base::Time dynamic_spki_hashes_expiry_time = + base::Time::FromDoubleT(dynamic_spki_hashes_expiry); base::Time created_time; if (state->GetDouble("created", &created)) { created_time = base::Time::FromDoubleT(created); @@ -739,7 +977,8 @@ bool TransportSecurityState::Deserialise( created_time = base::Time::Now(); } - if (expiry_time <= current_time) { + if (expiry_time <= current_time && + dynamic_spki_hashes_expiry_time <= current_time) { // Make sure we dirty the state if we drop an entry. dirtied = true; continue; @@ -756,7 +995,9 @@ bool TransportSecurityState::Deserialise( new_state.created = created_time; new_state.expiry = expiry_time; new_state.include_subdomains = include_subdomains; - new_state.public_key_hashes = public_key_hashes; + new_state.preloaded_spki_hashes = preloaded_spki_hashes; + new_state.dynamic_spki_hashes = dynamic_spki_hashes; + new_state.dynamic_spki_hashes_expiry = dynamic_spki_hashes_expiry_time; (*out)[hashed] = new_state; } @@ -881,7 +1122,7 @@ static bool HasPreload(const struct HSTSPreload* entries, size_t num_entries, if (entries[j].pins.required_hashes) { const char* const* hash = entries[j].pins.required_hashes; while (*hash) { - bool ok = AddHash(*hash, &out->public_key_hashes); + bool ok = AddHash(*hash, &out->preloaded_spki_hashes); DCHECK(ok) << " failed to parse " << *hash; hash++; } @@ -889,7 +1130,7 @@ static bool HasPreload(const struct HSTSPreload* entries, size_t num_entries, if (entries[j].pins.excluded_hashes) { const char* const* hash = entries[j].pins.excluded_hashes; while (*hash) { - bool ok = AddHash(*hash, &out->bad_public_key_hashes); + bool ok = AddHash(*hash, &out->bad_preloaded_spki_hashes); DCHECK(ok) << " failed to parse " << *hash; hash++; } @@ -1330,9 +1571,9 @@ bool TransportSecurityState::IsPreloadedSTS( } static std::string HashesToBase64String( - const std::vector<net::SHA1Fingerprint>& hashes) { + const FingerprintVector& hashes) { std::vector<std::string> hashes_strs; - for (std::vector<net::SHA1Fingerprint>::const_iterator + for (FingerprintVector::const_iterator i = hashes.begin(); i != hashes.end(); i++) { std::string s; const std::string hash_str(reinterpret_cast<const char*>(i->data), @@ -1355,39 +1596,39 @@ TransportSecurityState::DomainState::~DomainState() { } bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted( - const std::vector<net::SHA1Fingerprint>& hashes) { - for (std::vector<net::SHA1Fingerprint>::const_iterator - i = hashes.begin(); i != hashes.end(); ++i) { - for (std::vector<net::SHA1Fingerprint>::const_iterator - j = bad_public_key_hashes.begin(); j != bad_public_key_hashes.end(); - ++j) { - if (i->Equals(*j)) { - LOG(ERROR) << "Rejecting public key chain for domain " << domain - << ". Validated chain: " << HashesToBase64String(hashes) - << ", matches one or more bad hashes: " - << HashesToBase64String(bad_public_key_hashes); - return false; - } - } + const FingerprintVector& hashes) { + + if (HashesIntersect(bad_preloaded_spki_hashes, hashes)) { + LOG(ERROR) << "Rejecting public key chain for domain " << domain + << ". Validated chain: " << HashesToBase64String(hashes) + << ", matches one or more bad hashes: " + << HashesToBase64String(bad_preloaded_spki_hashes); + return false; } - if (public_key_hashes.empty()) - return true; + if (!(dynamic_spki_hashes.empty() && preloaded_spki_hashes.empty()) && + !HashesIntersect(dynamic_spki_hashes, hashes) && + !HashesIntersect(preloaded_spki_hashes, hashes)) { + LOG(ERROR) << "Rejecting public key chain for domain " << domain + << ". Validated chain: " << HashesToBase64String(hashes) + << ", expected: " << HashesToBase64String(dynamic_spki_hashes) + << " or: " << HashesToBase64String(preloaded_spki_hashes); - 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; - } + return false; } - LOG(ERROR) << "Rejecting public key chain for domain " << domain - << ". Validated chain: " << HashesToBase64String(hashes) - << ", expected: " << HashesToBase64String(public_key_hashes); + return true; +} - return false; +bool TransportSecurityState::DomainState::IsMoreStrict( + const TransportSecurityState::DomainState& other) { + if (this->dynamic_spki_hashes.empty() && !other.dynamic_spki_hashes.empty()) + return false; + + if (!this->include_subdomains && other.include_subdomains) + return false; + + return true; } bool TransportSecurityState::DomainState::ShouldRedirectHTTPToHTTPS() diff --git a/net/base/transport_security_state.h b/net/base/transport_security_state.h index 7ff4a2f..5bf4ace 100644 --- a/net/base/transport_security_state.h +++ b/net/base/transport_security_state.h @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -15,10 +15,15 @@ #include "base/threading/non_thread_safe.h" #include "base/time.h" #include "net/base/net_export.h" +#include "net/base/x509_certificate.h" #include "net/base/x509_cert_types.h" namespace net { +class SSLInfo; + +typedef std::vector<SHA1Fingerprint> FingerprintVector; + // TransportSecurityState // // Tracks which hosts have enabled *-Transport-Security. This object manages @@ -55,26 +60,32 @@ class NET_EXPORT TransportSecurityState DomainState(); ~DomainState(); - // IsChainOfPublicKeysPermitted takes a set of public key hashes and - // returns true if: - // 1) None of the hashes are in |bad_public_key_hashes| AND - // 2) |public_key_hashes| is empty, i.e. no public keys have been pinned. - // OR - // 3) |hashes| and |public_key_hashes| are not disjoint. + // Takes a set of SubjectPublicKeyInfo |hashes| and returns true if: + // 1) |bad_preloaded_spki_hashes| does not intersect |hashes|; AND + // 2) Both |preloaded_spki_hashes| and |dynamic_spki_hashes| are empty + // or at least one of them intersects |hashes|. // - // |public_key_hashes| is intended to contain a number of trusted public - // keys for the chain in question, any one of which is sufficient. The - // public keys could be of a root CA, intermediate CA or leaf certificate, - // depending on the security vs disaster recovery tradeoff selected. - // (Pinning only to leaf certifiates increases security because you no - // longer trust any CAs, but it hampers disaster recovery because you can't - // just get a new certificate signed by the CA.) + // |{dynamic,preloaded}_spki_hashes| contain trustworthy public key + // hashes, any one of which is sufficient to validate the certificate + // chain in question. The public keys could be of a root CA, intermediate + // CA, or leaf certificate, depending on the security vs. disaster + // recovery tradeoff selected. (Pinning only to leaf certifiates increases + // security because you no longer trust any CAs, but it hampers disaster + // recovery because you can't just get a new certificate signed by the + // CA.) // - // |bad_public_key_hashes| is intended to contain unwanted intermediate CA - // certifciates that those trusted public keys may have issued but that we - // don't want to trust. - bool IsChainOfPublicKeysPermitted( - const std::vector<SHA1Fingerprint>& hashes); + // |bad_preloaded_spki_hashes| contains public keys that we don't want to + // trust. + bool IsChainOfPublicKeysPermitted(const FingerprintVector& hashes); + + // Returns true if |this| describes a more strict policy than |other|. + // Used to see if a dynamic DomainState should override a preloaded one. + bool IsMoreStrict(const DomainState& other); + + // ShouldCertificateErrorsBeFatal returns true iff, given the |mode| of this + // DomainState, certificate errors on this domain should be fatal (i.e. no + // user bypass). + bool ShouldCertificateErrorsBeFatal() const; // ShouldRedirectHTTPToHTTPS returns true iff, given the |mode| of this // DomainState, HTTP requests should be internally redirected to HTTPS. @@ -84,10 +95,35 @@ class NET_EXPORT TransportSecurityState base::Time created; // when this host entry was first created base::Time expiry; // the absolute time (UTC) when this record expires bool include_subdomains; // subdomains included? - std::vector<SHA1Fingerprint> public_key_hashes; // optional; permitted keys - std::vector<SHA1Fingerprint> bad_public_key_hashes; // optional;rejectd keys - // The follow members are not valid when stored in |enabled_hosts_|. + // Optional; hashes of preloaded "pinned" SubjectPublicKeyInfos. Unless + // both are empty, at least one of |preloaded_spki_hashes| and + // |dynamic_spki_hashes| MUST intersect with the set of SPKIs in the TLS + // server's certificate chain. + // + // |dynamic_spki_hashes| take precedence over |preloaded_spki_hashes|. + // That is, when performing pin validation, first check dynamic and then + // check preloaded. + FingerprintVector preloaded_spki_hashes; + + // Optional; hashes of dynamically pinned SubjectPublicKeyInfos. (They + // could be set e.g. by an HTTP header or by a superfluous certificate.) + FingerprintVector dynamic_spki_hashes; + + // The absolute time (UTC) when the |dynamic_spki_hashes| expire. + base::Time dynamic_spki_hashes_expiry; + + // The max-age directive of the Public-Key-Pins header as parsed. Do not + // persist this; it is only for testing. TODO(palmer): Therefore, get rid + // of it and find a better way to test. + int max_age; + + // Optional; hashes of preloaded known-bad SubjectPublicKeyInfos which + // MUST NOT intersect with the set of SPKIs in the TLS server's + // certificate chain. + FingerprintVector bad_preloaded_spki_hashes; + + // The following members are not valid when stored in |enabled_hosts_|. bool preloaded; // is this a preloaded entry? std::string domain; // the domain which matched }; @@ -102,7 +138,7 @@ class NET_EXPORT TransportSecurityState virtual ~Delegate() {} }; - void SetDelegate(Delegate*); + void SetDelegate(Delegate* delegate); // Enable TransportSecurity for |host|. void EnableHost(const std::string& host, const DomainState& state); @@ -127,10 +163,9 @@ class NET_EXPORT TransportSecurityState const std::string& host, bool sni_available); - // Returns true if |host| has any HSTS metadata, in the context of - // |sni_available|. (This include cert-pin-only metadata). - // In that case, *result is filled out. - // Note that *result is always overwritten on every call. + // Returns true if |host| has any metadata, in the context of + // |sni_available|. In that case, *result is filled out. Note that *result + // is always overwritten on every call. bool HasMetadata(DomainState* result, const std::string& host, bool sni_available); @@ -142,11 +177,6 @@ class NET_EXPORT TransportSecurityState // // Note that like HasMetadata, if |host| matches both an exact entry and is a // subdomain of another entry, the exact match determines the return value. - // - // This function is used by ChromeFraudulentCertificateReporter to determine - // whether or not we can automatically post fraudulent certificate reports to - // Google; we only do so automatically in cases when the user was trying to - // connect to Google in the first place. static bool IsGooglePinnedProperty(const std::string& host, bool sni_available); @@ -155,9 +185,25 @@ class NET_EXPORT TransportSecurityState // mail.google.com), and only if |host| is a preloaded STS host. static void ReportUMAOnPinFailure(const std::string& host); + // Parses |cert|'s Subject Public Key Info structure, hashes it, and writes + // the hash into |spki_hash|. Returns true on parse success, false on + // failure. + static bool GetPublicKeyHash(const X509Certificate& cert, + SHA1Fingerprint* spki_hash); + + // Decodes a pin string |value| (e.g. "sha1/hvfkN/qlp/zhXR3cuerq6jd2Z7g=") + // and populates |out|. + static bool ParsePin(const std::string& value, SHA1Fingerprint* out); + // Deletes all records created since a given time. void DeleteSince(const base::Time& time); + // Parses |value| as a Public-Key-Pins header. If successful, returns |true| + // and updates |state|; otherwise, returns |false| without updating |state|. + static bool ParsePinsHeader(const std::string& value, + const SSLInfo& ssl_info, + DomainState* state); + // Returns |true| if |value| parses as a valid *-Transport-Security // header value. The values of max-age and and includeSubDomains are // returned in |max_age| and |include_subdomains|, respectively. The out @@ -173,7 +219,7 @@ class NET_EXPORT TransportSecurityState // is put into |out_pub_key_hash|. static bool ParseSidePin(const base::StringPiece& leaf_spki, const base::StringPiece& side_info, - std::vector<SHA1Fingerprint> *out_pub_key_hash); + FingerprintVector* out_pub_key_hash); bool Serialise(std::string* output); // Existing non-preloaded entries are cleared and repopulated from the diff --git a/net/base/transport_security_state_unittest.cc b/net/base/transport_security_state_unittest.cc index c2e3e9e..07d1f21 100644 --- a/net/base/transport_security_state_unittest.cc +++ b/net/base/transport_security_state_unittest.cc @@ -1,12 +1,23 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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 <algorithm> +#include <string> + #include "base/base64.h" +#include "base/file_path.h" #include "base/sha1.h" #include "base/string_piece.h" +#include "net/base/asn1_util.h" +#include "net/base/cert_test_util.h" +#include "net/base/cert_verifier.h" +#include "net/base/ssl_info.h" +#include "net/base/test_root_certs.h" +#include "net/base/x509_certificate.h" +#include "net/http/http_util.h" #include "testing/gtest/include/gtest/gtest.h" #if defined(USE_OPENSSL) @@ -102,6 +113,97 @@ TEST_F(TransportSecurityStateTest, BogusHeaders) { EXPECT_FALSE(include_subdomains); } +static std::string GetPinFromCert(X509Certificate* cert) { + SHA1Fingerprint spki_hash; + if (!TransportSecurityState::GetPublicKeyHash(*cert, &spki_hash)) + return ""; + std::string base64; + base::Base64Encode(base::StringPiece(reinterpret_cast<char*>(spki_hash.data), + sizeof(spki_hash.data)), + &base64); + return "pin-sha1=" + HttpUtil::Quote(base64); +} + +TEST_F(TransportSecurityStateTest, BogusPinsHeaders) { + TransportSecurityState::DomainState state; + state.max_age = 42; + SSLInfo ssl_info; + ssl_info.cert = + ImportCertFromFile(GetTestCertsDirectory(), "test_mail_google_com.pem"); + std::string good_pin = GetPinFromCert(ssl_info.cert); + + // The backup pin is fake --- it just has to not be in the chain. + std::string backup_pin = "pin-sha1=" + + HttpUtil::Quote("6dcfXufJLW3J6S/9rRe4vUlBj5g="); + + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " ", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "abc", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " abc", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " abc ", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age ", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age=", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age =", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age= ", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age = ", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age = xy", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + " max-age = 3488a923", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=3488a923 ", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-ag=3488923pins=" + good_pin + "," + backup_pin, + ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-aged=3488923" + backup_pin, + ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-aged=3488923; " + backup_pin, + ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-aged=3488923; " + backup_pin + ";" + backup_pin, + ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-aged=3488923; " + good_pin + ";" + good_pin, + ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-aged=3488923; " + good_pin, + ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age==3488923", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "amax-age=3488923", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=-3488923", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=3488923;", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=3488923 e", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=3488923 includesubdomain", ssl_info, &state)); + EXPECT_FALSE(TransportSecurityState::ParsePinsHeader( + "max-age=34889.23", ssl_info, &state)); + + EXPECT_EQ(state.max_age, 42); +} + TEST_F(TransportSecurityStateTest, ValidHeaders) { int max_age = 42; bool include_subdomains = true; @@ -138,7 +240,8 @@ TEST_F(TransportSecurityStateTest, ValidHeaders) { EXPECT_TRUE(include_subdomains); EXPECT_TRUE(TransportSecurityState::ParseHeader( - "max-age=394082038 ; incLudesUbdOmains", &max_age, &include_subdomains)); + "max-age=394082038 ; incLudesUbdOmains", &max_age, + &include_subdomains)); EXPECT_EQ(max_age, std::min(TransportSecurityState::kMaxHSTSAgeSecs, 394082038l)); EXPECT_TRUE(include_subdomains); @@ -156,6 +259,98 @@ TEST_F(TransportSecurityStateTest, ValidHeaders) { EXPECT_TRUE(include_subdomains); } +TEST_F(TransportSecurityStateTest, ValidPinsHeaders) { + TransportSecurityState::DomainState state; + state.max_age = 42; + + // Set up a realistic SSLInfo with a realistic cert chain. + FilePath certs_dir = GetTestCertsDirectory(); + scoped_refptr<X509Certificate> ee_cert = + ImportCertFromFile(certs_dir, "2048-rsa-ee-by-2048-rsa-intermediate.pem"); + ASSERT_NE(static_cast<X509Certificate*>(NULL), ee_cert); + scoped_refptr<X509Certificate> intermediate = + ImportCertFromFile(certs_dir, "2048-rsa-intermediate.pem"); + ASSERT_NE(static_cast<X509Certificate*>(NULL), intermediate); + X509Certificate::OSCertHandles intermediates; + intermediates.push_back(intermediate->os_cert_handle()); + SSLInfo ssl_info; + ssl_info.cert = X509Certificate::CreateFromHandle(ee_cert->os_cert_handle(), + intermediates); + + // Add the root that signed the intermediate for this test. + scoped_refptr<X509Certificate> root_cert = + ImportCertFromFile(certs_dir, "2048-rsa-root.pem"); + ASSERT_NE(static_cast<X509Certificate*>(NULL), root_cert); + TestRootCerts::GetInstance()->Add(root_cert.get()); + + // Verify has the side-effect of populating public_key_hashes, which + // ParsePinsHeader needs. (It wants to check pins against the validated + // chain, not just the presented chain.) + CertVerifyResult result; + int rv = ssl_info.cert->Verify("127.0.0.1", 0, NULL, &result); + ASSERT_EQ(0, rv); + // Normally, ssl_client_socket_nss would do this, but for a unit test we + // fake it. + ssl_info.public_key_hashes = result.public_key_hashes; + std::string good_pin = GetPinFromCert(ssl_info.cert); + + // The backup pin is fake --- we just need an SPKI hash that does not match + // the hash of any SPKI in the certificate chain. + std::string backup_pin = "pin-sha1=" + + HttpUtil::Quote("6dcfXufJLW3J6S/9rRe4vUlBj5g="); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + "max-age=243; " + good_pin + ";" + backup_pin, + ssl_info, &state)); + EXPECT_EQ(state.max_age, 243); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + " " + good_pin + "; " + backup_pin + " ; Max-agE = 567", + ssl_info, &state)); + EXPECT_EQ(state.max_age, 567); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + good_pin + ";" + backup_pin + " ; mAx-aGe = 890 ", + ssl_info, &state)); + EXPECT_EQ(state.max_age, 890); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + good_pin + ";" + backup_pin + "; max-age=123;IGNORED;", + ssl_info, &state)); + EXPECT_EQ(state.max_age, 123); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + "max-age=394082;" + backup_pin + ";" + good_pin + "; ", + ssl_info, &state)); + EXPECT_EQ(state.max_age, 394082); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + "max-age=39408299 ;" + backup_pin + ";" + good_pin + "; ", + ssl_info, &state)); + EXPECT_EQ(state.max_age, + std::min(TransportSecurityState::kMaxHSTSAgeSecs, 39408299l)); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + "max-age=39408038 ; cybers=39408038 ; " + + good_pin + ";" + backup_pin + "; ", + ssl_info, &state)); + EXPECT_EQ(state.max_age, + std::min(TransportSecurityState::kMaxHSTSAgeSecs, 394082038l)); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + " max-age=0 ; " + good_pin + ";" + backup_pin, + ssl_info, &state)); + EXPECT_EQ(state.max_age, 0); + + EXPECT_TRUE(TransportSecurityState::ParsePinsHeader( + " max-age=999999999999999999999999999999999999999999999 ; " + + backup_pin + ";" + good_pin + "; ", + ssl_info, &state)); + EXPECT_EQ(state.max_age, TransportSecurityState::kMaxHSTSAgeSecs); + + TestRootCerts::GetInstance()->Clear(); +} + TEST_F(TransportSecurityStateTest, SimpleMatches) { TransportSecurityState state(""); TransportSecurityState::DomainState domain_state; @@ -240,17 +435,21 @@ TEST_F(TransportSecurityStateTest, Serialise2) { EXPECT_TRUE(state.LoadEntries(output, &dirty)); EXPECT_TRUE(state.GetDomainState(&domain_state, "yahoo.com", true)); - EXPECT_EQ(domain_state.mode, TransportSecurityState::DomainState::MODE_STRICT); + EXPECT_EQ(domain_state.mode, + TransportSecurityState::DomainState::MODE_STRICT); EXPECT_TRUE(state.GetDomainState(&domain_state, "foo.yahoo.com", true)); - EXPECT_EQ(domain_state.mode, TransportSecurityState::DomainState::MODE_STRICT); + EXPECT_EQ(domain_state.mode, + TransportSecurityState::DomainState::MODE_STRICT); EXPECT_TRUE(state.GetDomainState(&domain_state, "foo.bar.yahoo.com", true)); - EXPECT_EQ(domain_state.mode, TransportSecurityState::DomainState::MODE_STRICT); + EXPECT_EQ(domain_state.mode, + TransportSecurityState::DomainState::MODE_STRICT); EXPECT_TRUE(state.GetDomainState(&domain_state, "foo.bar.baz.yahoo.com", true)); - EXPECT_EQ(domain_state.mode, TransportSecurityState::DomainState::MODE_STRICT); + EXPECT_EQ(domain_state.mode, + TransportSecurityState::DomainState::MODE_STRICT); EXPECT_FALSE(state.GetDomainState(&domain_state, "com", true)); } @@ -385,7 +584,7 @@ TEST_F(TransportSecurityStateTest, Preloaded) { EXPECT_FALSE(HasState("paypal.com")); EXPECT_FALSE(HasState("www2.paypal.com")); - EXPECT_FALSE(HasState("www2.paypal.com"));; + EXPECT_FALSE(HasState("www2.paypal.com")); // Google hosts: @@ -544,6 +743,22 @@ TEST_F(TransportSecurityStateTest, Preloaded) { EXPECT_TRUE(ShouldRedirect("www.dropcam.com")); EXPECT_FALSE(HasState("foo.dropcam.com")); + EXPECT_TRUE(state.GetDomainState(&domain_state, + "torproject.org", + false)); + EXPECT_FALSE(domain_state.preloaded_spki_hashes.empty()); + EXPECT_TRUE(state.GetDomainState(&domain_state, + "www.torproject.org", + false)); + EXPECT_FALSE(domain_state.preloaded_spki_hashes.empty()); + EXPECT_TRUE(state.GetDomainState(&domain_state, + "check.torproject.org", + false)); + EXPECT_FALSE(domain_state.preloaded_spki_hashes.empty()); + EXPECT_TRUE(state.GetDomainState(&domain_state, + "blog.torproject.org", + false)); + EXPECT_FALSE(domain_state.preloaded_spki_hashes.empty()); EXPECT_TRUE(ShouldRedirect("ebanking.indovinabank.com.vn")); EXPECT_TRUE(ShouldRedirect("foo.ebanking.indovinabank.com.vn")); @@ -608,12 +823,12 @@ TEST_F(TransportSecurityStateTest, PublicKeyHashes) { TransportSecurityState state(""); TransportSecurityState::DomainState domain_state; EXPECT_FALSE(state.GetDomainState(&domain_state, "example.com", false)); - std::vector<SHA1Fingerprint> hashes; + FingerprintVector hashes; EXPECT_TRUE(domain_state.IsChainOfPublicKeysPermitted(hashes)); SHA1Fingerprint hash; memset(hash.data, '1', sizeof(hash.data)); - domain_state.public_key_hashes.push_back(hash); + domain_state.preloaded_spki_hashes.push_back(hash); EXPECT_FALSE(domain_state.IsChainOfPublicKeysPermitted(hashes)); hashes.push_back(hash); @@ -630,9 +845,9 @@ TEST_F(TransportSecurityStateTest, PublicKeyHashes) { bool dirty; EXPECT_TRUE(state.LoadEntries(ser, &dirty)); EXPECT_TRUE(state.GetDomainState(&domain_state, "example.com", false)); - EXPECT_EQ(1u, domain_state.public_key_hashes.size()); - EXPECT_TRUE(0 == memcmp(domain_state.public_key_hashes[0].data, hash.data, - sizeof(hash.data))); + EXPECT_EQ(1u, domain_state.preloaded_spki_hashes.size()); + EXPECT_EQ(0, memcmp(domain_state.preloaded_spki_hashes[0].data, hash.data, + sizeof(hash.data))); } TEST_F(TransportSecurityStateTest, BuiltinCertPins) { @@ -642,7 +857,7 @@ TEST_F(TransportSecurityStateTest, BuiltinCertPins) { "chrome.google.com", true)); EXPECT_TRUE(state.HasPinsForHost(&domain_state, "chrome.google.com", true)); - std::vector<SHA1Fingerprint> hashes; + FingerprintVector hashes; // This essential checks that a built-in list does exist. EXPECT_FALSE(domain_state.IsChainOfPublicKeysPermitted(hashes)); EXPECT_FALSE(state.HasPinsForHost(&domain_state, "www.paypal.com", true)); @@ -700,14 +915,17 @@ TEST_F(TransportSecurityStateTest, BuiltinCertPins) { EXPECT_TRUE(state.HasPinsForHost(&domain_state, "oauth.twitter.com", true)); EXPECT_TRUE(state.HasPinsForHost(&domain_state, "mobile.twitter.com", true)); EXPECT_TRUE(state.HasPinsForHost(&domain_state, "dev.twitter.com", true)); - EXPECT_TRUE(state.HasPinsForHost(&domain_state, "business.twitter.com", true)); - EXPECT_TRUE(state.HasPinsForHost(&domain_state, "platform.twitter.com", true)); + EXPECT_TRUE(state.HasPinsForHost(&domain_state, "business.twitter.com", + true)); + EXPECT_TRUE(state.HasPinsForHost(&domain_state, "platform.twitter.com", + true)); EXPECT_TRUE(state.HasPinsForHost(&domain_state, "si0.twimg.com", true)); - EXPECT_TRUE(state.HasPinsForHost(&domain_state, "twimg0-a.akamaihd.net", true)); + EXPECT_TRUE(state.HasPinsForHost(&domain_state, "twimg0-a.akamaihd.net", + true)); } static bool AddHash(const std::string& type_and_base64, - std::vector<SHA1Fingerprint>* out) { + FingerprintVector* out) { std::string hash_str; if (type_and_base64.find("sha1/") == 0 && base::Base64Decode(type_and_base64.substr(5, type_and_base64.size() - 5), @@ -903,22 +1121,20 @@ static const uint8 kSidePinExpectedHash[20] = { }; TEST_F(TransportSecurityStateTest, ParseSidePins) { - base::StringPiece leaf_spki(reinterpret_cast<const char*>(kSidePinLeafSPKI), sizeof(kSidePinLeafSPKI)); base::StringPiece side_info(reinterpret_cast<const char*>(kSidePinInfo), sizeof(kSidePinInfo)); - std::vector<SHA1Fingerprint> pub_key_hashes; + FingerprintVector pub_key_hashes; EXPECT_TRUE(TransportSecurityState::ParseSidePin( leaf_spki, side_info, &pub_key_hashes)); ASSERT_EQ(1u, pub_key_hashes.size()); - EXPECT_TRUE(0 == memcmp(pub_key_hashes[0].data, kSidePinExpectedHash, - sizeof(kSidePinExpectedHash))); + EXPECT_EQ(0, memcmp(pub_key_hashes[0].data, kSidePinExpectedHash, + sizeof(kSidePinExpectedHash))); } TEST_F(TransportSecurityStateTest, ParseSidePinsFailsWithBadData) { - uint8 leaf_spki_copy[sizeof(kSidePinLeafSPKI)]; memcpy(leaf_spki_copy, kSidePinLeafSPKI, sizeof(leaf_spki_copy)); @@ -929,7 +1145,7 @@ TEST_F(TransportSecurityStateTest, ParseSidePinsFailsWithBadData) { sizeof(leaf_spki_copy)); base::StringPiece side_info(reinterpret_cast<const char*>(side_info_copy), sizeof(side_info_copy)); - std::vector<SHA1Fingerprint> pub_key_hashes; + FingerprintVector pub_key_hashes; // Tweak |leaf_spki| and expect a failure. leaf_spki_copy[10] ^= 1; @@ -954,7 +1170,7 @@ TEST_F(TransportSecurityStateTest, DISABLED_ParseSidePinsFuzz) { uint8 side_info_copy[sizeof(kSidePinInfo)]; base::StringPiece side_info(reinterpret_cast<const char*>(side_info_copy), sizeof(side_info_copy)); - std::vector<SHA1Fingerprint> pub_key_hashes; + FingerprintVector pub_key_hashes; static const size_t bit_length = sizeof(kSidePinInfo) * 8; for (size_t bit_to_flip = 0; bit_to_flip < bit_length; bit_to_flip++) { diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc index 3a4e9da..4ebd104 100644 --- a/net/url_request/url_request_http_job.cc +++ b/net/url_request/url_request_http_job.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -244,7 +244,9 @@ void URLRequestHttpJob::NotifyHeadersComplete() { &response_adapter); } + // The ordering of these calls is not important. ProcessStrictTransportSecurityHeader(); + ProcessPublicKeyPinsHeader(); if (SdchManager::Global() && SdchManager::Global()->IsInSupportedDomain(request_->url())) { @@ -638,6 +640,43 @@ void URLRequestHttpJob::ProcessStrictTransportSecurityHeader() { } } +void URLRequestHttpJob::ProcessPublicKeyPinsHeader() { + DCHECK(response_info_); + + const URLRequestContext* ctx = request_->context(); + const SSLInfo& ssl_info = response_info_->ssl_info; + + // Only accept pins on connections that have no errors. + if (!ssl_info.is_valid() || IsCertStatusError(ssl_info.cert_status) || + !ctx || !ctx->transport_security_state()) { + return; + } + + TransportSecurityState* security_state = ctx->transport_security_state(); + TransportSecurityState::DomainState domain_state; + const std::string& host = request_info_.url.host(); + + bool sni_available = + SSLConfigService::IsSNIAvailable(ctx->ssl_config_service()); + bool found = security_state->HasMetadata(&domain_state, host, sni_available); + if (!found) + domain_state.mode = TransportSecurityState::DomainState::MODE_PINNING_ONLY; + + HttpResponseHeaders* headers = GetResponseHeaders(); + void* iter = NULL; + std::string value; + + while (headers->EnumerateHeader(&iter, "Public-Key-Pins", &value)) { + // Note that ParsePinsHeader updates |domain_state| (iff the header parses + // correctly), but does not completely overwrite it. It just updates the + // dynamic pinning metadata. + if (TransportSecurityState::ParsePinsHeader(value, ssl_info, + &domain_state)) { + security_state->EnableHost(host, domain_state); + } + } +} + void URLRequestHttpJob::OnStartCompleted(int result) { RecordTimer(); diff --git a/net/url_request/url_request_http_job.h b/net/url_request/url_request_http_job.h index 41dec65..eebca14 100644 --- a/net/url_request/url_request_http_job.h +++ b/net/url_request/url_request_http_job.h @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -54,6 +54,9 @@ class URLRequestHttpJob : public URLRequestJob { // Process the Strict-Transport-Security header, if one exists. void ProcessStrictTransportSecurityHeader(); + // Process the Public-Key-Pins header, if one exists. + void ProcessPublicKeyPinsHeader(); + // |result| should be net::OK, or the request is canceled. void OnHeadersReceivedCallback(int result); void OnStartCompleted(int result); @@ -84,7 +87,7 @@ class URLRequestHttpJob : public URLRequestJob { virtual void ContinueWithCertificate(X509Certificate* client_cert) OVERRIDE; virtual void ContinueDespiteLastError() OVERRIDE; virtual bool ReadRawData(IOBuffer* buf, int buf_size, - int *bytes_read) OVERRIDE; + int* bytes_read) OVERRIDE; virtual void StopCaching() OVERRIDE; virtual void DoneReading() OVERRIDE; virtual HostPortPair GetSocketAddress() const OVERRIDE; |