1 // Copyright (c) 2009 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 "net/base/transport_security_state.h"
6
7 #include "base/base64.h"
8 #include "base/json/json_reader.h"
9 #include "base/json/json_writer.h"
10 #include "base/logging.h"
11 #include "base/scoped_ptr.h"
12 #include "base/sha2.h"
13 #include "base/string_tokenizer.h"
14 #include "base/string_util.h"
15 #include "base/values.h"
16 #include "googleurl/src/gurl.h"
17 #include "net/base/dns_util.h"
18
19 namespace net {
20
TransportSecurityState()21 TransportSecurityState::TransportSecurityState()
22 : delegate_(NULL) {
23 }
24
EnableHost(const std::string & host,const DomainState & state)25 void TransportSecurityState::EnableHost(const std::string& host,
26 const DomainState& state) {
27 const std::string canonicalised_host = CanonicaliseHost(host);
28 if (canonicalised_host.empty())
29 return;
30 char hashed[base::SHA256_LENGTH];
31 base::SHA256HashString(canonicalised_host, hashed, sizeof(hashed));
32
33 AutoLock lock(lock_);
34
35 enabled_hosts_[std::string(hashed, sizeof(hashed))] = state;
36 DirtyNotify();
37 }
38
IsEnabledForHost(DomainState * result,const std::string & host)39 bool TransportSecurityState::IsEnabledForHost(DomainState* result,
40 const std::string& host) {
41 const std::string canonicalised_host = CanonicaliseHost(host);
42 if (canonicalised_host.empty())
43 return false;
44
45 base::Time current_time(base::Time::Now());
46 AutoLock lock(lock_);
47
48 for (size_t i = 0; canonicalised_host[i]; i += canonicalised_host[i] + 1) {
49 char hashed_domain[base::SHA256_LENGTH];
50
51 base::SHA256HashString(&canonicalised_host[i], &hashed_domain,
52 sizeof(hashed_domain));
53 std::map<std::string, DomainState>::iterator j =
54 enabled_hosts_.find(std::string(hashed_domain, sizeof(hashed_domain)));
55 if (j == enabled_hosts_.end())
56 continue;
57
58 if (current_time > j->second.expiry) {
59 enabled_hosts_.erase(j);
60 DirtyNotify();
61 continue;
62 }
63
64 *result = j->second;
65
66 // If we matched the domain exactly, it doesn't matter what the value of
67 // include_subdomains is.
68 if (i == 0)
69 return true;
70
71 return j->second.include_subdomains;
72 }
73
74 return false;
75 }
76
77 // "Strict-Transport-Security" ":"
78 // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ]
ParseHeader(const std::string & value,int * max_age,bool * include_subdomains)79 bool TransportSecurityState::ParseHeader(const std::string& value,
80 int* max_age,
81 bool* include_subdomains) {
82 DCHECK(max_age);
83 DCHECK(include_subdomains);
84
85 int max_age_candidate;
86
87 enum ParserState {
88 START,
89 AFTER_MAX_AGE_LABEL,
90 AFTER_MAX_AGE_EQUALS,
91 AFTER_MAX_AGE,
92 AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER,
93 AFTER_INCLUDE_SUBDOMAINS,
94 } state = START;
95
96 StringTokenizer tokenizer(value, " \t=;");
97 tokenizer.set_options(StringTokenizer::RETURN_DELIMS);
98 while (tokenizer.GetNext()) {
99 DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1);
100 switch (state) {
101 case START:
102 if (IsAsciiWhitespace(*tokenizer.token_begin()))
103 continue;
104 if (!LowerCaseEqualsASCII(tokenizer.token(), "max-age"))
105 return false;
106 state = AFTER_MAX_AGE_LABEL;
107 break;
108
109 case AFTER_MAX_AGE_LABEL:
110 if (IsAsciiWhitespace(*tokenizer.token_begin()))
111 continue;
112 if (*tokenizer.token_begin() != '=')
113 return false;
114 DCHECK(tokenizer.token().length() == 1);
115 state = AFTER_MAX_AGE_EQUALS;
116 break;
117
118 case AFTER_MAX_AGE_EQUALS:
119 if (IsAsciiWhitespace(*tokenizer.token_begin()))
120 continue;
121 if (!StringToInt(tokenizer.token(), &max_age_candidate))
122 return false;
123 if (max_age_candidate < 0)
124 return false;
125 state = AFTER_MAX_AGE;
126 break;
127
128 case AFTER_MAX_AGE:
129 if (IsAsciiWhitespace(*tokenizer.token_begin()))
130 continue;
131 if (*tokenizer.token_begin() != ';')
132 return false;
133 state = AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER;
134 break;
135
136 case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
137 if (IsAsciiWhitespace(*tokenizer.token_begin()))
138 continue;
139 if (!LowerCaseEqualsASCII(tokenizer.token(), "includesubdomains"))
140 return false;
141 state = AFTER_INCLUDE_SUBDOMAINS;
142 break;
143
144 case AFTER_INCLUDE_SUBDOMAINS:
145 if (!IsAsciiWhitespace(*tokenizer.token_begin()))
146 return false;
147 break;
148
149 default:
150 NOTREACHED();
151 }
152 }
153
154 // We've consumed all the input. Let's see what state we ended up in.
155 switch (state) {
156 case START:
157 case AFTER_MAX_AGE_LABEL:
158 case AFTER_MAX_AGE_EQUALS:
159 return false;
160 case AFTER_MAX_AGE:
161 *max_age = max_age_candidate;
162 *include_subdomains = false;
163 return true;
164 case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
165 return false;
166 case AFTER_INCLUDE_SUBDOMAINS:
167 *max_age = max_age_candidate;
168 *include_subdomains = true;
169 return true;
170 default:
171 NOTREACHED();
172 return false;
173 }
174 }
175
SetDelegate(TransportSecurityState::Delegate * delegate)176 void TransportSecurityState::SetDelegate(
177 TransportSecurityState::Delegate* delegate) {
178 AutoLock lock(lock_);
179
180 delegate_ = delegate;
181 }
182
183 // This function converts the binary hashes, which we store in
184 // |enabled_hosts_|, to a base64 string which we can include in a JSON file.
HashedDomainToExternalString(const std::string & hashed)185 static std::wstring HashedDomainToExternalString(const std::string& hashed) {
186 std::string out;
187 CHECK(base::Base64Encode(hashed, &out));
188 return ASCIIToWide(out);
189 }
190
191 // This inverts |HashedDomainToExternalString|, above. It turns an external
192 // string (from a JSON file) into an internal (binary) string.
ExternalStringToHashedDomain(const std::wstring & external)193 static std::string ExternalStringToHashedDomain(const std::wstring& external) {
194 std::string external_ascii = WideToASCII(external);
195 std::string out;
196 if (!base::Base64Decode(external_ascii, &out) ||
197 out.size() != base::SHA256_LENGTH) {
198 return std::string();
199 }
200
201 return out;
202 }
203
Serialise(std::string * output)204 bool TransportSecurityState::Serialise(std::string* output) {
205 AutoLock lock(lock_);
206
207 DictionaryValue toplevel;
208 for (std::map<std::string, DomainState>::const_iterator
209 i = enabled_hosts_.begin(); i != enabled_hosts_.end(); ++i) {
210 DictionaryValue* state = new DictionaryValue;
211 state->SetBoolean(L"include_subdomains", i->second.include_subdomains);
212 state->SetReal(L"expiry", i->second.expiry.ToDoubleT());
213
214 switch (i->second.mode) {
215 case DomainState::MODE_STRICT:
216 state->SetString(L"mode", "strict");
217 break;
218 case DomainState::MODE_OPPORTUNISTIC:
219 state->SetString(L"mode", "opportunistic");
220 break;
221 case DomainState::MODE_SPDY_ONLY:
222 state->SetString(L"mode", "spdy-only");
223 break;
224 default:
225 NOTREACHED() << "DomainState with unknown mode";
226 delete state;
227 continue;
228 }
229
230 toplevel.Set(HashedDomainToExternalString(i->first), state);
231 }
232
233 base::JSONWriter::Write(&toplevel, true /* pretty print */, output);
234 return true;
235 }
236
Deserialise(const std::string & input)237 bool TransportSecurityState::Deserialise(const std::string& input) {
238 AutoLock lock(lock_);
239
240 enabled_hosts_.clear();
241
242 scoped_ptr<Value> value(
243 base::JSONReader::Read(input, false /* do not allow trailing commas */));
244 if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY))
245 return false;
246
247 DictionaryValue* dict_value = reinterpret_cast<DictionaryValue*>(value.get());
248 const base::Time current_time(base::Time::Now());
249
250 for (DictionaryValue::key_iterator i = dict_value->begin_keys();
251 i != dict_value->end_keys(); ++i) {
252 DictionaryValue* state;
253 if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &state))
254 continue;
255
256 bool include_subdomains;
257 std::string mode_string;
258 double expiry;
259
260 if (!state->GetBoolean(L"include_subdomains", &include_subdomains) ||
261 !state->GetString(L"mode", &mode_string) ||
262 !state->GetReal(L"expiry", &expiry)) {
263 continue;
264 }
265
266 DomainState::Mode mode;
267 if (mode_string == "strict") {
268 mode = DomainState::MODE_STRICT;
269 } else if (mode_string == "opportunistic") {
270 mode = DomainState::MODE_OPPORTUNISTIC;
271 } else if (mode_string == "spdy-only") {
272 mode = DomainState::MODE_SPDY_ONLY;
273 } else {
274 LOG(WARNING) << "Unknown TransportSecurityState mode string found: "
275 << mode_string;
276 continue;
277 }
278
279 base::Time expiry_time = base::Time::FromDoubleT(expiry);
280 if (expiry_time <= current_time)
281 continue;
282
283 std::string hashed = ExternalStringToHashedDomain(*i);
284 if (hashed.empty())
285 continue;
286
287 DomainState new_state;
288 new_state.mode = mode;
289 new_state.expiry = expiry_time;
290 new_state.include_subdomains = include_subdomains;
291 enabled_hosts_[hashed] = new_state;
292 }
293
294 return true;
295 }
296
DirtyNotify()297 void TransportSecurityState::DirtyNotify() {
298 if (delegate_)
299 delegate_->StateIsDirty(this);
300 }
301
302 // static
CanonicaliseHost(const std::string & host)303 std::string TransportSecurityState::CanonicaliseHost(const std::string& host) {
304 // We cannot perform the operations as detailed in the spec here as |host|
305 // has already undergone IDN processing before it reached us. Thus, we check
306 // that there are no invalid characters in the host and lowercase the result.
307
308 std::string new_host;
309 if (!DNSDomainFromDot(host, &new_host)) {
310 NOTREACHED();
311 return std::string();
312 }
313
314 for (size_t i = 0; new_host[i]; i += new_host[i] + 1) {
315 const unsigned label_length = static_cast<unsigned>(new_host[i]);
316 if (!label_length)
317 break;
318
319 for (size_t j = 0; j < label_length; ++j) {
320 // RFC 3490, 4.1, step 3
321 if (!IsSTD3ASCIIValidCharacter(new_host[i + 1 + j]))
322 return std::string();
323
324 new_host[i + 1 + j] = tolower(new_host[i + 1 + j]);
325 }
326
327 // step 3(b)
328 if (new_host[i + 1] == '-' ||
329 new_host[i + label_length] == '-') {
330 return std::string();
331 }
332 }
333
334 return new_host;
335 }
336
337 } // namespace
338