tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PublicKeyPinningService.cpp (13038B)


      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/. */
      4 
      5 #include "PublicKeyPinningService.h"
      6 
      7 #include "RootCertificateTelemetryUtils.h"
      8 #include "mozilla/Base64.h"
      9 #include "mozilla/BinarySearch.h"
     10 #include "mozilla/Casting.h"
     11 #include "mozilla/Logging.h"
     12 #include "mozilla/Span.h"
     13 #include "mozilla/StaticPrefs_security.h"
     14 #include "nsDependentString.h"
     15 #include "nsServiceManagerUtils.h"
     16 #include "nsSiteSecurityService.h"
     17 #include "mozpkix/pkixtypes.h"
     18 #include "mozpkix/pkixutil.h"
     19 #include "seccomon.h"
     20 #include "sechash.h"
     21 
     22 #include "StaticHPKPins.h"  // autogenerated by genHPKPStaticpins.js
     23 
     24 using namespace mozilla;
     25 using namespace mozilla::pkix;
     26 using namespace mozilla::psm;
     27 
     28 LazyLogModule gPublicKeyPinningLog("PublicKeyPinningService");
     29 
     30 NS_IMPL_ISUPPORTS(PublicKeyPinningService, nsIPublicKeyPinningService)
     31 
     32 enum class PinningMode : uint32_t {
     33  Disabled = 0,
     34  AllowUserCAMITM = 1,
     35  Strict = 2,
     36  EnforceTestMode = 3
     37 };
     38 
     39 PinningMode GetPinningMode() {
     40  PinningMode pinningMode = static_cast<PinningMode>(
     41      StaticPrefs::security_cert_pinning_enforcement_level_DoNotUseDirectly());
     42  switch (pinningMode) {
     43    case PinningMode::Disabled:
     44      return PinningMode::Disabled;
     45    case PinningMode::AllowUserCAMITM:
     46      return PinningMode::AllowUserCAMITM;
     47    case PinningMode::Strict:
     48      return PinningMode::Strict;
     49    case PinningMode::EnforceTestMode:
     50      return PinningMode::EnforceTestMode;
     51    default:
     52      return PinningMode::Disabled;
     53  }
     54 }
     55 
     56 /**
     57 Computes in the location specified by base64Out the SHA256 digest
     58 of the DER Encoded subject Public Key Info for the given cert
     59 */
     60 static nsresult GetBase64HashSPKI(const BackCert& cert,
     61                                  nsACString& hashSPKIDigest) {
     62  Input derPublicKey = cert.GetSubjectPublicKeyInfo();
     63 
     64  hashSPKIDigest.Truncate();
     65  nsTArray<uint8_t> digestArray;
     66  nsresult nsrv =
     67      Digest::DigestBuf(SEC_OID_SHA256, derPublicKey.UnsafeGetData(),
     68                        derPublicKey.GetLength(), digestArray);
     69  if (NS_FAILED(nsrv)) {
     70    return nsrv;
     71  }
     72  return Base64Encode(nsDependentCSubstring(
     73                          BitwiseCast<char*, uint8_t*>(digestArray.Elements()),
     74                          digestArray.Length()),
     75                      hashSPKIDigest);
     76 }
     77 
     78 /*
     79 * Sets certMatchesPinset to true if a given cert matches any fingerprints from
     80 * the given pinset and false otherwise.
     81 */
     82 static nsresult EvalCert(const BackCert& cert,
     83                         const StaticFingerprints* fingerprints,
     84                         /*out*/ bool& certMatchesPinset) {
     85  certMatchesPinset = false;
     86  if (!fingerprints) {
     87    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
     88            ("pkpin: No hashes found\n"));
     89    return NS_ERROR_INVALID_ARG;
     90  }
     91 
     92  nsAutoCString base64Out;
     93  nsresult rv = GetBase64HashSPKI(cert, base64Out);
     94  if (NS_FAILED(rv)) {
     95    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
     96            ("pkpin: GetBase64HashSPKI failed!\n"));
     97    return rv;
     98  }
     99 
    100  if (fingerprints) {
    101    for (size_t i = 0; i < fingerprints->size; i++) {
    102      if (base64Out.Equals(fingerprints->data[i])) {
    103        MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
    104                ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
    105        certMatchesPinset = true;
    106        return NS_OK;
    107      }
    108    }
    109  }
    110  return NS_OK;
    111 }
    112 
    113 /*
    114 * Sets certListIntersectsPinset to true if a given chain matches any
    115 * fingerprints from the given static fingerprints and false otherwise.
    116 */
    117 static nsresult EvalChain(const nsTArray<Span<const uint8_t>>& derCertList,
    118                          const StaticFingerprints* fingerprints,
    119                          /*out*/ bool& certListIntersectsPinset) {
    120  certListIntersectsPinset = false;
    121  if (!fingerprints) {
    122    MOZ_ASSERT(false, "Must pass in at least one type of pinset");
    123    return NS_ERROR_FAILURE;
    124  }
    125 
    126  EndEntityOrCA endEntityOrCA = EndEntityOrCA::MustBeEndEntity;
    127  for (const auto& cert : derCertList) {
    128    Input certInput;
    129    mozilla::pkix::Result rv = certInput.Init(cert.data(), cert.size());
    130    if (rv != mozilla::pkix::Result::Success) {
    131      return NS_ERROR_INVALID_ARG;
    132    }
    133    BackCert backCert(certInput, endEntityOrCA, nullptr);
    134    rv = backCert.Init();
    135    if (rv != mozilla::pkix::Result::Success) {
    136      return NS_ERROR_INVALID_ARG;
    137    }
    138 
    139    nsresult nsrv = EvalCert(backCert, fingerprints, certListIntersectsPinset);
    140    if (NS_FAILED(nsrv)) {
    141      return nsrv;
    142    }
    143    if (certListIntersectsPinset) {
    144      break;
    145    }
    146    endEntityOrCA = EndEntityOrCA::MustBeCA;
    147  }
    148 
    149  if (!certListIntersectsPinset) {
    150    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
    151            ("pkpin: no matches found\n"));
    152  }
    153  return NS_OK;
    154 }
    155 
    156 class TransportSecurityPreloadBinarySearchComparator {
    157 public:
    158  explicit TransportSecurityPreloadBinarySearchComparator(
    159      const char* aTargetHost)
    160      : mTargetHost(aTargetHost) {}
    161 
    162  int operator()(const TransportSecurityPreload& val) const {
    163    return strcmp(mTargetHost, val.mHost);
    164  }
    165 
    166 private:
    167  const char* mTargetHost;  // non-owning
    168 };
    169 
    170 #ifdef DEBUG
    171 static Atomic<bool> sValidatedPinningPreloadList(false);
    172 
    173 static void ValidatePinningPreloadList() {
    174  if (sValidatedPinningPreloadList) {
    175    return;
    176  }
    177  for (const auto& entry : kPublicKeyPinningPreloadList) {
    178    // If and only if a static entry is a Mozilla entry, it has a telemetry ID.
    179    MOZ_ASSERT((entry.mIsMoz && entry.mId != kUnknownId) ||
    180               (!entry.mIsMoz && entry.mId == kUnknownId));
    181  }
    182  sValidatedPinningPreloadList = true;
    183 }
    184 #endif  // DEBUG
    185 
    186 // Returns via one of the output parameters the most relevant pinning
    187 // information that is valid for the given host at the given time.
    188 static nsresult FindPinningInformation(
    189    const char* hostname, mozilla::pkix::Time time,
    190    /*out*/ const TransportSecurityPreload*& staticFingerprints) {
    191 #ifdef DEBUG
    192  ValidatePinningPreloadList();
    193 #endif
    194  if (!hostname || hostname[0] == 0) {
    195    return NS_ERROR_INVALID_ARG;
    196  }
    197  staticFingerprints = nullptr;
    198  const TransportSecurityPreload* foundEntry = nullptr;
    199  const char* evalHost = hostname;
    200  const char* evalPart;
    201  // Notice how the (xx = strchr) prevents pins for unqualified domain names.
    202  while (!foundEntry && (evalPart = strchr(evalHost, '.'))) {
    203    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
    204            ("pkpin: Querying pinsets for host: '%s'\n", evalHost));
    205    size_t foundEntryIndex;
    206    if (BinarySearchIf(kPublicKeyPinningPreloadList, 0,
    207                       std::size(kPublicKeyPinningPreloadList),
    208                       TransportSecurityPreloadBinarySearchComparator(evalHost),
    209                       &foundEntryIndex)) {
    210      foundEntry = &kPublicKeyPinningPreloadList[foundEntryIndex];
    211      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
    212              ("pkpin: Found pinset for host: '%s'\n", evalHost));
    213      if (evalHost != hostname) {
    214        if (!foundEntry->mIncludeSubdomains) {
    215          // Does not apply to this host, continue iterating
    216          foundEntry = nullptr;
    217        }
    218      }
    219    } else {
    220      MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
    221              ("pkpin: Didn't find pinset for host: '%s'\n", evalHost));
    222    }
    223    // Add one for '.'
    224    evalHost = evalPart + 1;
    225  }
    226 
    227  if (foundEntry && foundEntry->pinset) {
    228    if (time > TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime /
    229                                      PR_USEC_PER_SEC)) {
    230      return NS_OK;
    231    }
    232    staticFingerprints = foundEntry;
    233  }
    234  return NS_OK;
    235 }
    236 
    237 // Returns true via the output parameter if the given certificate list meets
    238 // pinning requirements for the given host at the given time. It must be the
    239 // case that either there is an intersection between the set of hashes of
    240 // subject public key info data in the list and the most relevant non-expired
    241 // pinset for the host or there is no pinning information for the host.
    242 static nsresult CheckPinsForHostname(
    243    const nsTArray<Span<const uint8_t>>& certList, const char* hostname,
    244    bool enforceTestMode, mozilla::pkix::Time time,
    245    /*out*/ bool& chainHasValidPins,
    246    /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo) {
    247  chainHasValidPins = false;
    248  if (certList.IsEmpty()) {
    249    return NS_ERROR_INVALID_ARG;
    250  }
    251  if (!hostname || hostname[0] == 0) {
    252    return NS_ERROR_INVALID_ARG;
    253  }
    254 
    255  const TransportSecurityPreload* staticFingerprints = nullptr;
    256  nsresult rv = FindPinningInformation(hostname, time, staticFingerprints);
    257  if (NS_FAILED(rv)) {
    258    return rv;
    259  }
    260  // If we have no pinning information, the certificate chain trivially
    261  // validates with respect to pinning.
    262  if (!staticFingerprints) {
    263    chainHasValidPins = true;
    264    return NS_OK;
    265  }
    266  if (staticFingerprints) {
    267    bool enforceTestModeResult;
    268    rv = EvalChain(certList, staticFingerprints->pinset, enforceTestModeResult);
    269    if (NS_FAILED(rv)) {
    270      return rv;
    271    }
    272    chainHasValidPins = enforceTestModeResult;
    273    if (staticFingerprints->mTestMode && !enforceTestMode) {
    274      chainHasValidPins = true;
    275    }
    276 
    277    if (pinningTelemetryInfo) {
    278      // If and only if a static entry is a Mozilla entry, it has a telemetry
    279      // ID.
    280      if ((staticFingerprints->mIsMoz &&
    281           staticFingerprints->mId == kUnknownId) ||
    282          (!staticFingerprints->mIsMoz &&
    283           staticFingerprints->mId != kUnknownId)) {
    284        return NS_ERROR_FAILURE;
    285      }
    286 
    287      int32_t bucket;
    288      // We can collect per-host pinning violations for this host because it is
    289      // operationally critical to Firefox.
    290      if (staticFingerprints->mIsMoz) {
    291        bucket = staticFingerprints->mId * 2 + (enforceTestModeResult ? 1 : 0);
    292      } else {
    293        bucket = enforceTestModeResult ? 1 : 0;
    294      }
    295      pinningTelemetryInfo->isMoz = staticFingerprints->mIsMoz;
    296      pinningTelemetryInfo->testMode = staticFingerprints->mTestMode;
    297      pinningTelemetryInfo->accumulateResult = true;
    298      pinningTelemetryInfo->certPinningResultBucket = bucket;
    299 
    300      // We only collect per-CA pinning statistics upon failures.
    301      if (!enforceTestModeResult) {
    302        int32_t binNumber = RootCABinNumber(certList.LastElement());
    303        if (binNumber != ROOT_CERTIFICATE_UNKNOWN) {
    304          pinningTelemetryInfo->accumulateForRoot = true;
    305          pinningTelemetryInfo->rootBucket = binNumber;
    306        }
    307      }
    308    }
    309 
    310    MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
    311            ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n",
    312             enforceTestModeResult ? "passed" : "failed",
    313             staticFingerprints->mIsMoz ? "mozilla" : "non-mozilla", hostname,
    314             staticFingerprints->mTestMode ? "test" : "production"));
    315  }
    316 
    317  return NS_OK;
    318 }
    319 
    320 nsresult PublicKeyPinningService::ChainHasValidPins(
    321    const nsTArray<Span<const uint8_t>>& certList, const char* hostname,
    322    mozilla::pkix::Time time, bool isBuiltInRoot,
    323    /*out*/ bool& chainHasValidPins,
    324    /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo) {
    325  PinningMode pinningMode(GetPinningMode());
    326  if (pinningMode == PinningMode::Disabled ||
    327      (!isBuiltInRoot && pinningMode == PinningMode::AllowUserCAMITM)) {
    328    chainHasValidPins = true;
    329    return NS_OK;
    330  }
    331 
    332  chainHasValidPins = false;
    333  if (certList.IsEmpty()) {
    334    return NS_ERROR_INVALID_ARG;
    335  }
    336  if (!hostname || hostname[0] == 0) {
    337    return NS_ERROR_INVALID_ARG;
    338  }
    339  nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
    340  bool enforceTestMode = pinningMode == PinningMode::EnforceTestMode;
    341  return CheckPinsForHostname(certList, canonicalizedHostname.get(),
    342                              enforceTestMode, time, chainHasValidPins,
    343                              pinningTelemetryInfo);
    344 }
    345 
    346 NS_IMETHODIMP
    347 PublicKeyPinningService::HostHasPins(nsIURI* aURI, bool* hostHasPins) {
    348  NS_ENSURE_ARG(aURI);
    349  NS_ENSURE_ARG(hostHasPins);
    350  *hostHasPins = false;
    351  PinningMode pinningMode(GetPinningMode());
    352  if (pinningMode == PinningMode::Disabled) {
    353    return NS_OK;
    354  }
    355  nsAutoCString hostname;
    356  nsresult rv = nsSiteSecurityService::GetHost(aURI, hostname);
    357  if (NS_FAILED(rv)) {
    358    return rv;
    359  }
    360  if (nsSiteSecurityService::HostIsIPAddress(hostname)) {
    361    return NS_OK;
    362  }
    363 
    364  const TransportSecurityPreload* staticFingerprints = nullptr;
    365  rv = FindPinningInformation(hostname.get(), Now(), staticFingerprints);
    366  if (NS_FAILED(rv)) {
    367    return rv;
    368  }
    369  if (staticFingerprints) {
    370    *hostHasPins = !staticFingerprints->mTestMode ||
    371                   pinningMode == PinningMode::EnforceTestMode;
    372  }
    373  return NS_OK;
    374 }
    375 
    376 nsAutoCString PublicKeyPinningService::CanonicalizeHostname(
    377    const char* hostname) {
    378  nsAutoCString canonicalizedHostname(hostname);
    379  ToLowerCase(canonicalizedHostname);
    380  while (canonicalizedHostname.Length() > 0 &&
    381         canonicalizedHostname.Last() == '.') {
    382    canonicalizedHostname.Truncate(canonicalizedHostname.Length() - 1);
    383  }
    384  return canonicalizedHostname;
    385 }