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/. */
5 #include "PublicKeyPinningService.h"
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"
24 #include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js
26 using namespace mozilla
;
27 using namespace mozilla::pkix
;
28 using namespace mozilla::psm
;
30 LazyLogModule
gPublicKeyPinningLog("PublicKeyPinningService");
32 NS_IMPL_ISUPPORTS(PublicKeyPinningService
, nsIPublicKeyPinningService
)
34 enum class PinningMode
: uint32_t {
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
;
54 return PinningMode::Disabled
;
59 Computes in the location specified by base64Out the SHA256 digest
60 of the DER Encoded subject Public Key Info for the given cert
62 static nsresult
GetBase64HashSPKI(const BackCert
& cert
,
63 nsACString
& hashSPKIDigest
) {
64 Input derPublicKey
= cert
.GetSubjectPublicKeyInfo();
66 hashSPKIDigest
.Truncate();
67 nsTArray
<uint8_t> digestArray
;
69 Digest::DigestBuf(SEC_OID_SHA256
, derPublicKey
.UnsafeGetData(),
70 derPublicKey
.GetLength(), digestArray
);
71 if (NS_FAILED(nsrv
)) {
74 return Base64Encode(nsDependentCSubstring(
75 BitwiseCast
<char*, uint8_t*>(digestArray
.Elements()),
76 digestArray
.Length()),
81 * Sets certMatchesPinset to true if a given cert matches any fingerprints from
82 * the given pinset and false otherwise.
84 static nsresult
EvalCert(const BackCert
& cert
,
85 const StaticFingerprints
* fingerprints
,
86 /*out*/ bool& certMatchesPinset
) {
87 certMatchesPinset
= false;
89 MOZ_LOG(gPublicKeyPinningLog
, LogLevel::Debug
,
90 ("pkpin: No hashes found\n"));
91 return NS_ERROR_INVALID_ARG
;
94 nsAutoCString base64Out
;
95 nsresult rv
= GetBase64HashSPKI(cert
, base64Out
);
97 MOZ_LOG(gPublicKeyPinningLog
, LogLevel::Debug
,
98 ("pkpin: GetBase64HashSPKI failed!\n"));
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;
116 * Sets certListIntersectsPinset to true if a given chain matches any
117 * fingerprints from the given static fingerprints and false otherwise.
119 static nsresult
EvalChain(const nsTArray
<Span
<const uint8_t>>& derCertList
,
120 const StaticFingerprints
* fingerprints
,
121 /*out*/ bool& certListIntersectsPinset
) {
122 certListIntersectsPinset
= false;
124 MOZ_ASSERT(false, "Must pass in at least one type of pinset");
125 return NS_ERROR_FAILURE
;
128 EndEntityOrCA endEntityOrCA
= EndEntityOrCA::MustBeEndEntity
;
129 for (const auto& cert
: derCertList
) {
131 mozilla::pkix::Result rv
= certInput
.Init(cert
.data(), cert
.size());
132 if (rv
!= mozilla::pkix::Result::Success
) {
133 return NS_ERROR_INVALID_ARG
;
135 BackCert
backCert(certInput
, endEntityOrCA
, nullptr);
136 rv
= backCert
.Init();
137 if (rv
!= mozilla::pkix::Result::Success
) {
138 return NS_ERROR_INVALID_ARG
;
141 nsresult nsrv
= EvalCert(backCert
, fingerprints
, certListIntersectsPinset
);
142 if (NS_FAILED(nsrv
)) {
145 if (certListIntersectsPinset
) {
148 endEntityOrCA
= EndEntityOrCA::MustBeCA
;
151 if (!certListIntersectsPinset
) {
152 MOZ_LOG(gPublicKeyPinningLog
, LogLevel::Debug
,
153 ("pkpin: no matches found\n"));
158 class TransportSecurityPreloadBinarySearchComparator
{
160 explicit TransportSecurityPreloadBinarySearchComparator(
161 const char* aTargetHost
)
162 : mTargetHost(aTargetHost
) {}
164 int operator()(const TransportSecurityPreload
& val
) const {
165 return strcmp(mTargetHost
, val
.mHost
);
169 const char* mTargetHost
; // non-owning
173 static Atomic
<bool> sValidatedPinningPreloadList(false);
175 static void ValidatePinningPreloadList() {
176 if (sValidatedPinningPreloadList
) {
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
));
184 sValidatedPinningPreloadList
= true;
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
) {
194 ValidatePinningPreloadList();
196 if (!hostname
|| hostname
[0] == 0) {
197 return NS_ERROR_INVALID_ARG
;
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 std::size(kPublicKeyPinningPreloadList
),
210 TransportSecurityPreloadBinarySearchComparator(evalHost
),
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;
222 MOZ_LOG(gPublicKeyPinningLog
, LogLevel::Debug
,
223 ("pkpin: Didn't find pinset for host: '%s'\n", evalHost
));
226 evalHost
= evalPart
+ 1;
229 if (foundEntry
&& foundEntry
->pinset
) {
230 if (time
> TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime
/
234 staticFingerprints
= foundEntry
;
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
;
253 if (!hostname
|| hostname
[0] == 0) {
254 return NS_ERROR_INVALID_ARG
;
257 const TransportSecurityPreload
* staticFingerprints
= nullptr;
258 nsresult rv
= FindPinningInformation(hostname
, time
, staticFingerprints
);
262 // If we have no pinning information, the certificate chain trivially
263 // validates with respect to pinning.
264 if (!staticFingerprints
) {
265 chainHasValidPins
= true;
268 if (staticFingerprints
) {
269 bool enforceTestModeResult
;
270 rv
= EvalChain(certList
, staticFingerprints
->pinset
, enforceTestModeResult
);
274 chainHasValidPins
= enforceTestModeResult
;
275 if (staticFingerprints
->mTestMode
&& !enforceTestMode
) {
276 chainHasValidPins
= true;
279 if (pinningTelemetryInfo
) {
280 // If and only if a static entry is a Mozilla entry, it has a telemetry
282 if ((staticFingerprints
->mIsMoz
&&
283 staticFingerprints
->mId
== kUnknownId
) ||
284 (!staticFingerprints
->mIsMoz
&&
285 staticFingerprints
->mId
!= kUnknownId
)) {
286 return NS_ERROR_FAILURE
;
289 Telemetry::HistogramID histogram
;
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);
299 histogram
= staticFingerprints
->mTestMode
300 ? Telemetry::CERT_PINNING_TEST_RESULTS
301 : Telemetry::CERT_PINNING_RESULTS
;
302 bucket
= enforceTestModeResult
? 1 : 0;
304 pinningTelemetryInfo
->accumulateResult
= true;
305 pinningTelemetryInfo
->certPinningResultHistogram
= Some(histogram
);
306 pinningTelemetryInfo
->certPinningResultBucket
= bucket
;
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
;
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"));
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;
340 chainHasValidPins
= false;
341 if (certList
.IsEmpty()) {
342 return NS_ERROR_INVALID_ARG
;
344 if (!hostname
|| hostname
[0] == 0) {
345 return NS_ERROR_INVALID_ARG
;
347 nsAutoCString
canonicalizedHostname(CanonicalizeHostname(hostname
));
348 bool enforceTestMode
= pinningMode
== PinningMode::EnforceTestMode
;
349 return CheckPinsForHostname(certList
, canonicalizedHostname
.get(),
350 enforceTestMode
, time
, chainHasValidPins
,
351 pinningTelemetryInfo
);
355 PublicKeyPinningService::HostHasPins(nsIURI
* aURI
, bool* hostHasPins
) {
357 NS_ENSURE_ARG(hostHasPins
);
358 *hostHasPins
= false;
359 PinningMode
pinningMode(GetPinningMode());
360 if (pinningMode
== PinningMode::Disabled
) {
363 nsAutoCString hostname
;
364 nsresult rv
= nsSiteSecurityService::GetHost(aURI
, hostname
);
368 if (nsSiteSecurityService::HostIsIPAddress(hostname
)) {
372 const TransportSecurityPreload
* staticFingerprints
= nullptr;
373 rv
= FindPinningInformation(hostname
.get(), Now(), staticFingerprints
);
377 if (staticFingerprints
) {
378 *hostHasPins
= !staticFingerprints
->mTestMode
||
379 pinningMode
== PinningMode::EnforceTestMode
;
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);
392 return canonicalizedHostname
;