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