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
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/.
10 /* Copyright 2013 Mozilla Contributors
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
16 * http://www.apache.org/licenses/LICENSE-2.0
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.
25 #include "OCSPCache.h"
29 #include "NSSCertDBTrustDomain.h"
31 #include "mozilla/Logging.h"
32 #include "mozilla/StaticPrefs_privacy.h"
33 #include "mozpkix/pkixnss.h"
34 #include "ScopedNSSTypes.h"
37 extern mozilla::LazyLogModule gCertVerifierLog
;
39 using namespace mozilla::pkix
;
44 typedef mozilla::pkix::Result Result
;
46 static SECStatus
DigestLength(UniquePK11Context
& context
, uint32_t length
) {
47 // Restrict length to 2 bytes because it should be big enough for all
48 // inputs this code will actually see and that it is well-defined and
49 // type-size-independent.
50 if (length
>= 65536) {
53 unsigned char array
[2];
54 array
[0] = length
& 255;
55 array
[1] = (length
>> 8) & 255;
57 return PK11_DigestOp(context
.get(), array
, MOZ_ARRAY_LENGTH(array
));
60 // Let derIssuer be the DER encoding of the issuer of certID.
61 // Let derPublicKey be the DER encoding of the public key of certID.
62 // Let serialNumber be the bytes of the serial number of certID.
63 // Let serialNumberLen be the number of bytes of serialNumber.
64 // Let firstPartyDomain be the first party domain of originAttributes.
65 // It is only non-empty when "privacy.firstParty.isolate" is enabled, in order
66 // to isolate OCSP cache by first party.
67 // Let firstPartyDomainLen be the number of bytes of firstPartyDomain.
68 // Let partitionKey be the partition key of originAttributes.
69 // Let partitionKeyLen be the number of bytes of partitionKey.
70 // The value calculated is SHA384(derIssuer || derPublicKey || serialNumberLen
71 // || serialNumber || firstPartyDomainLen || firstPartyDomain || partitionKeyLen
73 // Because the DER encodings include the length of the data encoded, and we also
74 // include the length of serialNumber and originAttributes, there do not exist
75 // A(derIssuerA, derPublicKeyA, serialNumberLenA, serialNumberA,
76 // originAttributesLenA, originAttributesA) and B(derIssuerB, derPublicKeyB,
77 // serialNumberLenB, serialNumberB, originAttributesLenB, originAttributesB)
78 // such that the concatenation of each tuple results in the same string of
79 // bytes but where each part in A is not equal to its counterpart in B. This is
80 // important because as a result it is computationally infeasible to find
81 // collisions that would subvert this cache (given that SHA384 is a
82 // cryptographically-secure hash function).
83 static SECStatus
CertIDHash(SHA384Buffer
& buf
, const CertID
& certID
,
84 const OriginAttributes
& originAttributes
) {
85 UniquePK11Context
context(PK11_CreateDigestContext(SEC_OID_SHA384
));
89 SECStatus rv
= PK11_DigestBegin(context
.get());
90 if (rv
!= SECSuccess
) {
93 SECItem certIDIssuer
= UnsafeMapInputToSECItem(certID
.issuer
);
94 rv
= PK11_DigestOp(context
.get(), certIDIssuer
.data
, certIDIssuer
.len
);
95 if (rv
!= SECSuccess
) {
98 SECItem certIDIssuerSubjectPublicKeyInfo
=
99 UnsafeMapInputToSECItem(certID
.issuerSubjectPublicKeyInfo
);
100 rv
= PK11_DigestOp(context
.get(), certIDIssuerSubjectPublicKeyInfo
.data
,
101 certIDIssuerSubjectPublicKeyInfo
.len
);
102 if (rv
!= SECSuccess
) {
105 SECItem certIDSerialNumber
= UnsafeMapInputToSECItem(certID
.serialNumber
);
106 rv
= DigestLength(context
, certIDSerialNumber
.len
);
107 if (rv
!= SECSuccess
) {
110 rv
= PK11_DigestOp(context
.get(), certIDSerialNumber
.data
,
111 certIDSerialNumber
.len
);
112 if (rv
!= SECSuccess
) {
116 auto populateOriginAttributesKey
= [&context
](const nsString
& aKey
) {
117 NS_ConvertUTF16toUTF8
key(aKey
);
123 SECStatus rv
= DigestLength(context
, key
.Length());
124 if (rv
!= SECSuccess
) {
128 return PK11_DigestOp(context
.get(),
129 BitwiseCast
<const unsigned char*>(key
.get()),
133 // OCSP should be isolated by firstPartyDomain and partitionKey, but not
135 rv
= populateOriginAttributesKey(originAttributes
.mFirstPartyDomain
);
136 if (rv
!= SECSuccess
) {
140 bool isolateByPartitionKey
=
141 originAttributes
.mPrivateBrowsingId
> 0
142 ? StaticPrefs::privacy_partition_network_state_ocsp_cache_pbmode()
143 : StaticPrefs::privacy_partition_network_state_ocsp_cache();
144 if (isolateByPartitionKey
) {
145 rv
= populateOriginAttributesKey(originAttributes
.mPartitionKey
);
146 if (rv
!= SECSuccess
) {
151 rv
= PK11_DigestFinal(context
.get(), buf
, &outLen
, SHA384_LENGTH
);
152 if (outLen
!= SHA384_LENGTH
) {
158 Result
OCSPCache::Entry::Init(const CertID
& aCertID
,
159 const OriginAttributes
& aOriginAttributes
) {
160 SECStatus srv
= CertIDHash(mIDHash
, aCertID
, aOriginAttributes
);
161 if (srv
!= SECSuccess
) {
162 return MapPRErrorCodeToResult(PR_GetError());
167 OCSPCache::OCSPCache() : mMutex("OCSPCache-mutex") {}
169 OCSPCache::~OCSPCache() { Clear(); }
171 // Returns false with index in an undefined state if no matching entry was
173 bool OCSPCache::FindInternal(const CertID
& aCertID
,
174 const OriginAttributes
& aOriginAttributes
,
175 /*out*/ size_t& index
,
176 const MutexAutoLock
& /* aProofOfLock */) {
177 mMutex
.AssertCurrentThreadOwns();
178 if (mEntries
.length() == 0) {
183 SECStatus rv
= CertIDHash(idHash
, aCertID
, aOriginAttributes
);
184 if (rv
!= SECSuccess
) {
188 // mEntries is sorted with the most-recently-used entry at the end.
189 // Thus, searching from the end will often be fastest.
190 index
= mEntries
.length();
193 if (memcmp(mEntries
[index
]->mIDHash
, idHash
, SHA384_LENGTH
) == 0) {
200 static inline void LogWithCertID(const char* aMessage
, const CertID
& aCertID
,
201 const OriginAttributes
& aOriginAttributes
) {
202 nsAutoString info
= u
"firstPartyDomain: "_ns
+
203 aOriginAttributes
.mFirstPartyDomain
+
204 u
", partitionKey: "_ns
+ aOriginAttributes
.mPartitionKey
;
205 MOZ_LOG(gCertVerifierLog
, LogLevel::Debug
,
206 (aMessage
, &aCertID
, NS_ConvertUTF16toUTF8(info
).get()));
209 void OCSPCache::MakeMostRecentlyUsed(size_t aIndex
,
210 const MutexAutoLock
& /* aProofOfLock */) {
211 mMutex
.AssertCurrentThreadOwns();
212 Entry
* entry
= mEntries
[aIndex
];
213 // Since mEntries is sorted with the most-recently-used entry at the end,
214 // aIndex is likely to be near the end, so this is likely to be fast.
215 mEntries
.erase(mEntries
.begin() + aIndex
);
216 // erase() does not shrink or realloc memory, so the append below should
218 MOZ_RELEASE_ASSERT(mEntries
.append(entry
));
221 bool OCSPCache::Get(const CertID
& aCertID
,
222 const OriginAttributes
& aOriginAttributes
, Result
& aResult
,
223 Time
& aValidThrough
) {
224 MutexAutoLock
lock(mMutex
);
227 if (!FindInternal(aCertID
, aOriginAttributes
, index
, lock
)) {
228 LogWithCertID("OCSPCache::Get(%p,\"%s\") not in cache", aCertID
,
232 LogWithCertID("OCSPCache::Get(%p,\"%s\") in cache", aCertID
,
234 aResult
= mEntries
[index
]->mResult
;
235 aValidThrough
= mEntries
[index
]->mValidThrough
;
236 MakeMostRecentlyUsed(index
, lock
);
240 Result
OCSPCache::Put(const CertID
& aCertID
,
241 const OriginAttributes
& aOriginAttributes
, Result aResult
,
242 Time aThisUpdate
, Time aValidThrough
) {
243 MutexAutoLock
lock(mMutex
);
246 if (FindInternal(aCertID
, aOriginAttributes
, index
, lock
)) {
247 // Never replace an entry indicating a revoked certificate.
248 if (mEntries
[index
]->mResult
== Result::ERROR_REVOKED_CERTIFICATE
) {
250 "OCSPCache::Put(%p, \"%s\") already in cache as revoked - "
252 aCertID
, aOriginAttributes
);
253 MakeMostRecentlyUsed(index
, lock
);
257 // Never replace a newer entry with an older one unless the older entry
258 // indicates a revoked certificate, which we want to remember.
259 if (mEntries
[index
]->mThisUpdate
> aThisUpdate
&&
260 aResult
!= Result::ERROR_REVOKED_CERTIFICATE
) {
262 "OCSPCache::Put(%p, \"%s\") already in cache with more "
263 "recent validity - not replacing",
264 aCertID
, aOriginAttributes
);
265 MakeMostRecentlyUsed(index
, lock
);
269 // Only known good responses or responses indicating an unknown
270 // or revoked certificate should replace previously known responses.
271 if (aResult
!= Success
&& aResult
!= Result::ERROR_OCSP_UNKNOWN_CERT
&&
272 aResult
!= Result::ERROR_REVOKED_CERTIFICATE
) {
274 "OCSPCache::Put(%p, \"%s\") already in cache - not "
275 "replacing with less important status",
276 aCertID
, aOriginAttributes
);
277 MakeMostRecentlyUsed(index
, lock
);
281 LogWithCertID("OCSPCache::Put(%p, \"%s\") already in cache - replacing",
282 aCertID
, aOriginAttributes
);
283 mEntries
[index
]->mResult
= aResult
;
284 mEntries
[index
]->mThisUpdate
= aThisUpdate
;
285 mEntries
[index
]->mValidThrough
= aValidThrough
;
286 MakeMostRecentlyUsed(index
, lock
);
290 if (mEntries
.length() == MaxEntries
) {
291 LogWithCertID("OCSPCache::Put(%p, \"%s\") too full - evicting an entry",
292 aCertID
, aOriginAttributes
);
293 for (Entry
** toEvict
= mEntries
.begin(); toEvict
!= mEntries
.end();
295 // Never evict an entry that indicates a revoked or unknokwn certificate,
296 // because revoked responses are more security-critical to remember.
297 if ((*toEvict
)->mResult
!= Result::ERROR_REVOKED_CERTIFICATE
&&
298 (*toEvict
)->mResult
!= Result::ERROR_OCSP_UNKNOWN_CERT
) {
300 mEntries
.erase(toEvict
);
304 // Well, we tried, but apparently everything is revoked or unknown.
305 // We don't want to remove a cached revoked or unknown response. If we're
306 // trying to insert a good response, we can just return "successfully"
307 // without doing so. This means we'll lose some speed, but it's not a
308 // security issue. If we're trying to insert a revoked or unknown response,
309 // we can't. We should return with an error that causes the current
310 // verification to fail.
311 if (mEntries
.length() == MaxEntries
) {
317 new (std::nothrow
) Entry(aResult
, aThisUpdate
, aValidThrough
);
318 // Normally we don't have to do this in Gecko, because OOM is fatal.
319 // However, if we want to embed this in another project, OOM might not
320 // be fatal, so handle this case.
322 return Result::FATAL_ERROR_NO_MEMORY
;
324 Result rv
= newEntry
->Init(aCertID
, aOriginAttributes
);
329 if (!mEntries
.append(newEntry
)) {
331 return Result::FATAL_ERROR_NO_MEMORY
;
333 LogWithCertID("OCSPCache::Put(%p, \"%s\") added to cache", aCertID
,
338 void OCSPCache::Clear() {
339 MutexAutoLock
lock(mMutex
);
340 MOZ_LOG(gCertVerifierLog
, LogLevel::Debug
,
341 ("OCSPCache::Clear: clearing cache"));
342 // First go through and delete the memory being pointed to by the pointers
344 for (Entry
** entry
= mEntries
.begin(); entry
< mEntries
.end(); entry
++) {
347 // Then remove the pointers themselves.
348 mEntries
.clearAndFree();
352 } // namespace mozilla