security/manager/ssl/PublicKeyPinningService.cpp
author Carsten "Tomcat" Book <cbook@mozilla.com>
Tue, 02 Jun 2015 13:05:56 +0200
changeset 246724 3c8ed81098ddbe4a4c09e7aa652b5288dc4ce0d3
parent 246699 7c3b45a47811b55f4e973d996dd149c5d575721b
child 247076 f52c18aac7ce0949190da943ec5d4ee86627d0f8
permissions -rw-r--r--
Backed out 14 changesets (bug 1165515) for linux x64 e10s m2 test failures Backed out changeset d68dcf2ef372 (bug 1165515) Backed out changeset 7c3b45a47811 (bug 1165515) Backed out changeset b668b617bef2 (bug 1165515) Backed out changeset d0916e1283a2 (bug 1165515) Backed out changeset ac4dc7489942 (bug 1165515) Backed out changeset e9632ce8bc65 (bug 1165515) Backed out changeset c16d215cc7e4 (bug 1165515) Backed out changeset e4d474f3c51a (bug 1165515) Backed out changeset d87680bf9f7c (bug 1165515) Backed out changeset b3c0a45ba99e (bug 1165515) Backed out changeset 9370fa197674 (bug 1165515) Backed out changeset 50970d668ca1 (bug 1165515) Backed out changeset ffa4eb6d24b9 (bug 1165515) Backed out changeset 5fcf1203cc1d (bug 1165515)

/* 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 "PublicKeyPinningService.h"

#include "mozilla/Base64.h"
#include "mozilla/Telemetry.h"
#include "nsISiteSecurityService.h"
#include "nsServiceManagerUtils.h"
#include "nsSiteSecurityService.h"
#include "nssb64.h"
#include "pkix/pkixtypes.h"
#include "mozilla/Logging.h"
#include "RootCertificateTelemetryUtils.h"
#include "ScopedNSSTypes.h"
#include "seccomon.h"
#include "sechash.h"

#include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js

using namespace mozilla;
using namespace mozilla::pkix;
using namespace mozilla::psm;

PRLogModuleInfo* gPublicKeyPinningLog =
  PR_NewLogModule("PublicKeyPinningService");

/**
 Computes in the location specified by base64Out the SHA256 digest
 of the DER Encoded subject Public Key Info for the given cert
*/
static nsresult
GetBase64HashSPKI(const CERTCertificate* cert, SECOidTag hashType,
                  nsACString& hashSPKIDigest)
{
  hashSPKIDigest.Truncate();
  Digest digest;
  nsresult rv = digest.DigestBuf(hashType, cert->derPublicKey.data,
                                 cert->derPublicKey.len);
  if (NS_FAILED(rv)) {
    return rv;
  }
  return Base64Encode(nsDependentCSubstring(
                        reinterpret_cast<const char*>(digest.get().data),
                        digest.get().len),
                      hashSPKIDigest);
}

/*
 * Returns true if a given cert matches any hashType fingerprints from the
 * given pinset or the dynamicFingeprints array, false otherwise.
 */
static nsresult
EvalCertWithHashType(const CERTCertificate* cert, SECOidTag hashType,
                     const StaticFingerprints* fingerprints,
                     const nsTArray<nsCString>* dynamicFingerprints,
             /*out*/ bool& certMatchesPinset)
{
  certMatchesPinset = false;
  if (!fingerprints && !dynamicFingerprints) {
    MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
           ("pkpin: No hashes found for hash type: %d\n", hashType));
    return NS_ERROR_INVALID_ARG;
  }

  nsAutoCString base64Out;
  nsresult rv = GetBase64HashSPKI(cert, hashType, base64Out);
  if (NS_FAILED(rv)) {
    MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
           ("pkpin: GetBase64HashSPKI failed!\n"));
    return rv;
  }

  if (fingerprints) {
    for (size_t i = 0; i < fingerprints->size; i++) {
      if (base64Out.Equals(fingerprints->data[i])) {
        MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
               ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
        certMatchesPinset = true;
        return NS_OK;
      }
    }
  }
  if (dynamicFingerprints) {
    for (size_t i = 0; i < dynamicFingerprints->Length(); i++) {
      if (base64Out.Equals((*dynamicFingerprints)[i])) {
        MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
               ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
        certMatchesPinset = true;
        return NS_OK;
      }
    }
  }
  return NS_OK;
}

/*
 * Returns true if a given chain matches any hashType fingerprints from the
 * given pinset or the dynamicFingerprints array, false otherwise.
 */
