michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "PublicKeyPinningService.h" michael@0: #include "pkix/nullptr.h" michael@0: #include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js michael@0: michael@0: #include "cert.h" michael@0: #include "mozilla/Base64.h" michael@0: #include "mozilla/Telemetry.h" michael@0: #include "nsString.h" michael@0: #include "nssb64.h" michael@0: #include "pkix/pkixtypes.h" michael@0: #include "prlog.h" michael@0: #include "ScopedNSSTypes.h" michael@0: #include "seccomon.h" michael@0: #include "sechash.h" michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::pkix; michael@0: using namespace mozilla::psm; michael@0: michael@0: #if defined(PR_LOGGING) michael@0: PRLogModuleInfo* gPublicKeyPinningLog = michael@0: PR_NewLogModule("PublicKeyPinningService"); michael@0: #endif michael@0: michael@0: /** michael@0: Computes in the location specified by base64Out the SHA256 digest michael@0: of the DER Encoded subject Public Key Info for the given cert michael@0: */ michael@0: static SECStatus michael@0: GetBase64HashSPKI(const CERTCertificate* cert, SECOidTag hashType, michael@0: nsACString& hashSPKIDigest) michael@0: { michael@0: hashSPKIDigest.Truncate(); michael@0: Digest digest; michael@0: nsresult rv = digest.DigestBuf(hashType, cert->derPublicKey.data, michael@0: cert->derPublicKey.len); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return SECFailure; michael@0: } michael@0: rv = Base64Encode(nsDependentCSubstring( michael@0: reinterpret_cast(digest.get().data), michael@0: digest.get().len), michael@0: hashSPKIDigest); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) { michael@0: return SECFailure; michael@0: } michael@0: return SECSuccess; michael@0: } michael@0: michael@0: /* michael@0: * Returns true if a given cert matches any hashType fingerprints from the michael@0: * given pinset, false otherwise. michael@0: */ michael@0: static bool michael@0: EvalCertWithHashType(const CERTCertificate* cert, SECOidTag hashType, michael@0: const StaticFingerprints* fingerprints) michael@0: { michael@0: if (!fingerprints) { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: No hashes found for hash type: %d\n", hashType)); michael@0: return false; michael@0: } michael@0: michael@0: nsAutoCString base64Out; michael@0: SECStatus srv = GetBase64HashSPKI(cert, hashType, base64Out); michael@0: if (srv != SECSuccess) { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: GetBase64HashSPKI failed!\n")); michael@0: return false; michael@0: } michael@0: michael@0: for (size_t i = 0; i < fingerprints->size; i++) { michael@0: if (base64Out.Equals(fingerprints->data[i])) { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: found pin base_64 ='%s'\n", base64Out.get())); michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: /* michael@0: * Returns true if a given chain matches any hashType fingerprints from the michael@0: * given pinset, false otherwise. michael@0: */ michael@0: static bool michael@0: EvalChainWithHashType(const CERTCertList* certList, SECOidTag hashType, michael@0: const StaticPinset* pinset) michael@0: { michael@0: CERTCertificate* currentCert; michael@0: michael@0: const StaticFingerprints* fingerprints = nullptr; michael@0: if (hashType == SEC_OID_SHA256) { michael@0: fingerprints = pinset->sha256; michael@0: } else if (hashType == SEC_OID_SHA1) { michael@0: fingerprints = pinset->sha1; michael@0: } michael@0: if (!fingerprints) { michael@0: return false; michael@0: } michael@0: michael@0: CERTCertListNode* node; michael@0: for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList); michael@0: node = CERT_LIST_NEXT(node)) { michael@0: currentCert = node->cert; michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: certArray subject: '%s'\n", michael@0: currentCert->subjectName)); michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: certArray common_name: '%s'\n", michael@0: CERT_GetCommonName(&(currentCert->issuer)))); michael@0: if (EvalCertWithHashType(currentCert, hashType, fingerprints)) { michael@0: return true; michael@0: } michael@0: } michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: no matches found\n")); michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Given a pinset and certlist, return true if one of the certificates on michael@0: * the list matches a fingerprint in the pinset, false otherwise. michael@0: */ michael@0: static bool michael@0: EvalChainWithPinset(const CERTCertList* certList, michael@0: const StaticPinset* pinset) { michael@0: // SHA256 is more trustworthy, try that first. michael@0: if (EvalChainWithHashType(certList, SEC_OID_SHA256, pinset)) { michael@0: return true; michael@0: } michael@0: return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset); michael@0: } michael@0: michael@0: /** michael@0: Comparator for the is public key pinned host. michael@0: */ michael@0: static int michael@0: TransportSecurityPreloadCompare(const void *key, const void *entry) { michael@0: const char *keyStr = reinterpret_cast(key); michael@0: const TransportSecurityPreload *preloadEntry = michael@0: reinterpret_cast(entry); michael@0: michael@0: return strcmp(keyStr, preloadEntry->mHost); michael@0: } michael@0: michael@0: /** michael@0: * Check PKPins on the given certlist against the specified hostname michael@0: */ michael@0: static bool michael@0: CheckPinsForHostname(const CERTCertList *certList, const char *hostname, michael@0: bool enforceTestMode) michael@0: { michael@0: if (!certList) { michael@0: return false; michael@0: } michael@0: if (!hostname || hostname[0] == 0) { michael@0: return false; michael@0: } michael@0: michael@0: TransportSecurityPreload *foundEntry = nullptr; michael@0: char *evalHost = const_cast(hostname); michael@0: char *evalPart; michael@0: // Notice how the (xx = strchr) prevents pins for unqualified domain names. michael@0: while (!foundEntry && (evalPart = strchr(evalHost, '.'))) { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: Querying pinsets for host: '%s'\n", evalHost)); michael@0: foundEntry = (TransportSecurityPreload *)bsearch(evalHost, michael@0: kPublicKeyPinningPreloadList, michael@0: sizeof(kPublicKeyPinningPreloadList) / sizeof(TransportSecurityPreload), michael@0: sizeof(TransportSecurityPreload), michael@0: TransportSecurityPreloadCompare); michael@0: if (foundEntry) { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: Found pinset for host: '%s'\n", evalHost)); michael@0: if (evalHost != hostname) { michael@0: if (!foundEntry->mIncludeSubdomains) { michael@0: // Does not apply to this host, continue iterating michael@0: foundEntry = nullptr; michael@0: } michael@0: } michael@0: } else { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: Didn't find pinset for host: '%s'\n", evalHost)); michael@0: } michael@0: // Add one for '.' michael@0: evalHost = evalPart + 1; michael@0: } michael@0: michael@0: if (foundEntry && foundEntry->pinset) { michael@0: bool result = EvalChainWithPinset(certList, foundEntry->pinset); michael@0: bool retval = result; michael@0: Telemetry::ID histogram = foundEntry->mIsMoz michael@0: ? Telemetry::CERT_PINNING_MOZ_RESULTS michael@0: : Telemetry::CERT_PINNING_RESULTS; michael@0: if (foundEntry->mTestMode) { michael@0: histogram = foundEntry->mIsMoz michael@0: ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS michael@0: : Telemetry::CERT_PINNING_TEST_RESULTS; michael@0: if (!enforceTestMode) { michael@0: retval = true; michael@0: } michael@0: } michael@0: // We can collect per-host pinning violations for this host because it is michael@0: // operationally critical to Firefox. michael@0: if (foundEntry->mId != kUnknownId) { michael@0: int32_t bucket = foundEntry->mId * 2 + (result ? 1 : 0); michael@0: histogram = foundEntry->mTestMode michael@0: ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST michael@0: : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST; michael@0: Telemetry::Accumulate(histogram, bucket); michael@0: } else { michael@0: Telemetry::Accumulate(histogram, result ? 1 : 0); michael@0: } michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n", michael@0: result ? "passed" : "failed", michael@0: foundEntry->mIsMoz ? "mozilla" : "non-mozilla", michael@0: hostname, foundEntry->mTestMode ? "test" : "production")); michael@0: return retval; michael@0: } michael@0: return true; // No pinning information for this hostname michael@0: } michael@0: michael@0: /** michael@0: * Extract all the DNS names for a host (including CN) and evaluate the michael@0: * certifiate pins against all of them (Currently is an OR so we stop michael@0: * evaluating at the first OK pin). michael@0: */ michael@0: static bool michael@0: CheckChainAgainstAllNames(const CERTCertList* certList, bool enforceTestMode) michael@0: { michael@0: PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, michael@0: ("pkpin: top of checkChainAgainstAllNames")); michael@0: CERTCertListNode* node = CERT_LIST_HEAD(certList); michael@0: if (!node) { michael@0: return false; michael@0: } michael@0: CERTCertificate* cert = node->cert; michael@0: if (!cert) { michael@0: return false; michael@0: } michael@0: michael@0: ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); michael@0: if (!arena) { michael@0: return false; michael@0: } michael@0: michael@0: bool hasValidPins = false; michael@0: CERTGeneralName* nameList; michael@0: CERTGeneralName* currentName; michael@0: nameList = CERT_GetConstrainedCertificateNames(cert, arena.get(), PR_TRUE); michael@0: if (!nameList) { michael@0: return false; michael@0: } michael@0: michael@0: currentName = nameList; michael@0: do { michael@0: if (currentName->type == certDNSName michael@0: && currentName->name.other.data[0] != 0) { michael@0: // no need to cleaup, as the arena cleanup will do michael@0: char *hostName = (char *)PORT_ArenaAlloc(arena.get(), michael@0: currentName->name.other.len + 1); michael@0: if (!hostName) { michael@0: break; michael@0: } michael@0: // We use a temporary buffer as the hostname as returned might not be michael@0: // null terminated. michael@0: hostName[currentName->name.other.len] = 0; michael@0: memcpy(hostName, currentName->name.other.data, michael@0: currentName->name.other.len); michael@0: if (!hostName[0]) { michael@0: // cannot call CheckPinsForHostname on empty or null hostname michael@0: break; michael@0: } michael@0: if (CheckPinsForHostname(certList, hostName, enforceTestMode)) { michael@0: hasValidPins = true; michael@0: break; michael@0: } michael@0: } michael@0: currentName = CERT_GetNextGeneralName(currentName); michael@0: } while (currentName != nameList); michael@0: michael@0: return hasValidPins; michael@0: } michael@0: michael@0: bool michael@0: PublicKeyPinningService::ChainHasValidPins(const CERTCertList* certList, michael@0: const char* hostname, michael@0: const PRTime time, michael@0: bool enforceTestMode) michael@0: { michael@0: if (!certList) { michael@0: return false; michael@0: } michael@0: if (time > kPreloadPKPinsExpirationTime) { michael@0: return true; michael@0: } michael@0: if (!hostname || hostname[0] == 0) { michael@0: return CheckChainAgainstAllNames(certList, enforceTestMode); michael@0: } michael@0: return CheckPinsForHostname(certList, hostname, enforceTestMode); michael@0: }