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

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

Powered by Google App Engine
This is Rietveld 408576698