summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorpalmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-01-05 02:26:15 +0000
committerpalmer@chromium.org <palmer@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-01-05 02:26:15 +0000
commitfecef22a0e8711a8f6c3309540f27098eeeabc57 (patch)
treec728ffcbb7f8bec5843e53eeee3aa53915d84d4f
parentddc9c9e6708ceeb6428455546b0550715e5fb49f (diff)
downloadchromium_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.js25
-rw-r--r--chrome/browser/ui/webui/net_internals_ui.cc54
-rw-r--r--chrome/test/data/webui/net_internals/hsts_view.js23
-rw-r--r--net/base/transport_security_state.cc403
-rw-r--r--net/base/transport_security_state.h112
-rw-r--r--net/base/transport_security_state_unittest.cc264
-rw-r--r--net/url_request/url_request_http_job.cc41
-rw-r--r--net/url_request/url_request_http_job.h7
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;