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

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

Powered by Google App Engine
This is Rietveld 408576698