1// Copyright (c) 2011 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/command_line.h"
9#include "base/json/json_reader.h"
10#include "base/json/json_writer.h"
11#include "base/logging.h"
12#include "base/memory/scoped_ptr.h"
13#include "base/sha1.h"
14#include "base/string_number_conversions.h"
15#include "base/string_split.h"
16#include "base/string_tokenizer.h"
17#include "base/string_util.h"
18#include "base/utf_string_conversions.h"
19#include "base/values.h"
20#include "crypto/sha2.h"
21#include "googleurl/src/gurl.h"
22#include "net/base/dns_util.h"
23#include "net/base/net_switches.h"
24
25namespace net {
26
27const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365;  // 1 year
28
29TransportSecurityState::TransportSecurityState()
30    : delegate_(NULL) {
31}
32
33static std::string HashHost(const std::string& canonicalized_host) {
34  char hashed[crypto::SHA256_LENGTH];
35  crypto::SHA256HashString(canonicalized_host, hashed, sizeof(hashed));
36  return std::string(hashed, sizeof(hashed));
37}
38
39void TransportSecurityState::EnableHost(const std::string& host,
40                                        const DomainState& state) {
41  const std::string canonicalized_host = CanonicalizeHost(host);
42  if (canonicalized_host.empty())
43    return;
44
45  // TODO(cevans) -- we likely want to permit a host to override a built-in,
46  // for at least the case where the override is stricter (i.e. includes
47  // subdomains, or includes certificate pinning).
48  DomainState temp;
49  if (IsPreloadedSTS(canonicalized_host, true, &temp))
50    return;
51
52  // Use the original creation date if we already have this host.
53  DomainState state_copy(state);
54  DomainState existing_state;
55  if (IsEnabledForHost(&existing_state, host, true))
56    state_copy.created = existing_state.created;
57
58  // We don't store these values.
59  state_copy.preloaded = false;
60  state_copy.domain.clear();
61
62  enabled_hosts_[HashHost(canonicalized_host)] = state_copy;
63  DirtyNotify();
64}
65
66bool TransportSecurityState::DeleteHost(const std::string& host) {
67  const std::string canonicalized_host = CanonicalizeHost(host);
68  if (canonicalized_host.empty())
69    return false;
70
71  std::map<std::string, DomainState>::iterator i = enabled_hosts_.find(
72      HashHost(canonicalized_host));
73  if (i != enabled_hosts_.end()) {
74    enabled_hosts_.erase(i);
75    DirtyNotify();
76    return true;
77  }
78  return false;
79}
80
81// IncludeNUL converts a char* to a std::string and includes the terminating
82// NUL in the result.
83static std::string IncludeNUL(const char* in) {
84  return std::string(in, strlen(in) + 1);
85}
86
87bool TransportSecurityState::IsEnabledForHost(DomainState* result,
88                                              const std::string& host,
89                                              bool sni_available) {
90  const std::string canonicalized_host = CanonicalizeHost(host);
91  if (canonicalized_host.empty())
92    return false;
93
94  if (IsPreloadedSTS(canonicalized_host, sni_available, result))
95    return result->mode != DomainState::MODE_NONE;
96
97  *result = DomainState();
98
99  base::Time current_time(base::Time::Now());
100
101  for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) {
102    std::string hashed_domain(HashHost(IncludeNUL(&canonicalized_host[i])));
103
104    std::map<std::string, DomainState>::iterator j =
105        enabled_hosts_.find(hashed_domain);
106    if (j == enabled_hosts_.end())
107      continue;
108
109    if (current_time > j->second.expiry) {
110      enabled_hosts_.erase(j);
111      DirtyNotify();
112      continue;
113    }
114
115    *result = j->second;
116    result->domain = DNSDomainToString(
117        canonicalized_host.substr(i, canonicalized_host.size() - i));
118
119    // If we matched the domain exactly, it doesn't matter what the value of
120    // include_subdomains is.
121    if (i == 0)
122      return true;
123
124    return j->second.include_subdomains;
125  }
126
127  return false;
128}
129
130void TransportSecurityState::DeleteSince(const base::Time& time) {
131  bool dirtied = false;
132
133  std::map<std::string, DomainState>::iterator i = enabled_hosts_.begin();
134  while (i != enabled_hosts_.end()) {
135    if (i->second.created >= time) {
136      dirtied = true;
137      enabled_hosts_.erase(i++);
138    } else {
139      i++;
140    }
141  }
142
143  if (dirtied)
144    DirtyNotify();
145}
146
147// MaxAgeToInt converts a string representation of a number of seconds into a
148// int. We use strtol in order to handle overflow correctly. The string may
149// contain an arbitary number which we should truncate correctly rather than
150// throwing a parse failure.
151static bool MaxAgeToInt(std::string::const_iterator begin,
152                        std::string::const_iterator end,
153                        int* result) {
154  const std::string s(begin, end);
155  char* endptr;
156  long int i = strtol(s.data(), &endptr, 10 /* base */);
157  if (*endptr || i < 0)
158    return false;
159  if (i > TransportSecurityState::kMaxHSTSAgeSecs)
160    i = TransportSecurityState::kMaxHSTSAgeSecs;
161  *result = i;
162  return true;
163}
164
165// "Strict-Transport-Security" ":"
166//     "max-age" "=" delta-seconds [ ";" "includeSubDomains" ]
167bool TransportSecurityState::ParseHeader(const std::string& value,
168                                         int* max_age,
169                                         bool* include_subdomains) {
170  DCHECK(max_age);
171  DCHECK(include_subdomains);
172
173  int max_age_candidate = 0;
174
175  enum ParserState {
176    START,
177    AFTER_MAX_AGE_LABEL,
178    AFTER_MAX_AGE_EQUALS,
179    AFTER_MAX_AGE,
180    AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER,
181    AFTER_INCLUDE_SUBDOMAINS,
182  } state = START;
183
184  StringTokenizer tokenizer(value, " \t=;");
185  tokenizer.set_options(StringTokenizer::RETURN_DELIMS);
186  while (tokenizer.GetNext()) {
187    DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1);
188    switch (state) {
189      case START:
190        if (IsAsciiWhitespace(*tokenizer.token_begin()))
191          continue;
192        if (!LowerCaseEqualsASCII(tokenizer.token(), "max-age"))
193          return false;
194        state = AFTER_MAX_AGE_LABEL;
195        break;
196
197      case AFTER_MAX_AGE_LABEL:
198        if (IsAsciiWhitespace(*tokenizer.token_begin()))
199          continue;
200        if (*tokenizer.token_begin() != '=')
201          return false;
202        DCHECK(tokenizer.token().length() ==  1);
203        state = AFTER_MAX_AGE_EQUALS;
204        break;
205
206      case AFTER_MAX_AGE_EQUALS:
207        if (IsAsciiWhitespace(*tokenizer.token_begin()))
208          continue;
209        if (!MaxAgeToInt(tokenizer.token_begin(),
210                         tokenizer.token_end(),
211                         &max_age_candidate))
212          return false;
213        state = AFTER_MAX_AGE;
214        break;
215
216      case AFTER_MAX_AGE:
217        if (IsAsciiWhitespace(*tokenizer.token_begin()))
218          continue;
219        if (*tokenizer.token_begin() != ';')
220          return false;
221        state = AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER;
222        break;
223
224      case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
225        if (IsAsciiWhitespace(*tokenizer.token_begin()))
226          continue;
227        if (!LowerCaseEqualsASCII(tokenizer.token(), "includesubdomains"))
228          return false;
229        state = AFTER_INCLUDE_SUBDOMAINS;
230        break;
231
232      case AFTER_INCLUDE_SUBDOMAINS:
233        if (!IsAsciiWhitespace(*tokenizer.token_begin()))
234          return false;
235        break;
236
237      default:
238        NOTREACHED();
239    }
240  }
241
242  // We've consumed all the input.  Let's see what state we ended up in.
243  switch (state) {
244    case START:
245    case AFTER_MAX_AGE_LABEL:
246    case AFTER_MAX_AGE_EQUALS:
247      return false;
248    case AFTER_MAX_AGE:
249      *max_age = max_age_candidate;
250      *include_subdomains = false;
251      return true;
252    case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
253      return false;
254    case AFTER_INCLUDE_SUBDOMAINS:
255      *max_age = max_age_candidate;
256      *include_subdomains = true;
257      return true;
258    default:
259      NOTREACHED();
260      return false;
261  }
262}
263
264void TransportSecurityState::SetDelegate(
265    TransportSecurityState::Delegate* delegate) {
266  delegate_ = delegate;
267}
268
269// This function converts the binary hashes, which we store in
270// |enabled_hosts_|, to a base64 string which we can include in a JSON file.
271static std::string HashedDomainToExternalString(const std::string& hashed) {
272  std::string out;
273  CHECK(base::Base64Encode(hashed, &out));
274  return out;
275}
276
277// This inverts |HashedDomainToExternalString|, above. It turns an external
278// string (from a JSON file) into an internal (binary) string.
279static std::string ExternalStringToHashedDomain(const std::string& external) {
280  std::string out;
281  if (!base::Base64Decode(external, &out) ||
282      out.size() != crypto::SHA256_LENGTH) {
283    return std::string();
284  }
285
286  return out;
287}
288
289bool TransportSecurityState::Serialise(std::string* output) {
290  DictionaryValue toplevel;
291  for (std::map<std::string, DomainState>::const_iterator
292       i = enabled_hosts_.begin(); i != enabled_hosts_.end(); ++i) {
293    DictionaryValue* state = new DictionaryValue;
294    state->SetBoolean("include_subdomains", i->second.include_subdomains);
295    state->SetDouble("created", i->second.created.ToDoubleT());
296    state->SetDouble("expiry", i->second.expiry.ToDoubleT());
297
298    switch (i->second.mode) {
299      case DomainState::MODE_STRICT:
300        state->SetString("mode", "strict");
301        break;
302      case DomainState::MODE_OPPORTUNISTIC:
303        state->SetString("mode", "opportunistic");
304        break;
305      case DomainState::MODE_SPDY_ONLY:
306        state->SetString("mode", "spdy-only");
307        break;
308      default:
309        NOTREACHED() << "DomainState with unknown mode";
310        delete state;
311        continue;
312    }
313
314    ListValue* pins = new ListValue;
315    for (std::vector<SHA1Fingerprint>::const_iterator
316         j = i->second.public_key_hashes.begin();
317         j != i->second.public_key_hashes.end(); ++j) {
318      std::string hash_str(reinterpret_cast<const char*>(j->data),
319                           sizeof(j->data));
320      std::string b64;
321      base::Base64Encode(hash_str, &b64);
322      pins->Append(new StringValue("sha1/" + b64));
323    }
324    state->Set("public_key_hashes", pins);
325
326    toplevel.Set(HashedDomainToExternalString(i->first), state);
327  }
328
329  base::JSONWriter::Write(&toplevel, true /* pretty print */, output);
330  return true;
331}
332
333bool TransportSecurityState::LoadEntries(const std::string& input,
334                                         bool* dirty) {
335  enabled_hosts_.clear();
336  return Deserialise(input, dirty, &enabled_hosts_);
337}
338
339// static
340bool TransportSecurityState::Deserialise(
341    const std::string& input,
342    bool* dirty,
343    std::map<std::string, DomainState>* out) {
344  scoped_ptr<Value> value(
345      base::JSONReader::Read(input, false /* do not allow trailing commas */));
346  if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY))
347    return false;
348
349  DictionaryValue* dict_value = reinterpret_cast<DictionaryValue*>(value.get());
350  const base::Time current_time(base::Time::Now());
351  bool dirtied = false;
352
353  for (DictionaryValue::key_iterator i = dict_value->begin_keys();
354       i != dict_value->end_keys(); ++i) {
355    DictionaryValue* state;
356    if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &state))
357      continue;
358
359    bool include_subdomains;
360    std::string mode_string;
361    double created;
362    double expiry;
363
364    if (!state->GetBoolean("include_subdomains", &include_subdomains) ||
365        !state->GetString("mode", &mode_string) ||
366        !state->GetDouble("expiry", &expiry)) {
367      continue;
368    }
369
370    ListValue* pins_list = NULL;
371    std::vector<SHA1Fingerprint> public_key_hashes;
372    if (state->GetList("public_key_hashes", &pins_list)) {
373      size_t num_pins = pins_list->GetSize();
374      for (size_t i = 0; i < num_pins; ++i) {
375        std::string type_and_base64;
376        std::string hash_str;
377        SHA1Fingerprint hash;
378        if (pins_list->GetString(i, &type_and_base64) &&
379            type_and_base64.find("sha1/") == 0 &&
380            base::Base64Decode(
381                type_and_base64.substr(5, type_and_base64.size() - 5),
382                &hash_str) &&
383            hash_str.size() == base::SHA1_LENGTH) {
384          memcpy(hash.data, hash_str.data(), sizeof(hash.data));
385          public_key_hashes.push_back(hash);
386        }
387      }
388    }
389
390    DomainState::Mode mode;
391    if (mode_string == "strict") {
392      mode = DomainState::MODE_STRICT;
393    } else if (mode_string == "opportunistic") {
394      mode = DomainState::MODE_OPPORTUNISTIC;
395    } else if (mode_string == "spdy-only") {
396      mode = DomainState::MODE_SPDY_ONLY;
397    } else if (mode_string == "none") {
398      mode = DomainState::MODE_NONE;
399    } else {
400      LOG(WARNING) << "Unknown TransportSecurityState mode string found: "
401                   << mode_string;
402      continue;
403    }
404
405    base::Time expiry_time = base::Time::FromDoubleT(expiry);
406    base::Time created_time;
407    if (state->GetDouble("created", &created)) {
408      created_time = base::Time::FromDoubleT(created);
409    } else {
410      // We're migrating an old entry with no creation date. Make sure we
411      // write the new date back in a reasonable time frame.
412      dirtied = true;
413      created_time = base::Time::Now();
414    }
415
416    if (expiry_time <= current_time) {
417      // Make sure we dirty the state if we drop an entry.
418      dirtied = true;
419      continue;
420    }
421
422    std::string hashed = ExternalStringToHashedDomain(*i);
423    if (hashed.empty()) {
424      dirtied = true;
425      continue;
426    }
427
428    DomainState new_state;
429    new_state.mode = mode;
430    new_state.created = created_time;
431    new_state.expiry = expiry_time;
432    new_state.include_subdomains = include_subdomains;
433    new_state.public_key_hashes = public_key_hashes;
434    (*out)[hashed] = new_state;
435  }
436
437  *dirty = dirtied;
438  return true;
439}
440
441TransportSecurityState::~TransportSecurityState() {
442}
443
444void TransportSecurityState::DirtyNotify() {
445  if (delegate_)
446    delegate_->StateIsDirty(this);
447}
448
449// static
450std::string TransportSecurityState::CanonicalizeHost(const std::string& host) {
451  // We cannot perform the operations as detailed in the spec here as |host|
452  // has already undergone IDN processing before it reached us. Thus, we check
453  // that there are no invalid characters in the host and lowercase the result.
454
455  std::string new_host;
456  if (!DNSDomainFromDot(host, &new_host)) {
457    // DNSDomainFromDot can fail if any label is > 63 bytes or if the whole
458    // name is >255 bytes. However, search terms can have those properties.
459    return std::string();
460  }
461
462  for (size_t i = 0; new_host[i]; i += new_host[i] + 1) {
463    const unsigned label_length = static_cast<unsigned>(new_host[i]);
464    if (!label_length)
465      break;
466
467    for (size_t j = 0; j < label_length; ++j) {
468      // RFC 3490, 4.1, step 3
469      if (!IsSTD3ASCIIValidCharacter(new_host[i + 1 + j]))
470        return std::string();
471
472      new_host[i + 1 + j] = tolower(new_host[i + 1 + j]);
473    }
474
475    // step 3(b)
476    if (new_host[i + 1] == '-' ||
477        new_host[i + label_length] == '-') {
478      return std::string();
479    }
480  }
481
482  return new_host;
483}
484
485// IsPreloadedSTS returns true if the canonicalized hostname should always be
486// considered to have STS enabled.
487// static
488bool TransportSecurityState::IsPreloadedSTS(
489    const std::string& canonicalized_host,
490    bool sni_available,
491    DomainState* out) {
492  out->preloaded = true;
493  out->mode = DomainState::MODE_STRICT;
494  out->created = base::Time::FromTimeT(0);
495  out->expiry = out->created;
496  out->include_subdomains = false;
497
498  std::map<std::string, DomainState> hosts;
499  std::string cmd_line_hsts
500#ifdef ANDROID
501      ;
502#else
503      = CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
504          switches::kHstsHosts);
505#endif
506  if (!cmd_line_hsts.empty()) {
507    bool dirty;
508    Deserialise(cmd_line_hsts, &dirty, &hosts);
509  }
510
511  // In the medium term this list is likely to just be hardcoded here. This,
512  // slightly odd, form removes the need for additional relocations records.
513  static const struct {
514    uint8 length;
515    bool include_subdomains;
516    char dns_name[30];
517  } kPreloadedSTS[] = {
518    {16, false, "\003www\006paypal\003com"},
519    {16, false, "\003www\006elanex\003biz"},
520    {12, true,  "\006jottit\003com"},
521    {19, true,  "\015sunshinepress\003org"},
522    {21, false, "\003www\013noisebridge\003net"},
523    {10, false, "\004neg9\003org"},
524    {12, true, "\006riseup\003net"},
525    {11, false, "\006factor\002cc"},
526    {22, false, "\007members\010mayfirst\003org"},
527    {22, false, "\007support\010mayfirst\003org"},
528    {17, false, "\002id\010mayfirst\003org"},
529    {20, false, "\005lists\010mayfirst\003org"},
530    {19, true, "\015splendidbacon\003com"},
531    {19, true, "\006health\006google\003com"},
532    {21, true, "\010checkout\006google\003com"},
533    {19, true, "\006chrome\006google\003com"},
534    {26, false, "\006latest\006chrome\006google\003com"},
535    {28, false, "\016aladdinschools\007appspot\003com"},
536    {14, true, "\011ottospora\002nl"},
537    {17, true, "\004docs\006google\003com"},
538    {18, true, "\005sites\006google\003com"},
539    {25, true, "\014spreadsheets\006google\003com"},
540    {22, false, "\011appengine\006google\003com"},
541    {25, false, "\003www\017paycheckrecords\003com"},
542    {20, true, "\006market\007android\003com"},
543    {14, false, "\010lastpass\003com"},
544    {18, false, "\003www\010lastpass\003com"},
545    {14, true, "\010keyerror\003com"},
546    {22, true, "\011encrypted\006google\003com"},
547    {13, false, "\010entropia\002de"},
548    {17, false, "\003www\010entropia\002de"},
549    {21, true, "\010accounts\006google\003com"},
550#if defined(OS_CHROMEOS)
551    {17, true, "\004mail\006google\003com"},
552    {13, false, "\007twitter\003com"},
553    {17, false, "\003www\007twitter\003com"},
554    {17, false, "\003api\007twitter\003com"},
555    {17, false, "\003dev\007twitter\003com"},
556    {22, false, "\010business\007twitter\003com"},
557#endif
558  };
559  static const size_t kNumPreloadedSTS = ARRAYSIZE_UNSAFE(kPreloadedSTS);
560
561  static const struct {
562    uint8 length;
563    bool include_subdomains;
564    char dns_name[30];
565  } kPreloadedSNISTS[] = {
566    {11, false, "\005gmail\003com"},
567    {16, false, "\012googlemail\003com"},
568    {15, false, "\003www\005gmail\003com"},
569    {20, false, "\003www\012googlemail\003com"},
570  };
571  static const size_t kNumPreloadedSNISTS = ARRAYSIZE_UNSAFE(kPreloadedSNISTS);
572
573  for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) {
574    std::string host_sub_chunk(&canonicalized_host[i],
575                               canonicalized_host.size() - i);
576    out->domain = DNSDomainToString(host_sub_chunk);
577    std::string hashed_host(HashHost(host_sub_chunk));
578    if (hosts.find(hashed_host) != hosts.end()) {
579      *out = hosts[hashed_host];
580      out->domain = DNSDomainToString(host_sub_chunk);
581      out->preloaded = true;
582      return true;
583    }
584    for (size_t j = 0; j < kNumPreloadedSTS; j++) {
585      if (kPreloadedSTS[j].length == canonicalized_host.size() - i &&
586          memcmp(kPreloadedSTS[j].dns_name, &canonicalized_host[i],
587                 kPreloadedSTS[j].length) == 0) {
588        if (!kPreloadedSTS[j].include_subdomains && i != 0)
589          return false;
590        out->include_subdomains = kPreloadedSTS[j].include_subdomains;
591        return true;
592      }
593    }
594    if (sni_available) {
595      for (size_t j = 0; j < kNumPreloadedSNISTS; j++) {
596        if (kPreloadedSNISTS[j].length == canonicalized_host.size() - i &&
597            memcmp(kPreloadedSNISTS[j].dns_name, &canonicalized_host[i],
598                   kPreloadedSNISTS[j].length) == 0) {
599          if (!kPreloadedSNISTS[j].include_subdomains && i != 0)
600            return false;
601          out->include_subdomains = kPreloadedSNISTS[j].include_subdomains;
602          return true;
603        }
604      }
605    }
606  }
607
608  return false;
609}
610
611static std::string HashesToBase64String(
612    const std::vector<net::SHA1Fingerprint>& hashes) {
613  std::vector<std::string> hashes_strs;
614  for (std::vector<net::SHA1Fingerprint>::const_iterator
615       i = hashes.begin(); i != hashes.end(); i++) {
616    std::string s;
617    const std::string hash_str(reinterpret_cast<const char*>(i->data),
618                               sizeof(i->data));
619    base::Base64Encode(hash_str, &s);
620    hashes_strs.push_back(s);
621  }
622
623  return JoinString(hashes_strs, ',');
624}
625
626TransportSecurityState::DomainState::DomainState()
627    : mode(MODE_STRICT),
628      created(base::Time::Now()),
629      include_subdomains(false),
630      preloaded(false) {
631}
632
633TransportSecurityState::DomainState::~DomainState() {
634}
635
636bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted(
637    const std::vector<net::SHA1Fingerprint>& hashes) {
638  if (public_key_hashes.empty())
639    return true;
640
641  for (std::vector<net::SHA1Fingerprint>::const_iterator
642       i = hashes.begin(); i != hashes.end(); ++i) {
643    for (std::vector<net::SHA1Fingerprint>::const_iterator
644         j = public_key_hashes.begin(); j != public_key_hashes.end(); ++j) {
645      if (i->Equals(*j))
646        return true;
647    }
648  }
649
650  LOG(ERROR) << "Rejecting public key chain for domain " << domain
651             << ". Validated chain: " << HashesToBase64String(hashes)
652             << ", expected: " << HashesToBase64String(public_key_hashes);
653
654  return false;
655}
656
657}  // namespace
658