diff options
Diffstat (limited to 'chromium/net/http/http_security_headers.cc')
-rw-r--r-- | chromium/net/http/http_security_headers.cc | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/chromium/net/http/http_security_headers.cc b/chromium/net/http/http_security_headers.cc new file mode 100644 index 00000000000..9fc7627cc5e --- /dev/null +++ b/chromium/net/http/http_security_headers.cc @@ -0,0 +1,334 @@ +// 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/strings/string_number_conversions.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/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 std::string& value, + base::TimeDelta* max_age, + bool* include_subdomains) { + 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; + + base::StringTokenizer tokenizer(value, " \t=;"); + tokenizer.set_options(base::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: + *max_age = 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 std::string& value, + const HashValueVector& chain_hashes, + base::TimeDelta* max_age, + bool* include_subdomains, + HashValueVector* hashes) { + bool parsed_max_age = false; + bool include_subdomains_candidate = 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 if (LowerCaseEqualsASCII(equals.first, "includesubdomains")) { + include_subdomains_candidate = true; + } else { + // Silently ignore unknown directives for forward compatibility. + } + + source = semicolon.second; + } + + if (!parsed_max_age) + return false; + + if (!IsPinListValid(pins, chain_hashes)) + return false; + + *max_age = base::TimeDelta::FromSeconds(max_age_candidate); + *include_subdomains = include_subdomains_candidate; + for (HashValueVector::const_iterator i = pins.begin(); + i != pins.end(); ++i) { + hashes->push_back(*i); + } + + return true; +} + +} // namespace net |