summaryrefslogtreecommitdiffstats
path: root/net/http
diff options
context:
space:
mode:
authorunsafe@trevp.net <unsafe@trevp.net@0039d316-1c4b-4281-b951-d872f2087c98>2013-01-08 22:07:33 +0000
committerunsafe@trevp.net <unsafe@trevp.net@0039d316-1c4b-4281-b951-d872f2087c98>2013-01-08 22:07:33 +0000
commit6ed72be9850bc59df02b0ec19ce44cfabc805656 (patch)
tree9e278061bdcd61bec8b9a591780dd201a8ce2f65 /net/http
parent0807387aeac97df0e1140e7de2c5cfa99c968175 (diff)
downloadchromium_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.cc330
-rw-r--r--net/http/http_security_headers.h60
-rw-r--r--net/http/http_security_headers_unittest.cc424
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);
+}
+};
+