diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /security/certverifier/OCSPCache.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/certverifier/OCSPCache.cpp')
-rw-r--r-- | security/certverifier/OCSPCache.cpp | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/security/certverifier/OCSPCache.cpp b/security/certverifier/OCSPCache.cpp new file mode 100644 index 0000000000..557b501ad5 --- /dev/null +++ b/security/certverifier/OCSPCache.cpp @@ -0,0 +1,352 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This code is made available to you under your choice of the following sets + * of licensing terms: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +/* Copyright 2013 Mozilla Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OCSPCache.h" + +#include <limits> + +#include "NSSCertDBTrustDomain.h" +#include "pk11pub.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozpkix/pkixnss.h" +#include "ScopedNSSTypes.h" +#include "secerr.h" + +extern mozilla::LazyLogModule gCertVerifierLog; + +using namespace mozilla::pkix; + +namespace mozilla { +namespace psm { + +typedef mozilla::pkix::Result Result; + +static SECStatus DigestLength(UniquePK11Context& context, uint32_t length) { + // Restrict length to 2 bytes because it should be big enough for all + // inputs this code will actually see and that it is well-defined and + // type-size-independent. + if (length >= 65536) { + return SECFailure; + } + unsigned char array[2]; + array[0] = length & 255; + array[1] = (length >> 8) & 255; + + return PK11_DigestOp(context.get(), array, MOZ_ARRAY_LENGTH(array)); +} + +// Let derIssuer be the DER encoding of the issuer of certID. +// Let derPublicKey be the DER encoding of the public key of certID. +// Let serialNumber be the bytes of the serial number of certID. +// Let serialNumberLen be the number of bytes of serialNumber. +// Let firstPartyDomain be the first party domain of originAttributes. +// It is only non-empty when "privacy.firstParty.isolate" is enabled, in order +// to isolate OCSP cache by first party. +// Let firstPartyDomainLen be the number of bytes of firstPartyDomain. +// Let partitionKey be the partition key of originAttributes. +// Let partitionKeyLen be the number of bytes of partitionKey. +// The value calculated is SHA384(derIssuer || derPublicKey || serialNumberLen +// || serialNumber || firstPartyDomainLen || firstPartyDomain || partitionKeyLen +// || partitionKey). +// Because the DER encodings include the length of the data encoded, and we also +// include the length of serialNumber and originAttributes, there do not exist +// A(derIssuerA, derPublicKeyA, serialNumberLenA, serialNumberA, +// originAttributesLenA, originAttributesA) and B(derIssuerB, derPublicKeyB, +// serialNumberLenB, serialNumberB, originAttributesLenB, originAttributesB) +// such that the concatenation of each tuple results in the same string of +// bytes but where each part in A is not equal to its counterpart in B. This is +// important because as a result it is computationally infeasible to find +// collisions that would subvert this cache (given that SHA384 is a +// cryptographically-secure hash function). +static SECStatus CertIDHash(SHA384Buffer& buf, const CertID& certID, + const OriginAttributes& originAttributes) { + UniquePK11Context context(PK11_CreateDigestContext(SEC_OID_SHA384)); + if (!context) { + return SECFailure; + } + SECStatus rv = PK11_DigestBegin(context.get()); + if (rv != SECSuccess) { + return rv; + } + SECItem certIDIssuer = UnsafeMapInputToSECItem(certID.issuer); + rv = PK11_DigestOp(context.get(), certIDIssuer.data, certIDIssuer.len); + if (rv != SECSuccess) { + return rv; + } + SECItem certIDIssuerSubjectPublicKeyInfo = + UnsafeMapInputToSECItem(certID.issuerSubjectPublicKeyInfo); + rv = PK11_DigestOp(context.get(), certIDIssuerSubjectPublicKeyInfo.data, + certIDIssuerSubjectPublicKeyInfo.len); + if (rv != SECSuccess) { + return rv; + } + SECItem certIDSerialNumber = UnsafeMapInputToSECItem(certID.serialNumber); + rv = DigestLength(context, certIDSerialNumber.len); + if (rv != SECSuccess) { + return rv; + } + rv = PK11_DigestOp(context.get(), certIDSerialNumber.data, + certIDSerialNumber.len); + if (rv != SECSuccess) { + return rv; + } + + auto populateOriginAttributesKey = [&context](const nsString& aKey) { + NS_ConvertUTF16toUTF8 key(aKey); + + if (key.IsEmpty()) { + return SECSuccess; + } + + SECStatus rv = DigestLength(context, key.Length()); + if (rv != SECSuccess) { + return rv; + } + + return PK11_DigestOp(context.get(), + BitwiseCast<const unsigned char*>(key.get()), + key.Length()); + }; + + // OCSP should be isolated by firstPartyDomain and partitionKey, but not + // by containers. + rv = populateOriginAttributesKey(originAttributes.mFirstPartyDomain); + if (rv != SECSuccess) { + return rv; + } + + bool isolateByPartitionKey = + originAttributes.mPrivateBrowsingId > 0 + ? StaticPrefs::privacy_partition_network_state_ocsp_cache_pbmode() + : StaticPrefs::privacy_partition_network_state_ocsp_cache(); + if (isolateByPartitionKey) { + rv = populateOriginAttributesKey(originAttributes.mPartitionKey); + if (rv != SECSuccess) { + return rv; + } + } + uint32_t outLen = 0; + rv = PK11_DigestFinal(context.get(), buf, &outLen, SHA384_LENGTH); + if (outLen != SHA384_LENGTH) { + return SECFailure; + } + return rv; +} + +Result OCSPCache::Entry::Init(const CertID& aCertID, + const OriginAttributes& aOriginAttributes) { + SECStatus srv = CertIDHash(mIDHash, aCertID, aOriginAttributes); + if (srv != SECSuccess) { + return MapPRErrorCodeToResult(PR_GetError()); + } + return Success; +} + +OCSPCache::OCSPCache() : mMutex("OCSPCache-mutex") {} + +OCSPCache::~OCSPCache() { Clear(); } + +// Returns false with index in an undefined state if no matching entry was +// found. +bool OCSPCache::FindInternal(const CertID& aCertID, + const OriginAttributes& aOriginAttributes, + /*out*/ size_t& index, + const MutexAutoLock& /* aProofOfLock */) { + mMutex.AssertCurrentThreadOwns(); + if (mEntries.length() == 0) { + return false; + } + + SHA384Buffer idHash; + SECStatus rv = CertIDHash(idHash, aCertID, aOriginAttributes); + if (rv != SECSuccess) { + return false; + } + + // mEntries is sorted with the most-recently-used entry at the end. + // Thus, searching from the end will often be fastest. + index = mEntries.length(); + while (index > 0) { + --index; + if (memcmp(mEntries[index]->mIDHash, idHash, SHA384_LENGTH) == 0) { + return true; + } + } + return false; +} + +static inline void LogWithCertID(const char* aMessage, const CertID& aCertID, + const OriginAttributes& aOriginAttributes) { + nsAutoString info = u"firstPartyDomain: "_ns + + aOriginAttributes.mFirstPartyDomain + + u", partitionKey: "_ns + aOriginAttributes.mPartitionKey; + MOZ_LOG(gCertVerifierLog, LogLevel::Debug, + (aMessage, &aCertID, NS_ConvertUTF16toUTF8(info).get())); +} + +void OCSPCache::MakeMostRecentlyUsed(size_t aIndex, + const MutexAutoLock& /* aProofOfLock */) { + mMutex.AssertCurrentThreadOwns(); + Entry* entry = mEntries[aIndex]; + // Since mEntries is sorted with the most-recently-used entry at the end, + // aIndex is likely to be near the end, so this is likely to be fast. + mEntries.erase(mEntries.begin() + aIndex); + // erase() does not shrink or realloc memory, so the append below should + // always succeed. + MOZ_RELEASE_ASSERT(mEntries.append(entry)); +} + +bool OCSPCache::Get(const CertID& aCertID, + const OriginAttributes& aOriginAttributes, Result& aResult, + Time& aValidThrough) { + MutexAutoLock lock(mMutex); + + size_t index; + if (!FindInternal(aCertID, aOriginAttributes, index, lock)) { + LogWithCertID("OCSPCache::Get(%p,\"%s\") not in cache", aCertID, + aOriginAttributes); + return false; + } + LogWithCertID("OCSPCache::Get(%p,\"%s\") in cache", aCertID, + aOriginAttributes); + aResult = mEntries[index]->mResult; + aValidThrough = mEntries[index]->mValidThrough; + MakeMostRecentlyUsed(index, lock); + return true; +} + +Result OCSPCache::Put(const CertID& aCertID, + const OriginAttributes& aOriginAttributes, Result aResult, + Time aThisUpdate, Time aValidThrough) { + MutexAutoLock lock(mMutex); + + size_t index; + if (FindInternal(aCertID, aOriginAttributes, index, lock)) { + // Never replace an entry indicating a revoked certificate. + if (mEntries[index]->mResult == Result::ERROR_REVOKED_CERTIFICATE) { + LogWithCertID( + "OCSPCache::Put(%p, \"%s\") already in cache as revoked - " + "not replacing", + aCertID, aOriginAttributes); + MakeMostRecentlyUsed(index, lock); + return Success; + } + + // Never replace a newer entry with an older one unless the older entry + // indicates a revoked certificate, which we want to remember. + if (mEntries[index]->mThisUpdate > aThisUpdate && + aResult != Result::ERROR_REVOKED_CERTIFICATE) { + LogWithCertID( + "OCSPCache::Put(%p, \"%s\") already in cache with more " + "recent validity - not replacing", + aCertID, aOriginAttributes); + MakeMostRecentlyUsed(index, lock); + return Success; + } + + // Only known good responses or responses indicating an unknown + // or revoked certificate should replace previously known responses. + if (aResult != Success && aResult != Result::ERROR_OCSP_UNKNOWN_CERT && + aResult != Result::ERROR_REVOKED_CERTIFICATE) { + LogWithCertID( + "OCSPCache::Put(%p, \"%s\") already in cache - not " + "replacing with less important status", + aCertID, aOriginAttributes); + MakeMostRecentlyUsed(index, lock); + return Success; + } + + LogWithCertID("OCSPCache::Put(%p, \"%s\") already in cache - replacing", + aCertID, aOriginAttributes); + mEntries[index]->mResult = aResult; + mEntries[index]->mThisUpdate = aThisUpdate; + mEntries[index]->mValidThrough = aValidThrough; + MakeMostRecentlyUsed(index, lock); + return Success; + } + + if (mEntries.length() == MaxEntries) { + LogWithCertID("OCSPCache::Put(%p, \"%s\") too full - evicting an entry", + aCertID, aOriginAttributes); + for (Entry** toEvict = mEntries.begin(); toEvict != mEntries.end(); + toEvict++) { + // Never evict an entry that indicates a revoked or unknokwn certificate, + // because revoked responses are more security-critical to remember. + if ((*toEvict)->mResult != Result::ERROR_REVOKED_CERTIFICATE && + (*toEvict)->mResult != Result::ERROR_OCSP_UNKNOWN_CERT) { + delete *toEvict; + mEntries.erase(toEvict); + break; + } + } + // Well, we tried, but apparently everything is revoked or unknown. + // We don't want to remove a cached revoked or unknown response. If we're + // trying to insert a good response, we can just return "successfully" + // without doing so. This means we'll lose some speed, but it's not a + // security issue. If we're trying to insert a revoked or unknown response, + // we can't. We should return with an error that causes the current + // verification to fail. + if (mEntries.length() == MaxEntries) { + return aResult; + } + } + + Entry* newEntry = + new (std::nothrow) Entry(aResult, aThisUpdate, aValidThrough); + // Normally we don't have to do this in Gecko, because OOM is fatal. + // However, if we want to embed this in another project, OOM might not + // be fatal, so handle this case. + if (!newEntry) { + return Result::FATAL_ERROR_NO_MEMORY; + } + Result rv = newEntry->Init(aCertID, aOriginAttributes); + if (rv != Success) { + delete newEntry; + return rv; + } + if (!mEntries.append(newEntry)) { + delete newEntry; + return Result::FATAL_ERROR_NO_MEMORY; + } + LogWithCertID("OCSPCache::Put(%p, \"%s\") added to cache", aCertID, + aOriginAttributes); + return Success; +} + +void OCSPCache::Clear() { + MutexAutoLock lock(mMutex); + MOZ_LOG(gCertVerifierLog, LogLevel::Debug, + ("OCSPCache::Clear: clearing cache")); + // First go through and delete the memory being pointed to by the pointers + // in the vector. + for (Entry** entry = mEntries.begin(); entry < mEntries.end(); entry++) { + delete *entry; + } + // Then remove the pointers themselves. + mEntries.clearAndFree(); +} + +} // namespace psm +} // namespace mozilla |