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 /net/http | |
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
Diffstat (limited to 'net/http')
-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 |
3 files changed, 814 insertions, 0 deletions
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); +} +}; + |