/* -*- 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 #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(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