Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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"
6 #include "pkix/nullptr.h"
7 #include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js
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"
20 using namespace mozilla;
21 using namespace mozilla::pkix;
22 using namespace mozilla::psm;
24 #if defined(PR_LOGGING)
25 PRLogModuleInfo* gPublicKeyPinningLog =
26 PR_NewLogModule("PublicKeyPinningService");
27 #endif
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 }
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 }
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 }
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 }
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;
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 }
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 }
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 }
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);
147 return strcmp(keyStr, preloadEntry->mHost);
148 }
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 }
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 }
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 }
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 }
247 ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
248 if (!arena) {
249 return false;
250 }
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 }
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);
287 return hasValidPins;
288 }
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 }