• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
25 namespace net {
26 
27 const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365;  // 1 year
28 
TransportSecurityState()29 TransportSecurityState::TransportSecurityState()
30     : delegate_(NULL) {
31 }
32 
HashHost(const std::string & canonicalized_host)33 static 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 
EnableHost(const std::string & host,const DomainState & state)39 void 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 
DeleteHost(const std::string & host)66 bool 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.
IncludeNUL(const char * in)83 static std::string IncludeNUL(const char* in) {
84   return std::string(in, strlen(in) + 1);
85 }
86 
IsEnabledForHost(DomainState * result,const std::string & host,bool sni_available)87 bool 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 
DeleteSince(const base::Time & time)130 void 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.
MaxAgeToInt(std::string::const_iterator begin,std::string::const_iterator end,int * result)151 static 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" ]
ParseHeader(const std::string & value,int * max_age,bool * include_subdomains)167 bool 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 
SetDelegate(TransportSecurityState::Delegate * delegate)264 void 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.
HashedDomainToExternalString(const std::string & hashed)271 static 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.
ExternalStringToHashedDomain(const std::string & external)279 static 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 
Serialise(std::string * output)289 bool 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 
LoadEntries(const std::string & input,bool * dirty)333 bool TransportSecurityState::LoadEntries(const std::string& input,
334                                          bool* dirty) {
335   enabled_hosts_.clear();
336   return Deserialise(input, dirty, &enabled_hosts_);
337 }
338 
339 // static
Deserialise(const std::string & input,bool * dirty,std::map<std::string,DomainState> * out)340 bool 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 
~TransportSecurityState()441 TransportSecurityState::~TransportSecurityState() {
442 }
443 
DirtyNotify()444 void TransportSecurityState::DirtyNotify() {
445   if (delegate_)
446     delegate_->StateIsDirty(this);
447 }
448 
449 // static
CanonicalizeHost(const std::string & host)450 std::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
IsPreloadedSTS(const std::string & canonicalized_host,bool sni_available,DomainState * out)488 bool 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 
HashesToBase64String(const std::vector<net::SHA1Fingerprint> & hashes)611 static 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 
DomainState()626 TransportSecurityState::DomainState::DomainState()
627     : mode(MODE_STRICT),
628       created(base::Time::Now()),
629       include_subdomains(false),
630       preloaded(false) {
631 }
632 
~DomainState()633 TransportSecurityState::DomainState::~DomainState() {
634 }
635 
IsChainOfPublicKeysPermitted(const std::vector<net::SHA1Fingerprint> & hashes)636 bool 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