Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2032)

Unified Diff: net/base/transport_security_state_static.h

Issue 10003002: Revert 132012 - net: move HSTS preloaded and pinning info out of code. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: Created 8 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « net/base/transport_security_state.cc ('k') | net/base/transport_security_state_static.certs » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: net/base/transport_security_state_static.h
===================================================================
--- net/base/transport_security_state_static.h (revision 132015)
+++ net/base/transport_security_state_static.h (working copy)
@@ -1,19 +1,14 @@
-// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// This file is automatically generated by transport_security_state_static_generate.go
-
-#ifndef NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_
-#define NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_
+#ifndef NET_BASE_PUBLIC_KEY_HASHES_
+#define NET_BASE_PUBLIC_KEY_HASHES_
#pragma once
-// These are SubjectPublicKeyInfo hashes for public key pinning. The
+// This file contains SubjectPublicKeyInfo hashes for public key pinning. The
// hashes are base64 encoded, SHA1 digests.
-static const char kSPKIHash_TestSPKI[] =
- "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=";
-
#if 0
-----BEGIN CERTIFICATE-----
MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG
@@ -84,6 +79,7 @@
static const char kSPKIHash_Google1024[] =
"sha1/QMVAHW+MuvCLAO3vse6H0AWzuc0=";
+// Not yet used publicly.
static const char kSPKIHash_Google2048[] =
"sha1/AbkhxY0L343gKf+cki7NVWp+ozk=";
@@ -327,12 +323,11 @@
static const char kSPKIHash_DigiCertEVRoot[] =
"sha1/gzF+YoVCU9bXeDGQ7JGQVumRueM=";
+// Not public
static const char kSPKIHash_Tor1[] =
"sha1/juNxSTv9UANmpC9kF5GKpmWNx3Y=";
-
static const char kSPKIHash_Tor2[] =
"sha1/lia43lPolzSPVIq34Dw57uYcLD8=";
-
static const char kSPKIHash_Tor3[] =
"sha1/rzEyQIKOh77j87n5bjWUNguXF8Y=";
@@ -583,6 +578,7 @@
static const char kSPKIHash_VeriSignUniversal[] =
"sha1/u8I+KQuzKHcdrT6iTb30I70GsD0=";
+// Not public
static const char kSPKIHash_Twitter1[] =
"sha1/Vv7zwhR9TtOIN/29MFI4cgHld40=";
@@ -737,29 +733,25 @@
#if 0
-----BEGIN CERTIFICATE-----
-MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML
-RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp
-bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5
-IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp
-ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3
-MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
-LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
-YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
-A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq
-K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe
-sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX
-MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT
-XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/
-HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH
-4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
-HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub
-j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo
-U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
-zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b
-u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+
-bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er
-fF6adulZkMV8gzURZVE=
+MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u
+ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp
+bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV
+BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx
+NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3
+d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl
+MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u
+ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL
+Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr
+hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW
+nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi
+VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ
+KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy
+T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
+zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT
+J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e
+nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE=
-----END CERTIFICATE-----
#endif
static const char kSPKIHash_Entrust_2048[] =
@@ -1243,23 +1235,1162 @@
static const char kSPKIHash_GTECyberTrustGlobalRoot[] =
"sha1/WXkS3mF11m/EI7d3E3THlt5viHI=";
-// The following is static data describing the hosts that are hardcoded with
-// certificate pins or HSTS information.
+#endif // NET_BASE_PUBLIC_KEY_HASHES_
+// 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"
+
+#if defined(USE_OPENSSL)
+#include <openssl/ecdsa.h>
+#include <openssl/ssl.h>
+#else // !defined(USE_OPENSSL)
+#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"
+#include "base/logging.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/metrics/histogram.h"
+#include "base/sha1.h"
+#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"
+#endif
+
+namespace net {
+
+const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365; // 1 year
+
+TransportSecurityState::TransportSecurityState(const std::string& hsts_hosts)
+ : delegate_(NULL) {
+ if (!hsts_hosts.empty()) {
+ bool dirty;
+ Deserialise(hsts_hosts, &dirty, &forced_hosts_);
+ }
+}
+
+static std::string HashHost(const std::string& canonicalized_host) {
+ char hashed[crypto::kSHA256Length];
+ crypto::SHA256HashString(canonicalized_host, hashed, sizeof(hashed));
+ return std::string(hashed, sizeof(hashed));
+}
+
+void TransportSecurityState::SetDelegate(
+ TransportSecurityState::Delegate* delegate) {
+ delegate_ = delegate;
+}
+
+void TransportSecurityState::EnableHost(const std::string& host,
+ const DomainState& state) {
+ DCHECK(CalledOnValidThread());
+
+ const std::string canonicalized_host = CanonicalizeHost(host);
+ if (canonicalized_host.empty())
+ return;
+
+ // 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);
+ if (GetDomainState(&existing_state, host, true) &&
+ !existing_state.created.is_null()) {
+ state_copy.created = existing_state.created;
+ }
+
+ // We don't store these values.
+ state_copy.preloaded = false;
+ state_copy.domain.clear();
+
+ enabled_hosts_[HashHost(canonicalized_host)] = state_copy;
+ DirtyNotify();
+}
+
+bool TransportSecurityState::DeleteHost(const std::string& host) {
+ DCHECK(CalledOnValidThread());
+
+ const std::string canonicalized_host = CanonicalizeHost(host);
+ if (canonicalized_host.empty())
+ return false;
+
+ std::map<std::string, DomainState>::iterator i = enabled_hosts_.find(
+ HashHost(canonicalized_host));
+ if (i != enabled_hosts_.end()) {
+ enabled_hosts_.erase(i);
+ DirtyNotify();
+ return true;
+ }
+ return false;
+}
+
+bool TransportSecurityState::HasPinsForHost(DomainState* result,
+ const std::string& host,
+ bool sni_available) {
+ DCHECK(CalledOnValidThread());
+
+ return HasMetadata(result, host, sni_available) &&
+ (!result->dynamic_spki_hashes.empty() ||
+ !result->preloaded_spki_hashes.empty());
+}
+
+bool TransportSecurityState::GetDomainState(DomainState* result,
+ const std::string& host,
+ bool sni_available) {
+ DCHECK(CalledOnValidThread());
+
+ return HasMetadata(result, host, sni_available);
+}
+
+bool TransportSecurityState::HasMetadata(DomainState* result,
+ const std::string& host,
+ bool sni_available) {
+ DCHECK(CalledOnValidThread());
+
+ DomainState state;
+ const std::string canonicalized_host = CanonicalizeHost(host);
+ if (canonicalized_host.empty())
+ return false;
+
+ bool has_preload = IsPreloadedSTS(canonicalized_host, sni_available, &state);
+ std::string canonicalized_preload = CanonicalizeHost(state.domain);
+
+ base::Time current_time(base::Time::Now());
+
+ for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) {
+ std::string host_sub_chunk(&canonicalized_host[i],
+ canonicalized_host.size() - i);
+ // Exact match of a preload always wins.
+ if (has_preload && host_sub_chunk == canonicalized_preload) {
+ *result = state;
+ return true;
+ }
+
+ std::map<std::string, DomainState>::iterator j =
+ enabled_hosts_.find(HashHost(host_sub_chunk));
+ if (j == enabled_hosts_.end())
+ continue;
+
+ if (current_time > j->second.expiry &&
+ current_time > j->second.dynamic_spki_hashes_expiry) {
+ enabled_hosts_.erase(j);
+ DirtyNotify();
+ continue;
+ }
+
+ state = j->second;
+ state.domain = DNSDomainToString(host_sub_chunk);
+
+ // Succeed if we matched the domain exactly or if subdomain matches are
+ // allowed.
+ if (i == 0 || j->second.include_subdomains) {
+ *result = state;
+ return true;
+ }
+
+ return false;
+ }
+
+ return false;
+}
+
+void TransportSecurityState::DeleteSince(const base::Time& time) {
+ DCHECK(CalledOnValidThread());
+
+ bool dirtied = false;
+
+ std::map<std::string, DomainState>::iterator i = enabled_hosts_.begin();
+ while (i != enabled_hosts_.end()) {
+ if (i->second.created >= time) {
+ dirtied = true;
+ enabled_hosts_.erase(i++);
+ } else {
+ i++;
+ }
+ }
+
+ if (dirtied)
+ DirtyNotify();
+}
+
+// MaxAgeToInt converts a string representation of a number of seconds into a
+// int. We use strtol in order to handle overflow correctly. The string may
+// contain an arbitary number which we should truncate correctly rather than
+// throwing a parse failure.
+static bool MaxAgeToInt(std::string::const_iterator begin,
+ std::string::const_iterator end,
+ int* result) {
+ const std::string s(begin, end);
+ char* endptr;
+ long int i = strtol(s.data(), &endptr, 10 /* base */);
+ if (*endptr || i < 0)
+ return false;
+ if (i > TransportSecurityState::kMaxHSTSAgeSecs)
+ i = TransportSecurityState::kMaxHSTSAgeSecs;
+ *result = i;
+ return true;
+}
+
+// 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" ]
+//
+// static
+bool TransportSecurityState::ParseHeader(const std::string& value,
+ int* max_age,
+ bool* include_subdomains) {
+ DCHECK(max_age);
+ DCHECK(include_subdomains);
+
+ int max_age_candidate = 0;
+
+ enum ParserState {
+ START,
+ AFTER_MAX_AGE_LABEL,
+ AFTER_MAX_AGE_EQUALS,
+ AFTER_MAX_AGE,
+ AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER,
+ AFTER_INCLUDE_SUBDOMAINS,
+ } state = START;
+
+ StringTokenizer tokenizer(value, " \t=;");
+ tokenizer.set_options(StringTokenizer::RETURN_DELIMS);
+ while (tokenizer.GetNext()) {
+ DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1);
+ switch (state) {
+ case START:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (!LowerCaseEqualsASCII(tokenizer.token(), "max-age"))
+ return false;
+ state = AFTER_MAX_AGE_LABEL;
+ break;
+
+ case AFTER_MAX_AGE_LABEL:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (*tokenizer.token_begin() != '=')
+ return false;
+ DCHECK_EQ(tokenizer.token().length(), 1U);
+ state = AFTER_MAX_AGE_EQUALS;
+ break;
+
+ case AFTER_MAX_AGE_EQUALS:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (!MaxAgeToInt(tokenizer.token_begin(),
+ tokenizer.token_end(),
+ &max_age_candidate))
+ return false;
+ state = AFTER_MAX_AGE;
+ break;
+
+ case AFTER_MAX_AGE:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (*tokenizer.token_begin() != ';')
+ return false;
+ state = AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER;
+ break;
+
+ case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (!LowerCaseEqualsASCII(tokenizer.token(), "includesubdomains"))
+ return false;
+ state = AFTER_INCLUDE_SUBDOMAINS;
+ break;
+
+ case AFTER_INCLUDE_SUBDOMAINS:
+ if (!IsAsciiWhitespace(*tokenizer.token_begin()))
+ return false;
+ break;
+
+ default:
+ NOTREACHED();
+ }
+ }
+
+ // We've consumed all the input. Let's see what state we ended up in.
+ switch (state) {
+ case START:
+ case AFTER_MAX_AGE_LABEL:
+ case AFTER_MAX_AGE_EQUALS:
+ return false;
+ case AFTER_MAX_AGE:
+ *max_age = max_age_candidate;
+ *include_subdomains = false;
+ return true;
+ case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
+ return false;
+ case AFTER_INCLUDE_SUBDOMAINS:
+ *max_age = max_age_candidate;
+ *include_subdomains = true;
+ return true;
+ default:
+ NOTREACHED();
+ return false;
+ }
+}
+
+// Side pinning and superfluous certificates:
+//
+// In SSLClientSocketNSS::DoVerifyCertComplete we look for certificates with a
+// Subject of CN=meta. When we find one we'll currently try and parse side
+// pinned key from it.
+//
+// A side pin is a key which can be pinned to, but also can be kept offline and
+// still held by the site owner. The CN=meta certificate is just a backwards
+// compatiable method of carrying a lump of bytes to the client. (We could use
+// a TLS extension just as well, but it's a lot easier for admins to add extra
+// certificates to the chain.)
+
+// A TagMap represents the simple key-value structure that we use. Keys are
+// 32-bit ints. Values are byte strings.
+typedef std::map<uint32, base::StringPiece> TagMap;
+
+// ParseTags parses a list of key-value pairs from |in| to |out| and advances
+// |in| past the data. The key-value pair data is:
+// u16le num_tags
+// u32le tag[num_tags]
+// u16le lengths[num_tags]
+// ...data...
+static bool ParseTags(base::StringPiece* in, TagMap *out) {
+ // Many part of Chrome already assume little-endian. This is just to help
+ // anyone who should try to port it in the future.
+#if defined(__BYTE_ORDER)
+ // Linux check
+ COMPILE_ASSERT(__BYTE_ORDER == __LITTLE_ENDIAN, assumes_little_endian);
+#elif defined(__BIG_ENDIAN__)
+ // Mac check
+ #error assumes little endian
+#endif
+
+ uint16 num_tags_16;
+ if (in->size() < sizeof(num_tags_16))
+ return false;
+
+ 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)
+ return false;
+
+ const uint32* tags = reinterpret_cast<const uint32*>(in->data());
+ const uint16* lens = reinterpret_cast<const uint16*>(
+ in->data() + 4*num_tags);
+ in->remove_prefix(6*num_tags);
+
+ uint32 prev_tag = 0;
+ for (unsigned i = 0; i < num_tags; i++) {
+ size_t len = lens[i];
+ uint32 tag = tags[i];
+
+ if (in->size() < len)
+ return false;
+ // tags must be in ascending order.
+ if (i > 0 && prev_tag >= tag)
+ return false;
+ (*out)[tag] = base::StringPiece(in->data(), len);
+ in->remove_prefix(len);
+ prev_tag = tag;
+ }
+
+ return true;
+}
+
+// GetTag extracts the data associated with |tag| in |tags|.
+static bool GetTag(uint32 tag, const TagMap& tags, base::StringPiece* out) {
+ TagMap::const_iterator i = tags.find(tag);
+ if (i == tags.end())
+ return false;
+
+ *out = i->second;
+ return true;
+}
+
+// kP256SubjectPublicKeyInfoPrefix can be prepended onto a P256 elliptic curve
+// point in X9.62 format in order to make a valid SubjectPublicKeyInfo. The
+// ASN.1 interpretation of these bytes is:
+//
+// 0:d=0 hl=2 l= 89 cons: SEQUENCE
+// 2:d=1 hl=2 l= 19 cons: SEQUENCE
+// 4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
+// 13:d=2 hl=2 l= 8 prim: OBJECT :prime256v1
+// 23:d=1 hl=2 l= 66 prim: BIT STRING
+static const uint8 kP256SubjectPublicKeyInfoPrefix[] = {
+ 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86,
+ 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a,
+ 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
+ 0x42, 0x00,
+};
+
+// VerifySignature returns true iff |sig| is a valid signature of
+// |hash| by |pubkey|. The actual implementation is crypto library
+// specific.
+static bool VerifySignature(const base::StringPiece& pubkey,
+ const base::StringPiece& sig,
+ const base::StringPiece& hash);
+
+#if defined(USE_OPENSSL)
+
+static EVP_PKEY* DecodeX962P256PublicKey(
+ const base::StringPiece& pubkey_bytes) {
+ // The public key is an X9.62 encoded P256 point.
+ if (pubkey_bytes.size() != 1 + 2*32)
+ return NULL;
+
+ std::string pubkey_spki(
+ reinterpret_cast<const char*>(kP256SubjectPublicKeyInfoPrefix),
+ sizeof(kP256SubjectPublicKeyInfoPrefix));
+ pubkey_spki += pubkey_bytes.as_string();
+
+ EVP_PKEY* ret = NULL;
+ const unsigned char* der_pubkey =
+ reinterpret_cast<const unsigned char*>(pubkey_spki.data());
+ d2i_PUBKEY(&ret, &der_pubkey, pubkey_spki.size());
+ return ret;
+}
+
+static bool VerifySignature(const base::StringPiece& pubkey,
+ const base::StringPiece& sig,
+ const base::StringPiece& hash) {
+ crypto::ScopedOpenSSL<EVP_PKEY, EVP_PKEY_free> secpubkey(
+ DecodeX962P256PublicKey(pubkey));
+ if (!secpubkey.get())
+ return false;
+
+
+ crypto::ScopedOpenSSL<EC_KEY, EC_KEY_free> ec_key(
+ EVP_PKEY_get1_EC_KEY(secpubkey.get()));
+ if (!ec_key.get())
+ return false;
+
+ return ECDSA_verify(0, reinterpret_cast<const unsigned char*>(hash.data()),
+ hash.size(),
+ reinterpret_cast<const unsigned char*>(sig.data()),
+ sig.size(), ec_key.get()) == 1;
+}
+
+#else
+
+// DecodeX962P256PublicKey parses an uncompressed, X9.62 format, P256 elliptic
+// curve point from |pubkey_bytes| and returns it as a SECKEYPublicKey.
+static SECKEYPublicKey* DecodeX962P256PublicKey(
+ const base::StringPiece& pubkey_bytes) {
+ // The public key is an X9.62 encoded P256 point.
+ if (pubkey_bytes.size() != 1 + 2*32)
+ return NULL;
+
+ std::string pubkey_spki(
+ reinterpret_cast<const char*>(kP256SubjectPublicKeyInfoPrefix),
+ sizeof(kP256SubjectPublicKeyInfoPrefix));
+ pubkey_spki += pubkey_bytes.as_string();
+
+ SECItem der;
+ memset(&der, 0, sizeof(der));
+ der.data = reinterpret_cast<uint8*>(const_cast<char*>(pubkey_spki.data()));
+ der.len = pubkey_spki.size();
+
+ CERTSubjectPublicKeyInfo* spki = SECKEY_DecodeDERSubjectPublicKeyInfo(&der);
+ if (!spki)
+ return NULL;
+ SECKEYPublicKey* public_key = SECKEY_ExtractPublicKey(spki);
+ SECKEY_DestroySubjectPublicKeyInfo(spki);
+
+ return public_key;
+}
+
+static bool VerifySignature(const base::StringPiece& pubkey,
+ const base::StringPiece& sig,
+ const base::StringPiece& hash) {
+ SECKEYPublicKey* secpubkey = DecodeX962P256PublicKey(pubkey);
+ if (!secpubkey)
+ return false;
+
+ SECItem sigitem;
+ memset(&sigitem, 0, sizeof(sigitem));
+ sigitem.data = reinterpret_cast<uint8*>(const_cast<char*>(sig.data()));
+ sigitem.len = sig.size();
+
+ // |decoded_sigitem| is newly allocated, as is the data that it points to.
+ SECItem* decoded_sigitem = DSAU_DecodeDerSigToLen(
+ &sigitem, SECKEY_SignatureLen(secpubkey));
+
+ if (!decoded_sigitem) {
+ SECKEY_DestroyPublicKey(secpubkey);
+ return false;
+ }
+
+ SECItem hashitem;
+ memset(&hashitem, 0, sizeof(hashitem));
+ hashitem.data = reinterpret_cast<unsigned char*>(
+ const_cast<char*>(hash.data()));
+ hashitem.len = hash.size();
+
+ SECStatus rv = PK11_Verify(secpubkey, decoded_sigitem, &hashitem, NULL);
+ SECKEY_DestroyPublicKey(secpubkey);
+ SECITEM_FreeItem(decoded_sigitem, PR_TRUE);
+ return rv == SECSuccess;
+}
+
+#endif // !defined(USE_OPENSSL)
+
+// These are the tag values that we use. Tags are little-endian on the wire and
+// these values correspond to the ASCII of the name.
+static const uint32 kTagALGO = 0x4f474c41;
+static const uint32 kTagP256 = 0x36353250;
+static const uint32 kTagPUBK = 0x4b425550;
+static const uint32 kTagSIG = 0x474953;
+static const uint32 kTagSPIN = 0x4e495053;
+
+// static
+bool TransportSecurityState::ParseSidePin(
+ const base::StringPiece& leaf_spki,
+ const base::StringPiece& in_side_info,
+ FingerprintVector* out_pub_key_hash) {
+ base::StringPiece side_info(in_side_info);
+
+ TagMap outer;
+ if (!ParseTags(&side_info, &outer))
+ return false;
+ // trailing data is not allowed
+ if (side_info.size())
+ return false;
+
+ base::StringPiece side_pin_bytes;
+ if (!GetTag(kTagSPIN, outer, &side_pin_bytes))
+ return false;
+
+ bool have_parsed_a_key = false;
+ uint8 leaf_spki_hash[crypto::kSHA256Length];
+ bool have_leaf_spki_hash = false;
+
+ while (side_pin_bytes.size() > 0) {
+ TagMap side_pin;
+ if (!ParseTags(&side_pin_bytes, &side_pin))
+ return false;
+
+ base::StringPiece algo, pubkey, sig;
+ if (!GetTag(kTagALGO, side_pin, &algo) ||
+ !GetTag(kTagPUBK, side_pin, &pubkey) ||
+ !GetTag(kTagSIG, side_pin, &sig)) {
+ return false;
+ }
+
+ if (algo.size() != sizeof(kTagP256) ||
+ 0 != memcmp(algo.data(), &kTagP256, sizeof(kTagP256))) {
+ // We don't support anything but P256 at the moment.
+ continue;
+ }
+
+ if (!have_leaf_spki_hash) {
+ crypto::SHA256HashString(
+ leaf_spki.as_string(), leaf_spki_hash, sizeof(leaf_spki_hash));
+ have_leaf_spki_hash = true;
+ }
+
+ if (VerifySignature(pubkey, sig, base::StringPiece(
+ reinterpret_cast<const char*>(leaf_spki_hash),
+ sizeof(leaf_spki_hash)))) {
+ SHA1Fingerprint fpr;
+ base::SHA1HashBytes(
+ reinterpret_cast<const uint8*>(pubkey.data()),
+ pubkey.size(),
+ fpr.data);
+ out_pub_key_hash->push_back(fpr);
+ have_parsed_a_key = true;
+ }
+ }
+
+ return have_parsed_a_key;
+}
+
+// This function converts the binary hashes, which we store in
+// |enabled_hosts_|, to a base64 string which we can include in a JSON file.
+static std::string HashedDomainToExternalString(const std::string& hashed) {
+ std::string out;
+ CHECK(base::Base64Encode(hashed, &out));
+ return out;
+}
+
+// This inverts |HashedDomainToExternalString|, above. It turns an external
+// string (from a JSON file) into an internal (binary) string.
+static std::string ExternalStringToHashedDomain(const std::string& external) {
+ std::string out;
+ if (!base::Base64Decode(external, &out) ||
+ out.size() != crypto::kSHA256Length) {
+ return std::string();
+ }
+
+ 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:
+ state->SetString("mode", "strict");
+ break;
+ 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;
+ }
+
+ 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));
+ }
+
+ toplevel.Set(HashedDomainToExternalString(i->first), state);
+ }
+
+ base::JSONWriter::WriteWithOptions(&toplevel,
+ base::JSONWriter::OPTIONS_PRETTY_PRINT,
+ output);
+ return true;
+}
+
+bool TransportSecurityState::LoadEntries(const std::string& input,
+ bool* dirty) {
+ DCHECK(CalledOnValidThread());
+
+ enabled_hosts_.clear();
+ return Deserialise(input, dirty, &enabled_hosts_);
+}
+
+static bool AddHash(const std::string& type_and_base64,
+ 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);
+ }
+}
+
+// static
+bool TransportSecurityState::Deserialise(
+ const std::string& input,
+ bool* dirty,
+ std::map<std::string, DomainState>* out) {
+ scoped_ptr<Value> value(
+ base::JSONReader::Read(input, false /* do not allow trailing commas */));
+ if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY))
+ return false;
+
+ DictionaryValue* dict_value = reinterpret_cast<DictionaryValue*>(value.get());
+ const base::Time current_time(base::Time::Now());
+ bool dirtied = false;
+
+ for (DictionaryValue::key_iterator i = dict_value->begin_keys();
+ i != dict_value->end_keys(); ++i) {
+ DictionaryValue* state;
+ if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &state))
+ continue;
+
+ bool include_subdomains;
+ std::string mode_string;
+ double created;
+ double expiry;
+ double dynamic_spki_hashes_expiry = 0.0;
+
+ if (!state->GetBoolean("include_subdomains", &include_subdomains) ||
+ !state->GetString("mode", &mode_string) ||
+ !state->GetDouble("expiry", &expiry)) {
+ 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;
+ 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") {
+ mode = DomainState::MODE_STRICT;
+ } else if (mode_string == "spdy-only") {
+ mode = DomainState::MODE_SPDY_ONLY;
+ } else if (mode_string == "pinning-only") {
+ mode = DomainState::MODE_PINNING_ONLY;
+ } else {
+ LOG(WARNING) << "Unknown TransportSecurityState mode string found: "
+ << mode_string;
+ continue;
+ }
+
+ 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);
+ } else {
+ // We're migrating an old entry with no creation date. Make sure we
+ // write the new date back in a reasonable time frame.
+ dirtied = true;
+ created_time = base::Time::Now();
+ }
+
+ if (expiry_time <= current_time &&
+ dynamic_spki_hashes_expiry_time <= current_time) {
+ // Make sure we dirty the state if we drop an entry.
+ dirtied = true;
+ continue;
+ }
+
+ std::string hashed = ExternalStringToHashedDomain(*i);
+ if (hashed.empty()) {
+ dirtied = true;
+ continue;
+ }
+
+ DomainState new_state;
+ new_state.mode = mode;
+ new_state.created = created_time;
+ new_state.expiry = expiry_time;
+ new_state.include_subdomains = include_subdomains;
+ new_state.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;
+ }
+
+ *dirty = dirtied;
+ return true;
+}
+
+TransportSecurityState::~TransportSecurityState() {
+}
+
+void TransportSecurityState::DirtyNotify() {
+ DCHECK(CalledOnValidThread());
+
+ if (delegate_)
+ delegate_->StateIsDirty(this);
+}
+
+// static
+std::string TransportSecurityState::CanonicalizeHost(const std::string& host) {
+ // We cannot perform the operations as detailed in the spec here as |host|
+ // has already undergone IDN processing before it reached us. Thus, we check
+ // that there are no invalid characters in the host and lowercase the result.
+
+ std::string new_host;
+ if (!DNSDomainFromDot(host, &new_host)) {
+ // DNSDomainFromDot can fail if any label is > 63 bytes or if the whole
+ // name is >255 bytes. However, search terms can have those properties.
+ return std::string();
+ }
+
+ for (size_t i = 0; new_host[i]; i += new_host[i] + 1) {
+ const unsigned label_length = static_cast<unsigned>(new_host[i]);
+ if (!label_length)
+ break;
+
+ for (size_t j = 0; j < label_length; ++j) {
+ // RFC 3490, 4.1, step 3
+ if (!IsSTD3ASCIIValidCharacter(new_host[i + 1 + j]))
+ return std::string();
+
+ new_host[i + 1 + j] = tolower(new_host[i + 1 + j]);
+ }
+
+ // step 3(b)
+ if (new_host[i + 1] == '-' ||
+ new_host[i + label_length] == '-') {
+ return std::string();
+ }
+ }
+
+ return new_host;
+}
+
+// |ReportUMAOnPinFailure| uses these to report which domain was associated
+// with the public key pinning failure.
+//
+// DO NOT CHANGE THE ORDERING OF THESE NAMES OR REMOVE ANY OF THEM. Add new
+// domains at the END of the listing (but before DOMAIN_NUM_EVENTS).
+enum SecondLevelDomainName {
+ DOMAIN_NOT_PINNED,
+
+ DOMAIN_GOOGLE_COM,
+ DOMAIN_ANDROID_COM,
+ DOMAIN_GOOGLE_ANALYTICS_COM,
+ DOMAIN_GOOGLEPLEX_COM,
+ DOMAIN_YTIMG_COM,
+ DOMAIN_GOOGLEUSERCONTENT_COM,
+ DOMAIN_YOUTUBE_COM,
+ DOMAIN_GOOGLEAPIS_COM,
+ DOMAIN_GOOGLEADSERVICES_COM,
+ DOMAIN_GOOGLECODE_COM,
+ DOMAIN_APPSPOT_COM,
+ DOMAIN_GOOGLESYNDICATION_COM,
+ DOMAIN_DOUBLECLICK_NET,
+ DOMAIN_GSTATIC_COM,
+ DOMAIN_GMAIL_COM,
+ DOMAIN_GOOGLEMAIL_COM,
+ DOMAIN_GOOGLEGROUPS_COM,
+
+ DOMAIN_TORPROJECT_ORG,
+
+ DOMAIN_TWITTER_COM,
+ DOMAIN_TWIMG_COM,
+
+ DOMAIN_AKAMAIHD_NET,
+
+ // Boundary value for UMA_HISTOGRAM_ENUMERATION:
+ DOMAIN_NUM_EVENTS
+};
+
+// PublicKeyPins contains a number of SubjectPublicKeyInfo hashes for a site.
+// The validated certificate chain for the site must not include any of
+// |excluded_hashes| and must include one or more of |required_hashes|.
+struct PublicKeyPins {
+ const char* const* required_hashes;
+ const char* const* excluded_hashes;
+};
+
+struct HSTSPreload {
+ uint8 length;
+ bool include_subdomains;
+ char dns_name[30];
+ bool https_required;
+ PublicKeyPins pins;
+ SecondLevelDomainName second_level_domain_name;
+};
+
+static bool HasPreload(const struct HSTSPreload* entries, size_t num_entries,
+ const std::string& canonicalized_host, size_t i,
+ TransportSecurityState::DomainState* out, bool* ret) {
+ for (size_t j = 0; j < num_entries; j++) {
+ if (entries[j].length == canonicalized_host.size() - i &&
+ memcmp(entries[j].dns_name, &canonicalized_host[i],
+ entries[j].length) == 0) {
+ if (!entries[j].include_subdomains && i != 0) {
+ *ret = false;
+ } else {
+ out->include_subdomains = entries[j].include_subdomains;
+ *ret = true;
+ if (!entries[j].https_required)
+ out->mode = TransportSecurityState::DomainState::MODE_PINNING_ONLY;
+ if (entries[j].pins.required_hashes) {
+ const char* const* hash = entries[j].pins.required_hashes;
+ while (*hash) {
+ bool ok = AddHash(*hash, &out->preloaded_spki_hashes);
+ DCHECK(ok) << " failed to parse " << *hash;
+ hash++;
+ }
+ }
+ if (entries[j].pins.excluded_hashes) {
+ const char* const* hash = entries[j].pins.excluded_hashes;
+ while (*hash) {
+ bool ok = AddHash(*hash, &out->bad_preloaded_spki_hashes);
+ DCHECK(ok) << " failed to parse " << *hash;
+ hash++;
+ }
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
// kNoRejectedPublicKeys is a placeholder for when no public keys are rejected.
static const char* const kNoRejectedPublicKeys[] = {
NULL,
};
-static const char* const kTestAcceptableCerts[] = {
- kSPKIHash_TestSPKI,
- NULL,
-};
-#define kTestPins { \
- kTestAcceptableCerts, \
- kNoRejectedPublicKeys, \
-}
-
static const char* const kGoogleAcceptableCerts[] = {
kSPKIHash_VeriSignClass3,
kSPKIHash_VeriSignClass3_G3,
@@ -1320,6 +2451,8 @@
kNoRejectedPublicKeys, \
}
+// kTwitterCDNAcceptableCerts are the set of public keys valid for Twitter's
+// CDNs, which includes all the keys from kTwitterComAcceptableCerts.
static const char* const kTwitterCDNAcceptableCerts[] = {
kSPKIHash_VeriSignClass1,
kSPKIHash_VeriSignClass3,
@@ -1340,6 +2473,7 @@
kSPKIHash_GeoTrustPrimary_G2,
kSPKIHash_GeoTrustPrimary_G3,
kSPKIHash_Twitter1,
+
kSPKIHash_Entrust_2048,
kSPKIHash_Entrust_EV,
kSPKIHash_Entrust_G2,
@@ -1364,14 +2498,35 @@
kNoRejectedPublicKeys, \
}
-#define kNoPins {\
+// kTestAcceptableCerts doesn't actually match any public keys and is used
+// with "pinningtest.appspot.com", below, to test if pinning is active.
+static const char* const kTestAcceptableCerts[] = {
+ "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ NULL,
+};
+#define kTestPins { \
+ kTestAcceptableCerts, \
+ kNoRejectedPublicKeys, \
+}
+
+#define kNoPins { \
NULL, NULL, \
}
+#if defined(OS_CHROMEOS)
+ static const bool kTwitterHSTS = true;
+#else
+ static const bool kTwitterHSTS = false;
+#endif
+
+// In the medium term this list is likely to just be hardcoded here. This
+// slightly odd form removes the need for additional relocations records.
static const struct HSTSPreload kPreloadedSTS[] = {
+ // (*.)google.com, iff using SSL must use an acceptable certificate.
+ {12, true, "\006google\003com", false, kGooglePins, DOMAIN_GOOGLE_COM },
{25, true, "\013pinningtest\007appspot\003com", false, kTestPins, DOMAIN_APPSPOT_COM },
- {12, true, "\006google\003com", false, kGooglePins, DOMAIN_GOOGLE_COM },
- {19, true, "\006health\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
+ // Now we force HTTPS for subtrees of google.com.
+ {19, true, "\006health\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{21, true, "\010checkout\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{19, true, "\006chrome\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{17, true, "\004docs\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
@@ -1386,13 +2541,20 @@
{17, true, "\004talk\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{29, true, "\020hostedtalkgadget\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{17, true, "\004plus\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
+ // Other Google-related domains that must use HTTPS.
{20, true, "\006market\007android\003com", true, kGooglePins, DOMAIN_ANDROID_COM },
{26, true, "\003ssl\020google-analytics\003com", true, kGooglePins, DOMAIN_GOOGLE_ANALYTICS_COM },
{18, true, "\005drive\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{16, true, "\012googleplex\003com", true, kGooglePins, DOMAIN_GOOGLEPLEX_COM },
{19, true, "\006groups\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
{17, true, "\004apis\006google\003com", true, kGooglePins, DOMAIN_GOOGLE_COM },
- {23, true, "\005chart\004apis\006google\003com", false, kGooglePins, DOMAIN_GOOGLE_COM },
+ // chart.apis.google.com is *not* HSTS because the certificate doesn't match
+ // and there are lots of links out there that still use the name. The correct
+ // hostname for this is chart.googleapis.com.
+ {23, true, "\005chart\004apis\006google\003com", false, kGooglePins, DOMAIN_GOOGLE_COM},
+
+ // Other Google-related domains that must use an acceptable certificate
+ // iff using SSL.
{11, true, "\005ytimg\003com", false, kGooglePins, DOMAIN_YTIMG_COM },
{23, true, "\021googleusercontent\003com", false, kGooglePins, DOMAIN_GOOGLEUSERCONTENT_COM },
{13, true, "\007youtube\003com", false, kGooglePins, DOMAIN_YOUTUBE_COM },
@@ -1403,11 +2565,14 @@
{23, true, "\021googlesyndication\003com", false, kGooglePins, DOMAIN_GOOGLESYNDICATION_COM },
{17, true, "\013doubleclick\003net", false, kGooglePins, DOMAIN_DOUBLECLICK_NET },
{17, true, "\003ssl\007gstatic\003com", false, kGooglePins, DOMAIN_GSTATIC_COM },
+ // Exclude the learn.doubleclick.net subdomain because it uses a different
+ // CA.
{23, true, "\005learn\013doubleclick\003net", false, kNoPins, DOMAIN_NOT_PINNED },
+ // Now we force HTTPS for other sites that have requested it.
{16, false, "\003www\006paypal\003com", true, kNoPins, DOMAIN_NOT_PINNED },
{16, false, "\003www\006elanex\003biz", true, kNoPins, DOMAIN_NOT_PINNED },
- {12, true, "\006jottit\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {19, true, "\015sunshinepress\003org", true, kNoPins, DOMAIN_NOT_PINNED },
+ {12, true, "\006jottit\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {19, true, "\015sunshinepress\003org", true, kNoPins, DOMAIN_NOT_PINNED },
{21, false, "\003www\013noisebridge\003net", true, kNoPins, DOMAIN_NOT_PINNED },
{10, false, "\004neg9\003org", true, kNoPins, DOMAIN_NOT_PINNED },
{12, true, "\006riseup\003net", true, kNoPins, DOMAIN_NOT_PINNED },
@@ -1460,22 +2625,23 @@
{12, true, "\006ubertt\003org", true, kNoPins, DOMAIN_NOT_PINNED },
{9, true, "\004pixi\002me", true, kNoPins, DOMAIN_NOT_PINNED },
{14, true, "\010grepular\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {16, false, "\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {20, false, "\003www\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {26, false, "\011developer\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {30, false, "\003www\011developer\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {24, false, "\007sandbox\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
- {28, false, "\003www\007sandbox\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {16, false , "\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {20, false , "\003www\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {26, false , "\011developer\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {30, false , "\003www\011developer\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {24, false , "\007sandbox\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
+ {28, false , "\003www\007sandbox\012mydigipass\003com", true, kNoPins, DOMAIN_NOT_PINNED },
{12, true, "\006crypto\003cat", true, kNoPins, DOMAIN_NOT_PINNED },
{25, true, "\014bigshinylock\006minazo\003net", true, kNoPins, DOMAIN_NOT_PINNED },
{10, true, "\005crate\002io", true, kNoPins, DOMAIN_NOT_PINNED },
- {13, false, "\007twitter\003com", false, kTwitterComPins, DOMAIN_TWITTER_COM },
- {17, true, "\003www\007twitter\003com", false, kTwitterComPins, DOMAIN_TWITTER_COM },
- {17, true, "\003api\007twitter\003com", false, kTwitterCDNPins, DOMAIN_TWITTER_COM },
- {19, true, "\005oauth\007twitter\003com", false, kTwitterComPins, DOMAIN_TWITTER_COM },
- {20, true, "\006mobile\007twitter\003com", false, kTwitterComPins, DOMAIN_TWITTER_COM },
- {17, true, "\003dev\007twitter\003com", false, kTwitterComPins, DOMAIN_TWITTER_COM },
- {22, true, "\010business\007twitter\003com", false, kTwitterComPins, DOMAIN_TWITTER_COM },
+
+ {13, false, "\007twitter\003com", kTwitterHSTS, kTwitterComPins, DOMAIN_TWITTER_COM },
+ {17, true, "\003www\007twitter\003com", kTwitterHSTS, kTwitterComPins, DOMAIN_TWITTER_COM },
+ {17, true, "\003api\007twitter\003com", kTwitterHSTS, kTwitterCDNPins, DOMAIN_TWITTER_COM },
+ {19, true, "\005oauth\007twitter\003com", kTwitterHSTS, kTwitterComPins, DOMAIN_TWITTER_COM },
+ {20, true, "\006mobile\007twitter\003com", kTwitterHSTS, kTwitterComPins, DOMAIN_TWITTER_COM },
+ {17, true, "\003dev\007twitter\003com", kTwitterHSTS, kTwitterComPins, DOMAIN_TWITTER_COM },
+ {22, true, "\010business\007twitter\003com", kTwitterHSTS, kTwitterComPins, DOMAIN_TWITTER_COM },
{22, true, "\010platform\007twitter\003com", false, kTwitterCDNPins, DOMAIN_TWITTER_COM },
{15, true, "\003si0\005twimg\003com", false, kTwitterCDNPins, DOMAIN_TWIMG_COM },
{23, true, "\010twimg0-a\010akamaihd\003net", false, kTwitterCDNPins, DOMAIN_AKAMAIHD_NET },
@@ -1483,13 +2649,188 @@
static const size_t kNumPreloadedSTS = ARRAYSIZE_UNSAFE(kPreloadedSTS);
static const struct HSTSPreload kPreloadedSNISTS[] = {
+ // These SNI-only domains must always use HTTPS.
{11, false, "\005gmail\003com", true, kGooglePins, DOMAIN_GMAIL_COM },
{16, false, "\012googlemail\003com", true, kGooglePins, DOMAIN_GOOGLEMAIL_COM },
{15, false, "\003www\005gmail\003com", true, kGooglePins, DOMAIN_GMAIL_COM },
{20, false, "\003www\012googlemail\003com", true, kGooglePins, DOMAIN_GOOGLEMAIL_COM },
+ // These SNI-only domains must use an acceptable certificate iff using
+ // HTTPS.
{22, true, "\020google-analytics\003com", false, kGooglePins, DOMAIN_GOOGLE_ANALYTICS_COM },
+ // www. requires SNI.
{18, true, "\014googlegroups\003com", false, kGooglePins, DOMAIN_GOOGLEGROUPS_COM },
};
static const size_t kNumPreloadedSNISTS = ARRAYSIZE_UNSAFE(kPreloadedSNISTS);
-#endif // NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_
+// Returns the HSTSPreload entry for the |canonicalized_host| in |entries|,
+// or NULL if there is none. Prefers exact hostname matches to those that
+// match only because HSTSPreload.include_subdomains is true.
+//
+// |canonicalized_host| should be the hostname as canonicalized by
+// CanonicalizeHost.
+static const struct HSTSPreload* GetHSTSPreload(
+ const std::string& canonicalized_host,
+ const struct HSTSPreload* entries,
+ size_t num_entries) {
+ for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) {
+ for (size_t j = 0; j < num_entries; j++) {
+ const struct HSTSPreload* entry = entries + j;
+
+ if (i != 0 && !entry->include_subdomains)
+ continue;
+
+ if (entry->length == canonicalized_host.size() - i &&
+ memcmp(entry->dns_name, &canonicalized_host[i], entry->length) == 0) {
+ return entry;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+// static
+bool TransportSecurityState::IsGooglePinnedProperty(const std::string& host,
+ bool sni_available) {
+ std::string canonicalized_host = CanonicalizeHost(host);
+ const struct HSTSPreload* entry =
+ GetHSTSPreload(canonicalized_host, kPreloadedSTS, kNumPreloadedSTS);
+
+ if (entry && entry->pins.required_hashes == kGoogleAcceptableCerts)
+ return true;
+
+ if (sni_available) {
+ entry = GetHSTSPreload(canonicalized_host, kPreloadedSNISTS,
+ kNumPreloadedSNISTS);
+ if (entry && entry->pins.required_hashes == kGoogleAcceptableCerts)
+ return true;
+ }
+
+ return false;
+}
+
+// static
+void TransportSecurityState::ReportUMAOnPinFailure(const std::string& host) {
+ std::string canonicalized_host = CanonicalizeHost(host);
+
+ const struct HSTSPreload* entry =
+ GetHSTSPreload(canonicalized_host, kPreloadedSTS, kNumPreloadedSTS);
+
+ if (!entry) {
+ entry = GetHSTSPreload(canonicalized_host, kPreloadedSNISTS,
+ kNumPreloadedSNISTS);
+ }
+
+ DCHECK(entry);
+ DCHECK(entry->pins.required_hashes);
+ DCHECK(entry->second_level_domain_name != DOMAIN_NOT_PINNED);
+
+ UMA_HISTOGRAM_ENUMERATION("Net.PublicKeyPinFailureDomain",
+ entry->second_level_domain_name, DOMAIN_NUM_EVENTS);
+}
+
+// IsPreloadedSTS returns true if the canonicalized hostname should always be
+// considered to have STS enabled.
+bool TransportSecurityState::IsPreloadedSTS(
+ const std::string& canonicalized_host,
+ bool sni_available,
+ DomainState* out) {
+ DCHECK(CalledOnValidThread());
+
+ out->preloaded = true;
+ out->mode = DomainState::MODE_STRICT;
+ out->include_subdomains = false;
+
+ for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) {
+ std::string host_sub_chunk(&canonicalized_host[i],
+ canonicalized_host.size() - i);
+ out->domain = DNSDomainToString(host_sub_chunk);
+ std::string hashed_host(HashHost(host_sub_chunk));
+ if (forced_hosts_.find(hashed_host) != forced_hosts_.end()) {
+ *out = forced_hosts_[hashed_host];
+ out->domain = DNSDomainToString(host_sub_chunk);
+ out->preloaded = true;
+ return true;
+ }
+ bool ret;
+ if (HasPreload(kPreloadedSTS, kNumPreloadedSTS, canonicalized_host, i, out,
+ &ret)) {
+ return ret;
+ }
+ if (sni_available &&
+ HasPreload(kPreloadedSNISTS, kNumPreloadedSNISTS, canonicalized_host, i,
+ out, &ret)) {
+ return ret;
+ }
+ }
+
+ return false;
+}
+
+static std::string HashesToBase64String(
+ const FingerprintVector& hashes) {
+ std::vector<std::string> hashes_strs;
+ 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),
+ sizeof(i->data));
+ base::Base64Encode(hash_str, &s);
+ hashes_strs.push_back(s);
+ }
+
+ return JoinString(hashes_strs, ',');
+}
+
+TransportSecurityState::DomainState::DomainState()
+ : mode(MODE_STRICT),
+ created(base::Time::Now()),
+ include_subdomains(false),
+ preloaded(false) {
+}
+
+TransportSecurityState::DomainState::~DomainState() {
+}
+
+bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted(
+ const 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 (!(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);
+
+ return false;
+ }
+
+ return true;
+}
+
+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()
+ const {
+ return mode == MODE_STRICT;
+}
+
+} // namespace
« no previous file with comments | « net/base/transport_security_state.cc ('k') | net/base/transport_security_state_static.certs » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698