1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "base/base64.h"
6 #include "base/basictypes.h"
7 #include "base/strings/string_number_conversions.h"
8 #include "base/strings/string_piece.h"
9 #include "base/strings/string_tokenizer.h"
10 #include "base/strings/string_util.h"
11 #include "net/http/http_security_headers.h"
12 #include "net/http/http_util.h"
19 static_assert(kMaxHSTSAgeSecs
<= kuint32max
, "kMaxHSTSAgeSecs too large");
21 // MaxAgeToInt converts a string representation of a "whole number" of
22 // seconds into a uint32. The string may contain an arbitrarily large number,
23 // which will be clipped to kMaxHSTSAgeSecs and which is guaranteed to fit
24 // within a 32-bit unsigned integer. False is returned on any parse error.
25 bool MaxAgeToInt(std::string::const_iterator begin
,
26 std::string::const_iterator end
,
28 const base::StringPiece
s(begin
, end
);
34 // Return false on any StringToInt64 parse errors *except* for
35 // int64 overflow. StringToInt64 is used, rather than StringToUint64,
36 // in order to properly handle and reject negative numbers
37 // (StringToUint64 does not return false on negative numbers).
38 // For values too large to be stored in an int64, StringToInt64 will
39 // return false with i set to kint64max, so this case is detected
40 // by the immediately following if-statement and allowed to fall
41 // through so that i gets clipped to kMaxHSTSAgeSecs.
42 if (!base::StringToInt64(s
, &i
) && i
!= kint64max
)
46 if (i
> kMaxHSTSAgeSecs
)
52 // Returns true iff there is an item in |pins| which is not present in
53 // |from_cert_chain|. Such an SPKI hash is called a "backup pin".
54 bool IsBackupPinPresent(const HashValueVector
& pins
,
55 const HashValueVector
& from_cert_chain
) {
56 for (HashValueVector::const_iterator i
= pins
.begin(); i
!= pins
.end();
58 HashValueVector::const_iterator j
=
59 std::find_if(from_cert_chain
.begin(), from_cert_chain
.end(),
61 if (j
== from_cert_chain
.end())
68 // Returns true if the intersection of |a| and |b| is not empty. If either
69 // |a| or |b| is empty, returns false.
70 bool HashesIntersect(const HashValueVector
& a
,
71 const HashValueVector
& b
) {
72 for (HashValueVector::const_iterator i
= a
.begin(); i
!= a
.end(); ++i
) {
73 HashValueVector::const_iterator j
=
74 std::find_if(b
.begin(), b
.end(), HashValuesEqual(*i
));
81 // Returns true iff |pins| contains both a live and a backup pin. A live pin
82 // is a pin whose SPKI is present in the certificate chain in |ssl_info|. A
83 // backup pin is a pin intended for disaster recovery, not day-to-day use, and
84 // thus must be absent from the certificate chain. The Public-Key-Pins header
85 // specification requires both.
86 bool IsPinListValid(const HashValueVector
& pins
,
87 const HashValueVector
& from_cert_chain
) {
88 // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual
89 // liveness and backupness below.)
93 if (from_cert_chain
.empty())
96 return IsBackupPinPresent(pins
, from_cert_chain
) &&
97 HashesIntersect(pins
, from_cert_chain
);
100 bool ParseAndAppendPin(std::string::const_iterator begin
,
101 std::string::const_iterator end
,
103 HashValueVector
* hashes
) {
104 const base::StringPiece
value(begin
, end
);
109 if (!base::Base64Decode(value
, &decoded
))
113 if (decoded
.size() != hash
.size())
116 memcpy(hash
.data(), decoded
.data(), hash
.size());
117 hashes
->push_back(hash
);
123 // Parse the Strict-Transport-Security header, as currently defined in
124 // http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14:
126 // Strict-Transport-Security = "Strict-Transport-Security" ":"
127 // [ directive ] *( ";" [ directive ] )
129 // directive = directive-name [ "=" directive-value ]
130 // directive-name = token
131 // directive-value = token | quoted-string
133 // 1. The order of appearance of directives is not significant.
135 // 2. All directives MUST appear only once in an STS header field.
136 // Directives are either optional or required, as stipulated in
137 // their definitions.
139 // 3. Directive names are case-insensitive.
141 // 4. UAs MUST ignore any STS header fields containing directives, or
142 // other header field value data, that does not conform to the
143 // syntax defined in this specification.
145 // 5. If an STS header field contains directive(s) not recognized by
146 // the UA, the UA MUST ignore the unrecognized directives and if the
147 // STS header field otherwise satisfies the above requirements (1
148 // through 4), the UA MUST process the recognized directives.
149 bool ParseHSTSHeader(const std::string
& value
,
150 base::TimeDelta
* max_age
,
151 bool* include_subdomains
) {
152 uint32 max_age_candidate
= 0;
153 bool include_subdomains_candidate
= false;
155 // We must see max-age exactly once.
156 int max_age_observed
= 0;
157 // We must see includeSubdomains exactly 0 or 1 times.
158 int include_subdomains_observed
= 0;
163 AFTER_MAX_AGE_EQUALS
,
165 AFTER_INCLUDE_SUBDOMAINS
,
170 base::StringTokenizer
tokenizer(value
, " \t=;");
171 tokenizer
.set_options(base::StringTokenizer::RETURN_DELIMS
);
172 tokenizer
.set_quote_chars("\"");
173 std::string unquoted
;
174 while (tokenizer
.GetNext()) {
175 DCHECK(!tokenizer
.token_is_delim() || tokenizer
.token().length() == 1);
179 if (base::IsAsciiWhitespace(*tokenizer
.token_begin()))
181 if (base::LowerCaseEqualsASCII(tokenizer
.token(), "max-age")) {
182 state
= AFTER_MAX_AGE_LABEL
;
184 } else if (base::LowerCaseEqualsASCII(tokenizer
.token(),
185 "includesubdomains")) {
186 state
= AFTER_INCLUDE_SUBDOMAINS
;
187 include_subdomains_observed
++;
188 include_subdomains_candidate
= true;
190 state
= AFTER_UNKNOWN_LABEL
;
194 case AFTER_MAX_AGE_LABEL
:
195 if (base::IsAsciiWhitespace(*tokenizer
.token_begin()))
197 if (*tokenizer
.token_begin() != '=')
199 DCHECK_EQ(tokenizer
.token().length(), 1U);
200 state
= AFTER_MAX_AGE_EQUALS
;
203 case AFTER_MAX_AGE_EQUALS
:
204 if (base::IsAsciiWhitespace(*tokenizer
.token_begin()))
206 unquoted
= HttpUtil::Unquote(tokenizer
.token());
207 if (!MaxAgeToInt(unquoted
.begin(), unquoted
.end(), &max_age_candidate
))
209 state
= AFTER_MAX_AGE
;
213 case AFTER_INCLUDE_SUBDOMAINS
:
214 if (base::IsAsciiWhitespace(*tokenizer
.token_begin()))
216 else if (*tokenizer
.token_begin() == ';')
217 state
= DIRECTIVE_END
;
222 case AFTER_UNKNOWN_LABEL
:
223 // Consume and ignore the post-label contents (if any).
224 if (*tokenizer
.token_begin() != ';')
226 state
= DIRECTIVE_END
;
231 // We've consumed all the input. Let's see what state we ended up in.
232 if (max_age_observed
!= 1 ||
233 (include_subdomains_observed
!= 0 && include_subdomains_observed
!= 1)) {
240 case AFTER_INCLUDE_SUBDOMAINS
:
241 case AFTER_UNKNOWN_LABEL
:
242 *max_age
= base::TimeDelta::FromSeconds(max_age_candidate
);
243 *include_subdomains
= include_subdomains_candidate
;
246 case AFTER_MAX_AGE_LABEL
:
247 case AFTER_MAX_AGE_EQUALS
:
255 // "Public-Key-Pins[-Report-Only]" ":"
256 // "max-age" "=" delta-seconds ";"
257 // "pin-" algo "=" base64 [ ";" ... ]
258 // [ ";" "includeSubdomains" ]
259 // [ ";" "report-uri" "=" uri-reference ]
260 bool ParseHPKPHeader(const std::string
& value
,
261 const HashValueVector
& chain_hashes
,
262 base::TimeDelta
* max_age
,
263 bool* include_subdomains
,
264 HashValueVector
* hashes
,
266 bool parsed_max_age
= false;
267 bool include_subdomains_candidate
= false;
268 uint32 max_age_candidate
= 0;
269 GURL parsed_report_uri
;
270 HashValueVector pins
;
272 HttpUtil::NameValuePairsIterator
name_value_pairs(
273 value
.begin(), value
.end(), ';',
274 HttpUtil::NameValuePairsIterator::VALUES_OPTIONAL
);
276 while (name_value_pairs
.GetNext()) {
277 if (base::LowerCaseEqualsASCII(
278 base::StringPiece(name_value_pairs
.name_begin(),
279 name_value_pairs
.name_end()),
281 if (!MaxAgeToInt(name_value_pairs
.value_begin(),
282 name_value_pairs
.value_end(), &max_age_candidate
)) {
285 parsed_max_age
= true;
286 } else if (base::LowerCaseEqualsASCII(
287 base::StringPiece(name_value_pairs
.name_begin(),
288 name_value_pairs
.name_end()),
290 // Pins are always quoted.
291 if (!name_value_pairs
.value_is_quoted() ||
292 !ParseAndAppendPin(name_value_pairs
.value_begin(),
293 name_value_pairs
.value_end(), HASH_VALUE_SHA1
,
297 } else if (base::LowerCaseEqualsASCII(
298 base::StringPiece(name_value_pairs
.name_begin(),
299 name_value_pairs
.name_end()),
301 // Pins are always quoted.
302 if (!name_value_pairs
.value_is_quoted() ||
303 !ParseAndAppendPin(name_value_pairs
.value_begin(),
304 name_value_pairs
.value_end(), HASH_VALUE_SHA256
,
308 } else if (base::LowerCaseEqualsASCII(
309 base::StringPiece(name_value_pairs
.name_begin(),
310 name_value_pairs
.name_end()),
311 "includesubdomains")) {
312 include_subdomains_candidate
= true;
313 } else if (base::LowerCaseEqualsASCII(
314 base::StringPiece(name_value_pairs
.name_begin(),
315 name_value_pairs
.name_end()),
317 // report-uris are always quoted.
318 if (!name_value_pairs
.value_is_quoted())
321 parsed_report_uri
= GURL(name_value_pairs
.value());
322 if (parsed_report_uri
.is_empty() || !parsed_report_uri
.is_valid())
325 // Silently ignore unknown directives for forward compatibility.
329 if (!name_value_pairs
.valid())
335 if (!IsPinListValid(pins
, chain_hashes
))
338 *max_age
= base::TimeDelta::FromSeconds(max_age_candidate
);
339 *include_subdomains
= include_subdomains_candidate
;
341 *report_uri
= parsed_report_uri
;