• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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