static nsresult
EvalChainWithHashType(const CERTCertList* certList, SECOidTag hashType,
                      const StaticPinset* pinset,
                      const nsTArray<nsCString>* dynamicFingerprints,
              /*out*/ bool& certListIntersectsPinset)
{
  certListIntersectsPinset = false;
  CERTCertificate* currentCert;

  const StaticFingerprints* fingerprints = nullptr;
  if (pinset) {
    if (hashType == SEC_OID_SHA256) {
      fingerprints = pinset->sha256;
    } else if (hashType == SEC_OID_SHA1) {
      fingerprints = pinset->sha1;
    }
  }
  // This can happen if dynamicFingerprints is null and the static pinset
  // doesn't have any pins of this hash type.
  if (!fingerprints && !dynamicFingerprints) {
    return NS_OK;
  }

  CERTCertListNode* node;
  for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList);
       node = CERT_LIST_NEXT(node)) {
    currentCert = node->cert;
    MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
           ("pkpin: certArray subject: '%s'\n", currentCert->subjectName));
    MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
           ("pkpin: certArray issuer: '%s'\n", currentCert->issuerName));
    nsresult rv = EvalCertWithHashType(currentCert, hashType, fingerprints,
                                       dynamicFingerprints,
                                       certListIntersectsPinset);
    if (NS_FAILED(rv)) {
      return rv;
    }
    if (certListIntersectsPinset) {
      return NS_OK;
    }
  }
  MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: no matches found\n"));
  return NS_OK;
}

/**
 * Given a pinset and certlist, return true if one of the certificates on
 * the list matches a fingerprint in the pinset, false otherwise.
 */
static nsresult
EvalChainWithPinset(const CERTCertList* certList,
                    const StaticPinset* pinset,
            /*out*/ bool& certListIntersectsPinset)
{
  certListIntersectsPinset = false;
  // SHA256 is more trustworthy, try that first.
  nsresult rv = EvalChainWithHashType(certList, SEC_OID_SHA256, pinset,
                                      nullptr, certListIntersectsPinset);
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (certListIntersectsPinset) {
    return NS_OK;
  }
  return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset, nullptr,
                               certListIntersectsPinset);
}

/**
  Comparator for the is public key pinned host.
*/
static int
TransportSecurityPreloadCompare(const void *key, const void *entry) {
  const char *keyStr = reinterpret_cast<const char *>(key);
  const TransportSecurityPreload *preloadEntry =
    reinterpret_cast<const TransportSecurityPreload *>(entry);

  return strcmp(keyStr, preloadEntry->mHost);
}

nsresult
PublicKeyPinningService::ChainMatchesPinset(const CERTCertList* certList,
                                            const nsTArray<nsCString>& aSHA256keys,
                                    /*out*/ bool& chainMatchesPinset)
{
  return EvalChainWithHashType(certList, SEC_OID_SHA256, nullptr, &aSHA256keys,
                               chainMatchesPinset);
}

// Returns via one of the output parameters the most relevant pinning
// information that is valid for the given host at the given time.
// Dynamic pins are prioritized over static pins.
static nsresult
FindPinningInformation(const char* hostname, mozilla::pkix::Time time,
               /*out*/ nsTArray<nsCString>& dynamicFingerprints,
               /*out*/ TransportSecurityPreload*& staticFingerprints)
{
  if (!hostname || hostname[0] == 0) {
    return NS_ERROR_INVALID_ARG;
  }
  staticFingerprints = nullptr;
  dynamicFingerprints.Clear();
  nsCOMPtr<nsISiteSecurityService> sssService =
    do_GetService(NS_SSSERVICE_CONTRACTID);
  if (!sssService) {
    return NS_ERROR_FAILURE;
  }
  SiteHPKPState dynamicEntry;
  TransportSecurityPreload *foundEntry = nullptr;
  char *evalHost = const_cast<char*>(hostname);
  char *evalPart;
  // Notice how the (xx = strchr) prevents pins for unqualified domain names.
  while (!foundEntry && (evalPart = strchr(evalHost, '.'))) {
    MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
           ("pkpin: Querying pinsets for host: '%s'\n", evalHost));
    // Attempt dynamic pins first
    nsresult rv;
    bool found;
    bool includeSubdomains;
    nsTArray<nsCString> pinArray;
    rv = sssService->GetKeyPinsForHostname(evalHost, time, pinArray,
                                           &includeSubdomains, &found);
    if (NS_FAILED(rv)) {
      return rv;
    }
    if (found && (evalHost == hostname || includeSubdomains)) {
      MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
             ("pkpin: Found dyn match for host: '%s'\n", evalHost));
      dynamicFingerprints = pinArray;
      return NS_OK;
    }

    foundEntry = (TransportSecurityPreload *)bsearch(evalHost,
      kPublicKeyPinningPreloadList,
      sizeof(kPublicKeyPinningPreloadList) / sizeof(TransportSecurityPreload),
      sizeof(TransportSecurityPreload),
      TransportSecurityPreloadCompare);
    if (foundEntry) {
      MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
             ("pkpin: Found pinset for host: '%s'\n", evalHost));
      if (evalHost != hostname) {
        if (!foundEntry->mIncludeSubdomains) {
          // Does not apply to this host, continue iterating
          foundEntry = nullptr;
        }
      }
    } else {
      MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
             ("pkpin: Didn't find pinset for host: '%s'\n", evalHost));
    }
    // Add one for '.'
    evalHost = evalPart + 1;
  }

  if (foundEntry && foundEntry->pinset) {
    if (time > TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime /
                                      PR_USEC_PER_SEC)) {
      return NS_OK;
    }
    staticFingerprints = foundEntry;
  }
  return NS_OK;
}

