diff options
Diffstat (limited to 'security/manager/ssl/CertBlocklist.cpp')
-rw-r--r-- | security/manager/ssl/CertBlocklist.cpp | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/security/manager/ssl/CertBlocklist.cpp b/security/manager/ssl/CertBlocklist.cpp new file mode 100644 index 000000000..56473eca3 --- /dev/null +++ b/security/manager/ssl/CertBlocklist.cpp @@ -0,0 +1,671 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "CertBlocklist.h" + +#include "mozilla/Base64.h" +#include "mozilla/Casting.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/Unused.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceUtils.h" +#include "nsICryptoHash.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsISafeOutputStream.h" +#include "nsIX509Cert.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsTHashtable.h" +#include "nsThreadUtils.h" +#include "pkix/Input.h" +#include "prtime.h" + +NS_IMPL_ISUPPORTS(CertBlocklist, nsICertBlocklist) + +using namespace mozilla; +using namespace mozilla::pkix; + +#define PREF_BACKGROUND_UPDATE_TIMER "app.update.lastUpdateTime.blocklist-background-update-timer" +#define PREF_BLOCKLIST_ONECRL_CHECKED "services.blocklist.onecrl.checked" +#define PREF_MAX_STALENESS_IN_SECONDS "security.onecrl.maximum_staleness_in_seconds" +#define PREF_ONECRL_VIA_AMO "security.onecrl.via.amo" + +static LazyLogModule gCertBlockPRLog("CertBlock"); + +uint32_t CertBlocklist::sLastBlocklistUpdate = 0U; +uint32_t CertBlocklist::sLastKintoUpdate = 0U; +uint32_t CertBlocklist::sMaxStaleness = 0U; +bool CertBlocklist::sUseAMO = true; + +CertBlocklistItem::CertBlocklistItem(const uint8_t* DNData, + size_t DNLength, + const uint8_t* otherData, + size_t otherLength, + CertBlocklistItemMechanism itemMechanism) + : mIsCurrent(false) + , mItemMechanism(itemMechanism) +{ + mDNData = new uint8_t[DNLength]; + memcpy(mDNData, DNData, DNLength); + mDNLength = DNLength; + + mOtherData = new uint8_t[otherLength]; + memcpy(mOtherData, otherData, otherLength); + mOtherLength = otherLength; +} + +CertBlocklistItem::CertBlocklistItem(const CertBlocklistItem& aItem) +{ + mDNLength = aItem.mDNLength; + mDNData = new uint8_t[mDNLength]; + memcpy(mDNData, aItem.mDNData, mDNLength); + + mOtherLength = aItem.mOtherLength; + mOtherData = new uint8_t[mOtherLength]; + memcpy(mOtherData, aItem.mOtherData, mOtherLength); + + mItemMechanism = aItem.mItemMechanism; + + mIsCurrent = aItem.mIsCurrent; +} + +CertBlocklistItem::~CertBlocklistItem() +{ + delete[] mDNData; + delete[] mOtherData; +} + +nsresult +CertBlocklistItem::ToBase64(nsACString& b64DNOut, nsACString& b64OtherOut) +{ + nsDependentCSubstring DNString(BitwiseCast<char*, uint8_t*>(mDNData), + mDNLength); + nsDependentCSubstring otherString(BitwiseCast<char*, uint8_t*>(mOtherData), + mOtherLength); + nsresult rv = Base64Encode(DNString, b64DNOut); + if (NS_FAILED(rv)) { + return rv; + } + rv = Base64Encode(otherString, b64OtherOut); + return rv; +} + +bool +CertBlocklistItem::operator==(const CertBlocklistItem& aItem) const +{ + if (aItem.mItemMechanism != mItemMechanism) { + return false; + } + if (aItem.mDNLength != mDNLength || + aItem.mOtherLength != mOtherLength) { + return false; + } + return memcmp(aItem.mDNData, mDNData, mDNLength) == 0 && + memcmp(aItem.mOtherData, mOtherData, mOtherLength) == 0; +} + +uint32_t +CertBlocklistItem::Hash() const +{ + uint32_t hash; + // there's no requirement for a serial to be as large as the size of the hash + // key; if it's smaller, fall back to the first octet (otherwise, the last + // four) + if (mItemMechanism == BlockByIssuerAndSerial && + mOtherLength >= sizeof(hash)) { + memcpy(&hash, mOtherData + mOtherLength - sizeof(hash), sizeof(hash)); + } else { + hash = *mOtherData; + } + return hash; +} + +CertBlocklist::CertBlocklist() + : mMutex("CertBlocklist::mMutex") + , mModified(false) + , mBackingFileIsInitialized(false) + , mBackingFile(nullptr) +{ +} + +CertBlocklist::~CertBlocklist() +{ + Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged, + PREF_BACKGROUND_UPDATE_TIMER, + this); + Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged, + PREF_MAX_STALENESS_IN_SECONDS, + this); + Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged, + PREF_ONECRL_VIA_AMO, + this); + Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged, + PREF_BLOCKLIST_ONECRL_CHECKED, + this); +} + +nsresult +CertBlocklist::Init() +{ + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, ("CertBlocklist::Init")); + + // Init must be on main thread for getting the profile directory + if (!NS_IsMainThread()) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::Init - called off main thread")); + return NS_ERROR_NOT_SAME_THREAD; + } + + // Register preference callbacks + nsresult rv = + Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged, + PREF_BACKGROUND_UPDATE_TIMER, + this); + if (NS_FAILED(rv)) { + return rv; + } + rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged, + PREF_MAX_STALENESS_IN_SECONDS, + this); + if (NS_FAILED(rv)) { + return rv; + } + rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged, + PREF_ONECRL_VIA_AMO, + this); + if (NS_FAILED(rv)) { + return rv; + } + rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged, + PREF_BLOCKLIST_ONECRL_CHECKED, + this); + if (NS_FAILED(rv)) { + return rv; + } + + // Get the profile directory + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mBackingFile)); + if (NS_FAILED(rv) || !mBackingFile) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::Init - couldn't get profile dir")); + // Since we're returning NS_OK here, set mBackingFile to a safe value. + // (We need initialization to succeed and CertBlocklist to be in a + // well-defined state if the profile directory doesn't exist.) + mBackingFile = nullptr; + return NS_OK; + } + rv = mBackingFile->Append(NS_LITERAL_STRING("revocations.txt")); + if (NS_FAILED(rv)) { + return rv; + } + nsAutoCString path; + rv = mBackingFile->GetNativePath(path); + if (NS_FAILED(rv)) { + return rv; + } + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::Init certList path: %s", path.get())); + + return NS_OK; +} + +nsresult +CertBlocklist::EnsureBackingFileInitialized(MutexAutoLock& lock) +{ + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::EnsureBackingFileInitialized")); + if (mBackingFileIsInitialized || !mBackingFile) { + return NS_OK; + } + + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::EnsureBackingFileInitialized - not initialized")); + + bool exists = false; + nsresult rv = mBackingFile->Exists(&exists); + if (NS_FAILED(rv)) { + return rv; + } + if (!exists) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::EnsureBackingFileInitialized no revocations file")); + return NS_OK; + } + + // Load the revocations file into the cert blocklist + nsCOMPtr<nsIFileInputStream> fileStream( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = fileStream->Init(mBackingFile, -1, -1, false); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv)); + nsAutoCString line; + nsAutoCString DN; + nsAutoCString other; + CertBlocklistItemMechanism mechanism; + // read in the revocations file. The file format is as follows: each line + // contains a comment, base64 encoded DER for a DN, base64 encoded DER for a + // serial number or a Base64 encoded SHA256 hash of a public key. Comment + // lines start with '#', serial number lines, ' ' (a space), public key hashes + // with '\t' (a tab) and anything else is assumed to be a DN. + bool more = true; + do { + rv = lineStream->ReadLine(line, &more); + if (NS_FAILED(rv)) { + break; + } + // ignore comments and empty lines + if (line.IsEmpty() || line.First() == '#') { + continue; + } + if (line.First() != ' ' && line.First() != '\t') { + DN = line; + continue; + } + other = line; + if (line.First() == ' ') { + mechanism = BlockByIssuerAndSerial; + } else { + mechanism = BlockBySubjectAndPubKey; + } + other.Trim(" \t", true, false, false); + // Serial numbers and public key hashes 'belong' to the last DN line seen; + // if no DN has been seen, the serial number or public key hash is ignored. + if (DN.IsEmpty() || other.IsEmpty()) { + continue; + } + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::EnsureBackingFileInitialized adding: %s %s", + DN.get(), other.get())); + + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::EnsureBackingFileInitialized - pre-decode")); + + rv = AddRevokedCertInternal(DN, other, mechanism, CertOldFromLocalCache, + lock); + + if (NS_FAILED(rv)) { + // we warn here, rather than abandoning, since we need to + // ensure that as many items as possible are read + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::EnsureBackingFileInitialized adding revoked cert " + "failed")); + } + } while (more); + mBackingFileIsInitialized = true; + return NS_OK; +} + +NS_IMETHODIMP +CertBlocklist::RevokeCertBySubjectAndPubKey(const char* aSubject, + const char* aPubKeyHash) +{ + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::RevokeCertBySubjectAndPubKey - subject is: %s and pubKeyHash: %s", + aSubject, aPubKeyHash)); + MutexAutoLock lock(mMutex); + + return AddRevokedCertInternal(nsDependentCString(aSubject), + nsDependentCString(aPubKeyHash), + BlockBySubjectAndPubKey, + CertNewFromBlocklist, lock); +} + +NS_IMETHODIMP +CertBlocklist::RevokeCertByIssuerAndSerial(const char* aIssuer, + const char* aSerialNumber) +{ + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::RevokeCertByIssuerAndSerial - issuer is: %s and serial: %s", + aIssuer, aSerialNumber)); + MutexAutoLock lock(mMutex); + + return AddRevokedCertInternal(nsDependentCString(aIssuer), + nsDependentCString(aSerialNumber), + BlockByIssuerAndSerial, + CertNewFromBlocklist, lock); +} + +nsresult +CertBlocklist::AddRevokedCertInternal(const nsACString& aEncodedDN, + const nsACString& aEncodedOther, + CertBlocklistItemMechanism aMechanism, + CertBlocklistItemState aItemState, + MutexAutoLock& /*proofOfLock*/) +{ + nsCString decodedDN; + nsCString decodedOther; + + nsresult rv = Base64Decode(aEncodedDN, decodedDN); + if (NS_FAILED(rv)) { + return rv; + } + rv = Base64Decode(aEncodedOther, decodedOther); + if (NS_FAILED(rv)) { + return rv; + } + + CertBlocklistItem item( + BitwiseCast<const uint8_t*, const char*>(decodedDN.get()), + decodedDN.Length(), + BitwiseCast<const uint8_t*, const char*>(decodedOther.get()), + decodedOther.Length(), + aMechanism); + + if (aItemState == CertNewFromBlocklist) { + // We want SaveEntries to be a no-op if no new entries are added. + nsGenericHashKey<CertBlocklistItem>* entry = mBlocklist.GetEntry(item); + if (!entry) { + mModified = true; + } else { + // Ensure that any existing item is replaced by a fresh one so we can + // use mIsCurrent to decide which entries to write out. + mBlocklist.RemoveEntry(entry); + } + item.mIsCurrent = true; + } + mBlocklist.PutEntry(item); + + return NS_OK; +} + +// Write a line for a given string in the output stream +nsresult +WriteLine(nsIOutputStream* outputStream, const nsACString& string) +{ + nsAutoCString line(string); + line.Append('\n'); + + const char* data = line.get(); + uint32_t length = line.Length(); + nsresult rv = NS_OK; + while (NS_SUCCEEDED(rv) && length) { + uint32_t bytesWritten = 0; + rv = outputStream->Write(data, length, &bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + // if no data is written, something is wrong + if (!bytesWritten) { + return NS_ERROR_FAILURE; + } + length -= bytesWritten; + data += bytesWritten; + } + return rv; +} + +// void saveEntries(); +// Store the blockist in a text file containing base64 encoded issuers and +// serial numbers. +// +// Each item is stored on a separate line; each issuer is followed by its +// revoked serial numbers, indented by one space. +// +// lines starting with a # character are ignored +NS_IMETHODIMP +CertBlocklist::SaveEntries() +{ + MOZ_LOG(gCertBlockPRLog, LogLevel::Debug, + ("CertBlocklist::SaveEntries - not initialized")); + MutexAutoLock lock(mMutex); + if (!mModified) { + return NS_OK; + } + + nsresult rv = EnsureBackingFileInitialized(lock); + if (NS_FAILED(rv)) { + return rv; + } + + if (!mBackingFile) { + // We allow this to succeed with no profile directory for tests + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::SaveEntries no file in profile to write to")); + return NS_OK; + } + + // Data needed for writing blocklist items out to the revocations file + IssuerTable issuerTable; + BlocklistStringSet issuers; + nsCOMPtr<nsIOutputStream> outputStream; + + rv = NS_NewAtomicFileOutputStream(getter_AddRefs(outputStream), + mBackingFile, -1, -1, 0); + if (NS_FAILED(rv)) { + return rv; + } + + rv = WriteLine(outputStream, + NS_LITERAL_CSTRING("# Auto generated contents. Do not edit.")); + if (NS_FAILED(rv)) { + return rv; + } + + // Sort blocklist items into lists of serials for each issuer + for (auto iter = mBlocklist.Iter(); !iter.Done(); iter.Next()) { + CertBlocklistItem item = iter.Get()->GetKey(); + if (!item.mIsCurrent) { + continue; + } + + nsAutoCString encDN; + nsAutoCString encOther; + + nsresult rv = item.ToBase64(encDN, encOther); + if (NS_FAILED(rv)) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::SaveEntries writing revocation data failed")); + return NS_ERROR_FAILURE; + } + + // If it's a subject / public key block, write it straight out + if (item.mItemMechanism == BlockBySubjectAndPubKey) { + WriteLine(outputStream, encDN); + WriteLine(outputStream, NS_LITERAL_CSTRING("\t") + encOther); + continue; + } + + // Otherwise, we have to group entries by issuer + issuers.PutEntry(encDN); + BlocklistStringSet* issuerSet = issuerTable.Get(encDN); + if (!issuerSet) { + issuerSet = new BlocklistStringSet(); + issuerTable.Put(encDN, issuerSet); + } + issuerSet->PutEntry(encOther); + } + + for (auto iter = issuers.Iter(); !iter.Done(); iter.Next()) { + nsCStringHashKey* hashKey = iter.Get(); + nsAutoPtr<BlocklistStringSet> issuerSet; + issuerTable.RemoveAndForget(hashKey->GetKey(), issuerSet); + + nsresult rv = WriteLine(outputStream, hashKey->GetKey()); + if (NS_FAILED(rv)) { + break; + } + + // Write serial data to the output stream + for (auto iter = issuerSet->Iter(); !iter.Done(); iter.Next()) { + nsresult rv = WriteLine(outputStream, + NS_LITERAL_CSTRING(" ") + iter.Get()->GetKey()); + if (NS_FAILED(rv)) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::SaveEntries writing revocation data failed")); + return NS_ERROR_FAILURE; + } + } + } + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outputStream); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (!safeStream) { + return NS_ERROR_FAILURE; + } + rv = safeStream->Finish(); + if (NS_FAILED(rv)) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::SaveEntries saving revocation data failed")); + return rv; + } + mModified = false; + return NS_OK; +} + +NS_IMETHODIMP +CertBlocklist::IsCertRevoked(const uint8_t* aIssuer, + uint32_t aIssuerLength, + const uint8_t* aSerial, + uint32_t aSerialLength, + const uint8_t* aSubject, + uint32_t aSubjectLength, + const uint8_t* aPubKey, + uint32_t aPubKeyLength, + bool* _retval) +{ + MutexAutoLock lock(mMutex); + + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsCertRevoked?")); + nsresult rv = EnsureBackingFileInitialized(lock); + if (NS_FAILED(rv)) { + return rv; + } + + Input issuer; + Input serial; + if (issuer.Init(aIssuer, aIssuerLength) != Success) { + return NS_ERROR_FAILURE; + } + if (serial.Init(aSerial, aSerialLength) != Success) { + return NS_ERROR_FAILURE; + } + + CertBlocklistItem issuerSerial(aIssuer, aIssuerLength, aSerial, aSerialLength, + BlockByIssuerAndSerial); + + nsAutoCString encDN; + nsAutoCString encOther; + + issuerSerial.ToBase64(encDN, encOther); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsCertRevoked issuer %s - serial %s", + encDN.get(), encOther.get())); + + *_retval = mBlocklist.Contains(issuerSerial); + + if (*_retval) { + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("certblocklist::IsCertRevoked found by issuer / serial")); + return NS_OK; + } + + nsCOMPtr<nsICryptoHash> crypto; + crypto = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + + rv = crypto->Init(nsICryptoHash::SHA256); + if (NS_FAILED(rv)) { + return rv; + } + + rv = crypto->Update(aPubKey, aPubKeyLength); + if (NS_FAILED(rv)) { + return rv; + } + + nsCString hashString; + rv = crypto->Finish(false, hashString); + if (NS_FAILED(rv)) { + return rv; + } + + CertBlocklistItem subjectPubKey( + aSubject, + static_cast<size_t>(aSubjectLength), + BitwiseCast<const uint8_t*, const char*>(hashString.get()), + hashString.Length(), + BlockBySubjectAndPubKey); + + rv = subjectPubKey.ToBase64(encDN, encOther); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsCertRevoked subject %s - pubKey hash %s", + encDN.get(), encOther.get())); + *_retval = mBlocklist.Contains(subjectPubKey); + + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsCertRevoked by subject / pubkey? %s", + *_retval ? "true" : "false")); + + return NS_OK; +} + +NS_IMETHODIMP +CertBlocklist::IsBlocklistFresh(bool* _retval) +{ + MutexAutoLock lock(mMutex); + *_retval = false; + + uint32_t now = uint32_t(PR_Now() / PR_USEC_PER_SEC); + uint32_t lastUpdate = sUseAMO ? sLastBlocklistUpdate : sLastKintoUpdate; + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsBlocklistFresh using AMO? %i lastUpdate is %i", + sUseAMO, lastUpdate)); + + if (now > lastUpdate) { + int64_t interval = now - lastUpdate; + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsBlocklistFresh we're after the last BlocklistUpdate " + "interval is %i, staleness %u", interval, sMaxStaleness)); + *_retval = sMaxStaleness > interval; + } + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::IsBlocklistFresh ? %s", *_retval ? "true" : "false")); + return NS_OK; +} + + +/* static */ +void +CertBlocklist::PreferenceChanged(const char* aPref, void* aClosure) + +{ + auto blocklist = static_cast<CertBlocklist*>(aClosure); + MutexAutoLock lock(blocklist->mMutex); + + MOZ_LOG(gCertBlockPRLog, LogLevel::Warning, + ("CertBlocklist::PreferenceChanged %s changed", aPref)); + if (strcmp(aPref, PREF_BACKGROUND_UPDATE_TIMER) == 0) { + sLastBlocklistUpdate = Preferences::GetUint(PREF_BACKGROUND_UPDATE_TIMER, + uint32_t(0)); + } else if (strcmp(aPref, PREF_BLOCKLIST_ONECRL_CHECKED) == 0) { + sLastKintoUpdate = Preferences::GetUint(PREF_BLOCKLIST_ONECRL_CHECKED, + uint32_t(0)); + } else if (strcmp(aPref, PREF_MAX_STALENESS_IN_SECONDS) == 0) { + sMaxStaleness = Preferences::GetUint(PREF_MAX_STALENESS_IN_SECONDS, + uint32_t(0)); + } else if (strcmp(aPref, PREF_ONECRL_VIA_AMO) == 0) { + sUseAMO = Preferences::GetBool(PREF_ONECRL_VIA_AMO, true); + } +} |