Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(870)

Side by Side Diff: net/http/http_security_headers.cc

Issue 11274032: Separate http_security_headers from transport_security_state (Closed) Base URL: https://src.chromium.org/chrome/trunk/src/
Patch Set: Created 8 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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.
4
5 #include "base/base64.h"
6 #include "base/string_number_conversions.h"
7 #include "base/string_tokenizer.h"
8 #include "base/string_util.h"
9 #include "net/http/http_security_headers.h"
10 #include "net/http/http_util.h"
11
12 namespace net {
13
14 // MaxAgeToInt converts a string representation of a number of seconds into a
15 // int. We use strtol in order to handle overflow correctly. The string may
16 // contain an arbitary number which we should truncate correctly rather than
17 // throwing a parse failure.
18 static bool MaxAgeToInt(std::string::const_iterator begin,
Ryan Sleevi 2012/10/25 01:59:09 design: prefer placing this in an unnamed namespac
unsafe 2012/10/25 06:59:54 All of these things were as-before, just moved int
19 std::string::const_iterator end,
20 int* result) {
21 const std::string s(begin, end);
22 char* endptr;
23 long int i = strtol(s.data(), &endptr, 10 /* base */);
Ryan Sleevi 2012/10/25 01:59:09 BUG: Do not use strtol. Use base::StringToUint64.
24 if (*endptr || i < 0)
25 return false;
26 if (i > kMaxHSTSAgeSecs)
27 i = kMaxHSTSAgeSecs;
28 *result = i;
29 return true;
30 }
31
32 // Parse the Strict-Transport-Security header, as currently defined in
33 // http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14:
34 //
35 // Strict-Transport-Security = "Strict-Transport-Security" ":"
36 // [ directive ] *( ";" [ directive ] )
37 //
38 // directive = directive-name [ "=" directive-value ]
39 // directive-name = token
40 // directive-value = token | quoted-string
41 //
42 // 1. The order of appearance of directives is not significant.
43 //
44 // 2. All directives MUST appear only once in an STS header field.
45 // Directives are either optional or required, as stipulated in
46 // their definitions.
47 //
48 // 3. Directive names are case-insensitive.
49 //
50 // 4. UAs MUST ignore any STS header fields containing directives, or
51 // other header field value data, that does not conform to the
52 // syntax defined in this specification.
53 //
54 // 5. If an STS header field contains directive(s) not recognized by
55 // the UA, the UA MUST ignore the unrecognized directives and if the
56 // STS header field otherwise satisfies the above requirements (1
57 // through 4), the UA MUST process the recognized directives.
58 bool ParseHSTSHeader(const base::Time& now, const std::string& value,
59 base::Time* expiry, // OUT
60 bool* include_subdomains) { // OUT
61 int max_age_candidate = 0;
62 bool include_subdomains_candidate = false;
63
64 // We must see max-age exactly once.
65 int max_age_observed = 0;
66 // We must see includeSubdomains exactly 0 or 1 times.
67 int include_subdomains_observed = 0;
68
69 enum ParserState {
70 START,
71 AFTER_MAX_AGE_LABEL,
72 AFTER_MAX_AGE_EQUALS,
73 AFTER_MAX_AGE,
74 AFTER_INCLUDE_SUBDOMAINS,
75 AFTER_UNKNOWN_LABEL,
76 DIRECTIVE_END
77 } state = START;
78
79 StringTokenizer tokenizer(value, " \t=;");
80 tokenizer.set_options(StringTokenizer::RETURN_DELIMS);
81 tokenizer.set_quote_chars("\"");
82 std::string unquoted;
83 while (tokenizer.GetNext()) {
Ryan Sleevi 2012/10/25 01:59:09 DESIGN: Unless I'm misreading the ABNF, this would
84 DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1);
85 switch (state) {
86 case START:
87 case DIRECTIVE_END:
88 if (IsAsciiWhitespace(*tokenizer.token_begin()))
89 continue;
90 if (LowerCaseEqualsASCII(tokenizer.token(), "max-age")) {
91 state = AFTER_MAX_AGE_LABEL;
92 max_age_observed++;
93 } else if (LowerCaseEqualsASCII(tokenizer.token(),
94 "includesubdomains")) {
95 state = AFTER_INCLUDE_SUBDOMAINS;
96 include_subdomains_observed++;
97 include_subdomains_candidate = true;
98 } else {
99 state = AFTER_UNKNOWN_LABEL;
100 }
101 break;
102
103 case AFTER_MAX_AGE_LABEL:
104 if (IsAsciiWhitespace(*tokenizer.token_begin()))
105 continue;
106 if (*tokenizer.token_begin() != '=')
107 return false;
108 DCHECK_EQ(tokenizer.token().length(), 1U);
109 state = AFTER_MAX_AGE_EQUALS;
110 break;
111
112 case AFTER_MAX_AGE_EQUALS:
113 if (IsAsciiWhitespace(*tokenizer.token_begin()))
114 continue;
115 unquoted = HttpUtil::Unquote(tokenizer.token());
116 if (!MaxAgeToInt(unquoted.begin(),
117 unquoted.end(),
118 &max_age_candidate))
119 return false;
120 state = AFTER_MAX_AGE;
121 break;
122
123 case AFTER_MAX_AGE:
124 case AFTER_INCLUDE_SUBDOMAINS:
125 if (IsAsciiWhitespace(*tokenizer.token_begin()))
126 continue;
127 else if (*tokenizer.token_begin() == ';')
128 state = DIRECTIVE_END;
129 else
130 return false;
131 break;
132
133 case AFTER_UNKNOWN_LABEL:
134 // Consume and ignore the post-label contents (if any).
135 if (*tokenizer.token_begin() != ';')
136 continue;
137 state = DIRECTIVE_END;
138 break;
139 }
140 }
141
142 // We've consumed all the input. Let's see what state we ended up in.
143 if (max_age_observed != 1 ||
144 (include_subdomains_observed != 0 && include_subdomains_observed != 1)) {
145 return false;
146 }
147
148 switch (state) {
149 case AFTER_MAX_AGE:
150 case AFTER_INCLUDE_SUBDOMAINS:
151 case AFTER_UNKNOWN_LABEL:
152 // BUG(156147), TODO(palmer): If max_age_candidate == 0, we should
Ryan Sleevi 2012/10/25 01:59:09 nit: TODO(palmer): http://crbug.com/156147 - if ma
153 // delete (or, not set) the HSTS record, rather than treat it as a
154 // normal value. However, now + 0 effectively deletes the entry
155 // because it will not be enforced (it expires immediately,
156 // essentially).
157 *expiry = now + base::TimeDelta::FromSeconds(max_age_candidate);
158 *include_subdomains = include_subdomains_candidate;
159 return true;
160 case START:
161 case DIRECTIVE_END:
162 case AFTER_MAX_AGE_LABEL:
163 case AFTER_MAX_AGE_EQUALS:
164 return false;
165 default:
166 NOTREACHED();
167 return false;
168 }
169 }
170
171 // Returns true iff there is an item in |pins| which is not present in
172 // |from_cert_chain|. Such an SPKI hash is called a "backup pin".
173 static bool IsBackupPinPresent(const HashValueVector& pins,
174 const HashValueVector& from_cert_chain) {
175 for (HashValueVector::const_iterator
176 i = pins.begin(); i != pins.end(); ++i) {
177 HashValueVector::const_iterator j =
178 std::find_if(from_cert_chain.begin(), from_cert_chain.end(),
179 HashValuesEqualPredicate(*i));
180 if (j == from_cert_chain.end())
181 return true;
182 }
183
184 return false;
185 }
186
187 // Returns true iff |pins| contains both a live and a backup pin. A live pin
188 // is a pin whose SPKI is present in the certificate chain in |ssl_info|. A
189 // backup pin is a pin intended for disaster recovery, not day-to-day use, and
190 // thus must be absent from the certificate chain. The Public-Key-Pins header
191 // specification requires both.
192 static bool IsPinListValid(const HashValueVector& pins,
193 const SSLInfo& ssl_info) {
194 // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual
195 // liveness and backupness below.)
196 if (pins.size() < 2)
197 return false;
198
199 const HashValueVector& from_cert_chain = ssl_info.public_key_hashes;
200 if (from_cert_chain.empty())
201 return false;
202
203 return IsBackupPinPresent(pins, from_cert_chain) &&
204 HashesIntersect(pins, from_cert_chain);
205 }
206
207 // Strip, Split, StringPair, and ParsePins are private implementation details
208 // of ParsePinsHeader(std::string&, DomainState&).
209 static std::string Strip(const std::string& source) {
210 if (source.empty())
211 return source;
212
213 std::string::const_iterator start = source.begin();
214 std::string::const_iterator end = source.end();
215 HttpUtil::TrimLWS(&start, &end);
216 return std::string(start, end);
217 }
218
219 typedef std::pair<std::string, std::string> StringPair;
Ryan Sleevi 2012/10/25 01:59:09 nit: This would be better as pair<base::StringPiec
220
221 static StringPair Split(const std::string& source, char delimiter) {
222 StringPair pair;
223 size_t point = source.find(delimiter);
224
225 pair.first = source.substr(0, point);
226 if (std::string::npos != point)
227 pair.second = source.substr(point + 1);
228
229 return pair;
230 }
231
232 static bool ParseAndAppendPin(const std::string& value,
233 HashValueTag tag,
234 HashValueVector* hashes) {
235 std::string unquoted = HttpUtil::Unquote(value);
236 std::string decoded;
237
238 // This code has to assume that 32 bytes is SHA-256 and 20 bytes is SHA-1.
239 // Currently, those are the only two possibilities, so the assumption is
240 // valid.
241 if (!base::Base64Decode(unquoted, &decoded))
242 return false;
243
244 HashValue hash(tag);
245 if (decoded.size() != hash.size())
246 return false;
247
248 memcpy(hash.data(), decoded.data(), hash.size());
249 hashes->push_back(hash);
250 return true;
251 }
252
253 // "Public-Key-Pins" ":"
254 // "max-age" "=" delta-seconds ";"
255 // "pin-" algo "=" base64 [ ";" ... ]
256 bool ParseHPKPHeader(
257 const base::Time& now,
258 const std::string& value,
259 const SSLInfo& ssl_info,
260 base::Time* expiry,
261 HashValueVector* hashes) {
262 bool parsed_max_age = false;
263 int max_age_candidate = 0;
264 HashValueVector pins;
265
266 std::string source = value;
267
268 while (!source.empty()) {
Ryan Sleevi 2012/10/25 01:59:09 again with NameValuePairsIterator
269 StringPair semicolon = Split(source, ';');
270 semicolon.first = Strip(semicolon.first);
271 semicolon.second = Strip(semicolon.second);
272 StringPair equals = Split(semicolon.first, '=');
273 equals.first = Strip(equals.first);
274 equals.second = Strip(equals.second);
275
276 if (LowerCaseEqualsASCII(equals.first, "max-age")) {
277 if (equals.second.empty() ||
278 !MaxAgeToInt(equals.second.begin(), equals.second.end(),
279 &max_age_candidate)) {
280 return false;
281 }
282 parsed_max_age = true;
283 } else if (StartsWithASCII(equals.first, "pin-", false)) {
284 HashValueTag tag;
285 if (LowerCaseEqualsASCII(equals.first, "pin-sha1")) {
286 tag = HASH_VALUE_SHA1;
287 } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) {
288 tag = HASH_VALUE_SHA256;
289 } else {
290 return false;
291 }
292 if (!ParseAndAppendPin(equals.second, tag, &pins)) {
293 return false;
294 }
295 } else {
296 // Silently ignore unknown directives for forward compatibility.
297 }
298
299 source = semicolon.second;
300 }
301
302 if (!parsed_max_age)
303 return false;
304
305 // Check that the header is valid
306 if (!IsPinListValid(pins, ssl_info))
307 return false;
308
309 // If ssl_info wasn't passed in, this is a good idea...
310 if (pins.size() == 0)
311 return false;
312
313 *expiry = now + base::TimeDelta::FromSeconds(max_age_candidate);
314 for (HashValueVector::const_iterator i = pins.begin();
315 i != pins.end(); ++i) {
316 hashes->push_back(*i);
317 }
318
319 return true;
320 }
321
322 } // namespace net
323
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698