diff options
author | unsafe@trevp.net <unsafe@trevp.net@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-08 22:07:33 +0000 |
---|---|---|
committer | unsafe@trevp.net <unsafe@trevp.net@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-08 22:07:33 +0000 |
commit | 6ed72be9850bc59df02b0ec19ce44cfabc805656 (patch) | |
tree | 9e278061bdcd61bec8b9a591780dd201a8ce2f65 | |
parent | 0807387aeac97df0e1140e7de2c5cfa99c968175 (diff) | |
download | chromium_src-6ed72be9850bc59df02b0ec19ce44cfabc805656.zip chromium_src-6ed72be9850bc59df02b0ec19ce44cfabc805656.tar.gz chromium_src-6ed72be9850bc59df02b0ec19ce44cfabc805656.tar.bz2 |
This is the first in an intended sequence of CLs to refactor
TransportSecurityState, fix some book-keeping bugs, and hopefully add TACK.
This sequence of CLs will be derived from the original, overly-large CL
#11191005.
This CL does a few things:
- Adds a high-level API for processing HSTS/HPKP
- Move the code for handling HSTS/HPKP headers out of transport_security_state
- Move HashValue out of x509_cert_types
- Addresses several HSTS/HPKP parsing bugs identified during review of the cleanup
- Ignore unknown HSTS/HPKP directives
- Ignore unknown hash algorithms
- Handle overly-large (> int64) expirations without parsing issues
- Reject invalid pins entered by users
Review URL: https://chromiumcodereview.appspot.com/11274032
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@175595 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | chrome/browser/net/transport_security_persister.cc | 14 | ||||
-rw-r--r-- | chrome/browser/ui/webui/net_internals/net_internals_ui.cc | 76 | ||||
-rw-r--r-- | net/base/hash_value.cc | 123 | ||||
-rw-r--r-- | net/base/hash_value.h | 125 | ||||
-rw-r--r-- | net/base/transport_security_state.cc | 486 | ||||
-rw-r--r-- | net/base/transport_security_state.h | 29 | ||||
-rw-r--r-- | net/base/transport_security_state_static.h | 158 | ||||
-rw-r--r-- | net/base/transport_security_state_unittest.cc | 402 | ||||
-rw-r--r-- | net/base/x509_cert_types.cc | 65 | ||||
-rw-r--r-- | net/base/x509_cert_types.h | 83 | ||||
-rw-r--r-- | net/http/http_security_headers.cc | 330 | ||||
-rw-r--r-- | net/http/http_security_headers.h | 60 | ||||
-rw-r--r-- | net/http/http_security_headers_unittest.cc | 424 | ||||
-rw-r--r-- | net/net.gyp | 5 | ||||
-rw-r--r-- | net/url_request/url_request_http_job.cc | 83 |
16 files changed, 1327 insertions, 1137 deletions
@@ -215,3 +215,4 @@ Sungguk Lim <limasdf@gmail.com> Martin Bednorz <m.s.bednorz@gmail.com> Kamil Jiwa <kamil.jiwa@gmail.com> Keene Pan <keenepan@linpus.com> +Trevor Perrin <unsafe@trevp.net> diff --git a/chrome/browser/net/transport_security_persister.cc b/chrome/browser/net/transport_security_persister.cc index 7e80742..b288e21 100644 --- a/chrome/browser/net/transport_security_persister.cc +++ b/chrome/browser/net/transport_security_persister.cc @@ -29,16 +29,8 @@ namespace { ListValue* SPKIHashesToListValue(const HashValueVector& hashes) { ListValue* pins = new ListValue; - - for (HashValueVector::const_iterator i = hashes.begin(); - i != hashes.end(); ++i) { - std::string hash_str(reinterpret_cast<const char*>(i->data()), i->size()); - std::string b64; - if (base::Base64Encode(hash_str, &b64)) - pins->Append(new StringValue(TransportSecurityState::HashValueLabel(*i) + - b64)); - } - + for (size_t i = 0; i != hashes.size(); i++) + pins->Append(new StringValue(hashes[i].ToString())); return pins; } @@ -48,7 +40,7 @@ void SPKIHashesFromListValue(const ListValue& pins, HashValueVector* hashes) { std::string type_and_base64; HashValue fingerprint; if (pins.GetString(i, &type_and_base64) && - TransportSecurityState::ParsePin(type_and_base64, &fingerprint)) { + fingerprint.FromString(type_and_base64)) { hashes->push_back(fingerprint); } } diff --git a/chrome/browser/ui/webui/net_internals/net_internals_ui.cc b/chrome/browser/ui/webui/net_internals/net_internals_ui.cc index 2cb52ad..9b5e742 100644 --- a/chrome/browser/ui/webui/net_internals/net_internals_ui.cc +++ b/chrome/browser/ui/webui/net_internals/net_internals_ui.cc @@ -110,6 +110,41 @@ net::HostCache* GetHostResolverCache(net::URLRequestContext* context) { return context->host_resolver()->GetHostCache(); } +std::string HashesToBase64String(const net::HashValueVector& hashes) { + std::string str; + for (size_t i = 0; i != hashes.size(); ++i) { + if (i != 0) + str += ","; + str += hashes[i].ToString(); + } + return str; +} + +bool Base64StringToHashes(const std::string& hashes_str, + net::HashValueVector* hashes) { + hashes->clear(); + std::vector<std::string> vector_hash_str; + base::SplitString(hashes_str, ',', &vector_hash_str); + + for (size_t i = 0; i != vector_hash_str.size(); ++i) { + std::string hash_str; + RemoveChars(vector_hash_str[i], " \t\r\n", &hash_str); + net::HashValue hash; + // Skip past unrecognized hash algos + // But return false on malformatted input + if (hash_str.empty()) + return false; + if (hash_str.compare(0, 5, "sha1/") != 0 && + hash_str.compare(0, 7, "sha256/") != 0) { + continue; + } + if (!hash.FromString(hash_str)) + return false; + hashes->push_back(hash); + } + return true; +} + // Returns a Value representing the state of a pre-existing URLRequest when // net-internals was opened. Value* RequestStateToValue(const net::URLRequest* request, @@ -1176,21 +1211,6 @@ void NetInternalsMessageHandler::IOThreadImpl::OnStartConnectionTests( connection_tester_->RunAllTests(url); } -void SPKIHashesToString(const net::HashValueVector& hashes, - std::string* string) { - for (net::HashValueVector::const_iterator - i = hashes.begin(); i != hashes.end(); ++i) { - base::StringPiece hash_str(reinterpret_cast<const char*>(i->data()), - i->size()); - std::string encoded; - base::Base64Encode(hash_str, &encoded); - - if (i != hashes.begin()) - *string += ","; - *string += net::TransportSecurityState::HashValueLabel(*i) + encoded; - } -} - void NetInternalsMessageHandler::IOThreadImpl::OnHSTSQuery( const ListValue* list) { // |list| should be: [<domain to query>]. @@ -1219,13 +1239,10 @@ void NetInternalsMessageHandler::IOThreadImpl::OnHSTSQuery( result->SetDouble("dynamic_spki_hashes_expiry", state.dynamic_spki_hashes_expiry.ToDoubleT()); - std::string hashes; - SPKIHashesToString(state.static_spki_hashes, &hashes); - result->SetString("static_spki_hashes", hashes); - - hashes.clear(); - SPKIHashesToString(state.dynamic_spki_hashes, &hashes); - result->SetString("dynamic_spki_hashes", hashes); + result->SetString("static_spki_hashes", + HashesToBase64String(state.static_spki_hashes)); + result->SetString("dynamic_spki_hashes", + HashesToBase64String(state.dynamic_spki_hashes)); } } } @@ -1257,20 +1274,9 @@ void NetInternalsMessageHandler::IOThreadImpl::OnHSTSAdd( state.upgrade_expiry = state.created + base::TimeDelta::FromDays(1000); state.include_subdomains = include_subdomains; if (!hashes_str.empty()) { - std::vector<std::string> type_and_b64s; - base::SplitString(hashes_str, ',', &type_and_b64s); - for (std::vector<std::string>::const_iterator - i = type_and_b64s.begin(); i != type_and_b64s.end(); ++i) { - std::string type_and_b64; - RemoveChars(*i, " \t\r\n", &type_and_b64); - net::HashValue hash; - if (!net::TransportSecurityState::ParsePin(type_and_b64, &hash)) - continue; - - state.dynamic_spki_hashes.push_back(hash); - } + if (!Base64StringToHashes(hashes_str, &state.dynamic_spki_hashes)) + return; } - transport_security_state->EnableHost(domain, state); } diff --git a/net/base/hash_value.cc b/net/base/hash_value.cc new file mode 100644 index 0000000..1ab3a51 --- /dev/null +++ b/net/base/hash_value.cc @@ -0,0 +1,123 @@ +// 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/hash_value.h" + +#include "base/base64.h" +#include "base/logging.h" +#include "base/sha1.h" +#include "base/string_split.h" +#include "base/string_util.h" + +namespace net { + +namespace { + +// CompareSHA1Hashes is a helper function for using bsearch() with an array of +// SHA1 hashes. +int CompareSHA1Hashes(const void* a, const void* b) { + return memcmp(a, b, base::kSHA1Length); +} + +} // namespace + + +bool SHA1HashValue::Equals(const SHA1HashValue& other) const { + return memcmp(data, other.data, sizeof(data)) == 0; +} + +bool SHA256HashValue::Equals(const SHA256HashValue& other) const { + return memcmp(data, other.data, sizeof(data)) == 0; +} + +bool HashValue::Equals(const HashValue& other) const { + if (tag != other.tag) + return false; + switch (tag) { + case HASH_VALUE_SHA1: + return fingerprint.sha1.Equals(other.fingerprint.sha1); + case HASH_VALUE_SHA256: + return fingerprint.sha256.Equals(other.fingerprint.sha256); + default: + NOTREACHED() << "Unknown HashValueTag " << tag; + return false; + } +} + +bool HashValue::FromString(const base::StringPiece value) { + base::StringPiece base64_str; + if (value.starts_with("sha1/")) { + tag = HASH_VALUE_SHA1; + base64_str = value.substr(5); + } else if (value.starts_with("sha256/")) { + tag = HASH_VALUE_SHA256; + base64_str = value.substr(7); + } else { + return false; + } + + std::string decoded; + if (!base::Base64Decode(base64_str, &decoded) || decoded.size() != size()) + return false; + + memcpy(data(), decoded.data(), size()); + return true; +} + +std::string HashValue::ToString() const { + std::string base64_str; + base::Base64Encode(base::StringPiece(reinterpret_cast<const char*>(data()), + size()), &base64_str); + switch (tag) { + case HASH_VALUE_SHA1: + return std::string("sha1/") + base64_str; + case HASH_VALUE_SHA256: + return std::string("sha256/") + base64_str; + default: + NOTREACHED() << "Unknown HashValueTag " << tag; + return std::string("unknown/" + base64_str); + } +} + +size_t HashValue::size() const { + switch (tag) { + case HASH_VALUE_SHA1: + return sizeof(fingerprint.sha1.data); + case HASH_VALUE_SHA256: + return sizeof(fingerprint.sha256.data); + default: + NOTREACHED() << "Unknown HashValueTag " << tag; + // While an invalid tag should not happen, return a non-zero length + // to avoid compiler warnings when the result of size() is + // used with functions like memset. + return sizeof(fingerprint.sha1.data); + } +} + +unsigned char* HashValue::data() { + return const_cast<unsigned char*>(const_cast<const HashValue*>(this)->data()); +} + +const unsigned char* HashValue::data() const { + switch (tag) { + case HASH_VALUE_SHA1: + return fingerprint.sha1.data; + case HASH_VALUE_SHA256: + return fingerprint.sha256.data; + default: + NOTREACHED() << "Unknown HashValueTag " << tag; + return NULL; + } +} + +bool IsSHA1HashInSortedArray(const SHA1HashValue& hash, + const uint8* array, + size_t array_byte_len) { + DCHECK_EQ(0u, array_byte_len % base::kSHA1Length); + const size_t arraylen = array_byte_len / base::kSHA1Length; + return NULL != bsearch(hash.data, array, arraylen, base::kSHA1Length, + CompareSHA1Hashes); +} + +} // namespace net diff --git a/net/base/hash_value.h b/net/base/hash_value.h new file mode 100644 index 0000000..fc8d163 --- /dev/null +++ b/net/base/hash_value.h @@ -0,0 +1,125 @@ +// 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. + +#ifndef NET_BASE_HASH_VALUE_H_ +#define NET_BASE_HASH_VALUE_H_ + +#include <string.h> + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/string_piece.h" +#include "build/build_config.h" +#include "net/base/net_export.h" + +namespace net { + +struct NET_EXPORT SHA1HashValue { + bool Equals(const SHA1HashValue& other) const; + + unsigned char data[20]; +}; + +struct NET_EXPORT SHA256HashValue { + bool Equals(const SHA256HashValue& other) const; + + unsigned char data[32]; +}; + +enum HashValueTag { + HASH_VALUE_SHA1, + HASH_VALUE_SHA256, + + // This must always be last. + HASH_VALUE_TAGS_COUNT +}; + +class NET_EXPORT HashValue { + public: + explicit HashValue(HashValueTag tag) : tag(tag) {} + HashValue() : tag(HASH_VALUE_SHA1) {} + + // Check for equality of hash values + // This function may have VARIABLE timing which leaks information + // about its inputs. For example it may exit early once a + // nonequal character is discovered. Thus, for security reasons + // this function MUST NOT be used with secret values (such as + // password hashes, MAC tags, etc.) + bool Equals(const HashValue& other) const; + + // Serializes/Deserializes hashes in the form of + // <hash-name>"/"<base64-hash-value> + // (eg: "sha1/...") + // This format may be persisted to permanent storage, so + // care should be taken before changing the serialization. + // + // This format is used for: + // - net_internals display/setting public-key pins + // - logging public-key pins + // - serializing public-key pins + + // Deserializes a HashValue from a string. On error, returns + // false and MAY change the contents of HashValue to contain invalid data. + bool FromString(const base::StringPiece input); + + // Serializes the HashValue to a string. If an invalid HashValue + // is supplied (eg: an unknown hash tag), returns "unknown"/<base64> + std::string ToString() const; + + size_t size() const; + unsigned char* data(); + const unsigned char* data() const; + + HashValueTag tag; + + private: + union { + SHA1HashValue sha1; + SHA256HashValue sha256; + } fingerprint; +}; + +typedef std::vector<HashValue> HashValueVector; + + +class SHA1HashValueLessThan { + public: + bool operator()(const SHA1HashValue& lhs, + const SHA1HashValue& rhs) const { + return memcmp(lhs.data, rhs.data, sizeof(lhs.data)) < 0; + } +}; + +class SHA256HashValueLessThan { + public: + bool operator()(const SHA256HashValue& lhs, + const SHA256HashValue& rhs) const { + return memcmp(lhs.data, rhs.data, sizeof(lhs.data)) < 0; + } +}; + +class HashValuesEqual { + public: + explicit HashValuesEqual(const HashValue& fingerprint) : + fingerprint_(fingerprint) {} + + bool operator()(const HashValue& other) const { + return fingerprint_.Equals(other); + } + + const HashValue& fingerprint_; +}; + + +// IsSHA1HashInSortedArray returns true iff |hash| is in |array|, a sorted +// array of SHA1 hashes. +bool IsSHA1HashInSortedArray(const SHA1HashValue& hash, + const uint8* array, + size_t array_byte_len); + +} // namespace net + +#endif // NET_BASE_HASH_VALUE_H_ diff --git a/net/base/transport_security_state.cc b/net/base/transport_security_state.cc index 75d26cb..0fc9a68 100644 --- a/net/base/transport_security_state.cc +++ b/net/base/transport_security_state.cc @@ -24,7 +24,6 @@ #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" @@ -35,7 +34,7 @@ #include "net/base/ssl_info.h" #include "net/base/x509_cert_types.h" #include "net/base/x509_certificate.h" -#include "net/http/http_util.h" +#include "net/http/http_security_headers.h" #if defined(USE_OPENSSL) #include "crypto/openssl_util.h" @@ -43,14 +42,47 @@ namespace net { -const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365; // 1 year +namespace { -static std::string HashHost(const std::string& canonicalized_host) { +std::string HashesToBase64String(const HashValueVector& hashes) { + std::string str; + for (size_t i = 0; i != hashes.size(); ++i) { + if (i != 0) + str += ","; + str += hashes[i].ToString(); + } + return str; +} + +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)); } +// Returns true if the intersection of |a| and |b| is not empty. If either +// |a| or |b| is empty, returns false. +bool HashesIntersect(const HashValueVector& a, + const HashValueVector& b) { + for (HashValueVector::const_iterator i = a.begin(); i != a.end(); ++i) { + HashValueVector::const_iterator j = + std::find_if(b.begin(), b.end(), HashValuesEqual(*i)); + if (j != b.end()) + return true; + } + return false; +} + +bool AddHash(const char* sha1_hash, + HashValueVector* out) { + HashValue hash(HASH_VALUE_SHA1); + memcpy(hash.data(), sha1_hash, hash.size()); + out->push_back(hash); + return true; +} + +} // namespace + TransportSecurityState::TransportSecurityState() : delegate_(NULL) { } @@ -183,370 +215,6 @@ void TransportSecurityState::DeleteSince(const base::Time& time) { 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; -} - -// static -bool TransportSecurityState::ParsePin(const std::string& value, - HashValue* out) { - StringPair slash = Split(Strip(value), '/'); - - if (slash.first == "sha1") - out->tag = HASH_VALUE_SHA1; - else if (slash.first == "sha256") - out->tag = HASH_VALUE_SHA256; - else - return false; - - std::string decoded; - if (!base::Base64Decode(slash.second, &decoded) || - decoded.size() != out->size()) { - return false; - } - - memcpy(out->data(), decoded.data(), out->size()); - return true; -} - -static bool ParseAndAppendPin(const std::string& value, - HashValueTag tag, - HashValueVector* hashes) { - std::string unquoted = HttpUtil::Unquote(value); - std::string decoded; - - // This code has to assume that 32 bytes is SHA-256 and 20 bytes is SHA-1. - // Currently, those are the only two possibilities, so the assumption is - // valid. - if (!base::Base64Decode(unquoted, &decoded)) - return false; - - HashValue hash(tag); - if (decoded.size() != hash.size()) - return false; - - memcpy(hash.data(), decoded.data(), hash.size()); - hashes->push_back(hash); - return true; -} - -struct HashValuesEqualPredicate { - explicit HashValuesEqualPredicate(const HashValue& fingerprint) : - fingerprint_(fingerprint) {} - - bool operator()(const HashValue& other) const { - return fingerprint_.Equals(other); - } - - const HashValue& 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 HashValueVector& pins, - const HashValueVector& from_cert_chain) { - for (HashValueVector::const_iterator - i = pins.begin(); i != pins.end(); ++i) { - HashValueVector::const_iterator j = - std::find_if(from_cert_chain.begin(), from_cert_chain.end(), - HashValuesEqualPredicate(*i)); - if (j == from_cert_chain.end()) - return true; - } - - return false; -} - -// Returns true if the intersection of |a| and |b| is not empty. If either -// |a| or |b| is empty, returns false. -static bool HashesIntersect(const HashValueVector& a, - const HashValueVector& b) { - for (HashValueVector::const_iterator i = a.begin(); i != a.end(); ++i) { - HashValueVector::const_iterator j = - std::find_if(b.begin(), b.end(), HashValuesEqualPredicate(*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 HashValueVector& pins, - const SSLInfo& ssl_info) { - // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual - // liveness and backupness below.) - if (pins.size() < 2) - return false; - - const HashValueVector& 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 [ ";" ... ] -bool TransportSecurityState::DomainState::ParsePinsHeader( - const base::Time& now, - const std::string& value, - const SSLInfo& ssl_info) { - bool parsed_max_age = false; - int max_age_candidate = 0; - HashValueVector 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_candidate)) { - return false; - } - if (max_age_candidate > kMaxHSTSAgeSecs) - max_age_candidate = kMaxHSTSAgeSecs; - parsed_max_age = true; - } else if (StartsWithASCII(equals.first, "pin-", false)) { - HashValueTag tag; - if (LowerCaseEqualsASCII(equals.first, "pin-sha1")) { - tag = HASH_VALUE_SHA1; - } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) { - tag = HASH_VALUE_SHA256; - } else { - LOG(WARNING) << "Ignoring pin of unknown type: " << equals.first; - return false; - } - if (!ParseAndAppendPin(equals.second, tag, &pins)) - return false; - } else { - // Silently ignore unknown directives for forward compatibility. - } - - source = semicolon.second; - } - - if (!parsed_max_age || !IsPinListValid(pins, ssl_info)) - return false; - - dynamic_spki_hashes_expiry = - now + base::TimeDelta::FromSeconds(max_age_candidate); - - dynamic_spki_hashes.clear(); - if (max_age_candidate > 0) { - for (HashValueVector::const_iterator i = pins.begin(); - i != pins.end(); ++i) { - dynamic_spki_hashes.push_back(*i); - } - } - - return true; -} - -// Parse the Strict-Transport-Security header, as currently defined in -// http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14: -// -// Strict-Transport-Security = "Strict-Transport-Security" ":" -// [ directive ] *( ";" [ directive ] ) -// -// directive = directive-name [ "=" directive-value ] -// directive-name = token -// directive-value = token | quoted-string -// -// 1. The order of appearance of directives is not significant. -// -// 2. All directives MUST appear only once in an STS header field. -// Directives are either optional or required, as stipulated in -// their definitions. -// -// 3. Directive names are case-insensitive. -// -// 4. UAs MUST ignore any STS header fields containing directives, or -// other header field value data, that does not conform to the -// syntax defined in this specification. -// -// 5. If an STS header field contains directive(s) not recognized by -// the UA, the UA MUST ignore the unrecognized directives and if the -// STS header field otherwise satisfies the above requirements (1 -// through 4), the UA MUST process the recognized directives. -bool TransportSecurityState::DomainState::ParseSTSHeader( - const base::Time& now, - const std::string& value) { - int max_age_candidate = 0; - bool include_subdomains_candidate = false; - - // We must see max-age exactly once. - int max_age_observed = 0; - // We must see includeSubdomains exactly 0 or 1 times. - int include_subdomains_observed = 0; - - enum ParserState { - START, - AFTER_MAX_AGE_LABEL, - AFTER_MAX_AGE_EQUALS, - AFTER_MAX_AGE, - AFTER_INCLUDE_SUBDOMAINS, - AFTER_UNKNOWN_LABEL, - DIRECTIVE_END - } state = START; - - StringTokenizer tokenizer(value, " \t=;"); - tokenizer.set_options(StringTokenizer::RETURN_DELIMS); - tokenizer.set_quote_chars("\""); - std::string unquoted; - while (tokenizer.GetNext()) { - DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1); - switch (state) { - case START: - case DIRECTIVE_END: - if (IsAsciiWhitespace(*tokenizer.token_begin())) - continue; - if (LowerCaseEqualsASCII(tokenizer.token(), "max-age")) { - state = AFTER_MAX_AGE_LABEL; - max_age_observed++; - } else if (LowerCaseEqualsASCII(tokenizer.token(), - "includesubdomains")) { - state = AFTER_INCLUDE_SUBDOMAINS; - include_subdomains_observed++; - include_subdomains_candidate = true; - } else { - state = AFTER_UNKNOWN_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; - unquoted = HttpUtil::Unquote(tokenizer.token()); - if (!MaxAgeToInt(unquoted.begin(), - unquoted.end(), - &max_age_candidate)) - return false; - state = AFTER_MAX_AGE; - break; - - case AFTER_MAX_AGE: - case AFTER_INCLUDE_SUBDOMAINS: - if (IsAsciiWhitespace(*tokenizer.token_begin())) - continue; - else if (*tokenizer.token_begin() == ';') - state = DIRECTIVE_END; - else - return false; - break; - - case AFTER_UNKNOWN_LABEL: - // Consume and ignore the post-label contents (if any). - if (*tokenizer.token_begin() != ';') - continue; - state = DIRECTIVE_END; - break; - } - } - - // We've consumed all the input. Let's see what state we ended up in. - if (max_age_observed != 1 || - (include_subdomains_observed != 0 && include_subdomains_observed != 1)) { - return false; - } - - switch (state) { - case AFTER_MAX_AGE: - case AFTER_INCLUDE_SUBDOMAINS: - case AFTER_UNKNOWN_LABEL: - if (max_age_candidate > 0) { - upgrade_expiry = now + base::TimeDelta::FromSeconds(max_age_candidate); - upgrade_mode = MODE_FORCE_HTTPS; - } else { - upgrade_expiry = now; - upgrade_mode = MODE_DEFAULT; - } - include_subdomains = include_subdomains_candidate; - return true; - case START: - case DIRECTIVE_END: - case AFTER_MAX_AGE_LABEL: - case AFTER_MAX_AGE_EQUALS: - return false; - default: - NOTREACHED(); - return false; - } -} - -static bool AddHash(const std::string& type_and_base64, - HashValueVector* out) { - HashValue hash; - - if (!TransportSecurityState::ParsePin(type_and_base64, &hash)) - return false; - - out->push_back(hash); - return true; -} - TransportSecurityState::~TransportSecurityState() {} void TransportSecurityState::DirtyNotify() { @@ -890,19 +558,17 @@ static bool HasPreload(const struct HSTSPreload* entries, size_t num_entries, if (!entries[j].https_required) out->upgrade_mode = TransportSecurityState::DomainState::MODE_DEFAULT; if (entries[j].pins.required_hashes) { - const char* const* hash = entries[j].pins.required_hashes; - while (*hash) { - bool ok = AddHash(*hash, &out->static_spki_hashes); - DCHECK(ok) << " failed to parse " << *hash; - hash++; + const char* const* sha1_hash = entries[j].pins.required_hashes; + while (*sha1_hash) { + AddHash(*sha1_hash, &out->static_spki_hashes); + sha1_hash++; } } if (entries[j].pins.excluded_hashes) { - const char* const* hash = entries[j].pins.excluded_hashes; - while (*hash) { - bool ok = AddHash(*hash, &out->bad_static_spki_hashes); - DCHECK(ok) << " failed to parse " << *hash; - hash++; + const char* const* sha1_hash = entries[j].pins.excluded_hashes; + while (*sha1_hash) { + AddHash(*sha1_hash, &out->bad_static_spki_hashes); + sha1_hash++; } } } @@ -941,6 +607,40 @@ static const struct HSTSPreload* GetHSTSPreload( return NULL; } +bool TransportSecurityState::AddHSTSHeader(const std::string& host, + const std::string& value) { + base::Time now = base::Time::Now(); + TransportSecurityState::DomainState domain_state; + if (ParseHSTSHeader(now, value, &domain_state.upgrade_expiry, + &domain_state.include_subdomains)) { + // Handle max-age == 0 + if (now == domain_state.upgrade_expiry) + domain_state.upgrade_mode = DomainState::MODE_DEFAULT; + else + domain_state.upgrade_mode = DomainState::MODE_FORCE_HTTPS; + domain_state.created = now; + EnableHost(host, domain_state); + return true; + } + return false; +} + +bool TransportSecurityState::AddHPKPHeader(const std::string& host, + const std::string& value, + const SSLInfo& ssl_info) { + base::Time now = base::Time::Now(); + TransportSecurityState::DomainState domain_state; + if (ParseHPKPHeader(now, value, ssl_info.public_key_hashes, + &domain_state.dynamic_spki_hashes_expiry, + &domain_state.dynamic_spki_hashes)) { + domain_state.upgrade_mode = DomainState::MODE_DEFAULT; + domain_state.created = now; + EnableHost(host, domain_state); + return true; + } + return false; +} + // static bool TransportSecurityState::IsGooglePinnedProperty(const std::string& host, bool sni_enabled) { @@ -987,21 +687,6 @@ void TransportSecurityState::ReportUMAOnPinFailure(const std::string& host) { } // static -const char* TransportSecurityState::HashValueLabel( - const HashValue& hash_value) { - switch (hash_value.tag) { - case HASH_VALUE_SHA1: - return "sha1/"; - case HASH_VALUE_SHA256: - return "sha256/"; - default: - NOTREACHED(); - LOG(WARNING) << "Invalid fingerprint of unknown type " << hash_value.tag; - return "unknown/"; - } -} - -// static bool TransportSecurityState::IsBuildTimely() { const base::Time build_time = base::GetBuildTime(); // We consider built-in information to be timely for 10 weeks. @@ -1056,21 +741,6 @@ void TransportSecurityState::AddOrUpdateForcedHosts( forced_hosts_[hashed_host] = state; } -static std::string HashesToBase64String( - const HashValueVector& hashes) { - std::vector<std::string> hashes_strs; - for (HashValueVector::const_iterator - i = hashes.begin(); i != hashes.end(); i++) { - std::string s; - const std::string hash_str(reinterpret_cast<const char*>(i->data()), - i->size()); - base::Base64Encode(hash_str, &s); - hashes_strs.push_back(s); - } - - return JoinString(hashes_strs, ','); -} - TransportSecurityState::DomainState::DomainState() : upgrade_mode(MODE_FORCE_HTTPS), created(base::Time::Now()), diff --git a/net/base/transport_security_state.h b/net/base/transport_security_state.h index 5f73f74..06fee49 100644 --- a/net/base/transport_security_state.h +++ b/net/base/transport_security_state.h @@ -61,20 +61,6 @@ class NET_EXPORT TransportSecurityState DomainState(); ~DomainState(); - // Parses |value| as a Public-Key-Pins header. If successful, returns true - // and updates the |dynamic_spki_hashes| and |dynamic_spki_hashes_expiry| - // fields; otherwise, returns false without updating any fields. - // Interprets the max-age directive relative to |now|. - bool ParsePinsHeader(const base::Time& now, - const std::string& value, - const SSLInfo& ssl_info); - - // Parses |value| as a Strict-Transport-Security header. If successful, - // returns true and updates the |upgrade_mode|, |upgrade_expiry| and - // |include_subdomains| fields; otherwise, returns false without updating - // any fields. Interprets the max-age directive relative to |now|. - bool ParseSTSHeader(const base::Time& now, const std::string& value); - // Takes a set of SubjectPublicKeyInfo |hashes| and returns true if: // 1) |bad_static_spki_hashes| does not intersect |hashes|; AND // 2) Both |static_spki_hashes| and |dynamic_spki_hashes| are empty @@ -237,6 +223,16 @@ class NET_EXPORT TransportSecurityState void AddOrUpdateForcedHosts(const std::string& hashed_host, const DomainState& state); + // Processes an HSTS header value from the host, adding entries to + // dynamic state if necessary. + bool AddHSTSHeader(const std::string& host, const std::string& value); + + // Processes an HPKP header value from the host, adding entries to + // dynamic state if necessary. ssl_info is used to check that + // the specified pins overlap with the certificate chain. + bool AddHPKPHeader(const std::string& host, const std::string& value, + const SSLInfo& ssl_info); + // Returns true iff we have any static public key pins for the |host| and // iff its set of required pins is the set we expect for Google // properties. @@ -249,11 +245,6 @@ class NET_EXPORT TransportSecurityState static bool IsGooglePinnedProperty(const std::string& host, bool sni_enabled); - // Decodes a pin string |value| (e.g. "sha1/hvfkN/qlp/zhXR3cuerq6jd2Z7g="). - // If parsing succeeded, updates |*out| and returns true; otherwise returns - // false without updating |*out|. - static bool ParsePin(const std::string& value, HashValue* out); - // The maximum number of seconds for which we'll cache an HSTS request. static const long int kMaxHSTSAgeSecs; diff --git a/net/base/transport_security_state_static.h b/net/base/transport_security_state_static.h index 48710e5..cbfa06c 100644 --- a/net/base/transport_security_state_static.h +++ b/net/base/transport_security_state_static.h @@ -8,163 +8,215 @@ #define NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_ // These are SubjectPublicKeyInfo hashes for public key pinning. The -// hashes are base64 encoded, SHA1 digests. +// hashes are SHA1 digests. static const char kSPKIHash_TestSPKI[] = - "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA="; + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; static const char kSPKIHash_VeriSignClass3[] = - "sha1/4n972HfV354KP560yw4uqe/baXc="; + "\xe2\x7f\x7b\xd8\x77\xd5\xdf\x9e\x0a\x3f" + "\x9e\xb4\xcb\x0e\x2e\xa9\xef\xdb\x69\x77"; static const char kSPKIHash_VeriSignClass3_G3[] = - "sha1/IvGeLsbqzPxdI0b0wuj2xVTdXgc="; + "\x22\xf1\x9e\x2e\xc6\xea\xcc\xfc\x5d\x23" + "\x46\xf4\xc2\xe8\xf6\xc5\x54\xdd\x5e\x07"; static const char kSPKIHash_Google1024[] = - "sha1/QMVAHW+MuvCLAO3vse6H0AWzuc0="; + "\x40\xc5\x40\x1d\x6f\x8c\xba\xf0\x8b\x00" + "\xed\xef\xb1\xee\x87\xd0\x05\xb3\xb9\xcd"; static const char kSPKIHash_Google2048[] = - "sha1/AbkhxY0L343gKf+cki7NVWp+ozk="; + "\x01\xb9\x21\xc5\x8d\x0b\xdf\x8d\xe0\x29" + "\xff\x9c\x92\x2e\xcd\x55\x6a\x7e\xa3\x39"; static const char kSPKIHash_EquifaxSecureCA[] = - "sha1/SOZo+SvSspXXR9gjIBBPM5iQn9Q="; + "\x48\xe6\x68\xf9\x2b\xd2\xb2\x95\xd7\x47" + "\xd8\x23\x20\x10\x4f\x33\x98\x90\x9f\xd4"; static const char kSPKIHash_Aetna[] = - "sha1/klKqFN6/gK4wqtlOYDhwJKVDLxo="; + "\x92\x52\xaa\x14\xde\xbf\x80\xae\x30\xaa" + "\xd9\x4e\x60\x38\x70\x24\xa5\x43\x2f\x1a"; static const char kSPKIHash_GeoTrustGlobal[] = - "sha1/wHqYaI2J+6sFZAwRfap9ZbjKzE4="; + "\xc0\x7a\x98\x68\x8d\x89\xfb\xab\x05\x64" + "\x0c\x11\x7d\xaa\x7d\x65\xb8\xca\xcc\x4e"; static const char kSPKIHash_GeoTrustPrimary[] = - "sha1/sBmJ5+/7Sq/LFI9YRjl2IkFQ4bo="; + "\xb0\x19\x89\xe7\xef\xfb\x4a\xaf\xcb\x14" + "\x8f\x58\x46\x39\x76\x22\x41\x50\xe1\xba"; static const char kSPKIHash_Intel[] = - "sha1/DsYq91myCBCQJW/D3f2KZjEwK8U="; + "\x0e\xc6\x2a\xf7\x59\xb2\x08\x10\x90\x25" + "\x6f\xc3\xdd\xfd\x8a\x66\x31\x30\x2b\xc5"; static const char kSPKIHash_TCTrustCenter[] = - "sha1/gzuEEAB/bkqdQS3EIjk2by7lW+k="; + "\x83\x3b\x84\x10\x00\x7f\x6e\x4a\x9d\x41" + "\x2d\xc4\x22\x39\x36\x6f\x2e\xe5\x5b\xe9"; static const char kSPKIHash_Vodafone[] = - "sha1/DX/hXFUUNmiZ/EDWIgjvIuvRFRw="; + "\x0d\x7f\xe1\x5c\x55\x14\x36\x68\x99\xfc" + "\x40\xd6\x22\x08\xef\x22\xeb\xd1\x15\x1c"; static const char kSPKIHash_RapidSSL[] = - "sha1/o5OZxATDsgmwgcIfIWIneMJ0jkw="; + "\xa3\x93\x99\xc4\x04\xc3\xb2\x09\xb0\x81" + "\xc2\x1f\x21\x62\x27\x78\xc2\x74\x8e\x4c"; static const char kSPKIHash_DigiCertEVRoot[] = - "sha1/gzF+YoVCU9bXeDGQ7JGQVumRueM="; + "\x83\x31\x7e\x62\x85\x42\x53\xd6\xd7\x78" + "\x31\x90\xec\x91\x90\x56\xe9\x91\xb9\xe3"; static const char kSPKIHash_Tor1[] = - "sha1/juNxSTv9UANmpC9kF5GKpmWNx3Y="; + "\x8e\xe3\x71\x49\x3b\xfd\x50\x03\x66\xa4" + "\x2f\x64\x17\x91\x8a\xa6\x65\x8d\xc7\x76"; static const char kSPKIHash_Tor2[] = - "sha1/lia43lPolzSPVIq34Dw57uYcLD8="; + "\x96\x26\xb8\xde\x53\xe8\x97\x34\x8f\x54" + "\x8a\xb7\xe0\x3c\x39\xee\xe6\x1c\x2c\x3f"; static const char kSPKIHash_Tor3[] = - "sha1/rzEyQIKOh77j87n5bjWUNguXF8Y="; + "\xaf\x31\x32\x40\x82\x8e\x87\xbe\xe3\xf3" + "\xb9\xf9\x6e\x35\x94\x36\x0b\x97\x17\xc6"; static const char kSPKIHash_VeriSignClass1[] = - "sha1/I0PRSKJViZuUfUYaeX7ATP7RcLc="; + "\x23\x43\xd1\x48\xa2\x55\x89\x9b\x94\x7d" + "\x46\x1a\x79\x7e\xc0\x4c\xfe\xd1\x70\xb7"; static const char kSPKIHash_VeriSignClass3_G4[] = - "sha1/7WYxNdMb1OymFMQp4xkGn5TBJlA="; + "\xed\x66\x31\x35\xd3\x1b\xd4\xec\xa6\x14" + "\xc4\x29\xe3\x19\x06\x9f\x94\xc1\x26\x50"; static const char kSPKIHash_VeriSignClass4_G3[] = - "sha1/PANDaGiVHPNpKri0Jtq6j+ki5b0="; + "\x3c\x03\x43\x68\x68\x95\x1c\xf3\x69\x2a" + "\xb8\xb4\x26\xda\xba\x8f\xe9\x22\xe5\xbd"; static const char kSPKIHash_VeriSignClass1_G3[] = - "sha1/VRmyeKyygdftp6vBg5nDu2kEJLU="; + "\x55\x19\xb2\x78\xac\xb2\x81\xd7\xed\xa7" + "\xab\xc1\x83\x99\xc3\xbb\x69\x04\x24\xb5"; static const char kSPKIHash_VeriSignClass2_G3[] = - "sha1/Wr7Fddyu87COJxlD/H8lDD32YeM="; + "\x5a\xbe\xc5\x75\xdc\xae\xf3\xb0\x8e\x27" + "\x19\x43\xfc\x7f\x25\x0c\x3d\xf6\x61\xe3"; static const char kSPKIHash_VeriSignClass3_G2[] = - "sha1/GiG0lStik84Ys2XsnA6TTLOB5tQ="; + "\x1a\x21\xb4\x95\x2b\x62\x93\xce\x18\xb3" + "\x65\xec\x9c\x0e\x93\x4c\xb3\x81\xe6\xd4"; static const char kSPKIHash_VeriSignClass2_G2[] = - "sha1/Eje6RRfurSkm/cHN/r7t8t7ZFFw="; + "\x12\x37\xba\x45\x17\xee\xad\x29\x26\xfd" + "\xc1\xcd\xfe\xbe\xed\xf2\xde\xd9\x14\x5c"; static const char kSPKIHash_VeriSignClass3_G5[] = - "sha1/sYEIGhmkwJQf+uiVKMEkyZs0rMc="; + "\xb1\x81\x08\x1a\x19\xa4\xc0\x94\x1f\xfa" + "\xe8\x95\x28\xc1\x24\xc9\x9b\x34\xac\xc7"; static const char kSPKIHash_VeriSignUniversal[] = - "sha1/u8I+KQuzKHcdrT6iTb30I70GsD0="; + "\xbb\xc2\x3e\x29\x0b\xb3\x28\x77\x1d\xad" + "\x3e\xa2\x4d\xbd\xf4\x23\xbd\x06\xb0\x3d"; static const char kSPKIHash_Twitter1[] = - "sha1/Vv7zwhR9TtOIN/29MFI4cgHld40="; + "\x56\xfe\xf3\xc2\x14\x7d\x4e\xd3\x88\x37" + "\xfd\xbd\x30\x52\x38\x72\x01\xe5\x77\x8d"; static const char kSPKIHash_GeoTrustGlobal2[] = - "sha1/cTg28gIxU0crbrplRqkQFVggBQk="; + "\x71\x38\x36\xf2\x02\x31\x53\x47\x2b\x6e" + "\xba\x65\x46\xa9\x10\x15\x58\x20\x05\x09"; static const char kSPKIHash_GeoTrustUniversal[] = - "sha1/h+hbY1PGI6MSjLD/u/VR/lmADiI="; + "\x87\xe8\x5b\x63\x53\xc6\x23\xa3\x12\x8c" + "\xb0\xff\xbb\xf5\x51\xfe\x59\x80\x0e\x22"; static const char kSPKIHash_GeoTrustUniversal2[] = - "sha1/Xk9ThoXdT57KX9wNRW99UbHcm3s="; + "\x5e\x4f\x53\x86\x85\xdd\x4f\x9e\xca\x5f" + "\xdc\x0d\x45\x6f\x7d\x51\xb1\xdc\x9b\x7b"; static const char kSPKIHash_GeoTrustPrimary_G2[] = - "sha1/vb6nG6txV/nkddlU0rcngBqCJoI="; + "\xbd\xbe\xa7\x1b\xab\x71\x57\xf9\xe4\x75" + "\xd9\x54\xd2\xb7\x27\x80\x1a\x82\x26\x82"; static const char kSPKIHash_GeoTrustPrimary_G3[] = - "sha1/nKmNAK90Dd2BgNITRaWLjy6UONY="; + "\x9c\xa9\x8d\x00\xaf\x74\x0d\xdd\x81\x80" + "\xd2\x13\x45\xa5\x8b\x8f\x2e\x94\x38\xd6"; static const char kSPKIHash_Entrust_2048[] = - "sha1/VeSB0RGAvtiJuQijMfmhJAkWuXA="; + "\x55\xe4\x81\xd1\x11\x80\xbe\xd8\x89\xb9" + "\x08\xa3\x31\xf9\xa1\x24\x09\x16\xb9\x70"; static const char kSPKIHash_Entrust_EV[] = - "sha1/ukKwgYhTiB2GY71MwF4I/upuu3c="; + "\xba\x42\xb0\x81\x88\x53\x88\x1d\x86\x63" + "\xbd\x4c\xc0\x5e\x08\xfe\xea\x6e\xbb\x77"; static const char kSPKIHash_Entrust_G2[] = - "sha1/qzDTr0vY8WtYae5FaSnahLhzlIg="; + "\xab\x30\xd3\xaf\x4b\xd8\xf1\x6b\x58\x69" + "\xee\x45\x69\x29\xda\x84\xb8\x73\x94\x88"; static const char kSPKIHash_Entrust_SSL[] = - "sha1/8BdiE1U9s/8KAGv7UISX8+1i0Bo="; + "\xf0\x17\x62\x13\x55\x3d\xb3\xff\x0a\x00" + "\x6b\xfb\x50\x84\x97\xf3\xed\x62\xd0\x1a"; static const char kSPKIHash_AAACertificateServices[] = - "sha1/xDAoxdPjCAwQRIssd7okU5dgu/k="; + "\xc4\x30\x28\xc5\xd3\xe3\x08\x0c\x10\x44" + "\x8b\x2c\x77\xba\x24\x53\x97\x60\xbb\xf9"; static const char kSPKIHash_AddTrustClass1CARoot[] = - "sha1/i9vXzKBoU0IW9MErJUT8Apyli0c="; + "\x8b\xdb\xd7\xcc\xa0\x68\x53\x42\x16\xf4" + "\xc1\x2b\x25\x44\xfc\x02\x9c\xa5\x8b\x47"; static const char kSPKIHash_AddTrustExternalCARoot[] = - "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c="; + "\x4f\x9c\x7d\x21\x79\x9c\xad\x0e\xd8\xb9" + "\x0c\x57\x9f\x1a\x02\x99\xe7\x90\xf3\x87"; static const char kSPKIHash_AddTrustPublicCARoot[] = - "sha1/qFdl1ugyyMUZY3Namhd0OoHf7i4="; + "\xa8\x57\x65\xd6\xe8\x32\xc8\xc5\x19\x63" + "\x73\x5a\x9a\x17\x74\x3a\x81\xdf\xee\x2e"; static const char kSPKIHash_AddTrustQualifiedCARoot[] = - "sha1/vOS3IxJVmOVjQRkcUOS2R8J2Bdc="; + "\xbc\xe4\xb7\x23\x12\x55\x98\xe5\x63\x41" + "\x19\x1c\x50\xe4\xb6\x47\xc2\x76\x05\xd7"; static const char kSPKIHash_COMODOCertificationAuthority[] = - "sha1/EeSR0cnkwOuazs9zVF3h8agwPsM="; + "\x11\xe4\x91\xd1\xc9\xe4\xc0\xeb\x9a\xce" + "\xcf\x73\x54\x5d\xe1\xf1\xa8\x30\x3e\xc3"; static const char kSPKIHash_SecureCertificateServices[] = - "sha1/PLQahC71XPIaPaVKyNG+OQh2N7w="; + "\x3c\xb4\x1a\x84\x2e\xf5\x5c\xf2\x1a\x3d" + "\xa5\x4a\xc8\xd1\xbe\x39\x08\x76\x37\xbc"; static const char kSPKIHash_TrustedCertificateServices[] = - "sha1//nLI678ML7sOJhOTkzwsqY3cJJQ="; + "\xfe\x72\xc8\xeb\xbf\x0c\x2f\xbb\x0e\x26" + "\x13\x93\x93\x3c\x2c\xa9\x8d\xdc\x24\x94"; static const char kSPKIHash_UTNDATACorpSGC[] = - "sha1/UzLRs89/+uDxoF2FTpLSnkUdtE8="; + "\x53\x32\xd1\xb3\xcf\x7f\xfa\xe0\xf1\xa0" + "\x5d\x85\x4e\x92\xd2\x9e\x45\x1d\xb4\x4f"; static const char kSPKIHash_UTNUSERFirstClientAuthenticationandEmail[] = - "sha1/iYJnfcSdJnAAS7RQSHzePa4Ebn0="; + "\x89\x82\x67\x7d\xc4\x9d\x26\x70\x00\x4b" + "\xb4\x50\x48\x7c\xde\x3d\xae\x04\x6e\x7d"; static const char kSPKIHash_UTNUSERFirstHardware[] = - "sha1/oXJfJhsomEOVXQc31YWWnUvSw0U="; + "\xa1\x72\x5f\x26\x1b\x28\x98\x43\x95\x5d" + "\x07\x37\xd5\x85\x96\x9d\x4b\xd2\xc3\x45"; static const char kSPKIHash_UTNUSERFirstObject[] = - "sha1/2u1kdBScFDyr3ZmpvVsoTYs8ydg="; + "\xda\xed\x64\x74\x14\x9c\x14\x3c\xab\xdd" + "\x99\xa9\xbd\x5b\x28\x4d\x8b\x3c\xc9\xd8"; static const char kSPKIHash_GTECyberTrustGlobalRoot[] = - "sha1/WXkS3mF11m/EI7d3E3THlt5viHI="; + "\x59\x79\x12\xde\x61\x75\xd6\x6f\xc4\x23" + "\xb7\x77\x13\x74\xc7\x96\xde\x6f\x88\x72"; static const char kSPKIHash_Tor2web[] = - "sha1/GeW1hxvUgy7I9ZSX/sZe+0jjM7E="; + "\x19\xe5\xb5\x87\x1b\xd4\x83\x2e\xc8\xf5" + "\x94\x97\xfe\xc6\x5e\xfb\x48\xe3\x33\xb1"; static const char kSPKIHash_AlphaSSL_G2[] = - "sha1/5STpjjF9yPytkFN8kecNpHCTkF8="; + "\xe5\x24\xe9\x8e\x31\x7d\xc8\xfc\xad\x90" + "\x53\x7c\x91\xe7\x0d\xa4\x70\x93\x90\x5f"; static const char kSPKIHash_CryptoCat1[] = - "sha1/TIfOhSz0wE1nqeDsUQx/OxSz6ck="; + "\x4c\x87\xce\x85\x2c\xf4\xc0\x4d\x67\xa9" + "\xe0\xec\x51\x0c\x7f\x3b\x14\xb3\xe9\xc9"; // The following is static data describing the hosts that are hardcoded with // certificate pins or HSTS information. diff --git a/net/base/transport_security_state_unittest.cc b/net/base/transport_security_state_unittest.cc index 3192f01..b68f083 100644 --- a/net/base/transport_security_state_unittest.cc +++ b/net/base/transport_security_state_unittest.cc @@ -46,405 +46,6 @@ class TransportSecurityStateTest : public testing::Test { } }; -TEST_F(TransportSecurityStateTest, BogusHeaders) { - TransportSecurityState::DomainState state; - base::Time now = base::Time::Now(); - - EXPECT_FALSE(state.ParseSTSHeader(now, "")); - EXPECT_FALSE(state.ParseSTSHeader(now, " ")); - EXPECT_FALSE(state.ParseSTSHeader(now, "abc")); - EXPECT_FALSE(state.ParseSTSHeader(now, " abc")); - EXPECT_FALSE(state.ParseSTSHeader(now, " abc ")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age ")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age=")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age =")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age= ")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age = ")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age = xy")); - EXPECT_FALSE(state.ParseSTSHeader(now, " max-age = 3488a923")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488a923 ")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-ag=3488923")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-aged=3488923")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age==3488923")); - EXPECT_FALSE(state.ParseSTSHeader(now, "amax-age=3488923")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=-3488923")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923;")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923 e")); - EXPECT_FALSE(state.ParseSTSHeader( - now, "max-age=3488923 includesubdomain")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923includesubdomains")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923=includesubdomains")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923 includesubdomainx")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923 includesubdomain=")); - EXPECT_FALSE(state.ParseSTSHeader( - now, "max-age=3488923 includesubdomain=true")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=3488923 includesubdomainsx")); - EXPECT_FALSE(state.ParseSTSHeader( - now, "max-age=3488923 includesubdomains x")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=34889.23 includesubdomains")); - EXPECT_FALSE(state.ParseSTSHeader(now, "max-age=34889 includesubdomains")); - - // Check that |state| was not updated by expecting the default - // values for its predictable fields. - EXPECT_EQ(state.upgrade_mode, - TransportSecurityState::DomainState::MODE_FORCE_HTTPS); - EXPECT_FALSE(state.include_subdomains); -} - -static bool GetPublicKeyHash(const net::X509Certificate::OSCertHandle& cert, - HashValue* hash) { - std::string der_bytes; - if (!net::X509Certificate::GetDEREncoded(cert, &der_bytes)) - return false; - - base::StringPiece spki; - if (!asn1::ExtractSPKIFromDERCert(der_bytes, &spki)) - return false; - - switch (hash->tag) { - case HASH_VALUE_SHA1: - base::SHA1HashBytes(reinterpret_cast<const unsigned char*>(spki.data()), - spki.size(), hash->data()); - break; - case HASH_VALUE_SHA256: - crypto::SHA256HashString(spki, hash->data(), crypto::kSHA256Length); - break; - default: - NOTREACHED() << "Unknown HashValueTag " << hash->tag; - } - - return true; -} - -static std::string GetPinFromCert(X509Certificate* cert, HashValueTag tag) { - HashValue spki_hash(tag); - EXPECT_TRUE(GetPublicKeyHash(cert->os_cert_handle(), &spki_hash)); - - std::string base64; - base::Base64Encode(base::StringPiece( - reinterpret_cast<char*>(spki_hash.data()), spki_hash.size()), &base64); - - std::string label; - switch (tag) { - case HASH_VALUE_SHA1: - label = "pin-sha1="; - break; - case HASH_VALUE_SHA256: - label = "pin-sha256="; - break; - default: - NOTREACHED() << "Unknown HashValueTag " << tag; - } - - return label + HttpUtil::Quote(base64); -} - -static void TestBogusPinsHeaders(HashValueTag tag) { - TransportSecurityState::DomainState state; - SSLInfo ssl_info; - ssl_info.cert = - ImportCertFromFile(GetTestCertsDirectory(), "test_mail_google_com.pem"); - std::string good_pin = GetPinFromCert(ssl_info.cert, tag); - base::Time now = base::Time::Now(); - - // 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(state.ParsePinsHeader(now, "", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " ", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "abc", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " abc", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " abc ", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age ", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age=", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age=", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age =", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age= ", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age = ", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, " max-age = xy", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader( - now, - " max-age = 3488a923", - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age=3488a923 ", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, - "max-ag=3488923pins=" + good_pin + "," + backup_pin, - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-aged=3488923" + backup_pin, - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-aged=3488923; " + backup_pin, - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, - "max-aged=3488923; " + backup_pin + ";" + backup_pin, - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, - "max-aged=3488923; " + good_pin + ";" + good_pin, - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-aged=3488923; " + good_pin, - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age==3488923", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "amax-age=3488923", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age=-3488923", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age=3488923;", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age=3488923 e", ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader( - now, - "max-age=3488923 includesubdomain", - ssl_info)); - EXPECT_FALSE(state.ParsePinsHeader(now, "max-age=34889.23", ssl_info)); - - // Check that |state| was not updated by expecting the default - // values for its predictable fields. - EXPECT_EQ(state.upgrade_mode, - TransportSecurityState::DomainState::MODE_FORCE_HTTPS); - EXPECT_FALSE(state.include_subdomains); -} - -TEST_F(TransportSecurityStateTest, BogusPinsHeadersSHA1) { - TestBogusPinsHeaders(HASH_VALUE_SHA1); -} - -TEST_F(TransportSecurityStateTest, BogusPinsHeadersSHA256) { - TestBogusPinsHeaders(HASH_VALUE_SHA256); -} - -TEST_F(TransportSecurityStateTest, ValidSTSHeaders) { - TransportSecurityState::DomainState state; - base::Time expiry; - base::Time now = base::Time::Now(); - - EXPECT_TRUE(state.ParseSTSHeader(now, "max-age=243")); - expiry = now + base::TimeDelta::FromSeconds(243); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_FALSE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, " Max-agE = 567")); - expiry = now + base::TimeDelta::FromSeconds(567); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_FALSE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, " mAx-aGe = 890 ")); - expiry = now + base::TimeDelta::FromSeconds(890); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_FALSE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, "max-age=123;incLudesUbdOmains")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, "incLudesUbdOmains; max-age=123")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, " incLudesUbdOmains; max-age=123")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, - " incLudesUbdOmains; max-age=123; pumpkin=kitten")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, - " pumpkin=894; incLudesUbdOmains; max-age=123 ")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, - " pumpkin; incLudesUbdOmains; max-age=123 ")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, - " pumpkin; incLudesUbdOmains; max-age=\"123\" ")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, - "animal=\"squirrel; distinguished\"; incLudesUbdOmains; max-age=123")); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader(now, "max-age=394082; incLudesUbdOmains")); - expiry = now + base::TimeDelta::FromSeconds(394082); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader( - now, "max-age=39408299 ;incLudesUbdOmains")); - expiry = now + base::TimeDelta::FromSeconds( - std::min(TransportSecurityState::kMaxHSTSAgeSecs, 39408299l)); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader( - now, "max-age=394082038 ; incLudesUbdOmains")); - expiry = now + base::TimeDelta::FromSeconds( - std::min(TransportSecurityState::kMaxHSTSAgeSecs, 394082038l)); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - - EXPECT_TRUE(state.ParseSTSHeader( - now, " max-age=0 ; incLudesUbdOmains ")); - expiry = now + base::TimeDelta::FromSeconds(0); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); - // When max-age == 0, we downgrade to MODE_DEFAULT rather than deleting - // the entire DomainState. (That is because we currently overload - // DomainState to also include pins, and we don't want to invalidate any - // opportunistic pins that may be in place.) - EXPECT_EQ(TransportSecurityState::DomainState::MODE_DEFAULT, - state.upgrade_mode); - - EXPECT_TRUE(state.ParseSTSHeader( - now, - " max-age=999999999999999999999999999999999999999999999 ;" - " incLudesUbdOmains ")); - expiry = now + base::TimeDelta::FromSeconds( - TransportSecurityState::kMaxHSTSAgeSecs); - EXPECT_EQ(expiry, state.upgrade_expiry); - EXPECT_TRUE(state.include_subdomains); -} - -static void TestValidPinsHeaders(HashValueTag tag) { - TransportSecurityState::DomainState state; - base::Time expiry; - base::Time now = base::Time::Now(); - - // 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); - ScopedTestRoot scoped_root(root_cert); - - // 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.) - int rv = ERR_FAILED; - CertVerifyResult result; - scoped_ptr<CertVerifier> verifier(CertVerifier::CreateDefault()); - TestCompletionCallback callback; - CertVerifier::RequestHandle handle = NULL; - rv = verifier->Verify(ssl_info.cert, "127.0.0.1", 0, NULL, &result, - callback.callback(), &handle, BoundNetLog()); - rv = callback.GetResult(rv); - ASSERT_EQ(OK, 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, /*tag*/HASH_VALUE_SHA1); - DLOG(WARNING) << "good pin: " << good_pin; - - // 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(state.ParsePinsHeader( - now, - "max-age=243; " + good_pin + ";" + backup_pin, - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds(243); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - " " + good_pin + "; " + backup_pin + " ; Max-agE = 567", - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds(567); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - good_pin + ";" + backup_pin + " ; mAx-aGe = 890 ", - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds(890); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - good_pin + ";" + backup_pin + "; max-age=123;IGNORED;", - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds(123); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - "max-age=394082;" + backup_pin + ";" + good_pin + "; ", - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds(394082); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - "max-age=39408299 ;" + backup_pin + ";" + good_pin + "; ", - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds( - std::min(TransportSecurityState::kMaxHSTSAgeSecs, 39408299l)); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - "max-age=39408038 ; cybers=39408038 ; " + - good_pin + ";" + backup_pin + "; ", - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds( - std::min(TransportSecurityState::kMaxHSTSAgeSecs, 394082038l)); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - " max-age=0 ; " + good_pin + ";" + backup_pin, - ssl_info)); - expiry = now + base::TimeDelta::FromSeconds(0); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); - - EXPECT_TRUE(state.ParsePinsHeader( - now, - " max-age=999999999999999999999999999999999999999999999 ; " + - backup_pin + ";" + good_pin + "; ", - ssl_info)); - expiry = now + - base::TimeDelta::FromSeconds(TransportSecurityState::kMaxHSTSAgeSecs); - EXPECT_EQ(expiry, state.dynamic_spki_hashes_expiry); -} - -TEST_F(TransportSecurityStateTest, ValidPinsHeadersSHA1) { - TestValidPinsHeaders(HASH_VALUE_SHA1); -} - -TEST_F(TransportSecurityStateTest, ValidPinsHeadersSHA256) { - TestValidPinsHeaders(HASH_VALUE_SHA256); -} - TEST_F(TransportSecurityStateTest, SimpleMatches) { TransportSecurityState state; TransportSecurityState::DomainState domain_state; @@ -920,8 +521,7 @@ TEST_F(TransportSecurityStateTest, BuiltinCertPins) { static bool AddHash(const std::string& type_and_base64, HashValueVector* out) { HashValue hash; - - if (!TransportSecurityState::ParsePin(type_and_base64, &hash)) + if (!hash.FromString(type_and_base64)) return false; out->push_back(hash); diff --git a/net/base/x509_cert_types.cc b/net/base/x509_cert_types.cc index 5e3c3d1..643454f 100644 --- a/net/base/x509_cert_types.cc +++ b/net/base/x509_cert_types.cc @@ -8,7 +8,6 @@ #include <cstring> #include "base/logging.h" -#include "base/sha1.h" #include "base/string_number_conversions.h" #include "base/string_piece.h" #include "base/time.h" @@ -29,22 +28,6 @@ int ParseIntAndAdvance(const char** field, size_t field_len, bool* ok) { return result; } -// CompareSHA1Hashes is a helper function for using bsearch() with an array of -// SHA1 hashes. -int CompareSHA1Hashes(const void* a, const void* b) { - return memcmp(a, b, base::kSHA1Length); -} - -} // namespace - -// static -bool IsSHA1HashInSortedArray(const SHA1HashValue& hash, - const uint8* array, - size_t array_byte_len) { - DCHECK_EQ(0u, array_byte_len % base::kSHA1Length); - const size_t arraylen = array_byte_len / base::kSHA1Length; - return NULL != bsearch(hash.data, array, arraylen, base::kSHA1Length, - CompareSHA1Hashes); } CertPrincipal::CertPrincipal() { @@ -143,52 +126,4 @@ bool ParseCertificateDate(const base::StringPiece& raw_date, return true; } -bool HashValue::Equals(const HashValue& other) const { - if (tag != other.tag) - return false; - switch (tag) { - case HASH_VALUE_SHA1: - return fingerprint.sha1.Equals(other.fingerprint.sha1); - case HASH_VALUE_SHA256: - return fingerprint.sha256.Equals(other.fingerprint.sha256); - default: - NOTREACHED() << "Unknown HashValueTag " << tag; - return false; - } -} - -size_t HashValue::size() const { - switch (tag) { - case HASH_VALUE_SHA1: - return sizeof(fingerprint.sha1.data); - case HASH_VALUE_SHA256: - return sizeof(fingerprint.sha256.data); - default: - NOTREACHED() << "Unknown HashValueTag " << tag; - // Although this is NOTREACHED, this function might be inlined and its - // return value can be passed to memset as the length argument. If we - // returned 0 here, it might result in what appears (in some stages of - // compilation) to be a call to to memset with a length argument of 0, - // which results in a warning. Therefore, we return a dummy value - // here. - return sizeof(fingerprint.sha1.data); - } -} - -unsigned char* HashValue::data() { - return const_cast<unsigned char*>(const_cast<const HashValue*>(this)->data()); -} - -const unsigned char* HashValue::data() const { - switch (tag) { - case HASH_VALUE_SHA1: - return fingerprint.sha1.data; - case HASH_VALUE_SHA256: - return fingerprint.sha256.data; - default: - NOTREACHED() << "Unknown HashValueTag " << tag; - return NULL; - } -} - } // namespace net diff --git a/net/base/x509_cert_types.h b/net/base/x509_cert_types.h index cebcbce..8ebc477 100644 --- a/net/base/x509_cert_types.h +++ b/net/base/x509_cert_types.h @@ -14,6 +14,7 @@ #include "base/logging.h" #include "base/string_piece.h" #include "build/build_config.h" +#include "net/base/hash_value.h" #include "net/base/net_export.h" #if defined(OS_MACOSX) && !defined(OS_IOS) @@ -28,88 +29,6 @@ namespace net { class X509Certificate; -// SHA-1 fingerprint (160 bits) of a certificate. -struct NET_EXPORT SHA1HashValue { - bool Equals(const SHA1HashValue& other) const { - return memcmp(data, other.data, sizeof(data)) == 0; - } - - unsigned char data[20]; -}; - -class NET_EXPORT SHA1HashValueLessThan { - public: - bool operator()(const SHA1HashValue& lhs, - const SHA1HashValue& rhs) const { - return memcmp(lhs.data, rhs.data, sizeof(lhs.data)) < 0; - } -}; - -struct NET_EXPORT SHA256HashValue { - bool Equals(const SHA256HashValue& other) const { - return memcmp(data, other.data, sizeof(data)) == 0; - } - - unsigned char data[32]; -}; - -class NET_EXPORT SHA256HashValueLessThan { - public: - bool operator()(const SHA256HashValue& lhs, - const SHA256HashValue& rhs) const { - return memcmp(lhs.data, rhs.data, sizeof(lhs.data)) < 0; - } -}; - -enum HashValueTag { - HASH_VALUE_SHA1, - HASH_VALUE_SHA256, - - // This must always be last. - HASH_VALUE_TAGS_COUNT -}; - -class NET_EXPORT HashValue { - public: - explicit HashValue(HashValueTag tag) : tag(tag) {} - HashValue() : tag(HASH_VALUE_SHA1) {} - - bool Equals(const HashValue& other) const; - size_t size() const; - unsigned char* data(); - const unsigned char* data() const; - - HashValueTag tag; - - private: - union { - SHA1HashValue sha1; - SHA256HashValue sha256; - } fingerprint; -}; - -class NET_EXPORT HashValueLessThan { - public: - bool operator()(const HashValue& lhs, - const HashValue& rhs) const { - size_t lhs_size = lhs.size(); - size_t rhs_size = rhs.size(); - - if (lhs_size != rhs_size) - return lhs_size < rhs_size; - - return memcmp(lhs.data(), rhs.data(), lhs_size) < 0; - } -}; - -typedef std::vector<HashValue> HashValueVector; - -// IsSHA1HashInSortedArray returns true iff |hash| is in |array|, a sorted -// array of SHA1 hashes. -bool NET_EXPORT IsSHA1HashInSortedArray(const SHA1HashValue& hash, - const uint8* array, - size_t array_byte_len); - // CertPrincipal represents the issuer or subject field of an X.509 certificate. struct NET_EXPORT CertPrincipal { CertPrincipal(); diff --git a/net/http/http_security_headers.cc b/net/http/http_security_headers.cc new file mode 100644 index 0000000..8018927 --- /dev/null +++ b/net/http/http_security_headers.cc @@ -0,0 +1,330 @@ +// 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 "base/base64.h" +#include "base/basictypes.h" +#include "base/string_number_conversions.h" +#include "base/string_tokenizer.h" +#include "base/string_util.h" +#include "net/http/http_security_headers.h" +#include "net/http/http_util.h" + +namespace net { + +namespace { + +COMPILE_ASSERT(kMaxHSTSAgeSecs <= kuint32max, kMaxHSTSAgeSecsTooLarge); + +// MaxAgeToInt converts a string representation of a "whole number" of +// seconds into a uint32. The string may contain an arbitrarily large number, +// which will be clipped to kMaxHSTSAgeSecs and which is guaranteed to fit +// within a 32-bit unsigned integer. False is returned on any parse error. +bool MaxAgeToInt(std::string::const_iterator begin, + std::string::const_iterator end, + uint32* result) { + const std::string s(begin, end); + int64 i = 0; + + // Return false on any StringToInt64 parse errors *except* for + // int64 overflow. StringToInt64 is used, rather than StringToUint64, + // in order to properly handle and reject negative numbers + // (StringToUint64 does not return false on negative numbers). + // For values too large to be stored in an int64, StringToInt64 will + // return false with i set to kint64max, so this case is detected + // by the immediately following if-statement and allowed to fall + // through so that i gets clipped to kMaxHSTSAgeSecs. + if (!base::StringToInt64(s, &i) && i != kint64max) + return false; + if (i < 0) + return false; + if (i > kMaxHSTSAgeSecs) + i = kMaxHSTSAgeSecs; + *result = (uint32)i; + return true; +} + +// 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". +bool IsBackupPinPresent(const HashValueVector& pins, + const HashValueVector& from_cert_chain) { + for (HashValueVector::const_iterator i = pins.begin(); i != pins.end(); + ++i) { + HashValueVector::const_iterator j = + std::find_if(from_cert_chain.begin(), from_cert_chain.end(), + HashValuesEqual(*i)); + if (j == from_cert_chain.end()) + return true; + } + + return false; +} + +// Returns true if the intersection of |a| and |b| is not empty. If either +// |a| or |b| is empty, returns false. +bool HashesIntersect(const HashValueVector& a, + const HashValueVector& b) { + for (HashValueVector::const_iterator i = a.begin(); i != a.end(); ++i) { + HashValueVector::const_iterator j = + std::find_if(b.begin(), b.end(), HashValuesEqual(*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. +bool IsPinListValid(const HashValueVector& pins, + const HashValueVector& from_cert_chain) { + // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual + // liveness and backupness below.) + if (pins.size() < 2) + return false; + + if (from_cert_chain.empty()) + return false; + + return IsBackupPinPresent(pins, from_cert_chain) && + HashesIntersect(pins, from_cert_chain); +} + +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; + +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; +} + +bool ParseAndAppendPin(const std::string& value, + HashValueTag tag, + HashValueVector* hashes) { + std::string unquoted = HttpUtil::Unquote(value); + std::string decoded; + + if (unquoted.empty()) + return false; + + if (!base::Base64Decode(unquoted, &decoded)) + return false; + + HashValue hash(tag); + if (decoded.size() != hash.size()) + return false; + + memcpy(hash.data(), decoded.data(), hash.size()); + hashes->push_back(hash); + return true; +} + +} // namespace + +// Parse the Strict-Transport-Security header, as currently defined in +// http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14: +// +// Strict-Transport-Security = "Strict-Transport-Security" ":" +// [ directive ] *( ";" [ directive ] ) +// +// directive = directive-name [ "=" directive-value ] +// directive-name = token +// directive-value = token | quoted-string +// +// 1. The order of appearance of directives is not significant. +// +// 2. All directives MUST appear only once in an STS header field. +// Directives are either optional or required, as stipulated in +// their definitions. +// +// 3. Directive names are case-insensitive. +// +// 4. UAs MUST ignore any STS header fields containing directives, or +// other header field value data, that does not conform to the +// syntax defined in this specification. +// +// 5. If an STS header field contains directive(s) not recognized by +// the UA, the UA MUST ignore the unrecognized directives and if the +// STS header field otherwise satisfies the above requirements (1 +// through 4), the UA MUST process the recognized directives. +bool ParseHSTSHeader(const base::Time& now, const std::string& value, + base::Time* expiry, // OUT + bool* include_subdomains) { // OUT + uint32 max_age_candidate = 0; + bool include_subdomains_candidate = false; + + // We must see max-age exactly once. + int max_age_observed = 0; + // We must see includeSubdomains exactly 0 or 1 times. + int include_subdomains_observed = 0; + + enum ParserState { + START, + AFTER_MAX_AGE_LABEL, + AFTER_MAX_AGE_EQUALS, + AFTER_MAX_AGE, + AFTER_INCLUDE_SUBDOMAINS, + AFTER_UNKNOWN_LABEL, + DIRECTIVE_END + } state = START; + + StringTokenizer tokenizer(value, " \t=;"); + tokenizer.set_options(StringTokenizer::RETURN_DELIMS); + tokenizer.set_quote_chars("\""); + std::string unquoted; + while (tokenizer.GetNext()) { + DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1); + switch (state) { + case START: + case DIRECTIVE_END: + if (IsAsciiWhitespace(*tokenizer.token_begin())) + continue; + if (LowerCaseEqualsASCII(tokenizer.token(), "max-age")) { + state = AFTER_MAX_AGE_LABEL; + max_age_observed++; + } else if (LowerCaseEqualsASCII(tokenizer.token(), + "includesubdomains")) { + state = AFTER_INCLUDE_SUBDOMAINS; + include_subdomains_observed++; + include_subdomains_candidate = true; + } else { + state = AFTER_UNKNOWN_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; + unquoted = HttpUtil::Unquote(tokenizer.token()); + if (!MaxAgeToInt(unquoted.begin(), unquoted.end(), &max_age_candidate)) + return false; + state = AFTER_MAX_AGE; + break; + + case AFTER_MAX_AGE: + case AFTER_INCLUDE_SUBDOMAINS: + if (IsAsciiWhitespace(*tokenizer.token_begin())) + continue; + else if (*tokenizer.token_begin() == ';') + state = DIRECTIVE_END; + else + return false; + break; + + case AFTER_UNKNOWN_LABEL: + // Consume and ignore the post-label contents (if any). + if (*tokenizer.token_begin() != ';') + continue; + state = DIRECTIVE_END; + break; + } + } + + // We've consumed all the input. Let's see what state we ended up in. + if (max_age_observed != 1 || + (include_subdomains_observed != 0 && include_subdomains_observed != 1)) { + return false; + } + + switch (state) { + case AFTER_MAX_AGE: + case AFTER_INCLUDE_SUBDOMAINS: + case AFTER_UNKNOWN_LABEL: + *expiry = now + base::TimeDelta::FromSeconds(max_age_candidate); + *include_subdomains = include_subdomains_candidate; + return true; + case START: + case DIRECTIVE_END: + case AFTER_MAX_AGE_LABEL: + case AFTER_MAX_AGE_EQUALS: + return false; + default: + NOTREACHED(); + return false; + } +} + +// "Public-Key-Pins" ":" +// "max-age" "=" delta-seconds ";" +// "pin-" algo "=" base64 [ ";" ... ] +bool ParseHPKPHeader(const base::Time& now, + const std::string& value, + const HashValueVector& chain_hashes, + base::Time* expiry, + HashValueVector* hashes) { + bool parsed_max_age = false; + uint32 max_age_candidate = 0; + HashValueVector 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_candidate)) { + return false; + } + parsed_max_age = true; + } else if (LowerCaseEqualsASCII(equals.first, "pin-sha1")) { + if (!ParseAndAppendPin(equals.second, HASH_VALUE_SHA1, &pins)) + return false; + } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) { + if (!ParseAndAppendPin(equals.second, HASH_VALUE_SHA256, &pins)) + return false; + } else { + // Silently ignore unknown directives for forward compatibility. + } + + source = semicolon.second; + } + + if (!parsed_max_age) + return false; + + if (!IsPinListValid(pins, chain_hashes)) + return false; + + *expiry = now + base::TimeDelta::FromSeconds(max_age_candidate); + for (HashValueVector::const_iterator i = pins.begin(); + i != pins.end(); ++i) { + hashes->push_back(*i); + } + + return true; +} + +} // namespace net diff --git a/net/http/http_security_headers.h b/net/http/http_security_headers.h new file mode 100644 index 0000000..bc465e9 --- /dev/null +++ b/net/http/http_security_headers.h @@ -0,0 +1,60 @@ +// 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. + +#ifndef NET_HTTP_HTTP_SECURITY_HEADERS_H_ +#define NET_HTTP_HTTP_SECURITY_HEADERS_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/gtest_prod_util.h" +#include "base/time.h" +#include "base/values.h" +#include "net/base/hash_value.h" +#include "net/base/net_export.h" + +namespace net { + +const int64 kMaxHSTSAgeSecs = 86400 * 365; // 1 year + +// Parses |value| as a Strict-Transport-Security header value. If successful, +// returns true and sets |*expiry| and |*include_subdomains|. +// Otherwise returns false and leaves the output parameters unchanged. +// Interprets the max-age directive relative to |now|. +// +// value is the right-hand side of: +// +// "Strict-Transport-Security" ":" +// [ directive ] *( ";" [ directive ] ) +bool NET_EXPORT_PRIVATE ParseHSTSHeader(const base::Time& now, + const std::string& value, + base::Time* expiry, + bool* include_subdomains); + +// Parses |value| as a Public-Key-Pins header value. If successful, +// returns true and populates the expiry and hashes values. +// Otherwise returns false and leaves the output parameters unchanged. +// Interprets the max-age directive relative to |now|. +// +// value is the right-hand side of: +// +// "Public-Key-Pins" ":" +// "max-age" "=" delta-seconds ";" +// "pin-" algo "=" base64 [ ";" ... ] +// +// For this function to return true, the key hashes specified by the HPKP +// header must pass two additional checks. There MUST be at least one +// key hash which matches the SSL certificate chain of the current site +// (as specified by the chain_hashes) parameter. In addition, there MUST +// be at least one key hash which does NOT match the site's SSL certificate +// chain (this is the "backup pin"). +bool NET_EXPORT_PRIVATE ParseHPKPHeader(const base::Time& now, + const std::string& value, + const HashValueVector& chain_hashes, + base::Time* expiry, + HashValueVector* hashes); + +} // namespace net + +#endif // NET_HTTP_HTTP_SECURITY_HEADERS_H_ diff --git a/net/http/http_security_headers_unittest.cc b/net/http/http_security_headers_unittest.cc new file mode 100644 index 0000000..a142486 --- /dev/null +++ b/net/http/http_security_headers_unittest.cc @@ -0,0 +1,424 @@ +// 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 "base/base64.h" +#include "base/sha1.h" +#include "base/string_piece.h" +#include "crypto/sha2.h" +#include "net/base/net_log.h" +#include "net/base/test_completion_callback.h" +#include "net/http/http_security_headers.h" +#include "net/http/http_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +HashValue GetTestHashValue(uint8 label, HashValueTag tag) { + HashValue hash_value(tag); + memset(hash_value.data(), label, hash_value.size()); + return hash_value; +} + +std::string GetTestPin(uint8 label, HashValueTag tag) { + HashValue hash_value = GetTestHashValue(label, tag); + std::string base64; + base::Base64Encode(base::StringPiece( + reinterpret_cast<char*>(hash_value.data()), hash_value.size()), &base64); + + switch (hash_value.tag) { + case HASH_VALUE_SHA1: + return std::string("pin-sha1=\"") + base64 + "\""; + case HASH_VALUE_SHA256: + return std::string("pin-sha256=\"") + base64 + "\""; + default: + NOTREACHED() << "Unknown HashValueTag " << hash_value.tag; + return std::string("ERROR"); + } +} + +}; + + +class HttpSecurityHeadersTest : public testing::Test { +}; + + +TEST_F(HttpSecurityHeadersTest, BogusHeaders) { + base::Time now = base::Time::Now(); + base::Time expiry = now; + bool include_subdomains = false; + + EXPECT_FALSE(ParseHSTSHeader(now, "", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " ", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "abc", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " abc", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " abc ", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age ", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=", &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age=", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age =", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age= ", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age = ", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age = xy", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, " max-age = 3488a923", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488a923 ", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-ag=3488923", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-aged=3488923", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age==3488923", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "amax-age=3488923", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=-3488923", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923;", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923 e", &expiry, + &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, + "max-age=3488923 includesubdomain", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923includesubdomains", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923=includesubdomains", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923 includesubdomainx", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923 includesubdomain=", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, + "max-age=3488923 includesubdomain=true", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=3488923 includesubdomainsx", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, + "max-age=3488923 includesubdomains x", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=34889.23 includesubdomains", + &expiry, &include_subdomains)); + EXPECT_FALSE(ParseHSTSHeader(now, "max-age=34889 includesubdomains", + &expiry, &include_subdomains)); + + // Check the out args were not updated by checking the default + // values for its predictable fields. + EXPECT_EQ(now, expiry); + EXPECT_FALSE(include_subdomains); +} + +static void TestBogusPinsHeaders(HashValueTag tag) { + base::Time now = base::Time::Now(); + base::Time expiry = now; + HashValueVector hashes; + HashValueVector chain_hashes; + + // Set some fake "chain" hashes + chain_hashes.push_back(GetTestHashValue(1, tag)); + chain_hashes.push_back(GetTestHashValue(2, tag)); + chain_hashes.push_back(GetTestHashValue(3, tag)); + + // The good pin must be in the chain, the backup pin must not be + std::string good_pin = GetTestPin(2, tag); + std::string backup_pin = GetTestPin(4, tag); + + EXPECT_FALSE(ParseHPKPHeader(now, "", chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " ", chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "abc", chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " abc", chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " abc ", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age ", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age=", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age=", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age =", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age= ", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age = ", chain_hashes, + &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, " max-age = xy", chain_hashes, + &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, + " max-age = 3488a923", + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age=3488a923 ", chain_hashes, + &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, + "max-ag=3488923pins=" + good_pin + "," + + backup_pin, + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-aged=3488923" + backup_pin, + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-aged=3488923; " + backup_pin, + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, + "max-aged=3488923; " + backup_pin + ";" + + backup_pin, + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, + "max-aged=3488923; " + good_pin + ";" + + good_pin, + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-aged=3488923; " + good_pin, + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age==3488923", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "amax-age=3488923", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age=-3488923", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age=3488923;", chain_hashes, &expiry, + &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age=3488923 e", chain_hashes, + &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, + "max-age=3488923 includesubdomain", + chain_hashes, &expiry, &hashes)); + EXPECT_FALSE(ParseHPKPHeader(now, "max-age=34889.23", chain_hashes, &expiry, + &hashes)); + + // Check the out args were not updated by checking the default + // values for its predictable fields. + EXPECT_EQ(now, expiry); + EXPECT_EQ(hashes.size(), (size_t)0); +} + +TEST_F(HttpSecurityHeadersTest, ValidSTSHeaders) { + base::Time now = base::Time::Now(); + base::Time expiry = now; + base::Time expect_expiry = now; + bool include_subdomains = false; + + EXPECT_TRUE(ParseHSTSHeader(now, "max-age=243", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(243); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_FALSE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, " Max-agE = 567", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(567); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_FALSE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, " mAx-aGe = 890 ", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(890); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_FALSE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, "max-age=123;incLudesUbdOmains", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, "incLudesUbdOmains; max-age=123", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, " incLudesUbdOmains; max-age=123", + &expiry, &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, + " incLudesUbdOmains; max-age=123; pumpkin=kitten", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, + " pumpkin=894; incLudesUbdOmains; max-age=123 ", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, + " pumpkin; incLudesUbdOmains; max-age=123 ", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, + " pumpkin; incLudesUbdOmains; max-age=\"123\" ", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, + "animal=\"squirrel; distinguished\"; incLudesUbdOmains; max-age=123", + &expiry, &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader(now, "max-age=394082; incLudesUbdOmains", + &expiry, &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(394082); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader( + now, "max-age=39408299 ;incLudesUbdOmains", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds( + std::min(kMaxHSTSAgeSecs, static_cast<int64>(GG_INT64_C(39408299)))); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader( + now, "max-age=394082038 ; incLudesUbdOmains", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds( + std::min(kMaxHSTSAgeSecs, static_cast<int64>(GG_INT64_C(394082038)))); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader( + now, " max-age=0 ; incLudesUbdOmains ", &expiry, + &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds(0); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); + + EXPECT_TRUE(ParseHSTSHeader( + now, + " max-age=999999999999999999999999999999999999999999999 ;" + " incLudesUbdOmains ", &expiry, &include_subdomains)); + expect_expiry = now + base::TimeDelta::FromSeconds( + kMaxHSTSAgeSecs); + EXPECT_EQ(expect_expiry, expiry); + EXPECT_TRUE(include_subdomains); +} + +static void TestValidPinsHeaders(HashValueTag tag) { + base::Time now = base::Time::Now(); + base::Time expiry = now; + base::Time expect_expiry = now; + HashValueVector hashes; + HashValueVector chain_hashes; + + // Set some fake "chain" hashes into chain_hashes + chain_hashes.push_back(GetTestHashValue(1, tag)); + chain_hashes.push_back(GetTestHashValue(2, tag)); + chain_hashes.push_back(GetTestHashValue(3, tag)); + + // The good pin must be in the chain, the backup pin must not be + std::string good_pin = GetTestPin(2, tag); + std::string backup_pin = GetTestPin(4, tag); + + EXPECT_TRUE(ParseHPKPHeader( + now, + "max-age=243; " + good_pin + ";" + backup_pin, + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds(243); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + " " + good_pin + "; " + backup_pin + " ; Max-agE = 567", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds(567); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + good_pin + ";" + backup_pin + " ; mAx-aGe = 890 ", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds(890); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + good_pin + ";" + backup_pin + "; max-age=123;IGNORED;", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds(123); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + "max-age=394082;" + backup_pin + ";" + good_pin + "; ", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds(394082); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + "max-age=39408299 ;" + backup_pin + ";" + good_pin + "; ", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds( + std::min(kMaxHSTSAgeSecs, static_cast<int64>(GG_INT64_C(39408299)))); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + "max-age=39408038 ; cybers=39408038 ; " + + good_pin + ";" + backup_pin + "; ", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds( + std::min(kMaxHSTSAgeSecs, static_cast<int64>(GG_INT64_C(394082038)))); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + " max-age=0 ; " + good_pin + ";" + backup_pin, + chain_hashes, &expiry, &hashes)); + expect_expiry = now + base::TimeDelta::FromSeconds(0); + EXPECT_EQ(expect_expiry, expiry); + + EXPECT_TRUE(ParseHPKPHeader( + now, + " max-age=999999999999999999999999999999999999999999999 ; " + + backup_pin + ";" + good_pin + "; ", + chain_hashes, &expiry, &hashes)); + expect_expiry = now + + base::TimeDelta::FromSeconds(kMaxHSTSAgeSecs); + EXPECT_EQ(expect_expiry, expiry); +} + +TEST_F(HttpSecurityHeadersTest, BogusPinsHeadersSHA1) { + TestBogusPinsHeaders(HASH_VALUE_SHA1); +} + +TEST_F(HttpSecurityHeadersTest, BogusPinsHeadersSHA256) { + TestBogusPinsHeaders(HASH_VALUE_SHA256); +} + +TEST_F(HttpSecurityHeadersTest, ValidPinsHeadersSHA1) { + TestValidPinsHeaders(HASH_VALUE_SHA1); +} + +TEST_F(HttpSecurityHeadersTest, ValidPinsHeadersSHA256) { + TestValidPinsHeaders(HASH_VALUE_SHA256); +} +}; + diff --git a/net/net.gyp b/net/net.gyp index d2d9a7f..5c1e43c 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -154,6 +154,8 @@ 'base/gzip_filter.h', 'base/gzip_header.cc', 'base/gzip_header.h', + 'base/hash_value.cc', + 'base/hash_value.h', 'base/host_cache.cc', 'base/host_cache.h', 'base/host_mapping_rules.cc', @@ -543,6 +545,8 @@ 'http/http_response_headers.h', 'http/http_response_info.cc', 'http/http_response_info.h', + 'http/http_security_headers.cc', + 'http/http_security_headers.h', 'http/http_server_properties.cc', 'http/http_server_properties.h', 'http/http_server_properties_impl.cc', @@ -1421,6 +1425,7 @@ 'http/http_request_headers_unittest.cc', 'http/http_response_body_drainer_unittest.cc', 'http/http_response_headers_unittest.cc', + 'http/http_security_headers_unittest.cc', 'http/http_server_properties_impl_unittest.cc', 'http/http_stream_factory_impl_unittest.cc', 'http/http_stream_parser_unittest.cc', diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc index 982e76f..fe5f325 100644 --- a/net/url_request/url_request_http_job.cc +++ b/net/url_request/url_request_http_job.cc @@ -685,93 +685,50 @@ void URLRequestHttpJob::FetchResponseCookies( // NOTE: |ProcessStrictTransportSecurityHeader| and // |ProcessPublicKeyPinsHeader| have very similar structures, by design. -// They manipulate different parts of |TransportSecurityState::DomainState|, -// and they must remain complementary. If, in future changes here, there is -// any conflict between their policies (such as in |domain_state.mode|), you -// should resolve the conflict in favor of the more strict policy. void URLRequestHttpJob::ProcessStrictTransportSecurityHeader() { DCHECK(response_info_); - - const URLRequestContext* ctx = request_->context(); + TransportSecurityState* security_state = + request_->context()->transport_security_state(); const SSLInfo& ssl_info = response_info_->ssl_info; - // Only accept strict transport security headers on HTTPS connections that - // have no certificate errors. + // Only accept HSTS headers on HTTPS connections that have no + // certificate errors. if (!ssl_info.is_valid() || IsCertStatusError(ssl_info.cert_status) || - !ctx->transport_security_state()) { + !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()); - if (!security_state->GetDomainState(host, sni_available, &domain_state)) - // |GetDomainState| may have altered |domain_state| while searching. If - // not found, start with a fresh state. - domain_state.upgrade_mode = - TransportSecurityState::DomainState::MODE_FORCE_HTTPS; - - HttpResponseHeaders* headers = GetResponseHeaders(); - std::string value; - void* iter = NULL; - base::Time now = base::Time::Now(); // http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec: // // If a UA receives more than one STS header field in a HTTP response // message over secure transport, then the UA MUST process only the // first such header field. - bool seen_sts = false; - while (headers->EnumerateHeader(&iter, "Strict-Transport-Security", &value)) { - if (seen_sts) - return; - seen_sts = true; - TransportSecurityState::DomainState domain_state; - if (domain_state.ParseSTSHeader(now, value)) - security_state->EnableHost(host, domain_state); - } + HttpResponseHeaders* headers = GetResponseHeaders(); + std::string value; + if (headers->EnumerateHeader(NULL, "Strict-Transport-Security", &value)) + security_state->AddHSTSHeader(request_info_.url.host(), value); } void URLRequestHttpJob::ProcessPublicKeyPinsHeader() { DCHECK(response_info_); - - const URLRequestContext* ctx = request_->context(); + TransportSecurityState* security_state = + request_->context()->transport_security_state(); const SSLInfo& ssl_info = response_info_->ssl_info; - // Only accept public key pins headers on HTTPS connections that have no + // Only accept HPKP headers on HTTPS connections that have no // certificate errors. if (!ssl_info.is_valid() || IsCertStatusError(ssl_info.cert_status) || - !ctx->transport_security_state()) { + !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()); - if (!security_state->GetDomainState(host, sni_available, &domain_state)) - // |GetDomainState| may have altered |domain_state| while searching. If - // not found, start with a fresh state. - domain_state.upgrade_mode = - TransportSecurityState::DomainState::MODE_DEFAULT; + // http://tools.ietf.org/html/draft-ietf-websec-key-pinning: + // + // If a UA receives more than one PKP header field in an HTTP + // response message over secure transport, then the UA MUST process + // only the first such header field. HttpResponseHeaders* headers = GetResponseHeaders(); - void* iter = NULL; std::string value; - base::Time now = base::Time::Now(); - - 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 (domain_state.ParsePinsHeader(now, value, ssl_info)) - security_state->EnableHost(host, domain_state); - } + if (headers->EnumerateHeader(NULL, "Public-Key-Pins", &value)) + security_state->AddHPKPHeader(request_info_.url.host(), value, ssl_info); } void URLRequestHttpJob::OnStartCompleted(int result) { |