OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "net/base/transport_security_state.h" | 5 #include "net/base/transport_security_state.h" |
6 | 6 |
7 #if defined(USE_OPENSSL) | 7 #if defined(USE_OPENSSL) |
8 #include <openssl/ecdsa.h> | 8 #include <openssl/ecdsa.h> |
9 #include <openssl/ssl.h> | 9 #include <openssl/ssl.h> |
10 #else // !defined(USE_OPENSSL) | 10 #else // !defined(USE_OPENSSL) |
(...skipping 15 matching lines...) Expand all Loading... |
26 #include "base/string_number_conversions.h" | 26 #include "base/string_number_conversions.h" |
27 #include "base/string_tokenizer.h" | 27 #include "base/string_tokenizer.h" |
28 #include "base/string_util.h" | 28 #include "base/string_util.h" |
29 #include "base/time.h" | 29 #include "base/time.h" |
30 #include "base/utf_string_conversions.h" | 30 #include "base/utf_string_conversions.h" |
31 #include "base/values.h" | 31 #include "base/values.h" |
32 #include "crypto/sha2.h" | 32 #include "crypto/sha2.h" |
33 #include "googleurl/src/gurl.h" | 33 #include "googleurl/src/gurl.h" |
34 #include "net/base/dns_util.h" | 34 #include "net/base/dns_util.h" |
35 #include "net/base/ssl_info.h" | 35 #include "net/base/ssl_info.h" |
| 36 #include "net/base/x509_cert_types.h" |
36 #include "net/base/x509_certificate.h" | 37 #include "net/base/x509_certificate.h" |
37 #include "net/http/http_util.h" | 38 #include "net/http/http_util.h" |
38 | 39 |
39 #if defined(USE_OPENSSL) | 40 #if defined(USE_OPENSSL) |
40 #include "crypto/openssl_util.h" | 41 #include "crypto/openssl_util.h" |
41 #endif | 42 #endif |
42 | 43 |
43 namespace net { | 44 namespace net { |
44 | 45 |
45 const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365; // 1 year | 46 const long int TransportSecurityState::kMaxHSTSAgeSecs = 86400 * 365; // 1 year |
(...skipping 165 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
211 StringPair pair; | 212 StringPair pair; |
212 size_t point = source.find(delimiter); | 213 size_t point = source.find(delimiter); |
213 | 214 |
214 pair.first = source.substr(0, point); | 215 pair.first = source.substr(0, point); |
215 if (std::string::npos != point) | 216 if (std::string::npos != point) |
216 pair.second = source.substr(point + 1); | 217 pair.second = source.substr(point + 1); |
217 | 218 |
218 return pair; | 219 return pair; |
219 } | 220 } |
220 | 221 |
221 // TODO(palmer): Support both sha256 and sha1. This will require additional | |
222 // infrastructure code changes and can come in a later patch. | |
223 // | |
224 // static | 222 // static |
225 bool TransportSecurityState::ParsePin(const std::string& value, | 223 bool TransportSecurityState::ParsePin(const std::string& value, |
226 SHA1Fingerprint* out) { | 224 HashValue* out) { |
227 StringPair slash = Split(Strip(value), '/'); | 225 StringPair slash = Split(Strip(value), '/'); |
228 if (slash.first != "sha1") | 226 |
| 227 if (slash.first == "sha1") |
| 228 out->tag = HASH_VALUE_SHA1; |
| 229 else if (slash.first == "sha256") |
| 230 out->tag = HASH_VALUE_SHA256; |
| 231 else |
229 return false; | 232 return false; |
230 | 233 |
231 std::string decoded; | 234 std::string decoded; |
232 if (!base::Base64Decode(slash.second, &decoded) || | 235 if (!base::Base64Decode(slash.second, &decoded) || |
233 decoded.size() != arraysize(out->data)) { | 236 decoded.size() != out->size()) { |
234 return false; | 237 return false; |
235 } | 238 } |
236 | 239 |
237 memcpy(out->data, decoded.data(), arraysize(out->data)); | 240 memcpy(out->data(), decoded.data(), out->size()); |
238 return true; | 241 return true; |
239 } | 242 } |
240 | 243 |
241 static bool ParseAndAppendPin(const std::string& value, | 244 static bool ParseAndAppendPin(const std::string& value, |
242 FingerprintVector* fingerprints) { | 245 HashValueVector* fingerprints) { |
243 // The base64'd fingerprint MUST be a quoted-string. 20 bytes base64'd is 28 | 246 // The base64'd fingerprint MUST be a quoted-string. 20 bytes base64'd is 28 |
244 // characters; 32 bytes base64'd is 44 characters. TODO(palmer): Support | 247 // characters; 32 bytes base64'd is 44 characters. |
245 // SHA256. | |
246 size_t size = value.size(); | 248 size_t size = value.size(); |
247 if (size != 30 || value[0] != '"' || value[size - 1] != '"') | 249 if ((size != 30 && size != 46) || value[0] != '"' || value[size - 1] != '"') |
248 return false; | 250 return false; |
249 | 251 |
250 std::string unquoted = HttpUtil::Unquote(value); | 252 std::string unquoted = HttpUtil::Unquote(value); |
251 std::string decoded; | 253 std::string decoded; |
252 SHA1Fingerprint fp; | 254 HashValue fp; |
253 | 255 |
254 if (!base::Base64Decode(unquoted, &decoded) || | 256 // This code has to assume that 32 bytes is SHA-256 and 20 bytes is SHA-1. |
255 decoded.size() != arraysize(fp.data)) { | 257 // Currently, those are the only two possibilities, so the assumption is |
| 258 // valid. |
| 259 if (!base::Base64Decode(unquoted, &decoded)) |
256 return false; | 260 return false; |
257 } | |
258 | 261 |
259 memcpy(fp.data, decoded.data(), arraysize(fp.data)); | 262 if (decoded.size() == 20) |
| 263 fp.tag = HASH_VALUE_SHA1; |
| 264 else if (decoded.size() == 32) |
| 265 fp.tag = HASH_VALUE_SHA256; |
| 266 else |
| 267 return false; |
| 268 |
| 269 memcpy(fp.data(), decoded.data(), fp.size()); |
260 fingerprints->push_back(fp); | 270 fingerprints->push_back(fp); |
261 return true; | 271 return true; |
262 } | 272 } |
263 | 273 |
264 struct FingerprintsEqualPredicate { | 274 struct HashValuesEqualPredicate { |
265 explicit FingerprintsEqualPredicate(const SHA1Fingerprint& fingerprint) : | 275 explicit HashValuesEqualPredicate(const HashValue& fingerprint) : |
266 fingerprint_(fingerprint) {} | 276 fingerprint_(fingerprint) {} |
267 | 277 |
268 bool operator()(const SHA1Fingerprint& other) const { | 278 bool operator()(const HashValue& other) const { |
269 return fingerprint_.Equals(other); | 279 return fingerprint_.Equals(other); |
270 } | 280 } |
271 | 281 |
272 const SHA1Fingerprint& fingerprint_; | 282 const HashValue& fingerprint_; |
273 }; | 283 }; |
274 | 284 |
275 // Returns true iff there is an item in |pins| which is not present in | 285 // Returns true iff there is an item in |pins| which is not present in |
276 // |from_cert_chain|. Such an SPKI hash is called a "backup pin". | 286 // |from_cert_chain|. Such an SPKI hash is called a "backup pin". |
277 static bool IsBackupPinPresent(const FingerprintVector& pins, | 287 static bool IsBackupPinPresent(const HashValueVector& pins, |
278 const FingerprintVector& from_cert_chain) { | 288 const HashValueVector& from_cert_chain) { |
279 for (FingerprintVector::const_iterator | 289 for (HashValueVector::const_iterator |
280 i = pins.begin(); i != pins.end(); ++i) { | 290 i = pins.begin(); i != pins.end(); ++i) { |
281 FingerprintVector::const_iterator j = | 291 HashValueVector::const_iterator j = |
282 std::find_if(from_cert_chain.begin(), from_cert_chain.end(), | 292 std::find_if(from_cert_chain.begin(), from_cert_chain.end(), |
283 FingerprintsEqualPredicate(*i)); | 293 HashValuesEqualPredicate(*i)); |
284 if (j == from_cert_chain.end()) | 294 if (j == from_cert_chain.end()) |
285 return true; | 295 return true; |
286 } | 296 } |
287 | 297 |
288 return false; | 298 return false; |
289 } | 299 } |
290 | 300 |
291 static bool HashesIntersect(const FingerprintVector& a, | 301 static bool HashesIntersect(const HashValueVector& a, |
292 const FingerprintVector& b) { | 302 const HashValueVector& b) { |
293 for (FingerprintVector::const_iterator | 303 for (HashValueVector::const_iterator |
294 i = a.begin(); i != a.end(); ++i) { | 304 i = a.begin(); i != a.end(); ++i) { |
295 FingerprintVector::const_iterator j = | 305 HashValueVector::const_iterator j = |
296 std::find_if(b.begin(), b.end(), FingerprintsEqualPredicate(*i)); | 306 std::find_if(b.begin(), b.end(), HashValuesEqualPredicate(*i)); |
297 if (j != b.end()) | 307 if (j != b.end()) |
298 return true; | 308 return true; |
299 } | 309 } |
300 | 310 |
301 return false; | 311 return false; |
302 } | 312 } |
303 | 313 |
304 // Returns true iff |pins| contains both a live and a backup pin. A live pin | 314 // Returns true iff |pins| contains both a live and a backup pin. A live pin |
305 // is a pin whose SPKI is present in the certificate chain in |ssl_info|. A | 315 // is a pin whose SPKI is present in the certificate chain in |ssl_info|. A |
306 // backup pin is a pin intended for disaster recovery, not day-to-day use, and | 316 // backup pin is a pin intended for disaster recovery, not day-to-day use, and |
307 // thus must be absent from the certificate chain. The Public-Key-Pins header | 317 // thus must be absent from the certificate chain. The Public-Key-Pins header |
308 // specification requires both. | 318 // specification requires both. |
309 static bool IsPinListValid(const FingerprintVector& pins, | 319 static bool IsPinListValid(const HashValueVector& pins, |
310 const SSLInfo& ssl_info) { | 320 const SSLInfo& ssl_info) { |
| 321 // Fast fail: 1 live + 1 backup = at least 2 pins. (Check for actual |
| 322 // liveness and backupness below.) |
311 if (pins.size() < 2) | 323 if (pins.size() < 2) |
312 return false; | 324 return false; |
313 | 325 |
314 const FingerprintVector& from_cert_chain = ssl_info.public_key_hashes; | 326 // Site operators might pin a key using either the SHA-1 or SHA-256 hash |
315 if (from_cert_chain.empty()) | 327 // of the SPKI. So check for success using either hash function. |
| 328 // |
| 329 // TODO(palmer): Make this generic so that it works regardless of what |
| 330 // HashValueTags are defined in the future. |
| 331 const HashValueVector& from_cert_chain_sha1 = |
| 332 ssl_info.public_key_hashes[HASH_VALUE_SHA1]; |
| 333 const HashValueVector& from_cert_chain_sha256 = |
| 334 ssl_info.public_key_hashes[HASH_VALUE_SHA256]; |
| 335 |
| 336 if (from_cert_chain_sha1.empty() && from_cert_chain_sha256.empty()) |
316 return false; | 337 return false; |
317 | 338 |
318 return IsBackupPinPresent(pins, from_cert_chain) && | 339 return (IsBackupPinPresent(pins, from_cert_chain_sha1) || |
319 HashesIntersect(pins, from_cert_chain); | 340 IsBackupPinPresent(pins, from_cert_chain_sha256)) && |
| 341 (HashesIntersect(pins, from_cert_chain_sha1) || |
| 342 HashesIntersect(pins, from_cert_chain_sha256)); |
320 } | 343 } |
321 | 344 |
322 // "Public-Key-Pins" ":" | 345 // "Public-Key-Pins" ":" |
323 // "max-age" "=" delta-seconds ";" | 346 // "max-age" "=" delta-seconds ";" |
324 // "pin-" algo "=" base64 [ ";" ... ] | 347 // "pin-" algo "=" base64 [ ";" ... ] |
325 bool TransportSecurityState::DomainState::ParsePinsHeader( | 348 bool TransportSecurityState::DomainState::ParsePinsHeader( |
326 const base::Time& now, | 349 const base::Time& now, |
327 const std::string& value, | 350 const std::string& value, |
328 const SSLInfo& ssl_info) { | 351 const SSLInfo& ssl_info) { |
329 bool parsed_max_age = false; | 352 bool parsed_max_age = false; |
330 int max_age_candidate = 0; | 353 int max_age_candidate = 0; |
331 FingerprintVector pins; | 354 HashValueVector pins; |
332 | 355 |
333 std::string source = value; | 356 std::string source = value; |
334 | 357 |
335 while (!source.empty()) { | 358 while (!source.empty()) { |
336 StringPair semicolon = Split(source, ';'); | 359 StringPair semicolon = Split(source, ';'); |
337 semicolon.first = Strip(semicolon.first); | 360 semicolon.first = Strip(semicolon.first); |
338 semicolon.second = Strip(semicolon.second); | 361 semicolon.second = Strip(semicolon.second); |
339 StringPair equals = Split(semicolon.first, '='); | 362 StringPair equals = Split(semicolon.first, '='); |
340 equals.first = Strip(equals.first); | 363 equals.first = Strip(equals.first); |
341 equals.second = Strip(equals.second); | 364 equals.second = Strip(equals.second); |
342 | 365 |
343 if (LowerCaseEqualsASCII(equals.first, "max-age")) { | 366 if (LowerCaseEqualsASCII(equals.first, "max-age")) { |
344 if (equals.second.empty() || | 367 if (equals.second.empty() || |
345 !MaxAgeToInt(equals.second.begin(), equals.second.end(), | 368 !MaxAgeToInt(equals.second.begin(), equals.second.end(), |
346 &max_age_candidate)) { | 369 &max_age_candidate)) { |
347 return false; | 370 return false; |
348 } | 371 } |
349 if (max_age_candidate > kMaxHSTSAgeSecs) | 372 if (max_age_candidate > kMaxHSTSAgeSecs) |
350 max_age_candidate = kMaxHSTSAgeSecs; | 373 max_age_candidate = kMaxHSTSAgeSecs; |
351 parsed_max_age = true; | 374 parsed_max_age = true; |
352 } else if (LowerCaseEqualsASCII(equals.first, "pin-sha1")) { | 375 } else if (LowerCaseEqualsASCII(equals.first, "pin-sha1") || |
| 376 LowerCaseEqualsASCII(equals.first, "pin-sha256")) { |
353 if (!ParseAndAppendPin(equals.second, &pins)) | 377 if (!ParseAndAppendPin(equals.second, &pins)) |
354 return false; | 378 return false; |
355 } else if (LowerCaseEqualsASCII(equals.first, "pin-sha256")) { | |
356 // TODO(palmer) | |
357 } else { | 379 } else { |
358 // Silently ignore unknown directives for forward compatibility. | 380 // Silently ignore unknown directives for forward compatibility. |
359 } | 381 } |
360 | 382 |
361 source = semicolon.second; | 383 source = semicolon.second; |
362 } | 384 } |
363 | 385 |
364 if (!parsed_max_age || !IsPinListValid(pins, ssl_info)) | 386 if (!parsed_max_age || !IsPinListValid(pins, ssl_info)) |
365 return false; | 387 return false; |
366 | 388 |
367 dynamic_spki_hashes_expiry = | 389 dynamic_spki_hashes_expiry = |
368 now + base::TimeDelta::FromSeconds(max_age_candidate); | 390 now + base::TimeDelta::FromSeconds(max_age_candidate); |
369 | 391 |
370 dynamic_spki_hashes.clear(); | 392 dynamic_spki_hashes.clear(); |
371 if (max_age_candidate > 0) { | 393 if (max_age_candidate > 0) { |
372 for (FingerprintVector::const_iterator i = pins.begin(); | 394 for (HashValueVector::const_iterator i = pins.begin(); |
373 i != pins.end(); ++i) { | 395 i != pins.end(); ++i) { |
374 dynamic_spki_hashes.push_back(*i); | 396 dynamic_spki_hashes.push_back(*i); |
375 } | 397 } |
376 } | 398 } |
377 | 399 |
378 return true; | 400 return true; |
379 } | 401 } |
380 | 402 |
381 // "Strict-Transport-Security" ":" | 403 // "Strict-Transport-Security" ":" |
382 // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ] | 404 // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ] |
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
469 include_subdomains = true; | 491 include_subdomains = true; |
470 upgrade_mode = MODE_FORCE_HTTPS; | 492 upgrade_mode = MODE_FORCE_HTTPS; |
471 return true; | 493 return true; |
472 default: | 494 default: |
473 NOTREACHED(); | 495 NOTREACHED(); |
474 return false; | 496 return false; |
475 } | 497 } |
476 } | 498 } |
477 | 499 |
478 static bool AddHash(const std::string& type_and_base64, | 500 static bool AddHash(const std::string& type_and_base64, |
479 FingerprintVector* out) { | 501 HashValueVector* out) { |
480 SHA1Fingerprint hash; | 502 HashValue hash; |
481 | 503 |
482 if (!TransportSecurityState::ParsePin(type_and_base64, &hash)) | 504 if (!TransportSecurityState::ParsePin(type_and_base64, &hash)) |
483 return false; | 505 return false; |
484 | 506 |
485 out->push_back(hash); | 507 out->push_back(hash); |
486 return true; | 508 return true; |
487 } | 509 } |
488 | 510 |
489 TransportSecurityState::~TransportSecurityState() {} | 511 TransportSecurityState::~TransportSecurityState() {} |
490 | 512 |
(...skipping 245 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
736 const std::string& hashed_host, const DomainState& state) { | 758 const std::string& hashed_host, const DomainState& state) { |
737 enabled_hosts_[hashed_host] = state; | 759 enabled_hosts_[hashed_host] = state; |
738 } | 760 } |
739 | 761 |
740 void TransportSecurityState::AddOrUpdateForcedHosts( | 762 void TransportSecurityState::AddOrUpdateForcedHosts( |
741 const std::string& hashed_host, const DomainState& state) { | 763 const std::string& hashed_host, const DomainState& state) { |
742 forced_hosts_[hashed_host] = state; | 764 forced_hosts_[hashed_host] = state; |
743 } | 765 } |
744 | 766 |
745 static std::string HashesToBase64String( | 767 static std::string HashesToBase64String( |
746 const FingerprintVector& hashes) { | 768 const HashValueVector& hashes) { |
747 std::vector<std::string> hashes_strs; | 769 std::vector<std::string> hashes_strs; |
748 for (FingerprintVector::const_iterator | 770 for (HashValueVector::const_iterator |
749 i = hashes.begin(); i != hashes.end(); i++) { | 771 i = hashes.begin(); i != hashes.end(); i++) { |
750 std::string s; | 772 std::string s; |
751 const std::string hash_str(reinterpret_cast<const char*>(i->data), | 773 const std::string hash_str(reinterpret_cast<const char*>(i->data()), |
752 sizeof(i->data)); | 774 i->size()); |
753 base::Base64Encode(hash_str, &s); | 775 base::Base64Encode(hash_str, &s); |
754 hashes_strs.push_back(s); | 776 hashes_strs.push_back(s); |
755 } | 777 } |
756 | 778 |
757 return JoinString(hashes_strs, ','); | 779 return JoinString(hashes_strs, ','); |
758 } | 780 } |
759 | 781 |
760 TransportSecurityState::DomainState::DomainState() | 782 TransportSecurityState::DomainState::DomainState() |
761 : upgrade_mode(MODE_FORCE_HTTPS), | 783 : upgrade_mode(MODE_FORCE_HTTPS), |
762 created(base::Time::Now()), | 784 created(base::Time::Now()), |
763 include_subdomains(false) { | 785 include_subdomains(false) { |
764 } | 786 } |
765 | 787 |
766 TransportSecurityState::DomainState::~DomainState() { | 788 TransportSecurityState::DomainState::~DomainState() { |
767 } | 789 } |
768 | 790 |
769 bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted( | 791 bool TransportSecurityState::DomainState::IsChainOfPublicKeysPermitted( |
770 const FingerprintVector& hashes) const { | 792 const std::vector<HashValueVector>& hashes) const { |
771 if (HashesIntersect(bad_static_spki_hashes, hashes)) { | 793 // Validate that hashes is not empty. By the time this code is called (in |
772 LOG(ERROR) << "Rejecting public key chain for domain " << domain | 794 // production), that should never happen, but it's good to be defensive. |
773 << ". Validated chain: " << HashesToBase64String(hashes) | 795 // And, hashes *can* be empty in some test scenarios. |
774 << ", matches one or more bad hashes: " | 796 bool empty = true; |
775 << HashesToBase64String(bad_static_spki_hashes); | 797 for (size_t i = 0; i < hashes.size(); ++i) { |
| 798 if (hashes[i].size()) { |
| 799 empty = false; |
| 800 break; |
| 801 } |
| 802 } |
| 803 if (empty) { |
| 804 LOG(ERROR) << "Rejecting empty certificate chain for public key pinned " |
| 805 "domain " << domain; |
776 return false; | 806 return false; |
777 } | 807 } |
778 | 808 |
779 if (!(dynamic_spki_hashes.empty() && static_spki_hashes.empty()) && | 809 for (size_t i = 0; i < hashes.size(); ++i) { |
780 !HashesIntersect(dynamic_spki_hashes, hashes) && | 810 if (HashesIntersect(bad_static_spki_hashes, hashes[i])) { |
781 !HashesIntersect(static_spki_hashes, hashes)) { | 811 LOG(ERROR) << "Rejecting public key chain for domain " << domain |
782 LOG(ERROR) << "Rejecting public key chain for domain " << domain | 812 << ". Validated chain: " << HashesToBase64String(hashes[i]) |
783 << ". Validated chain: " << HashesToBase64String(hashes) | 813 << ", matches one or more bad hashes: " |
784 << ", expected: " << HashesToBase64String(dynamic_spki_hashes) | 814 << HashesToBase64String(bad_static_spki_hashes); |
785 << " or: " << HashesToBase64String(static_spki_hashes); | 815 return false; |
| 816 } |
786 | 817 |
787 return false; | 818 if (!(dynamic_spki_hashes.empty() && static_spki_hashes.empty()) && |
| 819 hashes[i].size() > 0 && |
| 820 !HashesIntersect(dynamic_spki_hashes, hashes[i]) && |
| 821 !HashesIntersect(static_spki_hashes, hashes[i])) { |
| 822 LOG(ERROR) << "Rejecting public key chain for domain " << domain |
| 823 << ". Validated chain: " << HashesToBase64String(hashes[i]) |
| 824 << ", expected: " << HashesToBase64String(dynamic_spki_hashes) |
| 825 << " or: " << HashesToBase64String(static_spki_hashes); |
| 826 |
| 827 return false; |
| 828 } |
788 } | 829 } |
789 | 830 |
790 return true; | 831 return true; |
791 } | 832 } |
792 | 833 |
793 bool TransportSecurityState::DomainState::ShouldRedirectHTTPToHTTPS() const { | 834 bool TransportSecurityState::DomainState::ShouldRedirectHTTPToHTTPS() const { |
794 return upgrade_mode == MODE_FORCE_HTTPS; | 835 return upgrade_mode == MODE_FORCE_HTTPS; |
795 } | 836 } |
796 | 837 |
797 bool TransportSecurityState::DomainState::Equals( | 838 bool TransportSecurityState::DomainState::Equals( |
798 const DomainState& other) const { | 839 const DomainState& other) const { |
799 // TODO(palmer): Implement this | 840 // TODO(palmer): Implement this |
800 (void) other; | 841 (void) other; |
801 return true; | 842 return true; |
802 } | 843 } |
803 | 844 |
804 bool TransportSecurityState::DomainState::HasPins() const { | 845 bool TransportSecurityState::DomainState::HasPins() const { |
805 return static_spki_hashes.size() > 0 || | 846 return static_spki_hashes.size() > 0 || |
806 bad_static_spki_hashes.size() > 0 || | 847 bad_static_spki_hashes.size() > 0 || |
807 dynamic_spki_hashes.size() > 0; | 848 dynamic_spki_hashes.size() > 0; |
808 } | 849 } |
809 | 850 |
810 } // namespace | 851 } // namespace |
OLD | NEW |