Home | History | Annotate | Download | only in base
      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 
     21 TransportSecurityState::TransportSecurityState()
     22     : delegate_(NULL) {
     23 }
     24 
     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 
     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" ]
     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 
    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.
    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.
    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 
    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 
    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 
    297 void TransportSecurityState::DirtyNotify() {
    298   if (delegate_)
    299     delegate_->StateIsDirty(this);
    300 }
    301 
    302 // static
    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