|
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 #include "pkix/nullptr.h" |
|
7 #include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js |
|
8 |
|
9 #include "cert.h" |
|
10 #include "mozilla/Base64.h" |
|
11 #include "mozilla/Telemetry.h" |
|
12 #include "nsString.h" |
|
13 #include "nssb64.h" |
|
14 #include "pkix/pkixtypes.h" |
|
15 #include "prlog.h" |
|
16 #include "ScopedNSSTypes.h" |
|
17 #include "seccomon.h" |
|
18 #include "sechash.h" |
|
19 |
|
20 using namespace mozilla; |
|
21 using namespace mozilla::pkix; |
|
22 using namespace mozilla::psm; |
|
23 |
|
24 #if defined(PR_LOGGING) |
|
25 PRLogModuleInfo* gPublicKeyPinningLog = |
|
26 PR_NewLogModule("PublicKeyPinningService"); |
|
27 #endif |
|
28 |
|
29 /** |
|
30 Computes in the location specified by base64Out the SHA256 digest |
|
31 of the DER Encoded subject Public Key Info for the given cert |
|
32 */ |
|
33 static SECStatus |
|
34 GetBase64HashSPKI(const CERTCertificate* cert, SECOidTag hashType, |
|
35 nsACString& hashSPKIDigest) |
|
36 { |
|
37 hashSPKIDigest.Truncate(); |
|
38 Digest digest; |
|
39 nsresult rv = digest.DigestBuf(hashType, cert->derPublicKey.data, |
|
40 cert->derPublicKey.len); |
|
41 if (NS_WARN_IF(NS_FAILED(rv))) { |
|
42 return SECFailure; |
|
43 } |
|
44 rv = Base64Encode(nsDependentCSubstring( |
|
45 reinterpret_cast<const char*>(digest.get().data), |
|
46 digest.get().len), |
|
47 hashSPKIDigest); |
|
48 if (NS_WARN_IF(NS_FAILED(rv))) { |
|
49 return SECFailure; |
|
50 } |
|
51 return SECSuccess; |
|
52 } |
|
53 |
|
54 /* |
|
55 * Returns true if a given cert matches any hashType fingerprints from the |
|
56 * given pinset, false otherwise. |
|
57 */ |
|
58 static bool |
|
59 EvalCertWithHashType(const CERTCertificate* cert, SECOidTag hashType, |
|
60 const StaticFingerprints* fingerprints) |
|
61 { |
|
62 if (!fingerprints) { |
|
63 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
64 ("pkpin: No hashes found for hash type: %d\n", hashType)); |
|
65 return false; |
|
66 } |
|
67 |
|
68 nsAutoCString base64Out; |
|
69 SECStatus srv = GetBase64HashSPKI(cert, hashType, base64Out); |
|
70 if (srv != SECSuccess) { |
|
71 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
72 ("pkpin: GetBase64HashSPKI failed!\n")); |
|
73 return false; |
|
74 } |
|
75 |
|
76 for (size_t i = 0; i < fingerprints->size; i++) { |
|
77 if (base64Out.Equals(fingerprints->data[i])) { |
|
78 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
79 ("pkpin: found pin base_64 ='%s'\n", base64Out.get())); |
|
80 return true; |
|
81 } |
|
82 } |
|
83 return false; |
|
84 } |
|
85 |
|
86 /* |
|
87 * Returns true if a given chain matches any hashType fingerprints from the |
|
88 * given pinset, false otherwise. |
|
89 */ |
|
90 static bool |
|
91 EvalChainWithHashType(const CERTCertList* certList, SECOidTag hashType, |
|
92 const StaticPinset* pinset) |
|
93 { |
|
94 CERTCertificate* currentCert; |
|
95 |
|
96 const StaticFingerprints* fingerprints = nullptr; |
|
97 if (hashType == SEC_OID_SHA256) { |
|
98 fingerprints = pinset->sha256; |
|
99 } else if (hashType == SEC_OID_SHA1) { |
|
100 fingerprints = pinset->sha1; |
|
101 } |
|
102 if (!fingerprints) { |
|
103 return false; |
|
104 } |
|
105 |
|
106 CERTCertListNode* node; |
|
107 for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList); |
|
108 node = CERT_LIST_NEXT(node)) { |
|
109 currentCert = node->cert; |
|
110 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
111 ("pkpin: certArray subject: '%s'\n", |
|
112 currentCert->subjectName)); |
|
113 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
114 ("pkpin: certArray common_name: '%s'\n", |
|
115 CERT_GetCommonName(&(currentCert->issuer)))); |
|
116 if (EvalCertWithHashType(currentCert, hashType, fingerprints)) { |
|
117 return true; |
|
118 } |
|
119 } |
|
120 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: no matches found\n")); |
|
121 return false; |
|
122 } |
|
123 |
|
124 /** |
|
125 * Given a pinset and certlist, return true if one of the certificates on |
|
126 * the list matches a fingerprint in the pinset, false otherwise. |
|
127 */ |
|
128 static bool |
|
129 EvalChainWithPinset(const CERTCertList* certList, |
|
130 const StaticPinset* pinset) { |
|
131 // SHA256 is more trustworthy, try that first. |
|
132 if (EvalChainWithHashType(certList, SEC_OID_SHA256, pinset)) { |
|
133 return true; |
|
134 } |
|
135 return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset); |
|
136 } |
|
137 |
|
138 /** |
|
139 Comparator for the is public key pinned host. |
|
140 */ |
|
141 static int |
|
142 TransportSecurityPreloadCompare(const void *key, const void *entry) { |
|
143 const char *keyStr = reinterpret_cast<const char *>(key); |
|
144 const TransportSecurityPreload *preloadEntry = |
|
145 reinterpret_cast<const TransportSecurityPreload *>(entry); |
|
146 |
|
147 return strcmp(keyStr, preloadEntry->mHost); |
|
148 } |
|
149 |
|
150 /** |
|
151 * Check PKPins on the given certlist against the specified hostname |
|
152 */ |
|
153 static bool |
|
154 CheckPinsForHostname(const CERTCertList *certList, const char *hostname, |
|
155 bool enforceTestMode) |
|
156 { |
|
157 if (!certList) { |
|
158 return false; |
|
159 } |
|
160 if (!hostname || hostname[0] == 0) { |
|
161 return false; |
|
162 } |
|
163 |
|
164 TransportSecurityPreload *foundEntry = nullptr; |
|
165 char *evalHost = const_cast<char*>(hostname); |
|
166 char *evalPart; |
|
167 // Notice how the (xx = strchr) prevents pins for unqualified domain names. |
|
168 while (!foundEntry && (evalPart = strchr(evalHost, '.'))) { |
|
169 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
170 ("pkpin: Querying pinsets for host: '%s'\n", evalHost)); |
|
171 foundEntry = (TransportSecurityPreload *)bsearch(evalHost, |
|
172 kPublicKeyPinningPreloadList, |
|
173 sizeof(kPublicKeyPinningPreloadList) / sizeof(TransportSecurityPreload), |
|
174 sizeof(TransportSecurityPreload), |
|
175 TransportSecurityPreloadCompare); |
|
176 if (foundEntry) { |
|
177 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
178 ("pkpin: Found pinset for host: '%s'\n", evalHost)); |
|
179 if (evalHost != hostname) { |
|
180 if (!foundEntry->mIncludeSubdomains) { |
|
181 // Does not apply to this host, continue iterating |
|
182 foundEntry = nullptr; |
|
183 } |
|
184 } |
|
185 } else { |
|
186 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
187 ("pkpin: Didn't find pinset for host: '%s'\n", evalHost)); |
|
188 } |
|
189 // Add one for '.' |
|
190 evalHost = evalPart + 1; |
|
191 } |
|
192 |
|
193 if (foundEntry && foundEntry->pinset) { |
|
194 bool result = EvalChainWithPinset(certList, foundEntry->pinset); |
|
195 bool retval = result; |
|
196 Telemetry::ID histogram = foundEntry->mIsMoz |
|
197 ? Telemetry::CERT_PINNING_MOZ_RESULTS |
|
198 : Telemetry::CERT_PINNING_RESULTS; |
|
199 if (foundEntry->mTestMode) { |
|
200 histogram = foundEntry->mIsMoz |
|
201 ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS |
|
202 : Telemetry::CERT_PINNING_TEST_RESULTS; |
|
203 if (!enforceTestMode) { |
|
204 retval = true; |
|
205 } |
|
206 } |
|
207 // We can collect per-host pinning violations for this host because it is |
|
208 // operationally critical to Firefox. |
|
209 if (foundEntry->mId != kUnknownId) { |
|
210 int32_t bucket = foundEntry->mId * 2 + (result ? 1 : 0); |
|
211 histogram = foundEntry->mTestMode |
|
212 ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST |
|
213 : Telemetry::CERT_PINNING_MOZ_RESULTS_BY_HOST; |
|
214 Telemetry::Accumulate(histogram, bucket); |
|
215 } else { |
|
216 Telemetry::Accumulate(histogram, result ? 1 : 0); |
|
217 } |
|
218 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
219 ("pkpin: Pin check %s for %s host '%s' (mode=%s)\n", |
|
220 result ? "passed" : "failed", |
|
221 foundEntry->mIsMoz ? "mozilla" : "non-mozilla", |
|
222 hostname, foundEntry->mTestMode ? "test" : "production")); |
|
223 return retval; |
|
224 } |
|
225 return true; // No pinning information for this hostname |
|
226 } |
|
227 |
|
228 /** |
|
229 * Extract all the DNS names for a host (including CN) and evaluate the |
|
230 * certifiate pins against all of them (Currently is an OR so we stop |
|
231 * evaluating at the first OK pin). |
|
232 */ |
|
233 static bool |
|
234 CheckChainAgainstAllNames(const CERTCertList* certList, bool enforceTestMode) |
|
235 { |
|
236 PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, |
|
237 ("pkpin: top of checkChainAgainstAllNames")); |
|
238 CERTCertListNode* node = CERT_LIST_HEAD(certList); |
|
239 if (!node) { |
|
240 return false; |
|
241 } |
|
242 CERTCertificate* cert = node->cert; |
|
243 if (!cert) { |
|
244 return false; |
|
245 } |
|
246 |
|
247 ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); |
|
248 if (!arena) { |
|
249 return false; |
|
250 } |
|
251 |
|
252 bool hasValidPins = false; |
|
253 CERTGeneralName* nameList; |
|
254 CERTGeneralName* currentName; |
|
255 nameList = CERT_GetConstrainedCertificateNames(cert, arena.get(), PR_TRUE); |
|
256 if (!nameList) { |
|
257 return false; |
|
258 } |
|
259 |
|
260 currentName = nameList; |
|
261 do { |
|
262 if (currentName->type == certDNSName |
|
263 && currentName->name.other.data[0] != 0) { |
|
264 // no need to cleaup, as the arena cleanup will do |
|
265 char *hostName = (char *)PORT_ArenaAlloc(arena.get(), |
|
266 currentName->name.other.len + 1); |
|
267 if (!hostName) { |
|
268 break; |
|
269 } |
|
270 // We use a temporary buffer as the hostname as returned might not be |
|
271 // null terminated. |
|
272 hostName[currentName->name.other.len] = 0; |
|
273 memcpy(hostName, currentName->name.other.data, |
|
274 currentName->name.other.len); |
|
275 if (!hostName[0]) { |
|
276 // cannot call CheckPinsForHostname on empty or null hostname |
|
277 break; |
|
278 } |
|
279 if (CheckPinsForHostname(certList, hostName, enforceTestMode)) { |
|
280 hasValidPins = true; |
|
281 break; |
|
282 } |
|
283 } |
|
284 currentName = CERT_GetNextGeneralName(currentName); |
|
285 } while (currentName != nameList); |
|
286 |
|
287 return hasValidPins; |
|
288 } |
|
289 |
|
290 bool |
|
291 PublicKeyPinningService::ChainHasValidPins(const CERTCertList* certList, |
|
292 const char* hostname, |
|
293 const PRTime time, |
|
294 bool enforceTestMode) |
|
295 { |
|
296 if (!certList) { |
|
297 return false; |
|
298 } |
|
299 if (time > kPreloadPKPinsExpirationTime) { |
|
300 return true; |
|
301 } |
|
302 if (!hostname || hostname[0] == 0) { |
|
303 return CheckChainAgainstAllNames(certList, enforceTestMode); |
|
304 } |
|
305 return CheckPinsForHostname(certList, hostname, enforceTestMode); |
|
306 } |