// Returns true via the output parameter if the given certificate list meets
// pinning requirements for the given host at the given time. It must be the
// case that either there is an intersection between the set of hashes of
// subject public key info data in the list and the most relevant non-expired
// pinset for the host or there is no pinning information for the host.
static nsresult
CheckPinsForHostname(const CERTCertList* certList, const char* hostname,
                     bool enforceTestMode, mozilla::pkix::Time time,
             /*out*/ bool& chainHasValidPins)
{
  chainHasValidPins = false;
  if (!certList) {
    return NS_ERROR_INVALID_ARG;
  }
  if (!hostname || hostname[0] == 0) {
    return NS_ERROR_INVALID_ARG;
  }

  nsTArray<nsCString> dynamicFingerprints;
  TransportSecurityPreload* staticFingerprints = nullptr;
  nsresult rv = FindPinningInformation(hostname, time, dynamicFingerprints,
                                       staticFingerprints);
  // If we have no pinning information, the certificate chain trivially
  // validates with respect to pinning.
  if (dynamicFingerprints.Length() == 0 && !staticFingerprints) {
    chainHasValidPins = true;
    return NS_OK;
  }
  if (dynamicFingerprints.Length() > 0) {
    return EvalChainWithHashType(certList, SEC_OID_SHA256, nullptr,
                                 &dynamicFingerprints, chainHasValidPins);
  }
  if (staticFingerprints) {
    bool enforceTestModeResult;
    rv = EvalChainWithPinset(certList, staticFingerprints->pinset,
                             enforceTestModeResult);
    if (NS_FAILED(rv)) {
      return rv;
    }
    chainHasValidPins = enforceTestModeResult;
    Telemetry::ID histogram = staticFingerprints->mIsMoz
      ? Telemetry::CERT_PINNING_MOZ_RESULTS
      : Telemetry::CERT_PINNING_RESULTS;
    if (staticFingerprints->mTestMode) {
      histogram = staticFingerprints->mIsMoz
        ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS
        : Telemetry::CERT_PINNING_TEST_RESULTS;
      if (!enforceTestMode) {
        chainHasValidPins = true;
      }
    }
    // We can collect per-host pinning violations for this host because it is
    // operationally critical to Firefox.
    if (staticFingerprints->mId != kUnknownId) {
      int32_t bucket = staticFingerprints->mId * 2 + (enforceTestModeResult ? 1 : 0);
      histogram = staticFingerprints->mTestMode
        ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST
        : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST;
      Telemetry::Accumulate(histogram, bucket);
    } else {
      Telemetry::Accumulate(histogram, enforceTestModeResult ? 1 : 0);
    }

    // We only collect per-CA pinning statistics upon failures.
    CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
    // Only log telemetry if the certificate list is non-empty.
    if (!CERT_LIST_END(rootNode, certList)) {
      if (!enforceTestModeResult) {
        AccumulateTelemetryForRootCA(Telemetry::CERT_PINNING_FAILURES_BY_CA, rootNode->cert);
      }
    }

    MOZ_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG,
           ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n",
            enforceTestModeResult ? "passed" : "failed",
            staticFingerprints->mIsMoz ? "mozilla" : "non-mozilla",
            hostname, staticFingerprints->mTestMode ? "test" : "production"));
  }

  return NS_OK;
}

nsresult
PublicKeyPinningService::ChainHasValidPins(const CERTCertList* certList,
                                           const char* hostname,
                                           mozilla::pkix::Time time,
                                           bool enforceTestMode,
                                   /*out*/ bool& chainHasValidPins)
{
  chainHasValidPins = false;
  if (!certList) {
    return NS_ERROR_INVALID_ARG;
  }
  if (!hostname || hostname[0] == 0) {
    return NS_ERROR_INVALID_ARG;
  }
  nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
  return CheckPinsForHostname(certList, canonicalizedHostname.get(),
                              enforceTestMode, time, chainHasValidPins);
}

nsresult
PublicKeyPinningService::HostHasPins(const char* hostname,
                                     mozilla::pkix::Time time,
                                     bool enforceTestMode,
                                     /*out*/ bool& hostHasPins)
{
  hostHasPins = false;
  nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
  nsTArray<nsCString> dynamicFingerprints;
  TransportSecurityPreload* staticFingerprints = nullptr;
  nsresult rv = FindPinningInformation(canonicalizedHostname.get(), time,
                                       dynamicFingerprints, staticFingerprints);
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (dynamicFingerprints.Length() > 0) {
    hostHasPins = true;
  } else if (staticFingerprints) {
    hostHasPins = !staticFingerprints->mTestMode || enforceTestMode;
  }
  return NS_OK;
}

nsAutoCString
PublicKeyPinningService::CanonicalizeHostname(const char* hostname)
{
  nsAutoCString canonicalizedHostname(hostname);
  ToLowerCase(canonicalizedHostname);
  while (canonicalizedHostname.Length() > 0 &&
         canonicalizedHostname.Last() == '.') {
    canonicalizedHostname.Truncate(canonicalizedHostname.Length() - 1);
  }
  return canonicalizedHostname;
}