// Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/cert/trial_comparison_cert_verifier_util.h" #include "build/build_config.h" #include "crypto/sha2.h" #include "net/base/hash_value.h" #include "net/base/net_errors.h" #include "net/cert/cert_status_flags.h" #include "net/cert/ev_root_ca_metadata.h" #include "net/cert/pki/cert_errors.h" #include "net/cert/pki/parsed_certificate.h" #include "net/cert/x509_util.h" namespace net { namespace { std::shared_ptr ParsedCertificateFromBuffer( CRYPTO_BUFFER* cert_handle, CertErrors* errors) { return ParsedCertificate::Create(bssl::UpRef(cert_handle), x509_util::DefaultParseCertificateOptions(), errors); } ParsedCertificateList ParsedCertificateListFromX509Certificate( const X509Certificate* cert) { CertErrors parsing_errors; ParsedCertificateList certs; std::shared_ptr target = ParsedCertificateFromBuffer(cert->cert_buffer(), &parsing_errors); if (!target) return {}; certs.push_back(target); for (const auto& buf : cert->intermediate_buffers()) { std::shared_ptr intermediate = ParsedCertificateFromBuffer(buf.get(), &parsing_errors); if (!intermediate) return {}; certs.push_back(intermediate); } return certs; } // Tests whether cert has multiple EV policies, and at least one matches the // root. This is not a complete test of EV, but just enough to give a possible // explanation as to why the platform verifier did not validate as EV while // builtin did. (Since only the builtin verifier correctly handles multiple // candidate EV policies.) bool CertHasMultipleEVPoliciesAndOneMatchesRoot(const X509Certificate* cert) { if (cert->intermediate_buffers().empty()) return false; ParsedCertificateList certs = ParsedCertificateListFromX509Certificate(cert); if (certs.empty()) return false; const ParsedCertificate* leaf = certs.front().get(); const ParsedCertificate* root = certs.back().get(); if (!leaf->has_policy_oids()) return false; const EVRootCAMetadata* ev_metadata = EVRootCAMetadata::GetInstance(); std::set candidate_oids; for (const der::Input& oid : leaf->policy_oids()) { if (ev_metadata->IsEVPolicyOID(oid)) { candidate_oids.insert(oid); } } if (candidate_oids.size() <= 1) return false; SHA256HashValue root_fingerprint; crypto::SHA256HashString(root->der_cert().AsStringView(), root_fingerprint.data, sizeof(root_fingerprint.data)); for (const der::Input& oid : candidate_oids) { if (ev_metadata->HasEVPolicyOID(root_fingerprint, oid)) { return true; } } return false; } SHA256HashValue GetRootHash(const X509Certificate* cert) { SHA256HashValue sha256; if (cert->intermediate_buffers().empty()) { return sha256; } CRYPTO_BUFFER* root = cert->intermediate_buffers().back().get(); return X509Certificate::CalculateFingerprint256(root); } const SHA256HashValue lets_encrypt_dst_x3_sha256_fingerprint = { {0x06, 0x87, 0x26, 0x03, 0x31, 0xA7, 0x24, 0x03, 0xD9, 0x09, 0xF1, 0x05, 0xE6, 0x9B, 0xCF, 0x0D, 0x32, 0xE1, 0xBD, 0x24, 0x93, 0xFF, 0xC6, 0xD9, 0x20, 0x6D, 0x11, 0xBC, 0xD6, 0x77, 0x07, 0x39}}; const SHA256HashValue lets_encrypt_isrg_x1_sha256_fingerprint = { {0x96, 0xBC, 0xEC, 0x06, 0x26, 0x49, 0x76, 0xF3, 0x74, 0x60, 0x77, 0x9A, 0xCF, 0x28, 0xC5, 0xA7, 0xCF, 0xE8, 0xA3, 0xC0, 0xAA, 0xE1, 0x1A, 0x8F, 0xFC, 0xEE, 0x05, 0xC0, 0xBD, 0xDF, 0x08, 0xC6}}; } // namespace // Note: This ignores the result of stapled OCSP (which is the same for both // verifiers) and informational statuses about the certificate algorithms and // the hashes, since they will be the same if the certificate chains are the // same. bool CertVerifyResultEqual(const CertVerifyResult& a, const CertVerifyResult& b) { return std::tie(a.cert_status, a.is_issued_by_known_root) == std::tie(b.cert_status, b.is_issued_by_known_root) && (!!a.verified_cert == !!b.verified_cert) && (!a.verified_cert || a.verified_cert->EqualsIncludingChain(b.verified_cert.get())); } TrialComparisonResult IsSynchronouslyIgnorableDifference( int primary_error, const CertVerifyResult& primary_result, int trial_error, const CertVerifyResult& trial_result, bool sha1_local_anchors_enabled) { DCHECK(primary_result.verified_cert); DCHECK(trial_result.verified_cert); const bool chains_equal = primary_result.verified_cert->EqualsIncludingChain( trial_result.verified_cert.get()); if (chains_equal && (trial_result.cert_status & CERT_STATUS_IS_EV) && !(primary_result.cert_status & CERT_STATUS_IS_EV) && (primary_error == trial_error)) { // The platform CertVerifyProc impls only check a single potential EV // policy from the leaf. If the leaf had multiple policies, builtin // verifier may verify it as EV when the platform verifier did not. if (CertHasMultipleEVPoliciesAndOneMatchesRoot( trial_result.verified_cert.get())) { return TrialComparisonResult::kIgnoredMultipleEVPoliciesAndOneMatchesRoot; } } // SHA-1 signatures are not supported; ignore any results with expected SHA-1 // errors. There are however a few cases with SHA-1 signatures where we might // want to see the difference: // // * local anchors enabled, and one verifier built to a SHA-1 local root but // the other built to a known root. // * If a verifier returned a SHA-1 signature status but did not return an // error. if (!(sha1_local_anchors_enabled && (!primary_result.is_issued_by_known_root || !trial_result.is_issued_by_known_root)) && (primary_result.cert_status & CERT_STATUS_SHA1_SIGNATURE_PRESENT) && (trial_result.cert_status & CERT_STATUS_SHA1_SIGNATURE_PRESENT) && primary_error != OK && trial_error != OK) { return TrialComparisonResult::kIgnoredSHA1SignaturePresent; } // Differences in chain or errors don't matter much if both // return AUTHORITY_INVALID. if ((primary_result.cert_status & CERT_STATUS_AUTHORITY_INVALID) && (trial_result.cert_status & CERT_STATUS_AUTHORITY_INVALID)) { return TrialComparisonResult::kIgnoredBothAuthorityInvalid; } // Due to differences in path building preferences we may end up with // different chains in cross-signing situations. These cases are ignorable if // the errors are equivalent and both chains end up at a known_root. if (!chains_equal && (primary_error == trial_error) && primary_result.is_issued_by_known_root && trial_result.is_issued_by_known_root && (primary_result.cert_status == trial_result.cert_status)) { return TrialComparisonResult::kIgnoredBothKnownRoot; } // If the primary has an error and cert_status reports that a Symantec legacy // cert is present, ignore the error if trial reports // ERR_CERT_AUTHORITY_INVALID as trial will report AUTHORITY_INVALID and short // circuits other checks resulting in mismatching errors and cert status. if (primary_error != OK && trial_error == ERR_CERT_AUTHORITY_INVALID && (primary_result.cert_status & CERT_STATUS_SYMANTEC_LEGACY)) { return TrialComparisonResult:: kIgnoredBuiltinAuthorityInvalidPlatformSymantec; } // There is a fairly prevalant false positive where Windows users are getting // errors because the chain that is built goes to Lets Encrypt's old root // (https://crt.sh/?id=8395) due to the windows machine having an out of date // auth root, whereas CCV builds to Let's Encrypt's new root // (https://crt.sh/?id=9314791). This manifests itself as CCV saying OK // whereas platform reports DATE_INVALID. If we detect this case, ignore it. if (primary_error == ERR_CERT_DATE_INVALID && trial_error == OK && (primary_result.cert_status & CERT_STATUS_ALL_ERRORS) == CERT_STATUS_DATE_INVALID) { SHA256HashValue primary_root_hash = GetRootHash(primary_result.verified_cert.get()); SHA256HashValue trial_root_hash = GetRootHash(trial_result.verified_cert.get()); if (primary_root_hash == lets_encrypt_dst_x3_sha256_fingerprint && trial_root_hash == lets_encrypt_isrg_x1_sha256_fingerprint) { return TrialComparisonResult::kIgnoredLetsEncryptExpiredRoot; } } #if BUILDFLAG(IS_ANDROID) // In the case where a cert is expired and does not have a trusted root, // Android prefers ERR_CERT_DATE_INVALID whereas builtin prefers // ERR_CERT_AUTHORITY_INVALID. if (primary_error == ERR_CERT_DATE_INVALID && trial_error == ERR_CERT_AUTHORITY_INVALID && (primary_result.cert_status & CERT_STATUS_ALL_ERRORS) == CERT_STATUS_DATE_INVALID && (trial_result.cert_status & CERT_STATUS_ALL_ERRORS) == CERT_STATUS_AUTHORITY_INVALID) { return TrialComparisonResult::kIgnoredAndroidErrorDatePriority; } #endif return TrialComparisonResult::kInvalid; } } // namespace net