1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/security/manager/boot/src/PublicKeyPinningService.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,306 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +#include "PublicKeyPinningService.h" 1.9 +#include "pkix/nullptr.h" 1.10 +#include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js 1.11 + 1.12 +#include "cert.h" 1.13 +#include "mozilla/Base64.h" 1.14 +#include "mozilla/Telemetry.h" 1.15 +#include "nsString.h" 1.16 +#include "nssb64.h" 1.17 +#include "pkix/pkixtypes.h" 1.18 +#include "prlog.h" 1.19 +#include "ScopedNSSTypes.h" 1.20 +#include "seccomon.h" 1.21 +#include "sechash.h" 1.22 + 1.23 +using namespace mozilla; 1.24 +using namespace mozilla::pkix; 1.25 +using namespace mozilla::psm; 1.26 + 1.27 +#if defined(PR_LOGGING) 1.28 +PRLogModuleInfo* gPublicKeyPinningLog = 1.29 + PR_NewLogModule("PublicKeyPinningService"); 1.30 +#endif 1.31 + 1.32 +/** 1.33 + Computes in the location specified by base64Out the SHA256 digest 1.34 + of the DER Encoded subject Public Key Info for the given cert 1.35 +*/ 1.36 +static SECStatus 1.37 +GetBase64HashSPKI(const CERTCertificate* cert, SECOidTag hashType, 1.38 + nsACString& hashSPKIDigest) 1.39 +{ 1.40 + hashSPKIDigest.Truncate(); 1.41 + Digest digest; 1.42 + nsresult rv = digest.DigestBuf(hashType, cert->derPublicKey.data, 1.43 + cert->derPublicKey.len); 1.44 + if (NS_WARN_IF(NS_FAILED(rv))) { 1.45 + return SECFailure; 1.46 + } 1.47 + rv = Base64Encode(nsDependentCSubstring( 1.48 + reinterpret_cast<const char*>(digest.get().data), 1.49 + digest.get().len), 1.50 + hashSPKIDigest); 1.51 + if (NS_WARN_IF(NS_FAILED(rv))) { 1.52 + return SECFailure; 1.53 + } 1.54 + return SECSuccess; 1.55 +} 1.56 + 1.57 +/* 1.58 + * Returns true if a given cert matches any hashType fingerprints from the 1.59 + * given pinset, false otherwise. 1.60 + */ 1.61 +static bool 1.62 +EvalCertWithHashType(const CERTCertificate* cert, SECOidTag hashType, 1.63 + const StaticFingerprints* fingerprints) 1.64 +{ 1.65 + if (!fingerprints) { 1.66 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.67 + ("pkpin: No hashes found for hash type: %d\n", hashType)); 1.68 + return false; 1.69 + } 1.70 + 1.71 + nsAutoCString base64Out; 1.72 + SECStatus srv = GetBase64HashSPKI(cert, hashType, base64Out); 1.73 + if (srv != SECSuccess) { 1.74 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.75 + ("pkpin: GetBase64HashSPKI failed!\n")); 1.76 + return false; 1.77 + } 1.78 + 1.79 + for (size_t i = 0; i < fingerprints->size; i++) { 1.80 + if (base64Out.Equals(fingerprints->data[i])) { 1.81 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.82 + ("pkpin: found pin base_64 ='%s'\n", base64Out.get())); 1.83 + return true; 1.84 + } 1.85 + } 1.86 + return false; 1.87 +} 1.88 + 1.89 +/* 1.90 + * Returns true if a given chain matches any hashType fingerprints from the 1.91 + * given pinset, false otherwise. 1.92 + */ 1.93 +static bool 1.94 +EvalChainWithHashType(const CERTCertList* certList, SECOidTag hashType, 1.95 + const StaticPinset* pinset) 1.96 +{ 1.97 + CERTCertificate* currentCert; 1.98 + 1.99 + const StaticFingerprints* fingerprints = nullptr; 1.100 + if (hashType == SEC_OID_SHA256) { 1.101 + fingerprints = pinset->sha256; 1.102 + } else if (hashType == SEC_OID_SHA1) { 1.103 + fingerprints = pinset->sha1; 1.104 + } 1.105 + if (!fingerprints) { 1.106 + return false; 1.107 + } 1.108 + 1.109 + CERTCertListNode* node; 1.110 + for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList); 1.111 + node = CERT_LIST_NEXT(node)) { 1.112 + currentCert = node->cert; 1.113 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.114 + ("pkpin: certArray subject: '%s'\n", 1.115 + currentCert->subjectName)); 1.116 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.117 + ("pkpin: certArray common_name: '%s'\n", 1.118 + CERT_GetCommonName(&(currentCert->issuer)))); 1.119 + if (EvalCertWithHashType(currentCert, hashType, fingerprints)) { 1.120 + return true; 1.121 + } 1.122 + } 1.123 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: no matches found\n")); 1.124 + return false; 1.125 +} 1.126 + 1.127 +/** 1.128 + * Given a pinset and certlist, return true if one of the certificates on 1.129 + * the list matches a fingerprint in the pinset, false otherwise. 1.130 + */ 1.131 +static bool 1.132 +EvalChainWithPinset(const CERTCertList* certList, 1.133 + const StaticPinset* pinset) { 1.134 + // SHA256 is more trustworthy, try that first. 1.135 + if (EvalChainWithHashType(certList, SEC_OID_SHA256, pinset)) { 1.136 + return true; 1.137 + } 1.138 + return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset); 1.139 +} 1.140 + 1.141 +/** 1.142 + Comparator for the is public key pinned host. 1.143 +*/ 1.144 +static int 1.145 +TransportSecurityPreloadCompare(const void *key, const void *entry) { 1.146 + const char *keyStr = reinterpret_cast<const char *>(key); 1.147 + const TransportSecurityPreload *preloadEntry = 1.148 + reinterpret_cast<const TransportSecurityPreload *>(entry); 1.149 + 1.150 + return strcmp(keyStr, preloadEntry->mHost); 1.151 +} 1.152 + 1.153 +/** 1.154 + * Check PKPins on the given certlist against the specified hostname 1.155 + */ 1.156 +static bool 1.157 +CheckPinsForHostname(const CERTCertList *certList, const char *hostname, 1.158 + bool enforceTestMode) 1.159 +{ 1.160 + if (!certList) { 1.161 + return false; 1.162 + } 1.163 + if (!hostname || hostname[0] == 0) { 1.164 + return false; 1.165 + } 1.166 + 1.167 + TransportSecurityPreload *foundEntry = nullptr; 1.168 + char *evalHost = const_cast<char*>(hostname); 1.169 + char *evalPart; 1.170 + // Notice how the (xx = strchr) prevents pins for unqualified domain names. 1.171 + while (!foundEntry && (evalPart = strchr(evalHost, '.'))) { 1.172 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.173 + ("pkpin: Querying pinsets for host: '%s'\n", evalHost)); 1.174 + foundEntry = (TransportSecurityPreload *)bsearch(evalHost, 1.175 + kPublicKeyPinningPreloadList, 1.176 + sizeof(kPublicKeyPinningPreloadList) / sizeof(TransportSecurityPreload), 1.177 + sizeof(TransportSecurityPreload), 1.178 + TransportSecurityPreloadCompare); 1.179 + if (foundEntry) { 1.180 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.181 + ("pkpin: Found pinset for host: '%s'\n", evalHost)); 1.182 + if (evalHost != hostname) { 1.183 + if (!foundEntry->mIncludeSubdomains) { 1.184 + // Does not apply to this host, continue iterating 1.185 + foundEntry = nullptr; 1.186 + } 1.187 + } 1.188 + } else { 1.189 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.190 + ("pkpin: Didn't find pinset for host: '%s'\n", evalHost)); 1.191 + } 1.192 + // Add one for '.' 1.193 + evalHost = evalPart + 1; 1.194 + } 1.195 + 1.196 + if (foundEntry && foundEntry->pinset) { 1.197 + bool result = EvalChainWithPinset(certList, foundEntry->pinset); 1.198 + bool retval = result; 1.199 + Telemetry::ID histogram = foundEntry->mIsMoz 1.200 + ? Telemetry::CERT_PINNING_MOZ_RESULTS 1.201 + : Telemetry::CERT_PINNING_RESULTS; 1.202 + if (foundEntry->mTestMode) { 1.203 + histogram = foundEntry->mIsMoz 1.204 + ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS 1.205 + : Telemetry::CERT_PINNING_TEST_RESULTS; 1.206 + if (!enforceTestMode) { 1.207 + retval = true; 1.208 + } 1.209 + } 1.210 + // We can collect per-host pinning violations for this host because it is 1.211 + // operationally critical to Firefox. 1.212 + if (foundEntry->mId != kUnknownId) { 1.213 + int32_t bucket = foundEntry->mId * 2 + (result ? 1 : 0); 1.214 + histogram = foundEntry->mTestMode 1.215 + ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST 1.216 + : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST; 1.217 + Telemetry::Accumulate(histogram, bucket); 1.218 + } else { 1.219 + Telemetry::Accumulate(histogram, result ? 1 : 0); 1.220 + } 1.221 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.222 + ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n", 1.223 + result ? "passed" : "failed", 1.224 + foundEntry->mIsMoz ? "mozilla" : "non-mozilla", 1.225 + hostname, foundEntry->mTestMode ? "test" : "production")); 1.226 + return retval; 1.227 + } 1.228 + return true; // No pinning information for this hostname 1.229 +} 1.230 + 1.231 +/** 1.232 + * Extract all the DNS names for a host (including CN) and evaluate the 1.233 + * certifiate pins against all of them (Currently is an OR so we stop 1.234 + * evaluating at the first OK pin). 1.235 + */ 1.236 +static bool 1.237 +CheckChainAgainstAllNames(const CERTCertList* certList, bool enforceTestMode) 1.238 +{ 1.239 + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, 1.240 + ("pkpin: top of checkChainAgainstAllNames")); 1.241 + CERTCertListNode* node = CERT_LIST_HEAD(certList); 1.242 + if (!node) { 1.243 + return false; 1.244 + } 1.245 + CERTCertificate* cert = node->cert; 1.246 + if (!cert) { 1.247 + return false; 1.248 + } 1.249 + 1.250 + ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); 1.251 + if (!arena) { 1.252 + return false; 1.253 + } 1.254 + 1.255 + bool hasValidPins = false; 1.256 + CERTGeneralName* nameList; 1.257 + CERTGeneralName* currentName; 1.258 + nameList = CERT_GetConstrainedCertificateNames(cert, arena.get(), PR_TRUE); 1.259 + if (!nameList) { 1.260 + return false; 1.261 + } 1.262 + 1.263 + currentName = nameList; 1.264 + do { 1.265 + if (currentName->type == certDNSName 1.266 + && currentName->name.other.data[0] != 0) { 1.267 + // no need to cleaup, as the arena cleanup will do 1.268 + char *hostName = (char *)PORT_ArenaAlloc(arena.get(), 1.269 + currentName->name.other.len + 1); 1.270 + if (!hostName) { 1.271 + break; 1.272 + } 1.273 + // We use a temporary buffer as the hostname as returned might not be 1.274 + // null terminated. 1.275 + hostName[currentName->name.other.len] = 0; 1.276 + memcpy(hostName, currentName->name.other.data, 1.277 + currentName->name.other.len); 1.278 + if (!hostName[0]) { 1.279 + // cannot call CheckPinsForHostname on empty or null hostname 1.280 + break; 1.281 + } 1.282 + if (CheckPinsForHostname(certList, hostName, enforceTestMode)) { 1.283 + hasValidPins = true; 1.284 + break; 1.285 + } 1.286 + } 1.287 + currentName = CERT_GetNextGeneralName(currentName); 1.288 + } while (currentName != nameList); 1.289 + 1.290 + return hasValidPins; 1.291 +} 1.292 + 1.293 +bool 1.294 +PublicKeyPinningService::ChainHasValidPins(const CERTCertList* certList, 1.295 + const char* hostname, 1.296 + const PRTime time, 1.297 + bool enforceTestMode) 1.298 +{ 1.299 + if (!certList) { 1.300 + return false; 1.301 + } 1.302 + if (time > kPreloadPKPinsExpirationTime) { 1.303 + return true; 1.304 + } 1.305 + if (!hostname || hostname[0] == 0) { 1.306 + return CheckChainAgainstAllNames(certList, enforceTestMode); 1.307 + } 1.308 + return CheckPinsForHostname(certList, hostname, enforceTestMode); 1.309 +}