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