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 }