security/manager/boot/src/PublicKeyPinningService.cpp

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 #include "PublicKeyPinningService.h"
michael@0 6 #include "pkix/nullptr.h"
michael@0 7 #include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js
michael@0 8
michael@0 9 #include "cert.h"
michael@0 10 #include "mozilla/Base64.h"
michael@0 11 #include "mozilla/Telemetry.h"
michael@0 12 #include "nsString.h"
michael@0 13 #include "nssb64.h"
michael@0 14 #include "pkix/pkixtypes.h"
michael@0 15 #include "prlog.h"
michael@0 16 #include "ScopedNSSTypes.h"
michael@0 17 #include "seccomon.h"
michael@0 18 #include "sechash.h"
michael@0 19
michael@0 20 using namespace mozilla;
michael@0 21 using namespace mozilla::pkix;
michael@0 22 using namespace mozilla::psm;
michael@0 23
michael@0 24 #if defined(PR_LOGGING)
michael@0 25 PRLogModuleInfo* gPublicKeyPinningLog =
michael@0 26 PR_NewLogModule("PublicKeyPinningService");
michael@0 27 #endif
michael@0 28
michael@0 29 /**
michael@0 30 Computes in the location specified by base64Out the SHA256 digest
michael@0 31 of the DER Encoded subject Public Key Info for the given cert
michael@0 32 */
michael@0 33 static SECStatus
michael@0 34 GetBase64HashSPKI(const CERTCertificate* cert, SECOidTag hashType,
michael@0 35 nsACString& hashSPKIDigest)
michael@0 36 {
michael@0 37 hashSPKIDigest.Truncate();
michael@0 38 Digest digest;
michael@0 39 nsresult rv = digest.DigestBuf(hashType, cert->derPublicKey.data,
michael@0 40 cert->derPublicKey.len);
michael@0 41 if (NS_WARN_IF(NS_FAILED(rv))) {
michael@0 42 return SECFailure;
michael@0 43 }
michael@0 44 rv = Base64Encode(nsDependentCSubstring(
michael@0 45 reinterpret_cast<const char*>(digest.get().data),
michael@0 46 digest.get().len),
michael@0 47 hashSPKIDigest);
michael@0 48 if (NS_WARN_IF(NS_FAILED(rv))) {
michael@0 49 return SECFailure;
michael@0 50 }
michael@0 51 return SECSuccess;
michael@0 52 }
michael@0 53
michael@0 54 /*
michael@0 55 * Returns true if a given cert matches any hashType fingerprints from the
michael@0 56 * given pinset, false otherwise.
michael@0 57 */
michael@0 58 static bool
michael@0 59 EvalCertWithHashType(const CERTCertificate* cert, SECOidTag hashType,
michael@0 60 const StaticFingerprints* fingerprints)
michael@0 61 {
michael@0 62 if (!fingerprints) {
michael@0 63 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 64 ("pkpin: No hashes found for hash type: %d\n", hashType));
michael@0 65 return false;
michael@0 66 }
michael@0 67
michael@0 68 nsAutoCString base64Out;
michael@0 69 SECStatus srv = GetBase64HashSPKI(cert, hashType, base64Out);
michael@0 70 if (srv != SECSuccess) {
michael@0 71 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 72 ("pkpin: GetBase64HashSPKI failed!\n"));
michael@0 73 return false;
michael@0 74 }
michael@0 75
michael@0 76 for (size_t i = 0; i < fingerprints->size; i++) {
michael@0 77 if (base64Out.Equals(fingerprints->data[i])) {
michael@0 78 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 79 ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
michael@0 80 return true;
michael@0 81 }
michael@0 82 }
michael@0 83 return false;
michael@0 84 }
michael@0 85
michael@0 86 /*
michael@0 87 * Returns true if a given chain matches any hashType fingerprints from the
michael@0 88 * given pinset, false otherwise.
michael@0 89 */
michael@0 90 static bool
michael@0 91 EvalChainWithHashType(const CERTCertList* certList, SECOidTag hashType,
michael@0 92 const StaticPinset* pinset)
michael@0 93 {
michael@0 94 CERTCertificate* currentCert;
michael@0 95
michael@0 96 const StaticFingerprints* fingerprints = nullptr;
michael@0 97 if (hashType == SEC_OID_SHA256) {
michael@0 98 fingerprints = pinset->sha256;
michael@0 99 } else if (hashType == SEC_OID_SHA1) {
michael@0 100 fingerprints = pinset->sha1;
michael@0 101 }
michael@0 102 if (!fingerprints) {
michael@0 103 return false;
michael@0 104 }
michael@0 105
michael@0 106 CERTCertListNode* node;
michael@0 107 for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList);
michael@0 108 node = CERT_LIST_NEXT(node)) {
michael@0 109 currentCert = node->cert;
michael@0 110 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 111 ("pkpin: certArray subject: '%s'\n",
michael@0 112 currentCert->subjectName));
michael@0 113 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 114 ("pkpin: certArray common_name: '%s'\n",
michael@0 115 CERT_GetCommonName(&(currentCert->issuer))));
michael@0 116 if (EvalCertWithHashType(currentCert, hashType, fingerprints)) {
michael@0 117 return true;
michael@0 118 }
michael@0 119 }
michael@0 120 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: no matches found\n"));
michael@0 121 return false;
michael@0 122 }
michael@0 123
michael@0 124 /**
michael@0 125 * Given a pinset and certlist, return true if one of the certificates on
michael@0 126 * the list matches a fingerprint in the pinset, false otherwise.
michael@0 127 */
michael@0 128 static bool
michael@0 129 EvalChainWithPinset(const CERTCertList* certList,
michael@0 130 const StaticPinset* pinset) {
michael@0 131 // SHA256 is more trustworthy, try that first.
michael@0 132 if (EvalChainWithHashType(certList, SEC_OID_SHA256, pinset)) {
michael@0 133 return true;
michael@0 134 }
michael@0 135 return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset);
michael@0 136 }
michael@0 137
michael@0 138 /**
michael@0 139 Comparator for the is public key pinned host.
michael@0 140 */
michael@0 141 static int
michael@0 142 TransportSecurityPreloadCompare(const void *key, const void *entry) {
michael@0 143 const char *keyStr = reinterpret_cast<const char *>(key);
michael@0 144 const TransportSecurityPreload *preloadEntry =
michael@0 145 reinterpret_cast<const TransportSecurityPreload *>(entry);
michael@0 146
michael@0 147 return strcmp(keyStr, preloadEntry->mHost);
michael@0 148 }
michael@0 149
michael@0 150 /**
michael@0 151 * Check PKPins on the given certlist against the specified hostname
michael@0 152 */
michael@0 153 static bool
michael@0 154 CheckPinsForHostname(const CERTCertList *certList, const char *hostname,
michael@0 155 bool enforceTestMode)
michael@0 156 {
michael@0 157 if (!certList) {
michael@0 158 return false;
michael@0 159 }
michael@0 160 if (!hostname || hostname[0] == 0) {
michael@0 161 return false;
michael@0 162 }
michael@0 163
michael@0 164 TransportSecurityPreload *foundEntry = nullptr;
michael@0 165 char *evalHost = const_cast<char*>(hostname);
michael@0 166 char *evalPart;
michael@0 167 // Notice how the (xx = strchr) prevents pins for unqualified domain names.
michael@0 168 while (!foundEntry && (evalPart = strchr(evalHost, '.'))) {
michael@0 169 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 170 ("pkpin: Querying pinsets for host: '%s'\n", evalHost));
michael@0 171 foundEntry = (TransportSecurityPreload *)bsearch(evalHost,
michael@0 172 kPublicKeyPinningPreloadList,
michael@0 173 sizeof(kPublicKeyPinningPreloadList) / sizeof(TransportSecurityPreload),
michael@0 174 sizeof(TransportSecurityPreload),
michael@0 175 TransportSecurityPreloadCompare);
michael@0 176 if (foundEntry) {
michael@0 177 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 178 ("pkpin: Found pinset for host: '%s'\n", evalHost));
michael@0 179 if (evalHost != hostname) {
michael@0 180 if (!foundEntry->mIncludeSubdomains) {
michael@0 181 // Does not apply to this host, continue iterating
michael@0 182 foundEntry = nullptr;
michael@0 183 }
michael@0 184 }
michael@0 185 } else {
michael@0 186 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 187 ("pkpin: Didn't find pinset for host: '%s'\n", evalHost));
michael@0 188 }
michael@0 189 // Add one for '.'
michael@0 190 evalHost = evalPart + 1;
michael@0 191 }
michael@0 192
michael@0 193 if (foundEntry && foundEntry->pinset) {
michael@0 194 bool result = EvalChainWithPinset(certList, foundEntry->pinset);
michael@0 195 bool retval = result;
michael@0 196 Telemetry::ID histogram = foundEntry->mIsMoz
michael@0 197 ? Telemetry::CERT_PINNING_MOZ_RESULTS
michael@0 198 : Telemetry::CERT_PINNING_RESULTS;
michael@0 199 if (foundEntry->mTestMode) {
michael@0 200 histogram = foundEntry->mIsMoz
michael@0 201 ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS
michael@0 202 : Telemetry::CERT_PINNING_TEST_RESULTS;
michael@0 203 if (!enforceTestMode) {
michael@0 204 retval = true;
michael@0 205 }
michael@0 206 }
michael@0 207 // We can collect per-host pinning violations for this host because it is
michael@0 208 // operationally critical to Firefox.
michael@0 209 if (foundEntry->mId != kUnknownId) {
michael@0 210 int32_t bucket = foundEntry->mId * 2 + (result ? 1 : 0);
michael@0 211 histogram = foundEntry->mTestMode
michael@0 212 ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST
michael@0 213 : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST;
michael@0 214 Telemetry::Accumulate(histogram, bucket);
michael@0 215 } else {
michael@0 216 Telemetry::Accumulate(histogram, result ? 1 : 0);
michael@0 217 }
michael@0 218 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 219 ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n",
michael@0 220 result ? "passed" : "failed",
michael@0 221 foundEntry->mIsMoz ? "mozilla" : "non-mozilla",
michael@0 222 hostname, foundEntry->mTestMode ? "test" : "production"));
michael@0 223 return retval;
michael@0 224 }
michael@0 225 return true; // No pinning information for this hostname
michael@0 226 }
michael@0 227
michael@0 228 /**
michael@0 229 * Extract all the DNS names for a host (including CN) and evaluate the
michael@0 230 * certifiate pins against all of them (Currently is an OR so we stop
michael@0 231 * evaluating at the first OK pin).
michael@0 232 */
michael@0 233 static bool
michael@0 234 CheckChainAgainstAllNames(const CERTCertList* certList, bool enforceTestMode)
michael@0 235 {
michael@0 236 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
michael@0 237 ("pkpin: top of checkChainAgainstAllNames"));
michael@0 238 CERTCertListNode* node = CERT_LIST_HEAD(certList);
michael@0 239 if (!node) {
michael@0 240 return false;
michael@0 241 }
michael@0 242 CERTCertificate* cert = node->cert;
michael@0 243 if (!cert) {
michael@0 244 return false;
michael@0 245 }
michael@0 246
michael@0 247 ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
michael@0 248 if (!arena) {
michael@0 249 return false;
michael@0 250 }
michael@0 251
michael@0 252 bool hasValidPins = false;
michael@0 253 CERTGeneralName* nameList;
michael@0 254 CERTGeneralName* currentName;
michael@0 255 nameList = CERT_GetConstrainedCertificateNames(cert, arena.get(), PR_TRUE);
michael@0 256 if (!nameList) {
michael@0 257 return false;
michael@0 258 }
michael@0 259
michael@0 260 currentName = nameList;
michael@0 261 do {
michael@0 262 if (currentName->type == certDNSName
michael@0 263 && currentName->name.other.data[0] != 0) {
michael@0 264 // no need to cleaup, as the arena cleanup will do
michael@0 265 char *hostName = (char *)PORT_ArenaAlloc(arena.get(),
michael@0 266 currentName->name.other.len + 1);
michael@0 267 if (!hostName) {
michael@0 268 break;
michael@0 269 }
michael@0 270 // We use a temporary buffer as the hostname as returned might not be
michael@0 271 // null terminated.
michael@0 272 hostName[currentName->name.other.len] = 0;
michael@0 273 memcpy(hostName, currentName->name.other.data,
michael@0 274 currentName->name.other.len);
michael@0 275 if (!hostName[0]) {
michael@0 276 // cannot call CheckPinsForHostname on empty or null hostname
michael@0 277 break;
michael@0 278 }
michael@0 279 if (CheckPinsForHostname(certList, hostName, enforceTestMode)) {
michael@0 280 hasValidPins = true;
michael@0 281 break;
michael@0 282 }
michael@0 283 }
michael@0 284 currentName = CERT_GetNextGeneralName(currentName);
michael@0 285 } while (currentName != nameList);
michael@0 286
michael@0 287 return hasValidPins;
michael@0 288 }
michael@0 289
michael@0 290 bool
michael@0 291 PublicKeyPinningService::ChainHasValidPins(const CERTCertList* certList,
michael@0 292 const char* hostname,
michael@0 293 const PRTime time,
michael@0 294 bool enforceTestMode)
michael@0 295 {
michael@0 296 if (!certList) {
michael@0 297 return false;
michael@0 298 }
michael@0 299 if (time > kPreloadPKPinsExpirationTime) {
michael@0 300 return true;
michael@0 301 }
michael@0 302 if (!hostname || hostname[0] == 0) {
michael@0 303 return CheckChainAgainstAllNames(certList, enforceTestMode);
michael@0 304 }
michael@0 305 return CheckPinsForHostname(certList, hostname, enforceTestMode);
michael@0 306 }

mercurial