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