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_tokenizer.h"
9 #include "base/strings/string_util.h"
10 #include "net/http/http_security_headers.h"
11 #include "net/http/http_util.h"
17 COMPILE_ASSERT(kMaxHSTSAgeSecs
<= kuint32max
, kMaxHSTSAgeSecsTooLarge
);
19 // MaxAgeToInt converts a string representation of a "whole number" of
20 // seconds into a uint32. The string may contain an arbitrarily large number,
21 // which will be clipped to kMaxHSTSAgeSecs and which is guaranteed to fit
22 // within a 32-bit unsigned integer. False is returned on any parse error.
23 bool MaxAgeToInt(std::string::const_iterator begin
,
24 std::string::const_iterator end
,
26 const std::string
s(begin
, end
);
29 // Return false on any StringToInt64 parse errors *except* for
30 // int64 overflow. StringToInt64 is used, rather than StringToUint64,
31 // in order to properly handle and reject negative numbers
32 // (StringToUint64 does not return false on negative numbers).
33 // For values too large to be stored in an int64, StringToInt64 will
34 // return false with i set to kint64max, so this case is detected
35 // by the immediately following if-statement and allowed to fall
36 // through so that i gets clipped to kMaxHSTSAgeSecs.
37 if (!base::StringToInt64(s
, &i
) && i
!= kint64max
)
41 if (i
> kMaxHSTSAgeSecs
)
47 // Returns true iff there is an item in |pins| which is not present in
48 // |from_cert_chain|. Such an SPKI hash is called a "backup pin".
49 bool IsBackupPinPresent(const HashValueVector
& pins
,
50 const HashValueVector
& from_cert_chain
) {
51 for (HashValueVector::const_iterator i
= pins
.begin(); i
!= pins
.end();
53 HashValueVector::const_iterator j
=
54 std::find_if(from_cert_chain
.begin(), from_cert_chain
.end(),
56 if (j
== from_cert_chain
.end())
63 // Returns true if the intersection of |a| and |b| is not empty. If either
64 // |a| or |b| is empty, returns false.
65 bool HashesIntersect(const HashValueVector
& a
,
66 const HashValueVector
& b
) {
67 for (HashValueVector::const_iterator i
= a
.begin(); i
!= a
.end(); ++i
) {
68 HashValueVector::const_iterator j
=
69 std::find_if(b
.begin(), b
.end(), HashValuesEqual(*i
));
76 // Returns true iff |pins| contains both a live and a backup pin. A live pin
77 // is a pin whose SPKI is present in the certificate chain in |ssl_info|. A
78 // backup pin is a pin intended for disaster recovery, not day-to-day use, and
79 // thus must be absent from the certificate chain. The Public-Key-Pins header
80 // specification requires both.
81 bool IsPinListValid(const HashValueVector
& pins
,
82 const HashValueVector
& from_cert_chain
) {
83 // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual
84 // liveness and backupness below.)
88 if (from_cert_chain
.empty())
91 return IsBackupPinPresent(pins
, from_cert_chain
) &&
92 HashesIntersect(pins
, from_cert_chain
);
95 std::string
Strip(const std::string
& source
) {
99 std::string::const_iterator start
= source
.begin();
100 std::string::const_iterator end
= source
.end();
101 HttpUtil::TrimLWS(&start
, &end
);
102 return std::string(start
, end
);
105 typedef std::pair
<std::string
, std::string
> StringPair
;
107 StringPair
Split(const std::string
& source
, char delimiter
) {
109 size_t point
= source
.find(delimiter
);
111 pair
.first
= source
.substr(0, point
);
112 if (std::string::npos
!= point
)
113 pair
.second
= source
.substr(point
+ 1);
118 bool ParseAndAppendPin(const std::string
& value
,
120 HashValueVector
* hashes
) {
121 std::string unquoted
= HttpUtil::Unquote(value
);
124 if (unquoted
.empty())
127 if (!base::Base64Decode(unquoted
, &decoded
))
131 if (decoded
.size() != hash
.size())
134 memcpy(hash
.data(), decoded
.data(), hash
.size());
135 hashes
->push_back(hash
);
141 // Parse the Strict-Transport-Security header, as currently defined in
142 // http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14:
144 // Strict-Transport-Security = "Strict-Transport-Security" ":"
145 // [ directive ] *( ";" [ directive ] )
147 // directive = directive-name [ "=" directive-value ]
148 // directive-name = token
149 // directive-value = token | quoted-string
151 // 1. The order of appearance of directives is not significant.
153 // 2. All directives MUST appear only once in an STS header field.
154 // Directives are either optional or required, as stipulated in
155 // their definitions.
157 // 3. Directive names are case-insensitive.
159 // 4. UAs MUST ignore any STS header fields containing directives, or
160 // other header field value data, that does not conform to the
161 // syntax defined in this specification.
163 // 5. If an STS header field contains directive(s) not recognized by
164 // the UA, the UA MUST ignore the unrecognized directives and if the
165 // STS header field otherwise satisfies the above requirements (1
166 // through 4), the UA MUST process the recognized directives.
167 bool ParseHSTSHeader(const std::string
& value
,
168 base::TimeDelta
* max_age
,
169 bool* include_subdomains
) {
170 uint32 max_age_candidate
= 0;
171 bool include_subdomains_candidate
= false;
173 // We must see max-age exactly once.
174 int max_age_observed
= 0;
175 // We must see includeSubdomains exactly 0 or 1 times.
176 int include_subdomains_observed
= 0;
181 AFTER_MAX_AGE_EQUALS
,
183 AFTER_INCLUDE_SUBDOMAINS
,
188 base::StringTokenizer
tokenizer(value
, " \t=;");
189 tokenizer
.set_options(base::StringTokenizer::RETURN_DELIMS
);
190 tokenizer
.set_quote_chars("\"");
191 std::string unquoted
;
192 while (tokenizer
.GetNext()) {
193 DCHECK(!tokenizer
.token_is_delim() || tokenizer
.token().length() == 1);
197 if (IsAsciiWhitespace(*tokenizer
.token_begin()))
199 if (LowerCaseEqualsASCII(tokenizer
.token(), "max-age")) {
200 state
= AFTER_MAX_AGE_LABEL
;
202 } else if (LowerCaseEqualsASCII(tokenizer
.token(),
203 "includesubdomains")) {
204 state
= AFTER_INCLUDE_SUBDOMAINS
;
205 include_subdomains_observed
++;
206 include_subdomains_candidate
= true;
208 state
= AFTER_UNKNOWN_LABEL
;
212 case AFTER_MAX_AGE_LABEL
:
213 if (IsAsciiWhitespace(*tokenizer
.token_begin()))
215 if (*tokenizer
.token_begin() != '=')
217 DCHECK_EQ(tokenizer
.token().length(), 1U);
218 state
= AFTER_MAX_AGE_EQUALS
;
221 case AFTER_MAX_AGE_EQUALS
:
222 if (IsAsciiWhitespace(*tokenizer
.token_begin()))
224 unquoted
= HttpUtil::Unquote(tokenizer
.token());
225 if (!MaxAgeToInt(unquoted
.begin(), unquoted
.end(), &max_age_candidate
))
227 state
= AFTER_MAX_AGE
;
231 case AFTER_INCLUDE_SUBDOMAINS
:
232 if (IsAsciiWhitespace(*tokenizer
.token_begin()))
234 else if (*tokenizer
.token_begin() == ';')
235 state
= DIRECTIVE_END
;
240 case AFTER_UNKNOWN_LABEL
:
241 // Consume and ignore the post-label contents (if any).
242 if (*tokenizer
.token_begin() != ';')
244 state
= DIRECTIVE_END
;
249 // We've consumed all the input. Let's see what state we ended up in.
250 if (max_age_observed
!= 1 ||
251 (include_subdomains_observed
!= 0 && include_subdomains_observed
!= 1)) {
257 case AFTER_INCLUDE_SUBDOMAINS
:
258 case AFTER_UNKNOWN_LABEL
:
259 *max_age
= base::TimeDelta::FromSeconds(max_age_candidate
);
260 *include_subdomains
= include_subdomains_candidate
;
264 case AFTER_MAX_AGE_LABEL
:
265 case AFTER_MAX_AGE_EQUALS
:
273 // "Public-Key-Pins" ":"
274 // "max-age" "=" delta-seconds ";"
275 // "pin-" algo "=" base64 [ ";" ... ]
276 bool ParseHPKPHeader(const std::string
& value
,
277 const HashValueVector
& chain_hashes
,
278 base::TimeDelta
* max_age
,
279 bool* include_subdomains
,
280 HashValueVector
* hashes
) {
281 bool parsed_max_age
= false;
282 bool include_subdomains_candidate
= false;
283 uint32 max_age_candidate
= 0;
284 HashValueVector pins
;
286 std::string source
= value
;
288 while (!source
.empty()) {
289 StringPair semicolon
= Split(source
, ';');
290 semicolon
.first
= Strip(semicolon
.first
);
291 semicolon
.second
= Strip(semicolon
.second
);
292 StringPair equals
= Split(semicolon
.first
, '=');
293 equals
.first
= Strip(equals
.first
);
294 equals
.second
= Strip(equals
.second
);
296 if (LowerCaseEqualsASCII(equals
.first
, "max-age")) {
297 if (equals
.second
.empty() ||
298 !MaxAgeToInt(equals
.second
.begin(), equals
.second
.end(),
299 &max_age_candidate
)) {
302 parsed_max_age
= true;
303 } else if (LowerCaseEqualsASCII(equals
.first
, "pin-sha1")) {
304 if (!ParseAndAppendPin(equals
.second
, HASH_VALUE_SHA1
, &pins
))
306 } else if (LowerCaseEqualsASCII(equals
.first
, "pin-sha256")) {
307 if (!ParseAndAppendPin(equals
.second
, HASH_VALUE_SHA256
, &pins
))
309 } else if (LowerCaseEqualsASCII(equals
.first
, "includesubdomains")) {
310 include_subdomains_candidate
= true;
312 // Silently ignore unknown directives for forward compatibility.
315 source
= semicolon
.second
;
321 if (!IsPinListValid(pins
, chain_hashes
))
324 *max_age
= base::TimeDelta::FromSeconds(max_age_candidate
);
325 *include_subdomains
= include_subdomains_candidate
;
326 for (HashValueVector::const_iterator i
= pins
.begin();
327 i
!= pins
.end(); ++i
) {
330 for (HashValueVector::const_iterator j
= hashes
->begin();
331 j
!= hashes
->end(); ++j
) {
339 hashes
->push_back(*i
);