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 
GetPinningMode()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 */
GetBase64HashSPKI(const BackCert & cert,nsACString & hashSPKIDigest)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  */
EvalCert(const BackCert & cert,const StaticFingerprints * fingerprints,bool & certMatchesPinset)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  */
EvalChain(const nsTArray<Span<const uint8_t>> & derCertList,const StaticFingerprints * fingerprints,bool & certListIntersectsPinset)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:
TransportSecurityPreloadBinarySearchComparator(const char * aTargetHost)160   explicit TransportSecurityPreloadBinarySearchComparator(
161       const char* aTargetHost)
162       : mTargetHost(aTargetHost) {}
163 
operator ()(const TransportSecurityPreload & val) const164   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 
ValidatePinningPreloadList()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.
FindPinningInformation(const char * hostname,mozilla::pkix::Time time,const TransportSecurityPreload * & staticFingerprints)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.
CheckPinsForHostname(const nsTArray<Span<const uint8_t>> & certList,const char * hostname,bool enforceTestMode,mozilla::pkix::Time time,bool & chainHasValidPins,PinningTelemetryInfo * pinningTelemetryInfo)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 
ChainHasValidPins(const nsTArray<Span<const uint8_t>> & certList,const char * hostname,mozilla::pkix::Time time,bool isBuiltInRoot,bool & chainHasValidPins,PinningTelemetryInfo * pinningTelemetryInfo)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
HostHasPins(nsIURI * aURI,bool * hostHasPins)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 
CanonicalizeHostname(const char * hostname)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