/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "CertVerifier.h" #include #include "AppTrustDomain.h" #include "CTKnownLogs.h" #include "CTLogVerifier.h" #include "ExtendedValidation.h" #include "MultiLogCTVerifier.h" #include "NSSCertDBTrustDomain.h" #include "NSSErrorsService.h" #include "cert.h" #include "mozilla/Assertions.h" #include "mozilla/Casting.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/Logging.h" #include "mozilla/StaticPrefs_security.h" #include "mozilla/SyncRunnable.h" #include "mozpkix/pkix.h" #include "mozpkix/pkixcheck.h" #include "mozpkix/pkixnss.h" #include "mozpkix/pkixutil.h" #include "nsNSSComponent.h" #include "nsNetCID.h" #include "nsPromiseFlatString.h" #include "nsServiceManagerUtils.h" #include "pk11pub.h" #include "secmod.h" using namespace mozilla::ct; using namespace mozilla::pkix; using namespace mozilla::psm; mozilla::LazyLogModule gCertVerifierLog("certverifier"); namespace mozilla { namespace psm { const CertVerifier::Flags CertVerifier::FLAG_LOCAL_ONLY = 1; const CertVerifier::Flags CertVerifier::FLAG_MUST_BE_EV = 2; const CertVerifier::Flags CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST = 4; static const unsigned int MIN_RSA_BITS = 2048; static const unsigned int MIN_RSA_BITS_WEAK = 1024; void CertificateTransparencyInfo::Reset() { enabled = false; verifyResult.Reset(); policyCompliance.reset(); } CertVerifier::CertVerifier(OcspDownloadConfig odc, OcspStrictConfig osc, mozilla::TimeDuration ocspTimeoutSoft, mozilla::TimeDuration ocspTimeoutHard, uint32_t certShortLifetimeInDays, NetscapeStepUpPolicy netscapeStepUpPolicy, CertificateTransparencyConfig&& ctConfig, CRLiteMode crliteMode, const nsTArray& thirdPartyCerts) : mOCSPDownloadConfig(odc), mOCSPStrict(osc == ocspStrict), mOCSPTimeoutSoft(ocspTimeoutSoft), mOCSPTimeoutHard(ocspTimeoutHard), mCertShortLifetimeInDays(certShortLifetimeInDays), mNetscapeStepUpPolicy(netscapeStepUpPolicy), mCTConfig(std::move(ctConfig)), mCRLiteMode(crliteMode), mSignatureCache( signature_cache_new( StaticPrefs::security_pki_cert_signature_cache_size()), signature_cache_free), mTrustCache( trust_cache_new(StaticPrefs::security_pki_cert_trust_cache_size()), trust_cache_free) { LoadKnownCTLogs(); mThirdPartyCerts = thirdPartyCerts.Clone(); for (const auto& root : mThirdPartyCerts) { Input input; if (root.GetInput(input) == Success) { // mThirdPartyCerts consists of roots and intermediates. if (root.GetIsRoot()) { mThirdPartyRootInputs.AppendElement(input); } else { mThirdPartyIntermediateInputs.AppendElement(input); } } } } CertVerifier::~CertVerifier() = default; Result IsDelegatedCredentialAcceptable(const DelegatedCredentialInfo& dcInfo) { bool isEcdsa = dcInfo.scheme == ssl_sig_ecdsa_secp256r1_sha256 || dcInfo.scheme == ssl_sig_ecdsa_secp384r1_sha384 || dcInfo.scheme == ssl_sig_ecdsa_secp521r1_sha512; // Firefox currently does not advertise any RSA schemes for use // with Delegated Credentials. As a secondary (on top of NSS) // check, disallow any RSA SPKI here. When ssl_sig_rsa_pss_pss_* // schemes are supported, check the modulus size and allow RSA here. if (!isEcdsa) { return Result::ERROR_INVALID_KEY; } return Result::Success; } // The term "builtin root" traditionally refers to a root CA certificate that // has been added to the NSS trust store, because it has been approved // for inclusion according to the Mozilla CA policy, and might be accepted // by Mozilla applications as an issuer for certificates seen on the public web. Result IsCertBuiltInRoot(Input certInput, bool& result) { result = false; if (NS_FAILED(BlockUntilLoadableCertsLoaded())) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } #ifdef DEBUG nsCOMPtr component(do_GetService(PSM_COMPONENT_CONTRACTID)); if (!component) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } nsTArray certBytes; certBytes.AppendElements(certInput.UnsafeGetData(), certInput.GetLength()); if (NS_FAILED(component->IsCertTestBuiltInRoot(certBytes, &result))) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } if (result) { return Success; } #endif // DEBUG SECItem certItem(UnsafeMapInputToSECItem(certInput)); AutoSECMODListReadLock lock; for (SECMODModuleList* list = SECMOD_GetDefaultModuleList(); list; list = list->next) { for (int i = 0; i < list->module->slotCount; i++) { PK11SlotInfo* slot = list->module->slots[i]; // We're searching for the "builtin root module", which is a module that // contains an object with a CKA_CLASS of CKO_NETSCAPE_BUILTIN_ROOT_LIST. // We use PK11_HasRootCerts() to identify a module with that property. // In the past, we exclusively used the PKCS#11 module named nssckbi, // which is provided by the NSS library. // Nowadays, some distributions use a replacement module, which contains // the builtin roots, but which also contains additional CA certificates, // such as CAs trusted in a local deployment. // We want to be able to distinguish between these two categories, // because a CA, which may issue certificates for the public web, // is expected to comply with additional requirements. // If the certificate has attribute CKA_NSS_MOZILLA_CA_POLICY set to true, // then we treat it as a "builtin root". if (!PK11_IsPresent(slot) || !PK11_HasRootCerts(slot)) { continue; } CK_OBJECT_HANDLE handle = PK11_FindEncodedCertInSlot(slot, &certItem, nullptr); if (handle == CK_INVALID_HANDLE) { continue; } if (PK11_HasAttributeSet(slot, handle, CKA_NSS_MOZILLA_CA_POLICY, false)) { // Attribute was found, and is set to true result = true; break; } } } return Success; } static Result BuildCertChainForOneKeyUsage( NSSCertDBTrustDomain& trustDomain, Input certDER, Time time, KeyUsage ku1, KeyUsage ku2, KeyUsage ku3, KeyPurposeId eku, const CertPolicyId& requiredPolicy, const Input* stapledOCSPResponse, /*optional out*/ CertVerifier::OCSPStaplingStatus* ocspStaplingStatus) { trustDomain.ResetAccumulatedState(); Result rv = BuildCertChain(trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, ku1, eku, requiredPolicy, stapledOCSPResponse); if (rv == Result::ERROR_INADEQUATE_KEY_USAGE) { trustDomain.ResetAccumulatedState(); rv = BuildCertChain(trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, ku2, eku, requiredPolicy, stapledOCSPResponse); if (rv == Result::ERROR_INADEQUATE_KEY_USAGE) { trustDomain.ResetAccumulatedState(); rv = BuildCertChain(trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, ku3, eku, requiredPolicy, stapledOCSPResponse); if (rv != Success) { rv = Result::ERROR_INADEQUATE_KEY_USAGE; } } } if (ocspStaplingStatus) { *ocspStaplingStatus = trustDomain.GetOCSPStaplingStatus(); } return rv; } void CertVerifier::LoadKnownCTLogs() { if (mCTConfig.mMode == CertificateTransparencyMode::Disabled) { return; } mCTVerifier = MakeUnique(); for (const CTLogInfo& log : kCTLogList) { Input publicKey; Result rv = publicKey.Init( BitwiseCast(log.key), log.keyLength); if (rv != Success) { MOZ_ASSERT_UNREACHABLE("Failed reading a log key for a known CT Log"); continue; } const CTLogOperatorInfo& logOperator = kCTLogOperatorList[log.operatorIndex]; CTLogVerifier logVerifier(logOperator.id, log.state, log.timestamp); rv = logVerifier.Init(publicKey); if (rv != Success) { MOZ_ASSERT_UNREACHABLE("Failed initializing a known CT Log"); continue; } mCTVerifier->AddLog(std::move(logVerifier)); } } bool HostnameMatchesPolicy(const char* hostname, const nsCString& policy) { // Some contexts don't have a hostname (mostly tests), in which case the // policy doesn't apply. if (!hostname) { return false; } nsDependentCString hostnameString(hostname); // The policy is a comma-separated list of entries of the form // '.example.com', 'example.com', or an IP address. for (const auto& entry : policy.Split(',')) { if (entry.IsEmpty()) { continue; } // For '.example.com' entries, exact matches match the policy. if (entry[0] == '.' && Substring(entry, 1).EqualsIgnoreCase(hostnameString)) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("not enforcing CT for '%s' (matches policy '%s')", hostname, policy.get())); return true; } // For 'example.com' entries, exact matches or subdomains match the policy // (IP addresses match here too). if (StringEndsWith(hostnameString, entry) && (hostnameString.Length() == entry.Length() || hostnameString[hostnameString.Length() - entry.Length() - 1] == '.')) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("not enforcing CT for '%s' (matches policy '%s')", hostname, policy.get())); return true; } } return false; } bool CertificateListHasSPKIHashIn( const nsTArray>& certificates, const nsTArray>& spkiHashes) { if (spkiHashes.IsEmpty()) { return false; } for (const auto& certificate : certificates) { Input certificateInput; if (certificateInput.Init(certificate.Elements(), certificate.Length()) != Success) { return false; } // No path building is happening here, so this parameter doesn't matter. EndEntityOrCA notUsedForPathBuilding = EndEntityOrCA::MustBeEndEntity; BackCert decodedCertificate(certificateInput, notUsedForPathBuilding, nullptr); if (decodedCertificate.Init() != Success) { return false; } Input spki(decodedCertificate.GetSubjectPublicKeyInfo()); uint8_t spkiHash[SHA256_LENGTH]; if (DigestBufNSS(spki, DigestAlgorithm::sha256, spkiHash, sizeof(spkiHash)) != Success) { return false; } Span spkiHashSpan(reinterpret_cast(spkiHash), sizeof(spkiHash)); for (const auto& candidateSPKIHash : spkiHashes) { if (Span(candidateSPKIHash) == spkiHashSpan) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("found SPKI hash match - not enforcing CT")); return true; } } } return false; } Result CertVerifier::VerifyCertificateTransparencyPolicy( NSSCertDBTrustDomain& trustDomain, const nsTArray>& builtChain, Input sctsFromTLS, Time time, const char* hostname, /*optional out*/ CertificateTransparencyInfo* ctInfo) { if (builtChain.IsEmpty()) { return Result::FATAL_ERROR_INVALID_ARGS; } if (ctInfo) { ctInfo->Reset(); } if (mCTConfig.mMode == CertificateTransparencyMode::Disabled || !trustDomain.GetIsBuiltChainRootBuiltInRoot()) { return Success; } if (time > TimeFromEpochInSeconds(kCTExpirationTime / PR_USEC_PER_SEC)) { MOZ_LOG(gCertVerifierLog, LogLevel::Warning, ("skipping CT - built-in information has expired")); return Success; } if (ctInfo) { ctInfo->enabled = true; } Result rv = VerifyCertificateTransparencyPolicyInner( trustDomain, builtChain, sctsFromTLS, time, ctInfo); if (rv == Result::ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY && (mCTConfig.mMode != CertificateTransparencyMode::Enforce || HostnameMatchesPolicy(hostname, mCTConfig.mSkipForHosts) || CertificateListHasSPKIHashIn(builtChain, mCTConfig.mSkipForSPKIHashes))) { return Success; } return rv; } Result CertVerifier::VerifyCertificateTransparencyPolicyInner( NSSCertDBTrustDomain& trustDomain, const nsTArray>& builtChain, Input sctsFromTLS, Time time, /*optional out*/ CertificateTransparencyInfo* ctInfo) { Input embeddedSCTs = trustDomain.GetSCTListFromCertificate(); if (embeddedSCTs.GetLength() > 0) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Got embedded SCT data of length %zu\n", static_cast(embeddedSCTs.GetLength()))); } Input sctsFromOCSP = trustDomain.GetSCTListFromOCSPStapling(); if (sctsFromOCSP.GetLength() > 0) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Got OCSP SCT data of length %zu\n", static_cast(sctsFromOCSP.GetLength()))); } if (sctsFromTLS.GetLength() > 0) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Got TLS SCT data of length %zu\n", static_cast(sctsFromTLS.GetLength()))); } if (builtChain.Length() == 1) { // Issuer certificate is required for SCT verification. // If we've arrived here, we probably have a "trust chain" with only one // certificate (i.e. a self-signed end-entity that has been set as a trust // anchor either by a third party modifying our trust DB or via the // enterprise roots feature). If this is the case, certificate transparency // information will probably not be present, and it certainly won't verify // correctly. To simplify things, we return an empty CTVerifyResult and a // "not enough SCTs" CTPolicyCompliance result. if (ctInfo) { CTVerifyResult emptyResult; ctInfo->verifyResult = std::move(emptyResult); ctInfo->policyCompliance.emplace(CTPolicyCompliance::NotEnoughScts); } return Result::ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY; } const nsTArray& endEntityBytes = builtChain.ElementAt(0); Input endEntityInput; Result rv = endEntityInput.Init(endEntityBytes.Elements(), endEntityBytes.Length()); if (rv != Success) { return rv; } const nsTArray& issuerBytes = builtChain.ElementAt(1); Input issuerInput; rv = issuerInput.Init(issuerBytes.Elements(), issuerBytes.Length()); if (rv != Success) { return rv; } BackCert issuerBackCert(issuerInput, EndEntityOrCA::MustBeCA, nullptr); rv = issuerBackCert.Init(); if (rv != Success) { return rv; } Input issuerPublicKeyInput = issuerBackCert.GetSubjectPublicKeyInfo(); CTVerifyResult result; rv = mCTVerifier->Verify(endEntityInput, issuerPublicKeyInput, embeddedSCTs, sctsFromOCSP, sctsFromTLS, time, result); if (rv != Success) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("SCT verification failed with fatal error %" PRId32 "\n", static_cast(rv))); return rv; } if (MOZ_LOG_TEST(gCertVerifierLog, LogLevel::Debug)) { size_t validCount = 0; size_t retiredLogCount = 0; for (const VerifiedSCT& verifiedSct : result.verifiedScts) { switch (verifiedSct.logState) { case CTLogState::Admissible: validCount++; break; case CTLogState::Retired: retiredLogCount++; break; } } MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("SCT verification result: " "valid=%zu unknownLog=%zu retiredLog=%zu " "invalidSignature=%zu invalidTimestamp=%zu " "decodingErrors=%zu\n", validCount, result.sctsFromUnknownLogs, retiredLogCount, result.sctsWithInvalidSignatures, result.sctsWithInvalidTimestamps, result.decodingErrors)); } BackCert endEntityBackCert(endEntityInput, EndEntityOrCA::MustBeEndEntity, nullptr); rv = endEntityBackCert.Init(); if (rv != Success) { return rv; } Time notBefore(Time::uninitialized); Time notAfter(Time::uninitialized); rv = ParseValidity(endEntityBackCert.GetValidity(), ¬Before, ¬After); if (rv != Success) { return rv; } Duration certLifetime(notBefore, notAfter); CTPolicyCompliance ctPolicyCompliance = CheckCTPolicyCompliance(result.verifiedScts, certLifetime); if (ctInfo) { ctInfo->verifyResult = std::move(result); ctInfo->policyCompliance.emplace(ctPolicyCompliance); } if (ctPolicyCompliance != CTPolicyCompliance::Compliant) { return Result::ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY; } return Success; } Result CertVerifier::VerifyCert( const nsTArray& certBytes, VerifyUsage usage, Time time, void* pinArg, const char* hostname, /*out*/ nsTArray>& builtChain, /*optional*/ const Flags flags, /*optional*/ const Maybe>>& extraCertificates, /*optional*/ const Maybe>& stapledOCSPResponseArg, /*optional*/ const Maybe>& sctsFromTLS, /*optional*/ const OriginAttributes& originAttributes, /*optional out*/ EVStatus* evStatus, /*optional out*/ OCSPStaplingStatus* ocspStaplingStatus, /*optional out*/ KeySizeStatus* keySizeStatus, /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo, /*optional out*/ CertificateTransparencyInfo* ctInfo, /*optional out*/ bool* isBuiltChainRootBuiltInRoot, /*optional out*/ bool* madeOCSPRequests, /*optional out*/ IssuerSources* issuerSources) { MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Top of VerifyCert\n")); MOZ_ASSERT(usage == VerifyUsage::TLSServer || !(flags & FLAG_MUST_BE_EV)); MOZ_ASSERT(usage == VerifyUsage::TLSServer || !keySizeStatus); if (NS_FAILED(BlockUntilLoadableCertsLoaded())) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } if (NS_FAILED(CheckForSmartCardChanges())) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } if (evStatus) { *evStatus = EVStatus::NotEV; } if (ocspStaplingStatus) { if (usage != VerifyUsage::TLSServer) { return Result::FATAL_ERROR_INVALID_ARGS; } *ocspStaplingStatus = OCSP_STAPLING_NEVER_CHECKED; } if (keySizeStatus) { if (usage != VerifyUsage::TLSServer) { return Result::FATAL_ERROR_INVALID_ARGS; } *keySizeStatus = KeySizeStatus::NeverChecked; } if (usage != VerifyUsage::TLSServer && (flags & FLAG_MUST_BE_EV)) { return Result::FATAL_ERROR_INVALID_ARGS; } if (isBuiltChainRootBuiltInRoot) { *isBuiltChainRootBuiltInRoot = false; } if (madeOCSPRequests) { *madeOCSPRequests = false; } if (issuerSources) { issuerSources->clear(); } Input certDER; Result rv = certDER.Init(certBytes.Elements(), certBytes.Length()); if (rv != Success) { return rv; } // We configure the OCSP fetching modes separately for EV and non-EV // verifications. NSSCertDBTrustDomain::OCSPFetching defaultOCSPFetching = (mOCSPDownloadConfig == ocspOff) || (mOCSPDownloadConfig == ocspEVOnly) || (flags & FLAG_LOCAL_ONLY) ? NSSCertDBTrustDomain::NeverFetchOCSP : !mOCSPStrict ? NSSCertDBTrustDomain::FetchOCSPForDVSoftFail : NSSCertDBTrustDomain::FetchOCSPForDVHardFail; Input stapledOCSPResponseInput; const Input* stapledOCSPResponse = nullptr; if (stapledOCSPResponseArg) { rv = stapledOCSPResponseInput.Init(stapledOCSPResponseArg->Elements(), stapledOCSPResponseArg->Length()); if (rv != Success) { // The stapled OCSP response was too big. return Result::ERROR_OCSP_MALFORMED_RESPONSE; } stapledOCSPResponse = &stapledOCSPResponseInput; } Input sctsFromTLSInput; if (sctsFromTLS) { rv = sctsFromTLSInput.Init(sctsFromTLS->Elements(), sctsFromTLS->Length()); if (rv != Success && sctsFromTLSInput.GetLength() != 0) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } } switch (usage) { case VerifyUsage::TLSClient: { // XXX: We don't really have a trust bit for SSL client authentication so // just use trustEmail as it is the closest alternative. NSSCertDBTrustDomain trustDomain( trustEmail, defaultOCSPFetching, mOCSPCache, mSignatureCache.get(), mTrustCache.get(), pinArg, mOCSPTimeoutSoft, mOCSPTimeoutHard, mCertShortLifetimeInDays, MIN_RSA_BITS_WEAK, ValidityCheckingMode::CheckingOff, NetscapeStepUpPolicy::NeverMatch, mCRLiteMode, originAttributes, mThirdPartyRootInputs, mThirdPartyIntermediateInputs, extraCertificates, builtChain, nullptr, nullptr); rv = BuildCertChain( trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, KeyUsage::digitalSignature, KeyPurposeId::id_kp_clientAuth, CertPolicyId::anyPolicy, stapledOCSPResponse); if (madeOCSPRequests) { *madeOCSPRequests |= trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched; } break; } case VerifyUsage::TLSServer: { // TODO: When verifying a certificate in an SSL handshake, we should // restrict the acceptable key usage based on the key exchange method // chosen by the server. // Try to validate for EV first. NSSCertDBTrustDomain::OCSPFetching evOCSPFetching = (mOCSPDownloadConfig == ocspOff) || (flags & FLAG_LOCAL_ONLY) ? NSSCertDBTrustDomain::LocalOnlyOCSPForEV : NSSCertDBTrustDomain::FetchOCSPForEV; nsTArray evPolicies; GetKnownEVPolicies(certBytes, evPolicies); rv = Result::ERROR_UNKNOWN_ERROR; for (const auto& evPolicy : evPolicies) { NSSCertDBTrustDomain trustDomain( trustSSL, evOCSPFetching, mOCSPCache, mSignatureCache.get(), mTrustCache.get(), pinArg, mOCSPTimeoutSoft, mOCSPTimeoutHard, mCertShortLifetimeInDays, MIN_RSA_BITS, ValidityCheckingMode::CheckForEV, mNetscapeStepUpPolicy, mCRLiteMode, originAttributes, mThirdPartyRootInputs, mThirdPartyIntermediateInputs, extraCertificates, builtChain, pinningTelemetryInfo, hostname); rv = BuildCertChainForOneKeyUsage( trustDomain, certDER, time, KeyUsage::digitalSignature, // (EC)DHE KeyUsage::keyEncipherment, // RSA KeyUsage::keyAgreement, // (EC)DH KeyPurposeId::id_kp_serverAuth, evPolicy, stapledOCSPResponse, ocspStaplingStatus); if (madeOCSPRequests) { *madeOCSPRequests |= trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched; } if (issuerSources) { *issuerSources = trustDomain.GetIssuerSources(); } if (rv == Success) { rv = VerifyCertificateTransparencyPolicy(trustDomain, builtChain, sctsFromTLSInput, time, hostname, ctInfo); } if (rv == Success) { if (evStatus) { *evStatus = EVStatus::EV; } if (isBuiltChainRootBuiltInRoot) { *isBuiltChainRootBuiltInRoot = trustDomain.GetIsBuiltChainRootBuiltInRoot(); } break; } } if (rv == Success) { break; } if (flags & FLAG_MUST_BE_EV) { rv = Result::ERROR_POLICY_VALIDATION_FAILED; break; } // Now try non-EV. unsigned int keySizeOptions[] = {MIN_RSA_BITS, MIN_RSA_BITS_WEAK}; KeySizeStatus keySizeStatuses[] = {KeySizeStatus::LargeMinimumSucceeded, KeySizeStatus::CompatibilityRisk}; static_assert(std::size(keySizeOptions) == std::size(keySizeStatuses), "keySize array lengths differ"); size_t keySizeOptionsCount = std::size(keySizeStatuses); for (size_t i = 0; i < keySizeOptionsCount && rv != Success; i++) { // invalidate any telemetry info relating to failed chains if (pinningTelemetryInfo) { pinningTelemetryInfo->Reset(); } NSSCertDBTrustDomain trustDomain( trustSSL, defaultOCSPFetching, mOCSPCache, mSignatureCache.get(), mTrustCache.get(), pinArg, mOCSPTimeoutSoft, mOCSPTimeoutHard, mCertShortLifetimeInDays, keySizeOptions[i], ValidityCheckingMode::CheckingOff, mNetscapeStepUpPolicy, mCRLiteMode, originAttributes, mThirdPartyRootInputs, mThirdPartyIntermediateInputs, extraCertificates, builtChain, pinningTelemetryInfo, hostname); rv = BuildCertChainForOneKeyUsage( trustDomain, certDER, time, KeyUsage::digitalSignature, //(EC)DHE KeyUsage::keyEncipherment, // RSA KeyUsage::keyAgreement, //(EC)DH KeyPurposeId::id_kp_serverAuth, CertPolicyId::anyPolicy, stapledOCSPResponse, ocspStaplingStatus); if (madeOCSPRequests) { *madeOCSPRequests |= trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched; } if (issuerSources) { *issuerSources = trustDomain.GetIssuerSources(); } if (rv != Success && !IsFatalError(rv) && rv != Result::ERROR_REVOKED_CERTIFICATE && trustDomain.GetIsErrorDueToDistrustedCAPolicy()) { // Bug 1444440 - If there are multiple paths, at least one to a CA // distrusted-by-policy, and none of them ending in a trusted root, // then we might show a different error (UNKNOWN_ISSUER) than we // intend, confusing users. rv = Result::ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED; } if (rv == Success) { rv = VerifyCertificateTransparencyPolicy(trustDomain, builtChain, sctsFromTLSInput, time, hostname, ctInfo); } if (rv == Success) { if (keySizeStatus) { *keySizeStatus = keySizeStatuses[i]; } if (isBuiltChainRootBuiltInRoot) { *isBuiltChainRootBuiltInRoot = trustDomain.GetIsBuiltChainRootBuiltInRoot(); } break; } } if (rv != Success && keySizeStatus) { *keySizeStatus = KeySizeStatus::AlreadyBad; } break; } case VerifyUsage::EmailCA: case VerifyUsage::TLSClientCA: case VerifyUsage::TLSServerCA: { KeyPurposeId purpose; SECTrustType trustType; if (usage == VerifyUsage::EmailCA || usage == VerifyUsage::TLSClientCA) { purpose = KeyPurposeId::id_kp_clientAuth; trustType = trustEmail; } else if (usage == VerifyUsage::TLSServerCA) { purpose = KeyPurposeId::id_kp_serverAuth; trustType = trustSSL; } else { MOZ_ASSERT_UNREACHABLE("coding error"); return Result::FATAL_ERROR_LIBRARY_FAILURE; } NSSCertDBTrustDomain trustDomain( trustType, defaultOCSPFetching, mOCSPCache, mSignatureCache.get(), mTrustCache.get(), pinArg, mOCSPTimeoutSoft, mOCSPTimeoutHard, mCertShortLifetimeInDays, MIN_RSA_BITS_WEAK, ValidityCheckingMode::CheckingOff, mNetscapeStepUpPolicy, mCRLiteMode, originAttributes, mThirdPartyRootInputs, mThirdPartyIntermediateInputs, extraCertificates, builtChain, nullptr, nullptr); rv = BuildCertChain(trustDomain, certDER, time, EndEntityOrCA::MustBeCA, KeyUsage::keyCertSign, purpose, CertPolicyId::anyPolicy, stapledOCSPResponse); if (madeOCSPRequests) { *madeOCSPRequests |= trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched; } break; } case VerifyUsage::EmailSigner: { NSSCertDBTrustDomain trustDomain( trustEmail, defaultOCSPFetching, mOCSPCache, mSignatureCache.get(), mTrustCache.get(), pinArg, mOCSPTimeoutSoft, mOCSPTimeoutHard, mCertShortLifetimeInDays, MIN_RSA_BITS_WEAK, ValidityCheckingMode::CheckingOff, NetscapeStepUpPolicy::NeverMatch, mCRLiteMode, originAttributes, mThirdPartyRootInputs, mThirdPartyIntermediateInputs, extraCertificates, builtChain, nullptr, nullptr); rv = BuildCertChain( trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, KeyUsage::digitalSignature, KeyPurposeId::id_kp_emailProtection, CertPolicyId::anyPolicy, stapledOCSPResponse); if (rv == Result::ERROR_INADEQUATE_KEY_USAGE) { rv = BuildCertChain( trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, KeyUsage::nonRepudiation, KeyPurposeId::id_kp_emailProtection, CertPolicyId::anyPolicy, stapledOCSPResponse); } if (madeOCSPRequests) { *madeOCSPRequests |= trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched; } break; } case VerifyUsage::EmailRecipient: { // TODO: The higher level S/MIME processing should pass in which key // usage it is trying to verify for, and base its algorithm choices // based on the result of the verification(s). NSSCertDBTrustDomain trustDomain( trustEmail, defaultOCSPFetching, mOCSPCache, mSignatureCache.get(), mTrustCache.get(), pinArg, mOCSPTimeoutSoft, mOCSPTimeoutHard, mCertShortLifetimeInDays, MIN_RSA_BITS_WEAK, ValidityCheckingMode::CheckingOff, NetscapeStepUpPolicy::NeverMatch, mCRLiteMode, originAttributes, mThirdPartyRootInputs, mThirdPartyIntermediateInputs, extraCertificates, builtChain, nullptr, nullptr); rv = BuildCertChain(trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, KeyUsage::keyEncipherment, // RSA KeyPurposeId::id_kp_emailProtection, CertPolicyId::anyPolicy, stapledOCSPResponse); if (rv == Result::ERROR_INADEQUATE_KEY_USAGE) { rv = BuildCertChain(trustDomain, certDER, time, EndEntityOrCA::MustBeEndEntity, KeyUsage::keyAgreement, // ECDH/DH KeyPurposeId::id_kp_emailProtection, CertPolicyId::anyPolicy, stapledOCSPResponse); } if (madeOCSPRequests) { *madeOCSPRequests |= trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched; } break; } default: rv = Result::FATAL_ERROR_INVALID_ARGS; } if (rv != Success) { return rv; } return Success; } static bool CertIsSelfSigned(const BackCert& backCert, void* pinarg) { if (!InputsAreEqual(backCert.GetIssuer(), backCert.GetSubject())) { return false; } nsTArray> emptyCertList; // AppTrustDomain is only used for its signature verification callbacks // (AppTrustDomain::Verify{ECDSA,RSAPKCS1,RSAPSS}SignedData). mozilla::psm::AppTrustDomain trustDomain(std::move(emptyCertList)); Result rv = VerifySignedData(trustDomain, backCert.GetSignedData(), backCert.GetSubjectPublicKeyInfo()); return rv == Success; } static Result CheckCertHostnameHelper(Input peerCertInput, const nsACString& hostname, bool rootIsBuiltIn) { Input hostnameInput; Result rv = hostnameInput.Init( BitwiseCast(hostname.BeginReading()), hostname.Length()); if (rv != Success) { return Result::FATAL_ERROR_INVALID_ARGS; } SkipInvalidSANsForNonBuiltInRootsPolicy nameMatchingPolicy(rootIsBuiltIn); rv = CheckCertHostname(peerCertInput, hostnameInput, nameMatchingPolicy); // Treat malformed name information as a domain mismatch. if (rv == Result::ERROR_BAD_DER) { return Result::ERROR_BAD_CERT_DOMAIN; } return rv; } Result CertVerifier::VerifySSLServerCert( const nsTArray& peerCertBytes, Time time, /*optional*/ void* pinarg, const nsACString& hostname, /*out*/ nsTArray>& builtChain, /*optional*/ Flags flags, /*optional*/ const Maybe>>& extraCertificates, /*optional*/ const Maybe>& stapledOCSPResponse, /*optional*/ const Maybe>& sctsFromTLS, /*optional*/ const Maybe& dcInfo, /*optional*/ const OriginAttributes& originAttributes, /*optional out*/ EVStatus* evStatus, /*optional out*/ OCSPStaplingStatus* ocspStaplingStatus, /*optional out*/ KeySizeStatus* keySizeStatus, /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo, /*optional out*/ CertificateTransparencyInfo* ctInfo, /*optional out*/ bool* isBuiltChainRootBuiltInRoot, /*optional out*/ bool* madeOCSPRequests, /*optional out*/ IssuerSources* issuerSources) { // XXX: MOZ_ASSERT(pinarg); MOZ_ASSERT(!hostname.IsEmpty()); if (isBuiltChainRootBuiltInRoot) { *isBuiltChainRootBuiltInRoot = false; } if (evStatus) { *evStatus = EVStatus::NotEV; } if (hostname.IsEmpty()) { return Result::FATAL_ERROR_INVALID_ARGS; } // CreateCertErrorRunnable assumes that CheckCertHostname is only called // if VerifyCert succeeded. Input peerCertInput; Result rv = peerCertInput.Init(peerCertBytes.Elements(), peerCertBytes.Length()); if (rv != Success) { return rv; } bool isBuiltChainRootBuiltInRootLocal; rv = VerifyCert( peerCertBytes, VerifyUsage::TLSServer, time, pinarg, PromiseFlatCString(hostname).get(), builtChain, flags, extraCertificates, stapledOCSPResponse, sctsFromTLS, originAttributes, evStatus, ocspStaplingStatus, keySizeStatus, pinningTelemetryInfo, ctInfo, &isBuiltChainRootBuiltInRootLocal, madeOCSPRequests, issuerSources); if (rv != Success) { // we don't use the certificate for path building, so this parameter doesn't // matter EndEntityOrCA notUsedForPaths = EndEntityOrCA::MustBeEndEntity; BackCert peerBackCert(peerCertInput, notUsedForPaths, nullptr); if (peerBackCert.Init() != Success) { return rv; } if ((rv == Result::ERROR_UNKNOWN_ISSUER || rv == Result::ERROR_BAD_SIGNATURE || rv == Result::ERROR_INADEQUATE_KEY_USAGE) && CertIsSelfSigned(peerBackCert, pinarg)) { // In this case we didn't find any issuer for the certificate, or we did // find other certificates with the same subject but different keys, and // the certificate is self-signed. return Result::ERROR_SELF_SIGNED_CERT; } if (rv == Result::ERROR_UNKNOWN_ISSUER) { // In this case we didn't get any valid path for the cert. Let's see if // the issuer is the same as the issuer for our canary probe. If yes, this // connection is connecting via a misconfigured proxy. // Note: The MitM canary might not be set. In this case we consider this // an unknown issuer error. nsCOMPtr component( do_GetService(PSM_COMPONENT_CONTRACTID)); if (!component) { return Result::FATAL_ERROR_LIBRARY_FAILURE; } // IssuerMatchesMitmCanary succeeds if the issuer matches the canary and // the feature is enabled. Input issuerNameInput = peerBackCert.GetIssuer(); SECItem issuerNameItem = UnsafeMapInputToSECItem(issuerNameInput); UniquePORTString issuerName(CERT_DerNameToAscii(&issuerNameItem)); if (!issuerName) { return Result::ERROR_BAD_DER; } nsresult rv = component->IssuerMatchesMitmCanary(issuerName.get()); if (NS_SUCCEEDED(rv)) { return Result::ERROR_MITM_DETECTED; } } // If the certificate is expired or not yet valid, first check whether or // not it is valid for the indicated hostname, because that would be a more // serious error. if (rv == Result::ERROR_EXPIRED_CERTIFICATE || rv == Result::ERROR_NOT_YET_VALID_CERTIFICATE || rv == Result::ERROR_INVALID_DER_TIME) { Result hostnameResult = CheckCertHostnameHelper(peerCertInput, hostname, false); if (hostnameResult != Success) { return hostnameResult; } } return rv; } if (dcInfo) { rv = IsDelegatedCredentialAcceptable(*dcInfo); if (rv != Success) { return rv; } } Input stapledOCSPResponseInput; Input* responseInputPtr = nullptr; if (stapledOCSPResponse) { rv = stapledOCSPResponseInput.Init(stapledOCSPResponse->Elements(), stapledOCSPResponse->Length()); if (rv != Success) { // The stapled OCSP response was too big. return Result::ERROR_OCSP_MALFORMED_RESPONSE; } responseInputPtr = &stapledOCSPResponseInput; } if (!(flags & FLAG_TLS_IGNORE_STATUS_REQUEST)) { rv = CheckTLSFeaturesAreSatisfied(peerCertInput, responseInputPtr); if (rv != Success) { return rv; } } rv = CheckCertHostnameHelper(peerCertInput, hostname, isBuiltChainRootBuiltInRootLocal); if ((rv == Success || rv == Result::ERROR_BAD_CERT_DOMAIN) && isBuiltChainRootBuiltInRoot) { *isBuiltChainRootBuiltInRoot = isBuiltChainRootBuiltInRootLocal; } if (rv != Success) { return rv; } return Success; } // Take the (data, signature, subjectPublicKeyInfo, publicKeyAlgorithm, // digestAlgorithm) tuple that defines a signature and derive a hash that // uniquely identifies it. This is done by prefixing each variable-length // component (data, signature, and subjectPublicKeyInfo) with // sizeof(pkix::Input::size_type) bytes (currently 2) indicating the length of // that component and concatenating them together, followed by one byte for the // digestAlgorithm. The concatenation is then hashed with sha512. // It should be computationally infeasible to find two distinct sets of inputs // that have the same sha512 hash (and if it were possible, then it would be // possible to break the signature scheme itself). void HashSignatureParams(pkix::Input data, pkix::Input signature, pkix::Input subjectPublicKeyInfo, pkix::der::PublicKeyAlgorithm publicKeyAlgorithm, pkix::DigestAlgorithm digestAlgorithm, /*out*/ Maybe>& sha512Hash) { sha512Hash.reset(); Digest digest; if (NS_FAILED(digest.Begin(SEC_OID_SHA512))) { return; } pkix::Input::size_type dataLength = data.GetLength(); if (NS_FAILED(digest.Update(reinterpret_cast(&dataLength), sizeof(dataLength)))) { return; } if (NS_FAILED(digest.Update(data.UnsafeGetData(), dataLength))) { return; } pkix::Input::size_type signatureLength = signature.GetLength(); if (NS_FAILED( digest.Update(reinterpret_cast(&signatureLength), sizeof(signatureLength)))) { return; } if (NS_FAILED(digest.Update(signature.UnsafeGetData(), signatureLength))) { return; } pkix::Input::size_type spkiLength = subjectPublicKeyInfo.GetLength(); if (NS_FAILED(digest.Update(reinterpret_cast(&spkiLength), sizeof(spkiLength)))) { return; } if (NS_FAILED( digest.Update(subjectPublicKeyInfo.UnsafeGetData(), spkiLength))) { return; } if (NS_FAILED( digest.Update(reinterpret_cast(&publicKeyAlgorithm), sizeof(publicKeyAlgorithm)))) { return; } if (NS_FAILED( digest.Update(reinterpret_cast(&digestAlgorithm), sizeof(digestAlgorithm)))) { return; } nsTArray result; if (NS_FAILED(digest.End(result))) { return; } sha512Hash.emplace(std::move(result)); } Result VerifySignedDataWithCache( der::PublicKeyAlgorithm publicKeyAlg, mozilla::glean::impl::DenominatorMetric telemetryDenominator, mozilla::glean::impl::NumeratorMetric telemetryNumerator, Input data, DigestAlgorithm digestAlgorithm, Input signature, Input subjectPublicKeyInfo, SignatureCache* signatureCache, void* pinArg) { telemetryDenominator.Add(1); Maybe> sha512Hash; HashSignatureParams(data, signature, subjectPublicKeyInfo, publicKeyAlg, digestAlgorithm, sha512Hash); // If hashing the signature parameters succeeded, see if this signature is in // the signature cache. if (sha512Hash.isSome() && signature_cache_get(signatureCache, sha512Hash.ref().Elements())) { telemetryNumerator.AddToNumerator(1); return Success; } Result result; switch (publicKeyAlg) { case der::PublicKeyAlgorithm::ECDSA: result = VerifyECDSASignedDataNSS(data, digestAlgorithm, signature, subjectPublicKeyInfo, pinArg); break; case der::PublicKeyAlgorithm::RSA_PKCS1: result = VerifyRSAPKCS1SignedDataNSS(data, digestAlgorithm, signature, subjectPublicKeyInfo, pinArg); break; case der::PublicKeyAlgorithm::RSA_PSS: result = VerifyRSAPSSSignedDataNSS(data, digestAlgorithm, signature, subjectPublicKeyInfo, pinArg); break; default: MOZ_ASSERT_UNREACHABLE("unhandled public key algorithm"); return Result::FATAL_ERROR_LIBRARY_FAILURE; } // Add this signature to the signature cache. if (sha512Hash.isSome() && result == Success) { signature_cache_insert(signatureCache, sha512Hash.ref().Elements()); } return result; } } // namespace psm } // namespace mozilla