зеркало из https://github.com/mozilla/gecko-dev.git
350 строки
13 KiB
C++
350 строки
13 KiB
C++
/* -*- 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/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 */) {
|
|
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 */) {
|
|
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
|