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

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

Powered by Google App Engine
This is Rietveld 408576698