/src/mozilla-central/security/certverifier/OCSPCache.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
3 | | /* This code is made available to you under your choice of the following sets |
4 | | * of licensing terms: |
5 | | */ |
6 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
7 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
8 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
9 | | */ |
10 | | /* Copyright 2013 Mozilla Contributors |
11 | | * |
12 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
13 | | * you may not use this file except in compliance with the License. |
14 | | * You may obtain a copy of the License at |
15 | | * |
16 | | * http://www.apache.org/licenses/LICENSE-2.0 |
17 | | * |
18 | | * Unless required by applicable law or agreed to in writing, software |
19 | | * distributed under the License is distributed on an "AS IS" BASIS, |
20 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
21 | | * See the License for the specific language governing permissions and |
22 | | * limitations under the License. |
23 | | */ |
24 | | |
25 | | #include "OCSPCache.h" |
26 | | |
27 | | #include <limits> |
28 | | |
29 | | #include "NSSCertDBTrustDomain.h" |
30 | | #include "pk11pub.h" |
31 | | #include "pkix/pkixnss.h" |
32 | | #include "ScopedNSSTypes.h" |
33 | | #include "secerr.h" |
34 | | |
35 | | extern mozilla::LazyLogModule gCertVerifierLog; |
36 | | |
37 | | using namespace mozilla::pkix; |
38 | | |
39 | | namespace mozilla { namespace psm { |
40 | | |
41 | | typedef mozilla::pkix::Result Result; |
42 | | |
43 | | static SECStatus |
44 | | DigestLength(UniquePK11Context& context, uint32_t length) |
45 | 0 | { |
46 | 0 | // Restrict length to 2 bytes because it should be big enough for all |
47 | 0 | // inputs this code will actually see and that it is well-defined and |
48 | 0 | // type-size-independent. |
49 | 0 | if (length >= 65536) { |
50 | 0 | return SECFailure; |
51 | 0 | } |
52 | 0 | unsigned char array[2]; |
53 | 0 | array[0] = length & 255; |
54 | 0 | array[1] = (length >> 8) & 255; |
55 | 0 |
|
56 | 0 | return PK11_DigestOp(context.get(), array, MOZ_ARRAY_LENGTH(array)); |
57 | 0 | } |
58 | | |
59 | | // Let derIssuer be the DER encoding of the issuer of certID. |
60 | | // Let derPublicKey be the DER encoding of the public key of certID. |
61 | | // Let serialNumber be the bytes of the serial number of certID. |
62 | | // Let serialNumberLen be the number of bytes of serialNumber. |
63 | | // Let firstPartyDomain be the first party domain of originAttributes. |
64 | | // It is only non-empty when "privacy.firstParty.isolate" is enabled, in order |
65 | | // to isolate OCSP cache by first party. |
66 | | // Let firstPartyDomainLen be the number of bytes of firstPartyDomain. |
67 | | // The value calculated is SHA384(derIssuer || derPublicKey || serialNumberLen |
68 | | // || serialNumber || firstPartyDomainLen || firstPartyDomain). |
69 | | // Because the DER encodings include the length of the data encoded, and we also |
70 | | // include the length of serialNumber and originAttributes, there do not exist |
71 | | // A(derIssuerA, derPublicKeyA, serialNumberLenA, serialNumberA, |
72 | | // originAttributesLenA, originAttributesA) and B(derIssuerB, derPublicKeyB, |
73 | | // serialNumberLenB, serialNumberB, originAttributesLenB, originAttributesB) |
74 | | // such that the concatenation of each tuple results in the same string of |
75 | | // bytes but where each part in A is not equal to its counterpart in B. This is |
76 | | // important because as a result it is computationally infeasible to find |
77 | | // collisions that would subvert this cache (given that SHA384 is a |
78 | | // cryptographically-secure hash function). |
79 | | static SECStatus |
80 | | CertIDHash(SHA384Buffer& buf, const CertID& certID, |
81 | | const OriginAttributes& originAttributes) |
82 | 0 | { |
83 | 0 | UniquePK11Context context(PK11_CreateDigestContext(SEC_OID_SHA384)); |
84 | 0 | if (!context) { |
85 | 0 | return SECFailure; |
86 | 0 | } |
87 | 0 | SECStatus rv = PK11_DigestBegin(context.get()); |
88 | 0 | if (rv != SECSuccess) { |
89 | 0 | return rv; |
90 | 0 | } |
91 | 0 | SECItem certIDIssuer = UnsafeMapInputToSECItem(certID.issuer); |
92 | 0 | rv = PK11_DigestOp(context.get(), certIDIssuer.data, certIDIssuer.len); |
93 | 0 | if (rv != SECSuccess) { |
94 | 0 | return rv; |
95 | 0 | } |
96 | 0 | SECItem certIDIssuerSubjectPublicKeyInfo = |
97 | 0 | UnsafeMapInputToSECItem(certID.issuerSubjectPublicKeyInfo); |
98 | 0 | rv = PK11_DigestOp(context.get(), certIDIssuerSubjectPublicKeyInfo.data, |
99 | 0 | certIDIssuerSubjectPublicKeyInfo.len); |
100 | 0 | if (rv != SECSuccess) { |
101 | 0 | return rv; |
102 | 0 | } |
103 | 0 | SECItem certIDSerialNumber = |
104 | 0 | UnsafeMapInputToSECItem(certID.serialNumber); |
105 | 0 | rv = DigestLength(context, certIDSerialNumber.len); |
106 | 0 | if (rv != SECSuccess) { |
107 | 0 | return rv; |
108 | 0 | } |
109 | 0 | rv = PK11_DigestOp(context.get(), certIDSerialNumber.data, |
110 | 0 | certIDSerialNumber.len); |
111 | 0 | if (rv != SECSuccess) { |
112 | 0 | return rv; |
113 | 0 | } |
114 | 0 | |
115 | 0 | // OCSP should not be isolated by containers. |
116 | 0 | NS_ConvertUTF16toUTF8 firstPartyDomain(originAttributes.mFirstPartyDomain); |
117 | 0 | if (!firstPartyDomain.IsEmpty()) { |
118 | 0 | rv = DigestLength(context, firstPartyDomain.Length()); |
119 | 0 | if (rv != SECSuccess) { |
120 | 0 | return rv; |
121 | 0 | } |
122 | 0 | rv = PK11_DigestOp(context.get(), |
123 | 0 | BitwiseCast<const unsigned char*>(firstPartyDomain.get()), |
124 | 0 | firstPartyDomain.Length()); |
125 | 0 | if (rv != SECSuccess) { |
126 | 0 | return rv; |
127 | 0 | } |
128 | 0 | } |
129 | 0 | uint32_t outLen = 0; |
130 | 0 | rv = PK11_DigestFinal(context.get(), buf, &outLen, SHA384_LENGTH); |
131 | 0 | if (outLen != SHA384_LENGTH) { |
132 | 0 | return SECFailure; |
133 | 0 | } |
134 | 0 | return rv; |
135 | 0 | } |
136 | | |
137 | | Result |
138 | | OCSPCache::Entry::Init(const CertID& aCertID, |
139 | | const OriginAttributes& aOriginAttributes) |
140 | 0 | { |
141 | 0 | SECStatus srv = CertIDHash(mIDHash, aCertID, aOriginAttributes); |
142 | 0 | if (srv != SECSuccess) { |
143 | 0 | return MapPRErrorCodeToResult(PR_GetError()); |
144 | 0 | } |
145 | 0 | return Success; |
146 | 0 | } |
147 | | |
148 | | OCSPCache::OCSPCache() |
149 | | : mMutex("OCSPCache-mutex") |
150 | 0 | { |
151 | 0 | } |
152 | | |
153 | | OCSPCache::~OCSPCache() |
154 | 0 | { |
155 | 0 | Clear(); |
156 | 0 | } |
157 | | |
158 | | // Returns false with index in an undefined state if no matching entry was |
159 | | // found. |
160 | | bool |
161 | | OCSPCache::FindInternal(const CertID& aCertID, |
162 | | const OriginAttributes& aOriginAttributes, |
163 | | /*out*/ size_t& index, |
164 | | const MutexAutoLock& /* aProofOfLock */) |
165 | 0 | { |
166 | 0 | if (mEntries.length() == 0) { |
167 | 0 | return false; |
168 | 0 | } |
169 | 0 | |
170 | 0 | SHA384Buffer idHash; |
171 | 0 | SECStatus rv = CertIDHash(idHash, aCertID, aOriginAttributes); |
172 | 0 | if (rv != SECSuccess) { |
173 | 0 | return false; |
174 | 0 | } |
175 | 0 | |
176 | 0 | // mEntries is sorted with the most-recently-used entry at the end. |
177 | 0 | // Thus, searching from the end will often be fastest. |
178 | 0 | index = mEntries.length(); |
179 | 0 | while (index > 0) { |
180 | 0 | --index; |
181 | 0 | if (memcmp(mEntries[index]->mIDHash, idHash, SHA384_LENGTH) == 0) { |
182 | 0 | return true; |
183 | 0 | } |
184 | 0 | } |
185 | 0 | return false; |
186 | 0 | } |
187 | | |
188 | | static inline void |
189 | | LogWithCertID(const char* aMessage, const CertID& aCertID, |
190 | | const OriginAttributes& aOriginAttributes) |
191 | 0 | { |
192 | 0 | NS_ConvertUTF16toUTF8 firstPartyDomain(aOriginAttributes.mFirstPartyDomain); |
193 | 0 | MOZ_LOG(gCertVerifierLog, LogLevel::Debug, |
194 | 0 | (aMessage, &aCertID, firstPartyDomain.get())); |
195 | 0 | } |
196 | | |
197 | | void |
198 | | OCSPCache::MakeMostRecentlyUsed(size_t aIndex, |
199 | | const MutexAutoLock& /* aProofOfLock */) |
200 | 0 | { |
201 | 0 | Entry* entry = mEntries[aIndex]; |
202 | 0 | // Since mEntries is sorted with the most-recently-used entry at the end, |
203 | 0 | // aIndex is likely to be near the end, so this is likely to be fast. |
204 | 0 | mEntries.erase(mEntries.begin() + aIndex); |
205 | 0 | // erase() does not shrink or realloc memory, so the append below should |
206 | 0 | // always succeed. |
207 | 0 | MOZ_RELEASE_ASSERT(mEntries.append(entry)); |
208 | 0 | } |
209 | | |
210 | | bool |
211 | | OCSPCache::Get(const CertID& aCertID, |
212 | | const OriginAttributes& aOriginAttributes, |
213 | | Result& aResult, Time& aValidThrough) |
214 | 0 | { |
215 | 0 | MutexAutoLock lock(mMutex); |
216 | 0 |
|
217 | 0 | size_t index; |
218 | 0 | if (!FindInternal(aCertID, aOriginAttributes, index, lock)) { |
219 | 0 | LogWithCertID("OCSPCache::Get(%p,\"%s\") not in cache", aCertID, |
220 | 0 | aOriginAttributes); |
221 | 0 | return false; |
222 | 0 | } |
223 | 0 | LogWithCertID("OCSPCache::Get(%p,\"%s\") in cache", aCertID, |
224 | 0 | aOriginAttributes); |
225 | 0 | aResult = mEntries[index]->mResult; |
226 | 0 | aValidThrough = mEntries[index]->mValidThrough; |
227 | 0 | MakeMostRecentlyUsed(index, lock); |
228 | 0 | return true; |
229 | 0 | } |
230 | | |
231 | | Result |
232 | | OCSPCache::Put(const CertID& aCertID, |
233 | | const OriginAttributes& aOriginAttributes, |
234 | | Result aResult, Time aThisUpdate, Time aValidThrough) |
235 | 0 | { |
236 | 0 | MutexAutoLock lock(mMutex); |
237 | 0 |
|
238 | 0 | size_t index; |
239 | 0 | if (FindInternal(aCertID, aOriginAttributes, index, lock)) { |
240 | 0 | // Never replace an entry indicating a revoked certificate. |
241 | 0 | if (mEntries[index]->mResult == Result::ERROR_REVOKED_CERTIFICATE) { |
242 | 0 | LogWithCertID("OCSPCache::Put(%p, \"%s\") already in cache as revoked - " |
243 | 0 | "not replacing", aCertID, aOriginAttributes); |
244 | 0 | MakeMostRecentlyUsed(index, lock); |
245 | 0 | return Success; |
246 | 0 | } |
247 | 0 | |
248 | 0 | // Never replace a newer entry with an older one unless the older entry |
249 | 0 | // indicates a revoked certificate, which we want to remember. |
250 | 0 | if (mEntries[index]->mThisUpdate > aThisUpdate && |
251 | 0 | aResult != Result::ERROR_REVOKED_CERTIFICATE) { |
252 | 0 | LogWithCertID("OCSPCache::Put(%p, \"%s\") already in cache with more " |
253 | 0 | "recent validity - not replacing", aCertID, |
254 | 0 | aOriginAttributes); |
255 | 0 | MakeMostRecentlyUsed(index, lock); |
256 | 0 | return Success; |
257 | 0 | } |
258 | 0 | |
259 | 0 | // Only known good responses or responses indicating an unknown |
260 | 0 | // or revoked certificate should replace previously known responses. |
261 | 0 | if (aResult != Success && |
262 | 0 | aResult != Result::ERROR_OCSP_UNKNOWN_CERT && |
263 | 0 | aResult != Result::ERROR_REVOKED_CERTIFICATE) { |
264 | 0 | LogWithCertID("OCSPCache::Put(%p, \"%s\") already in cache - not " |
265 | 0 | "replacing with less important status", aCertID, |
266 | 0 | aOriginAttributes); |
267 | 0 | MakeMostRecentlyUsed(index, lock); |
268 | 0 | return Success; |
269 | 0 | } |
270 | 0 | |
271 | 0 | LogWithCertID("OCSPCache::Put(%p, \"%s\") already in cache - replacing", |
272 | 0 | aCertID, aOriginAttributes); |
273 | 0 | mEntries[index]->mResult = aResult; |
274 | 0 | mEntries[index]->mThisUpdate = aThisUpdate; |
275 | 0 | mEntries[index]->mValidThrough = aValidThrough; |
276 | 0 | MakeMostRecentlyUsed(index, lock); |
277 | 0 | return Success; |
278 | 0 | } |
279 | 0 | |
280 | 0 | if (mEntries.length() == MaxEntries) { |
281 | 0 | LogWithCertID("OCSPCache::Put(%p, \"%s\") too full - evicting an entry", |
282 | 0 | aCertID, aOriginAttributes); |
283 | 0 | for (Entry** toEvict = mEntries.begin(); toEvict != mEntries.end(); |
284 | 0 | toEvict++) { |
285 | 0 | // Never evict an entry that indicates a revoked or unknokwn certificate, |
286 | 0 | // because revoked responses are more security-critical to remember. |
287 | 0 | if ((*toEvict)->mResult != Result::ERROR_REVOKED_CERTIFICATE && |
288 | 0 | (*toEvict)->mResult != Result::ERROR_OCSP_UNKNOWN_CERT) { |
289 | 0 | delete *toEvict; |
290 | 0 | mEntries.erase(toEvict); |
291 | 0 | break; |
292 | 0 | } |
293 | 0 | } |
294 | 0 | // Well, we tried, but apparently everything is revoked or unknown. |
295 | 0 | // We don't want to remove a cached revoked or unknown response. If we're |
296 | 0 | // trying to insert a good response, we can just return "successfully" |
297 | 0 | // without doing so. This means we'll lose some speed, but it's not a |
298 | 0 | // security issue. If we're trying to insert a revoked or unknown response, |
299 | 0 | // we can't. We should return with an error that causes the current |
300 | 0 | // verification to fail. |
301 | 0 | if (mEntries.length() == MaxEntries) { |
302 | 0 | return aResult; |
303 | 0 | } |
304 | 0 | } |
305 | 0 | |
306 | 0 | Entry* newEntry = new (std::nothrow) Entry(aResult, aThisUpdate, |
307 | 0 | aValidThrough); |
308 | 0 | // Normally we don't have to do this in Gecko, because OOM is fatal. |
309 | 0 | // However, if we want to embed this in another project, OOM might not |
310 | 0 | // be fatal, so handle this case. |
311 | 0 | if (!newEntry) { |
312 | 0 | return Result::FATAL_ERROR_NO_MEMORY; |
313 | 0 | } |
314 | 0 | Result rv = newEntry->Init(aCertID, aOriginAttributes); |
315 | 0 | if (rv != Success) { |
316 | 0 | delete newEntry; |
317 | 0 | return rv; |
318 | 0 | } |
319 | 0 | if (!mEntries.append(newEntry)) { |
320 | 0 | delete newEntry; |
321 | 0 | return Result::FATAL_ERROR_NO_MEMORY; |
322 | 0 | } |
323 | 0 | LogWithCertID("OCSPCache::Put(%p, \"%s\") added to cache", aCertID, |
324 | 0 | aOriginAttributes); |
325 | 0 | return Success; |
326 | 0 | } |
327 | | |
328 | | void |
329 | | OCSPCache::Clear() |
330 | 0 | { |
331 | 0 | MutexAutoLock lock(mMutex); |
332 | 0 | MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("OCSPCache::Clear: clearing cache")); |
333 | 0 | // First go through and delete the memory being pointed to by the pointers |
334 | 0 | // in the vector. |
335 | 0 | for (Entry** entry = mEntries.begin(); entry < mEntries.end(); |
336 | 0 | entry++) { |
337 | 0 | delete *entry; |
338 | 0 | } |
339 | 0 | // Then remove the pointers themselves. |
340 | 0 | mEntries.clearAndFree(); |
341 | 0 | } |
342 | | |
343 | | } } // namespace mozilla::psm |