tenfourfox/security/manager/ssl/CertBlocklist.cpp
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

674 lines
21 KiB
C++

/* -*- 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/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 "mozilla/Logging.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_KINTO_ONECRL_CHECKED "services.kinto.onecrl.checked"
#define PREF_MAX_STALENESS_IN_SECONDS "security.onecrl.maximum_staleness_in_seconds"
#define PREF_ONECRL_VIA_AMO "security.onecrl.via.amo"
static PRLogModuleInfo* gCertBlockPRLog;
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(reinterpret_cast<char*>(mDNData),
mDNLength);
nsDependentCSubstring otherString(reinterpret_cast<char*>(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)
{
if (!gCertBlockPRLog) {
gCertBlockPRLog = PR_NewLogModule("CertBlock");
}
}
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_KINTO_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_KINTO_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(reinterpret_cast<const uint8_t*>(decodedDN.get()),
decodedDN.Length(),
reinterpret_cast<const uint8_t*>(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(reinterpret_cast<const unsigned char*>(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),
reinterpret_cast<const uint8_t*>(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)
{
CertBlocklist* blocklist = reinterpret_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_KINTO_ONECRL_CHECKED) == 0) {
sLastKintoUpdate = Preferences::GetUint(PREF_KINTO_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);
}
}