Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / net / http / http_security_headers.cc
blobed7ceccbb73080a7e5dfa1627083a728c3a044fe
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"
13 #include "url/gurl.h"
15 namespace net {
17 namespace {
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,
27 uint32* result) {
28 const base::StringPiece s(begin, end);
29 if (s.empty())
30 return false;
32 int64 i = 0;
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)
43 return false;
44 if (i < 0)
45 return false;
46 if (i > kMaxHSTSAgeSecs)
47 i = kMaxHSTSAgeSecs;
48 *result = (uint32)i;
49 return true;
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();
57 ++i) {
58 HashValueVector::const_iterator j =
59 std::find_if(from_cert_chain.begin(), from_cert_chain.end(),
60 HashValuesEqual(*i));
61 if (j == from_cert_chain.end())
62 return true;
65 return false;
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));
75 if (j != b.end())
76 return true;
78 return false;
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.)
90 if (pins.size() < 2)
91 return false;
93 if (from_cert_chain.empty())
94 return false;
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,
102 HashValueTag tag,
103 HashValueVector* hashes) {
104 const base::StringPiece value(begin, end);
105 if (value.empty())
106 return false;
108 std::string decoded;
109 if (!base::Base64Decode(value, &decoded))
110 return false;
112 HashValue hash(tag);
113 if (decoded.size() != hash.size())
114 return false;
116 memcpy(hash.data(), decoded.data(), hash.size());
117 hashes->push_back(hash);
118 return true;
121 } // namespace
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;
160 enum ParserState {
161 START,
162 AFTER_MAX_AGE_LABEL,
163 AFTER_MAX_AGE_EQUALS,
164 AFTER_MAX_AGE,
165 AFTER_INCLUDE_SUBDOMAINS,
166 AFTER_UNKNOWN_LABEL,
167 DIRECTIVE_END
168 } state = START;
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);
176 switch (state) {
177 case START:
178 case DIRECTIVE_END:
179 if (base::IsAsciiWhitespace(*tokenizer.token_begin()))
180 continue;
181 if (base::LowerCaseEqualsASCII(tokenizer.token(), "max-age")) {
182 state = AFTER_MAX_AGE_LABEL;
183 max_age_observed++;
184 } else if (base::LowerCaseEqualsASCII(tokenizer.token(),
185 "includesubdomains")) {
186 state = AFTER_INCLUDE_SUBDOMAINS;
187 include_subdomains_observed++;
188 include_subdomains_candidate = true;
189 } else {
190 state = AFTER_UNKNOWN_LABEL;
192 break;
194 case AFTER_MAX_AGE_LABEL:
195 if (base::IsAsciiWhitespace(*tokenizer.token_begin()))
196 continue;
197 if (*tokenizer.token_begin() != '=')
198 return false;
199 DCHECK_EQ(tokenizer.token().length(), 1U);
200 state = AFTER_MAX_AGE_EQUALS;
201 break;
203 case AFTER_MAX_AGE_EQUALS:
204 if (base::IsAsciiWhitespace(*tokenizer.token_begin()))
205 continue;
206 unquoted = HttpUtil::Unquote(tokenizer.token());
207 if (!MaxAgeToInt(unquoted.begin(), unquoted.end(), &max_age_candidate))
208 return false;
209 state = AFTER_MAX_AGE;
210 break;
212 case AFTER_MAX_AGE:
213 case AFTER_INCLUDE_SUBDOMAINS:
214 if (base::IsAsciiWhitespace(*tokenizer.token_begin()))
215 continue;
216 else if (*tokenizer.token_begin() == ';')
217 state = DIRECTIVE_END;
218 else
219 return false;
220 break;
222 case AFTER_UNKNOWN_LABEL:
223 // Consume and ignore the post-label contents (if any).
224 if (*tokenizer.token_begin() != ';')
225 continue;
226 state = DIRECTIVE_END;
227 break;
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)) {
234 return false;
237 switch (state) {
238 case DIRECTIVE_END:
239 case AFTER_MAX_AGE:
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;
244 return true;
245 case START:
246 case AFTER_MAX_AGE_LABEL:
247 case AFTER_MAX_AGE_EQUALS:
248 return false;
249 default:
250 NOTREACHED();
251 return false;
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,
265 GURL* report_uri) {
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()),
280 "max-age")) {
281 if (!MaxAgeToInt(name_value_pairs.value_begin(),
282 name_value_pairs.value_end(), &max_age_candidate)) {
283 return false;
285 parsed_max_age = true;
286 } else if (base::LowerCaseEqualsASCII(
287 base::StringPiece(name_value_pairs.name_begin(),
288 name_value_pairs.name_end()),
289 "pin-sha1")) {
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,
294 &pins)) {
295 return false;
297 } else if (base::LowerCaseEqualsASCII(
298 base::StringPiece(name_value_pairs.name_begin(),
299 name_value_pairs.name_end()),
300 "pin-sha256")) {
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,
305 &pins)) {
306 return false;
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()),
316 "report-uri")) {
317 // report-uris are always quoted.
318 if (!name_value_pairs.value_is_quoted())
319 return false;
321 parsed_report_uri = GURL(name_value_pairs.value());
322 if (parsed_report_uri.is_empty() || !parsed_report_uri.is_valid())
323 return false;
324 } else {
325 // Silently ignore unknown directives for forward compatibility.
329 if (!name_value_pairs.valid())
330 return false;
332 if (!parsed_max_age)
333 return false;
335 if (!IsPinListValid(pins, chain_hashes))
336 return false;
338 *max_age = base::TimeDelta::FromSeconds(max_age_candidate);
339 *include_subdomains = include_subdomains_candidate;
340 hashes->swap(pins);
341 *report_uri = parsed_report_uri;
343 return true;
346 } // namespace net