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

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

mercurial