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