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