Line data Source code
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/Telemetry.h"
14 : #include "nsDependentString.h"
15 : #include "nsISiteSecurityService.h"
16 : #include "nsServiceManagerUtils.h"
17 : #include "nsSiteSecurityService.h"
18 : #include "pkix/pkixtypes.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 : /**
31 : Computes in the location specified by base64Out the SHA256 digest
32 : of the DER Encoded subject Public Key Info for the given cert
33 : */
34 : static nsresult
35 0 : GetBase64HashSPKI(const CERTCertificate* cert, nsACString& hashSPKIDigest)
36 : {
37 0 : hashSPKIDigest.Truncate();
38 0 : Digest digest;
39 0 : nsresult rv = digest.DigestBuf(SEC_OID_SHA256, cert->derPublicKey.data,
40 0 : cert->derPublicKey.len);
41 0 : if (NS_FAILED(rv)) {
42 0 : return rv;
43 : }
44 0 : return Base64Encode(nsDependentCSubstring(
45 0 : BitwiseCast<char*, unsigned char*>(digest.get().data),
46 0 : digest.get().len),
47 0 : hashSPKIDigest);
48 : }
49 :
50 : /*
51 : * Sets certMatchesPinset to true if a given cert matches any fingerprints from
52 : * the given pinset or the dynamicFingerprints array, or to false otherwise.
53 : */
54 : static nsresult
55 0 : EvalCert(const CERTCertificate* cert, const StaticFingerprints* fingerprints,
56 : const nsTArray<nsCString>* dynamicFingerprints,
57 : /*out*/ bool& certMatchesPinset)
58 : {
59 0 : certMatchesPinset = false;
60 0 : if (!fingerprints && !dynamicFingerprints) {
61 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
62 : ("pkpin: No hashes found\n"));
63 0 : return NS_ERROR_INVALID_ARG;
64 : }
65 :
66 0 : nsAutoCString base64Out;
67 0 : nsresult rv = GetBase64HashSPKI(cert, base64Out);
68 0 : if (NS_FAILED(rv)) {
69 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
70 : ("pkpin: GetBase64HashSPKI failed!\n"));
71 0 : return rv;
72 : }
73 :
74 0 : if (fingerprints) {
75 0 : for (size_t i = 0; i < fingerprints->size; i++) {
76 0 : if (base64Out.Equals(fingerprints->data[i])) {
77 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
78 : ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
79 0 : certMatchesPinset = true;
80 0 : return NS_OK;
81 : }
82 : }
83 : }
84 0 : if (dynamicFingerprints) {
85 0 : for (size_t i = 0; i < dynamicFingerprints->Length(); i++) {
86 0 : if (base64Out.Equals((*dynamicFingerprints)[i])) {
87 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
88 : ("pkpin: found pin base_64 ='%s'\n", base64Out.get()));
89 0 : certMatchesPinset = true;
90 0 : return NS_OK;
91 : }
92 : }
93 : }
94 0 : return NS_OK;
95 : }
96 :
97 : /*
98 : * Sets certListIntersectsPinset to true if a given chain matches any
99 : * fingerprints from the given static fingerprints or the
100 : * dynamicFingerprints array, or to false otherwise.
101 : */
102 : static nsresult
103 0 : EvalChain(const UniqueCERTCertList& certList,
104 : const StaticFingerprints* fingerprints,
105 : const nsTArray<nsCString>* dynamicFingerprints,
106 : /*out*/ bool& certListIntersectsPinset)
107 : {
108 0 : certListIntersectsPinset = false;
109 : CERTCertificate* currentCert;
110 :
111 0 : if (!fingerprints && !dynamicFingerprints) {
112 0 : MOZ_ASSERT(false, "Must pass in at least one type of pinset");
113 : return NS_ERROR_FAILURE;
114 : }
115 :
116 : CERTCertListNode* node;
117 0 : for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList);
118 0 : node = CERT_LIST_NEXT(node)) {
119 0 : currentCert = node->cert;
120 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
121 : ("pkpin: certArray subject: '%s'\n", currentCert->subjectName));
122 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
123 : ("pkpin: certArray issuer: '%s'\n", currentCert->issuerName));
124 : nsresult rv = EvalCert(currentCert, fingerprints, dynamicFingerprints,
125 0 : certListIntersectsPinset);
126 0 : if (NS_FAILED(rv)) {
127 0 : return rv;
128 : }
129 0 : if (certListIntersectsPinset) {
130 0 : return NS_OK;
131 : }
132 : }
133 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug, ("pkpin: no matches found\n"));
134 0 : return NS_OK;
135 : }
136 :
137 : class TransportSecurityPreloadBinarySearchComparator
138 : {
139 : public:
140 0 : explicit TransportSecurityPreloadBinarySearchComparator(
141 : const char* aTargetHost)
142 0 : : mTargetHost(aTargetHost) { }
143 :
144 0 : int operator()(const TransportSecurityPreload& val) const
145 : {
146 0 : return strcmp(mTargetHost, val.mHost);
147 : }
148 :
149 : private:
150 : const char* mTargetHost; // non-owning
151 : };
152 :
153 : nsresult
154 0 : PublicKeyPinningService::ChainMatchesPinset(const UniqueCERTCertList& certList,
155 : const nsTArray<nsCString>& aSHA256keys,
156 : /*out*/ bool& chainMatchesPinset)
157 : {
158 0 : return EvalChain(certList, nullptr, &aSHA256keys, chainMatchesPinset);
159 : }
160 :
161 : // Returns via one of the output parameters the most relevant pinning
162 : // information that is valid for the given host at the given time.
163 : // Dynamic pins are prioritized over static pins.
164 : static nsresult
165 0 : FindPinningInformation(const char* hostname, mozilla::pkix::Time time,
166 : const OriginAttributes& originAttributes,
167 : /*out*/ nsTArray<nsCString>& dynamicFingerprints,
168 : /*out*/ const TransportSecurityPreload*& staticFingerprints)
169 : {
170 0 : if (!hostname || hostname[0] == 0) {
171 0 : return NS_ERROR_INVALID_ARG;
172 : }
173 0 : staticFingerprints = nullptr;
174 0 : dynamicFingerprints.Clear();
175 : nsCOMPtr<nsISiteSecurityService> sssService =
176 0 : do_GetService(NS_SSSERVICE_CONTRACTID);
177 0 : if (!sssService) {
178 0 : return NS_ERROR_FAILURE;
179 : }
180 0 : const TransportSecurityPreload* foundEntry = nullptr;
181 0 : const char* evalHost = hostname;
182 : const char* evalPart;
183 : // Notice how the (xx = strchr) prevents pins for unqualified domain names.
184 0 : while (!foundEntry && (evalPart = strchr(evalHost, '.'))) {
185 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
186 : ("pkpin: Querying pinsets for host: '%s'\n", evalHost));
187 : // Attempt dynamic pins first
188 : nsresult rv;
189 : bool found;
190 : bool includeSubdomains;
191 0 : nsTArray<nsCString> pinArray;
192 0 : rv = sssService->GetKeyPinsForHostname(nsDependentCString(evalHost), time,
193 : originAttributes, pinArray,
194 0 : &includeSubdomains, &found);
195 0 : if (NS_FAILED(rv)) {
196 0 : return rv;
197 : }
198 0 : if (found && (evalHost == hostname || includeSubdomains)) {
199 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
200 : ("pkpin: Found dyn match for host: '%s'\n", evalHost));
201 0 : dynamicFingerprints = pinArray;
202 0 : return NS_OK;
203 : }
204 :
205 : size_t foundEntryIndex;
206 0 : if (BinarySearchIf(kPublicKeyPinningPreloadList, 0,
207 : ArrayLength(kPublicKeyPinningPreloadList),
208 0 : TransportSecurityPreloadBinarySearchComparator(evalHost),
209 : &foundEntryIndex)) {
210 0 : foundEntry = &kPublicKeyPinningPreloadList[foundEntryIndex];
211 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
212 : ("pkpin: Found pinset for host: '%s'\n", evalHost));
213 0 : if (evalHost != hostname) {
214 0 : if (!foundEntry->mIncludeSubdomains) {
215 : // Does not apply to this host, continue iterating
216 0 : foundEntry = nullptr;
217 : }
218 : }
219 : } else {
220 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
221 : ("pkpin: Didn't find pinset for host: '%s'\n", evalHost));
222 : }
223 : // Add one for '.'
224 0 : evalHost = evalPart + 1;
225 : }
226 :
227 0 : if (foundEntry && foundEntry->pinset) {
228 0 : if (time > TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime /
229 : PR_USEC_PER_SEC)) {
230 0 : return NS_OK;
231 : }
232 0 : staticFingerprints = foundEntry;
233 : }
234 0 : 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
243 0 : CheckPinsForHostname(const UniqueCERTCertList& certList, const char* hostname,
244 : bool enforceTestMode, mozilla::pkix::Time time,
245 : const OriginAttributes& originAttributes,
246 : /*out*/ bool& chainHasValidPins,
247 : /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo)
248 : {
249 0 : chainHasValidPins = false;
250 0 : if (!certList) {
251 0 : return NS_ERROR_INVALID_ARG;
252 : }
253 0 : if (!hostname || hostname[0] == 0) {
254 0 : return NS_ERROR_INVALID_ARG;
255 : }
256 :
257 0 : nsTArray<nsCString> dynamicFingerprints;
258 0 : const TransportSecurityPreload* staticFingerprints = nullptr;
259 : nsresult rv = FindPinningInformation(hostname, time, originAttributes,
260 0 : dynamicFingerprints, staticFingerprints);
261 : // If we have no pinning information, the certificate chain trivially
262 : // validates with respect to pinning.
263 0 : if (dynamicFingerprints.Length() == 0 && !staticFingerprints) {
264 0 : chainHasValidPins = true;
265 0 : return NS_OK;
266 : }
267 0 : if (dynamicFingerprints.Length() > 0) {
268 0 : return EvalChain(certList, nullptr, &dynamicFingerprints, chainHasValidPins);
269 : }
270 0 : if (staticFingerprints) {
271 : bool enforceTestModeResult;
272 0 : rv = EvalChain(certList, staticFingerprints->pinset, nullptr,
273 0 : enforceTestModeResult);
274 0 : if (NS_FAILED(rv)) {
275 0 : return rv;
276 : }
277 0 : chainHasValidPins = enforceTestModeResult;
278 0 : Telemetry::HistogramID histogram = staticFingerprints->mIsMoz
279 0 : ? Telemetry::CERT_PINNING_MOZ_RESULTS
280 0 : : Telemetry::CERT_PINNING_RESULTS;
281 0 : if (staticFingerprints->mTestMode) {
282 0 : histogram = staticFingerprints->mIsMoz
283 0 : ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS
284 : : Telemetry::CERT_PINNING_TEST_RESULTS;
285 0 : if (!enforceTestMode) {
286 0 : chainHasValidPins = true;
287 : }
288 : }
289 : // We can collect per-host pinning violations for this host because it is
290 : // operationally critical to Firefox.
291 0 : if (pinningTelemetryInfo) {
292 0 : if (staticFingerprints->mId != kUnknownId) {
293 0 : int32_t bucket = staticFingerprints->mId * 2
294 0 : + (enforceTestModeResult ? 1 : 0);
295 0 : histogram = staticFingerprints->mTestMode
296 0 : ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST
297 : : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST;
298 0 : pinningTelemetryInfo->certPinningResultBucket = bucket;
299 : } else {
300 0 : pinningTelemetryInfo->certPinningResultBucket =
301 0 : enforceTestModeResult ? 1 : 0;
302 : }
303 0 : pinningTelemetryInfo->accumulateResult = true;
304 0 : pinningTelemetryInfo->certPinningResultHistogram = histogram;
305 : }
306 :
307 : // We only collect per-CA pinning statistics upon failures.
308 0 : CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
309 : // Only log telemetry if the certificate list is non-empty.
310 0 : if (!CERT_LIST_END(rootNode, certList)) {
311 0 : if (!enforceTestModeResult && pinningTelemetryInfo) {
312 0 : int32_t binNumber = RootCABinNumber(&rootNode->cert->derCert);
313 0 : if (binNumber != ROOT_CERTIFICATE_UNKNOWN ) {
314 0 : pinningTelemetryInfo->accumulateForRoot = true;
315 0 : pinningTelemetryInfo->rootBucket = binNumber;
316 : }
317 : }
318 : }
319 :
320 0 : MOZ_LOG(gPublicKeyPinningLog, LogLevel::Debug,
321 : ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n",
322 : enforceTestModeResult ? "passed" : "failed",
323 : staticFingerprints->mIsMoz ? "mozilla" : "non-mozilla",
324 : hostname, staticFingerprints->mTestMode ? "test" : "production"));
325 : }
326 :
327 0 : return NS_OK;
328 : }
329 :
330 : nsresult
331 0 : PublicKeyPinningService::ChainHasValidPins(
332 : const UniqueCERTCertList& certList,
333 : const char* hostname,
334 : mozilla::pkix::Time time,
335 : bool enforceTestMode,
336 : const OriginAttributes& originAttributes,
337 : /*out*/ bool& chainHasValidPins,
338 : /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo)
339 : {
340 0 : chainHasValidPins = false;
341 0 : if (!certList) {
342 0 : return NS_ERROR_INVALID_ARG;
343 : }
344 0 : if (!hostname || hostname[0] == 0) {
345 0 : return NS_ERROR_INVALID_ARG;
346 : }
347 0 : nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
348 0 : return CheckPinsForHostname(certList, canonicalizedHostname.get(),
349 : enforceTestMode, time, originAttributes,
350 0 : chainHasValidPins, pinningTelemetryInfo);
351 : }
352 :
353 : nsresult
354 0 : PublicKeyPinningService::HostHasPins(const char* hostname,
355 : mozilla::pkix::Time time,
356 : bool enforceTestMode,
357 : const OriginAttributes& originAttributes,
358 : /*out*/ bool& hostHasPins)
359 : {
360 0 : hostHasPins = false;
361 0 : nsAutoCString canonicalizedHostname(CanonicalizeHostname(hostname));
362 0 : nsTArray<nsCString> dynamicFingerprints;
363 0 : const TransportSecurityPreload* staticFingerprints = nullptr;
364 0 : nsresult rv = FindPinningInformation(canonicalizedHostname.get(), time,
365 : originAttributes, dynamicFingerprints,
366 0 : staticFingerprints);
367 0 : if (NS_FAILED(rv)) {
368 0 : return rv;
369 : }
370 0 : if (dynamicFingerprints.Length() > 0) {
371 0 : hostHasPins = true;
372 0 : } else if (staticFingerprints) {
373 0 : hostHasPins = !staticFingerprints->mTestMode || enforceTestMode;
374 : }
375 0 : return NS_OK;
376 : }
377 :
378 : nsAutoCString
379 17 : PublicKeyPinningService::CanonicalizeHostname(const char* hostname)
380 : {
381 17 : nsAutoCString canonicalizedHostname(hostname);
382 17 : ToLowerCase(canonicalizedHostname);
383 34 : while (canonicalizedHostname.Length() > 0 &&
384 17 : canonicalizedHostname.Last() == '.') {
385 0 : canonicalizedHostname.Truncate(canonicalizedHostname.Length() - 1);
386 : }
387 17 : return canonicalizedHostname;
388 : }
|