diff --git a/security/certverifier/NSSCertDBTrustDomain.cpp b/security/certverifier/NSSCertDBTrustDomain.cpp index 7a74246391c1..7505c9d3e75e 100644 --- a/security/certverifier/NSSCertDBTrustDomain.cpp +++ b/security/certverifier/NSSCertDBTrustDomain.cpp @@ -9,12 +9,13 @@ #include #include "ExtendedValidation.h" -#include "nsNSSCertificate.h" -#include "NSSErrorsService.h" #include "OCSPRequestor.h" #include "certdb.h" #include "mozilla/Telemetry.h" +#include "nsNSSCertificate.h" #include "nss.h" +#include "NSSErrorsService.h" +#include "nsServiceManagerUtils.h" #include "pk11pub.h" #include "pkix/pkix.h" #include "pkix/pkixnss.h" @@ -68,6 +69,7 @@ NSSCertDBTrustDomain::NSSCertDBTrustDomain(SECTrustType certDBTrustType, , mMinimumNonECCBits(forEV ? MINIMUM_NON_ECC_BITS_EV : MINIMUM_NON_ECC_BITS_DV) , mHostname(hostname) , mBuiltChain(builtChain) + , mCertBlocklist(do_GetService(NS_CERTBLOCKLIST_CONTRACTID)) { } @@ -338,6 +340,27 @@ NSSCertDBTrustDomain::CheckRevocation(EndEntityOrCA endEntityOrCA, maxOCSPLifetimeInDays = 365; } + if (!mCertBlocklist) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + bool isCertRevoked; + nsresult nsrv = mCertBlocklist->IsCertRevoked( + certID.issuer.UnsafeGetData(), + certID.issuer.GetLength(), + certID.serialNumber.UnsafeGetData(), + certID.serialNumber.GetLength(), + &isCertRevoked); + if (NS_FAILED(nsrv)) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + if (isCertRevoked) { + PR_LOG(gCertVerifierLog, PR_LOG_DEBUG, + ("NSSCertDBTrustDomain: certificate is in blocklist")); + return Result::ERROR_REVOKED_CERTIFICATE; + } + // If we have a stapled OCSP response then the verification of that response // determines the result unless the OCSP response is expired. We make an // exception for expired responses because some servers, nginx in particular, diff --git a/security/certverifier/NSSCertDBTrustDomain.h b/security/certverifier/NSSCertDBTrustDomain.h index 15de082b303b..259c5c4d6a7f 100644 --- a/security/certverifier/NSSCertDBTrustDomain.h +++ b/security/certverifier/NSSCertDBTrustDomain.h @@ -7,9 +7,10 @@ #ifndef mozilla_psm__NSSCertDBTrustDomain_h #define mozilla_psm__NSSCertDBTrustDomain_h +#include "CertVerifier.h" +#include "nsICertBlocklist.h" #include "pkix/pkixtypes.h" #include "secmodt.h" -#include "CertVerifier.h" namespace mozilla { namespace psm { @@ -110,6 +111,7 @@ private: const unsigned int mMinimumNonECCBits; const char* mHostname; // non-owning - only used for pinning checks ScopedCERTCertList* mBuiltChain; // non-owning + nsCOMPtr mCertBlocklist; }; } } // namespace mozilla::psm diff --git a/security/manager/boot/public/moz.build b/security/manager/boot/public/moz.build index 03601ec833d0..5f841e1c1d1c 100644 --- a/security/manager/boot/public/moz.build +++ b/security/manager/boot/public/moz.build @@ -6,6 +6,7 @@ XPIDL_SOURCES += [ 'nsIBufEntropyCollector.idl', + 'nsICertBlocklist.idl', 'nsISecurityUITelemetry.idl', 'nsISecurityWarningDialogs.idl', 'nsISSLStatusProvider.idl', diff --git a/security/manager/boot/public/nsICertBlocklist.idl b/security/manager/boot/public/nsICertBlocklist.idl new file mode 100644 index 000000000000..215294020717 --- /dev/null +++ b/security/manager/boot/public/nsICertBlocklist.idl @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 "nsISupports.idl" + +interface nsIX509Cert; + +%{C++ +#define NS_CERTBLOCKLIST_CONTRACTID "@mozilla.org/security/certblocklist;1" +%} + +/** + * Represents a service to add certificates as explicitly blocked/distrusted. + */ +[scriptable, uuid(44b0ee42-1af3-45e7-b601-7f17bd67c5cc)] +interface nsICertBlocklist : nsISupports { + /** + * Add details of a revoked certificate : + * issuer name (base-64 encoded DER) and serial number (base-64 encoded DER). + */ + void addRevokedCert(in string issuer, in string serialNumber); + + /** + * Persist (fresh) blocklist entries to the profile (if a profile directory is + * available). Note: calling this will result in synchronous I/O. + */ + void saveEntries(); + + /** + * Check if a certificate is blocked. + * isser - issuer name, DER encoded + * serial - serial number, DER encoded + */ + boolean isCertRevoked([const, array, size_is(issuer_length)] in octet issuer, + in unsigned long issuer_length, + [const, array, size_is(serial_length)] in octet serial, + in unsigned long serial_length); +}; diff --git a/security/manager/boot/src/CertBlocklist.cpp b/security/manager/boot/src/CertBlocklist.cpp new file mode 100644 index 000000000000..c67da93c384a --- /dev/null +++ b/security/manager/boot/src/CertBlocklist.cpp @@ -0,0 +1,469 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsAppDirectoryServiceDefs.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsIX509Cert.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsTHashtable.h" +#include "nsThreadUtils.h" +#include "pkix/Input.h" +#include "prlog.h" + +NS_IMPL_ISUPPORTS(CertBlocklist, nsICertBlocklist) + +static PRLogModuleInfo* gCertBlockPRLog; + +CertBlocklistItem::CertBlocklistItem(mozilla::pkix::Input aIssuer, + mozilla::pkix::Input aSerial) +{ + mIssuerData = new uint8_t[aIssuer.GetLength()]; + memcpy(mIssuerData, aIssuer.UnsafeGetData(), aIssuer.GetLength()); + mIssuer.Init(mIssuerData, aIssuer.GetLength()); + + mSerialData = new uint8_t[aSerial.GetLength()]; + memcpy(mSerialData, aSerial.UnsafeGetData(), aSerial.GetLength()); + mSerial.Init(mSerialData, aSerial.GetLength()); +} + +CertBlocklistItem::CertBlocklistItem(const CertBlocklistItem& aItem) +{ + uint32_t issuerLength = aItem.mIssuer.GetLength(); + mIssuerData = new uint8_t[issuerLength]; + memcpy(mIssuerData, aItem.mIssuerData, issuerLength); + mIssuer.Init(mIssuerData, issuerLength); + + uint32_t serialLength = aItem.mSerial.GetLength(); + mSerialData = new uint8_t[serialLength]; + memcpy(mSerialData, aItem.mSerialData, serialLength); + mSerial.Init(mSerialData, serialLength); + mIsCurrent = aItem.mIsCurrent; +} + +CertBlocklistItem::~CertBlocklistItem() +{ + delete[] mIssuerData; + delete[] mSerialData; +} + +nsresult +CertBlocklistItem::ToBase64(nsACString& b64IssuerOut, nsACString& b64SerialOut) +{ + nsDependentCSubstring issuerString(reinterpret_cast(mIssuerData), + mIssuer.GetLength()); + nsDependentCSubstring serialString(reinterpret_cast(mSerialData), + mSerial.GetLength()); + nsresult rv = mozilla::Base64Encode(issuerString, b64IssuerOut); + if (NS_FAILED(rv)) { + return rv; + } + rv = mozilla::Base64Encode(serialString, b64SerialOut); + return rv; +} + +bool +CertBlocklistItem::operator==(const CertBlocklistItem& aItem) const +{ + bool retval = InputsAreEqual(aItem.mIssuer, mIssuer) && + InputsAreEqual(aItem.mSerial, mSerial); + return retval; +} + +uint32_t +CertBlocklistItem::Hash() const +{ + uint32_t hash; + uint32_t serialLength = mSerial.GetLength(); + // there's no requirement for a serial to be as large as 32 bits; if it's + // smaller, fall back to the first octet (otherwise, the last four) + if (serialLength >= 4) { + hash = *(uint32_t *)(mSerialData + serialLength - 4); + } else { + hash = *mSerialData; + } + return hash; +} + +CertBlocklist::CertBlocklist() + : mMutex("CertBlocklist::mMutex") + , mModified(false) +{ + if (!gCertBlockPRLog) { + gCertBlockPRLog = PR_NewLogModule("CertBlock"); + } +} + +CertBlocklist::~CertBlocklist() +{ +} + +nsresult +CertBlocklist::Init() +{ + mozilla::MutexAutoLock lock(mMutex); + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, ("CertBlocklist::Init")); + if (!NS_IsMainThread()) { + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, + ("CertBlocklist::Init - called off main thread")); + return NS_ERROR_NOT_SAME_THREAD; + } + // Load the revocations file into the cert blocklist + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, + ("CertBlocklist::Init - not initialized; initializing")); + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mBackingFile)); + if (NS_FAILED(rv) || !mBackingFile) { + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, + ("CertBlocklist::Init - couldn't get profile dir")); + 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; + } + + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, + ("CertBlocklist::Init certList path: %s", path.get())); + + bool exists = false; + rv = mBackingFile->Exists(&exists); + if (NS_FAILED(rv)) { + return rv; + } + + if (!exists) { + PR_LOG(gCertBlockPRLog, PR_LOG_WARN, + ("CertBlocklist::Init no revocations file")); + return NS_OK; + } + + nsCOMPtr 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 lineStream(do_QueryInterface(fileStream, &rv)); + nsAutoCString line; + nsAutoCString issuer; + nsAutoCString serial; + // read in the revocations file. The file format is as follows: each line + // contains a comment, base64 encoded DER for an issuer or base64 encoded DER + // for a serial number. Comment lines start with '#', serial number lines, ' ' + // (a space) and anything else is assumed to be an issuer. + 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() != ' ') { + issuer = line; + continue; + } + serial = line; + serial.Trim(" ", true, false, false); + // serial numbers 'belong' to the last issuer line seen; if no issuer has + // been seen, the serial number is ignored + if (issuer.IsEmpty() || serial.IsEmpty()) { + continue; + } + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, + ("CertBlocklist::Init adding: %s %s", issuer.get(), serial.get())); + rv = AddRevokedCertInternal(issuer.get(), + serial.get(), + 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 + PR_LOG(gCertBlockPRLog, PR_LOG_WARN, + ("CertBlocklist::Init adding revoked cert failed")); + } + } while (more); + + return NS_OK; +} + +// void addRevokedCert (in string issuer, in string serialNumber); +NS_IMETHODIMP +CertBlocklist::AddRevokedCert(const char* aIssuer, + const char* aSerialNumber) +{ + PR_LOG(gCertBlockPRLog, PR_LOG_DEBUG, + ("CertBlocklist::addRevokedCert - issuer is: %s and serial: %s", + aIssuer, aSerialNumber)); + mozilla::MutexAutoLock lock(mMutex); + return AddRevokedCertInternal(aIssuer, + aSerialNumber, + CertNewFromBlocklist, + lock); +} + +nsresult +CertBlocklist::AddRevokedCertInternal(const char* aIssuer, + const char* aSerialNumber, + CertBlocklistItemState aItemState, + mozilla::MutexAutoLock& /*proofOfLock*/) +{ + nsCString decodedIssuer; + nsCString decodedSerial; + + nsresult rv; + rv = mozilla::Base64Decode(nsDependentCString(aIssuer), decodedIssuer); + if (NS_FAILED(rv)) { + return rv; + } + rv = mozilla::Base64Decode(nsDependentCString(aSerialNumber), decodedSerial); + if (NS_FAILED(rv)) { + return rv; + } + + mozilla::pkix::Input issuer; + mozilla::pkix::Input serial; + + mozilla::pkix::Result pkrv; + pkrv = issuer.Init(reinterpret_cast(decodedIssuer.get()), + decodedIssuer.Length()); + if (pkrv != mozilla::pkix::Success) { + return NS_ERROR_FAILURE; + } + pkrv = serial.Init(reinterpret_cast(decodedSerial.get()), + decodedSerial.Length()); + if (pkrv != mozilla::pkix::Success) { + return NS_ERROR_FAILURE; + } + + CertBlocklistItem item(issuer, serial); + + if (aItemState == CertNewFromBlocklist) { + // we want SaveEntries to be a no-op if no new entries are added + if (!mBlocklist.Contains(item)) { + mModified = true; + } + + // 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(item); + item.mIsCurrent = true; + } + mBlocklist.PutEntry(item); + + return NS_OK; +} + +// Data needed for writing blocklist items out to the revocations file +struct BlocklistSaveInfo +{ + IssuerTable issuerTable; + BlocklistStringSet issuers; + nsCOMPtr outputStream; + bool success = true; +}; + +// 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; +} + +// sort blocklist items into lists of serials for each issuer +PLDHashOperator +ProcessEntry(BlocklistItemKey* aHashKey, void* aUserArg) +{ + BlocklistSaveInfo* saveInfo = reinterpret_cast(aUserArg); + CertBlocklistItem item = aHashKey->GetKey(); + + if (!item.mIsCurrent) { + return PL_DHASH_NEXT; + } + + nsAutoCString encIssuer; + nsAutoCString encSerial; + + nsresult rv = item.ToBase64(encIssuer, encSerial); + if (NS_FAILED(rv)) { + saveInfo->success = false; + return PL_DHASH_STOP; + } + + saveInfo->issuers.PutEntry(encIssuer); + BlocklistStringSet* issuerSet = saveInfo->issuerTable.Get(encIssuer); + if (!issuerSet) { + issuerSet = new BlocklistStringSet(); + saveInfo->issuerTable.Put(encIssuer, issuerSet); + } + issuerSet->PutEntry(encSerial); + return PL_DHASH_NEXT; +} + +// write serial data to the output stream +PLDHashOperator +WriteSerial(nsCStringHashKey* aHashKey, void* aUserArg) +{ + BlocklistSaveInfo* saveInfo = reinterpret_cast(aUserArg); + + nsresult rv = WriteLine(saveInfo->outputStream, + NS_LITERAL_CSTRING(" ") + aHashKey->GetKey()); + if (NS_FAILED(rv)) { + saveInfo->success = false; + return PL_DHASH_STOP; + } + return PL_DHASH_NEXT; +} + +// Write issuer data to the output stream +PLDHashOperator +WriteIssuer(nsCStringHashKey* aHashKey, void* aUserArg) +{ + BlocklistSaveInfo* saveInfo = reinterpret_cast(aUserArg); + nsAutoPtr issuerSet; + + saveInfo->issuerTable.RemoveAndForget(aHashKey->GetKey(), issuerSet); + + nsresult rv = WriteLine(saveInfo->outputStream, aHashKey->GetKey()); + if (!NS_SUCCEEDED(rv)) { + return PL_DHASH_STOP; + } + + issuerSet->EnumerateEntries(WriteSerial, saveInfo); + if (!saveInfo->success) { + saveInfo->success = false; + return PL_DHASH_STOP; + } + return PL_DHASH_NEXT; +} + +// 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() +{ + mozilla::MutexAutoLock lock(mMutex); + if (!mModified) { + return NS_OK; + } + if (!mBackingFile) { + // We allow this to succeed with no profile directory for tests + PR_LOG(gCertBlockPRLog, PR_LOG_WARN, + ("CertBlocklist::SaveEntries no file in profile to write to")); + return NS_OK; + } + + BlocklistSaveInfo saveInfo; + nsresult rv; + rv = NS_NewAtomicFileOutputStream(getter_AddRefs(saveInfo.outputStream), + mBackingFile, -1, -1, 0); + if (NS_FAILED(rv)) { + return rv; + } + mBlocklist.EnumerateEntries(ProcessEntry, &saveInfo); + if (!saveInfo.success) { + PR_LOG(gCertBlockPRLog, PR_LOG_WARN, + ("CertBlocklist::SaveEntries writing revocation data failed")); + return NS_ERROR_FAILURE; + } + + rv = WriteLine(saveInfo.outputStream, + NS_LITERAL_CSTRING("# Auto generated contents. Do not edit.")); + if (NS_FAILED(rv)) { + return rv; + } + + saveInfo.issuers.EnumerateEntries(WriteIssuer, &saveInfo); + if (!saveInfo.success) { + PR_LOG(gCertBlockPRLog, PR_LOG_WARN, + ("CertBlocklist::SaveEntries writing revocation data failed")); + return NS_ERROR_FAILURE; + } + + nsCOMPtr safeStream = + do_QueryInterface(saveInfo.outputStream); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (!safeStream) { + return NS_ERROR_FAILURE; + } + rv = safeStream->Finish(); + if (NS_FAILED(rv)) { + PR_LOG(gCertBlockPRLog, PR_LOG_WARN, + ("CertBlocklist::SaveEntries saving revocation data failed")); + return rv; + } + mModified = false; + return NS_OK; +} + +// boolean isCertRevoked([const, array, size_is(issuerLength)] in octet issuer, +// in unsigned long issuerLength, +// [const, array, size_is(serialLength)] in octet serial, +// in unsigned long serialLength); +NS_IMETHODIMP CertBlocklist::IsCertRevoked(const uint8_t* aIssuer, uint32_t aIssuerLength, + const uint8_t* aSerial, uint32_t aSerialLength, + bool* _retval) +{ + mozilla::MutexAutoLock lock(mMutex); + + mozilla::pkix::Input issuer; + mozilla::pkix::Input serial; + if (issuer.Init(aIssuer, aIssuerLength) != mozilla::pkix::Success) { + return NS_ERROR_FAILURE; + } + if (serial.Init(aSerial, aSerialLength) != mozilla::pkix::Success) { + return NS_ERROR_FAILURE; + } + + CertBlocklistItem item(issuer, serial); + + *_retval = mBlocklist.Contains(item); + + return NS_OK; +} diff --git a/security/manager/boot/src/CertBlocklist.h b/security/manager/boot/src/CertBlocklist.h new file mode 100644 index 000000000000..9f2f0151b63a --- /dev/null +++ b/security/manager/boot/src/CertBlocklist.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef CertBlocklist_h +#define CertBlocklist_h + +#include "mozilla/Mutex.h" +#include "nsClassHashtable.h" +#include "nsCOMPtr.h" +#include "nsICertBlocklist.h" +#include "nsIOutputStream.h" +#include "nsTHashtable.h" +#include "nsIX509CertDB.h" +#include "pkix/Input.h" + +#define NS_CERT_BLOCKLIST_CID \ +{0x11aefd53, 0x2fbb, 0x4c92, {0xa0, 0xc1, 0x05, 0x32, 0x12, 0xae, 0x42, 0xd0} } + +enum CertBlocklistItemState { + CertNewFromBlocklist, + CertOldFromLocalCache +}; + +class CertBlocklistItem +{ +public: + CertBlocklistItem(mozilla::pkix::Input aIssuer, mozilla::pkix::Input aSerial); + CertBlocklistItem(const CertBlocklistItem& aItem); + ~CertBlocklistItem(); + nsresult ToBase64(nsACString& b64IssuerOut, nsACString& b64SerialOut); + bool operator==(const CertBlocklistItem& aItem) const; + uint32_t Hash() const; + bool mIsCurrent = false; + +private: + mozilla::pkix::Input mIssuer; + uint8_t* mIssuerData; + mozilla::pkix::Input mSerial; + uint8_t* mSerialData; +}; + +typedef nsGenericHashKey BlocklistItemKey; +typedef nsTHashtable BlocklistTable; +typedef nsTHashtable BlocklistStringSet; +typedef nsClassHashtable IssuerTable; + +class CertBlocklist : public nsICertBlocklist +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICERTBLOCKLIST + CertBlocklist(); + nsresult Init(); + +private: + BlocklistTable mBlocklist; + nsresult AddRevokedCertInternal(const char* aIssuer, + const char* aSerial, + CertBlocklistItemState aItemState, + mozilla::MutexAutoLock& /*proofOfLock*/); + mozilla::Mutex mMutex; + bool mModified = false; + nsCOMPtr mBackingFile; + +protected: + virtual ~CertBlocklist(); +}; + +#endif // CertBlocklist_h diff --git a/security/manager/boot/src/moz.build b/security/manager/boot/src/moz.build index f6de356b51bb..be97e636b721 100644 --- a/security/manager/boot/src/moz.build +++ b/security/manager/boot/src/moz.build @@ -9,6 +9,7 @@ EXPORTS.mozilla += [ ] UNIFIED_SOURCES += [ + 'CertBlocklist.cpp', 'DataStorage.cpp', 'nsBOOTModule.cpp', 'nsEntropyCollector.cpp', diff --git a/security/manager/boot/src/nsBOOTModule.cpp b/security/manager/boot/src/nsBOOTModule.cpp index ae95cb481e81..62e1b6374a3b 100644 --- a/security/manager/boot/src/nsBOOTModule.cpp +++ b/security/manager/boot/src/nsBOOTModule.cpp @@ -5,6 +5,7 @@ #include "mozilla/ModuleUtils.h" +#include "CertBlocklist.h" #include "nsEntropyCollector.h" #include "nsSecureBrowserUIImpl.h" #include "nsSecurityWarningDialogs.h" @@ -12,6 +13,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsEntropyCollector) NS_GENERIC_FACTORY_CONSTRUCTOR(nsSecureBrowserUIImpl) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(CertBlocklist, Init) NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsSecurityWarningDialogs, Init) NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsSiteSecurityService, Init) @@ -19,12 +21,14 @@ NS_DEFINE_NAMED_CID(NS_ENTROPYCOLLECTOR_CID); NS_DEFINE_NAMED_CID(NS_SECURITYWARNINGDIALOGS_CID); NS_DEFINE_NAMED_CID(NS_SECURE_BROWSER_UI_CID); NS_DEFINE_NAMED_CID(NS_SITE_SECURITY_SERVICE_CID); +NS_DEFINE_NAMED_CID(NS_CERT_BLOCKLIST_CID); static const mozilla::Module::CIDEntry kBOOTCIDs[] = { { &kNS_ENTROPYCOLLECTOR_CID, false, nullptr, nsEntropyCollectorConstructor }, { &kNS_SECURITYWARNINGDIALOGS_CID, false, nullptr, nsSecurityWarningDialogsConstructor }, { &kNS_SECURE_BROWSER_UI_CID, false, nullptr, nsSecureBrowserUIImplConstructor }, { &kNS_SITE_SECURITY_SERVICE_CID, false, nullptr, nsSiteSecurityServiceConstructor }, + { &kNS_CERT_BLOCKLIST_CID, false, nullptr, CertBlocklistConstructor}, { nullptr } }; @@ -33,6 +37,7 @@ static const mozilla::Module::ContractIDEntry kBOOTContracts[] = { { NS_SECURITYWARNINGDIALOGS_CONTRACTID, &kNS_SECURITYWARNINGDIALOGS_CID }, { NS_SECURE_BROWSER_UI_CONTRACTID, &kNS_SECURE_BROWSER_UI_CID }, { NS_SSSERVICE_CONTRACTID, &kNS_SITE_SECURITY_SERVICE_CID }, + { NS_CERTBLOCKLIST_CONTRACTID, &kNS_CERT_BLOCKLIST_CID }, { nullptr } }; diff --git a/security/manager/ssl/src/nsNSSComponent.cpp b/security/manager/ssl/src/nsNSSComponent.cpp index f5a2a5ad7625..2eec123e0c82 100644 --- a/security/manager/ssl/src/nsNSSComponent.cpp +++ b/security/manager/ssl/src/nsNSSComponent.cpp @@ -7,15 +7,16 @@ #include "nsNSSComponent.h" #include "ExtendedValidation.h" -#include "NSSCertDBTrustDomain.h" #include "mozilla/Telemetry.h" -#include "nsCertVerificationThread.h" #include "nsAppDirectoryServiceDefs.h" +#include "nsCertVerificationThread.h" #include "nsComponentManagerUtils.h" #include "nsDirectoryServiceDefs.h" +#include "nsICertBlocklist.h" #include "nsICertOverrideService.h" -#include "mozilla/Preferences.h" +#include "NSSCertDBTrustDomain.h" #include "nsThreadUtils.h" +#include "mozilla/Preferences.h" #include "mozilla/PublicSSL.h" #include "mozilla/StaticPtr.h" @@ -1071,6 +1072,12 @@ nsNSSComponent::InitializeNSS() return NS_ERROR_FAILURE; } + // ensure the CertBlocklist is initialised + nsCOMPtr certList = do_GetService(NS_CERTBLOCKLIST_CONTRACTID); + if (!certList) { + return NS_ERROR_FAILURE; + } + // dynamic options from prefs setValidationOptions(true, lock); diff --git a/security/manager/ssl/tests/unit/test_cert_blocklist.js b/security/manager/ssl/tests/unit/test_cert_blocklist.js new file mode 100644 index 000000000000..ecd56c83bbd1 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_cert_blocklist.js @@ -0,0 +1,264 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// This test checks a number of things: +// * it ensures that data loaded from revocations.txt on startup is present +// * it ensures that certItems in blocklist.xml are persisted correctly +// * it ensures that items in the CertBlocklist are seen as revoked by the +// cert verifier +// * it does a sanity check to ensure other cert verifier behavior is +// unmodified + +let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +// First, we need to setup appInfo for the blocklist service to work +let id = "xpcshell@tests.mozilla.org"; +let appName = "XPCShell"; +let version = "1"; +let platformVersion = "1.9.2"; +let appInfo = { + // nsIXULAppInfo + vendor: "Mozilla", + name: appName, + ID: id, + version: version, + appBuildID: "2007010101", + platformVersion: platformVersion ? platformVersion : "1.0", + platformBuildID: "2007010101", + + // nsIXULRuntime + inSafeMode: false, + logConsoleErrors: true, + OS: "XPCShell", + XPCOMABI: "noarch-spidermonkey", + invalidateCachesOnRestart: function invalidateCachesOnRestart() { + // Do nothing + }, + + // nsICrashReporter + annotations: {}, + + annotateCrashReport: function(key, data) { + this.annotations[key] = data; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, + Ci.nsIXULRuntime, + Ci.nsICrashReporter, + Ci.nsISupports]) +}; + +let XULAppInfoFactory = { + createInstance: function (outer, iid) { + appInfo.QueryInterface(iid); + if (outer != null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return appInfo.QueryInterface(iid); + } +}; + +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1"; +const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}"); +registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo", + XULAPPINFO_CONTRACTID, XULAppInfoFactory); + +// we need to ensure we setup revocation data before certDB, or we'll start with +// no revocation.txt in the profile +let profile = do_get_profile(); +let revocations = profile.clone(); +revocations.append("revocations.txt"); +if (!revocations.exists()) { + let existing = do_get_file("test_onecrl/sample_revocations.txt", false); + existing.copyTo(profile,"revocations.txt"); +} + +let certDB = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + +// set up a test server to serve the blocklist.xml +let testserver = new HttpServer(); + +let blocklist_contents = + "" + + "" + + // test with some bad data ... + "" + + "AkHVNA==" + + "" + + "some nonsense in serial" + + "" + + "and serial" + + // some mixed + // In this case, the issuer name and the valid serialNumber correspond + // to test-int.der in tlsserver/ + "" + + "oops! more nonsense." + + "BA==" + + // ... and some good + // This item corresponds to an entry in sample_revocations.txt where: + // isser name is "another imaginary issuer" base-64 encoded, and + // serialNumbers are: + // "serial2." base-64 encoded, and + // "another serial." base-64 encoded + // We need this to ensure that existing items are retained if they're + // also in the blocklist + "" + + "c2VyaWFsMi4=" + + "YW5vdGhlciBzZXJpYWwu" + + ""; +testserver.registerPathHandler("/push_blocked_cert/", + function serveResponse(request, response) { + response.write(blocklist_contents); + }); + +// start the test server +testserver.start(-1); +let port = testserver.identity.primaryPort; + +// Setup the addonManager +let addonManager = Cc["@mozilla.org/addons/integration;1"] + .getService(Ci.nsIObserver) + .QueryInterface(Ci.nsITimerCallback); +addonManager.observe(null, "addons-startup", null); + +let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); +converter.charset = "UTF-8"; + +function verify_cert(file, expectedError) { + let cert_der = readFile(do_get_file(file)); + let ee = certDB.constructX509(cert_der, cert_der.length); + equal(expectedError, certDB.verifyCertNow(ee, certificateUsageSSLServer, + NO_FLAGS, {}, {})); +} + +function load_cert(cert, trust) { + let file = "tlsserver/" + cert + ".der"; + addCertFromFile(certDB, file, trust); +} + +function testIsRevoked(certList, issuerString, serialString) { + let issuer = converter.convertToByteArray(issuerString, {}); + let serial = converter.convertToByteArray(serialString, {}); + return certList.isCertRevoked(issuer, + issuerString.length, + serial, + serialString.length); +} + +function run_test() { + // import the certificates we need + load_cert("test-ca", "CTu,CTu,CTu"); + load_cert("test-int", ",,"); + + let certList = Cc["@mozilla.org/security/certblocklist;1"] + .getService(Ci.nsICertBlocklist); + + // check some existing items in revocations.txt are blocked. Since the + // CertBlocklistItems don't know about the data they contain, we can use + // arbitrary data (not necessarily DER) to test if items are revoked or not. + // This test corresponds to: + // issuer: c29tZSBpbWFnaW5hcnkgaXNzdWVy + // serial: c2VyaWFsLg== + ok(testIsRevoked(certList, "some imaginary issuer","serial."), + "issuer / serial pair should be blocked"); + + // And this test corresponds to: + // issuer: YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy + // serial: c2VyaWFsMi4= + ok(testIsRevoked(certList, "another imaginary issuer","serial2."), + "issuer / serial pair should be blocked"); + + // Soon we'll load a blocklist which revokes test-int.der, which issued + // test-int-ee.der. + // Check the cert validates before we load the blocklist + let file = "tlsserver/test-int-ee.der"; + verify_cert(file, Cr.NS_OK); + + // blocklist load is async so we must use add_test from here + add_test(function() { + let certblockObserver = { + observe: function(aSubject, aTopic, aData) { + run_next_test(); + Services.obs.removeObserver(this, "blocklist-updated"); + } + } + + Services.obs.addObserver(certblockObserver, "blocklist-updated", false); + Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + + port + "/push_blocked_cert/"); + let blocklist = Cc["@mozilla.org/extensions/blocklist;1"] + .getService(Ci.nsITimerCallback); + blocklist.notify(null); + }); + + add_test(function() { + // The blocklist will be loaded now. Let's check the data is sane. + // In particular, we should still have the revoked issuer / serial pair + // that was in both revocations.txt and the blocklist.xml + ok(testIsRevoked(certList, "another imaginary issuer", "serial2."), + "issuer / serial pair should be blocked"); + + // Check that both serials in the certItem with multiple serials were read + // properly + ok(testIsRevoked(certList, "another imaginary issuer", "serial2."), + "issuer / serial pair should be blocked"); + ok(testIsRevoked(certList, "another imaginary issuer", "another serial."), + "issuer / serial pair should be blocked"); + + // Check the blocklist entry has been persisted properly to the backing + // file + let profile = do_get_profile(); + let revocations = profile.clone(); + revocations.append("revocations.txt"); + ok(revocations.exists(), "the revocations file should exist"); + let inputStream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + inputStream.init(revocations,-1, -1, 0); + inputStream.QueryInterface(Ci.nsILineInputStream); + let contents = ""; + let hasmore = false; + do { + var line = {}; + hasmore = inputStream.readLine(line); + contents = contents + (contents.length == 0 ? "" : "\n") + line.value; + } while (hasmore); + let expected = "# Auto generated contents. Do not edit.\n" + + "MBIxEDAOBgNVBAMTB1Rlc3QgQ0E=\n" + + " BA==\n" + + "YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy\n" + + " YW5vdGhlciBzZXJpYWwu\n" + + " c2VyaWFsMi4="; + equal(contents, expected, "revocations.txt should be as expected"); + + // Check the blocklisted intermediate now causes a failure + let file = "tlsserver/test-int-ee.der"; + verify_cert(file, SEC_ERROR_REVOKED_CERTIFICATE); + + // Check a non-blocklisted chain still validates OK + file = "tlsserver/default-ee.der"; + verify_cert(file, Cr.NS_OK); + + // Check a bad cert is still bad (unknown issuer) + file = "tlsserver/unknown-issuer.der"; + verify_cert(file, SEC_ERROR_UNKNOWN_ISSUER); + + // check that save with no further update is a no-op + let lastModified = revocations.lastModifiedTime; + // add an already existing entry + certList.addRevokedCert("YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy","c2VyaWFsMi4="); + certList.saveEntries(); + let newModified = revocations.lastModifiedTime; + equal(lastModified, newModified, + "saveEntries with no modifications should not update the backing file"); + + run_next_test(); + }); + + // we need to start the async portions of the test + run_next_test(); +} diff --git a/security/manager/ssl/tests/unit/test_onecrl/sample_revocations.txt b/security/manager/ssl/tests/unit/test_onecrl/sample_revocations.txt new file mode 100644 index 000000000000..d16b325d473b --- /dev/null +++ b/security/manager/ssl/tests/unit/test_onecrl/sample_revocations.txt @@ -0,0 +1,26 @@ +# a sample revocations.txt for tests +# Lines starting with '#' are ignored - as are empty lines like this: + +# otherwise: +# non-empty lines are treated as base-64 encoded DER issuer data +# ...unless the line starts with a ' ' (space) character, in which case it's +# assumed to be base-64 encoded DER serial data. + +# First a serial with no issuer to ensure this doesn't cause parsing to fail +# (there should be an issuer first, but we need to test this won't fail) + dGVzdA== +# next, let's ensure data that isn't valid base64 doesn't cause breakage. + this serial isn't valid base64 (but then there's no issuer anyway) +Neither is this issuer, though the serial is fine + dGVzdA== +dGVzdA== + in this case, issuer is fine but not the serial +# Next two entries; we can add valid base-64 encoded data for some basic tests: +# issuer is "some imaginary issuer" base-64 encoded +# and serial "serial." base-64 encoded +c29tZSBpbWFnaW5hcnkgaXNzdWVy + c2VyaWFsLg== +# issuer is "another imaginary issuer" base-64 encoded +# serial is "serial2." base-64 encoded +YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy + c2VyaWFsMi4= diff --git a/security/manager/ssl/tests/unit/tlsserver/cert9.db b/security/manager/ssl/tests/unit/tlsserver/cert9.db index 87d341115bbe..48dd5a9525d4 100644 Binary files a/security/manager/ssl/tests/unit/tlsserver/cert9.db and b/security/manager/ssl/tests/unit/tlsserver/cert9.db differ diff --git a/security/manager/ssl/tests/unit/tlsserver/generate_certs.sh b/security/manager/ssl/tests/unit/tlsserver/generate_certs.sh index 9fdebace9e69..48d964b974a7 100755 --- a/security/manager/ssl/tests/unit/tlsserver/generate_certs.sh +++ b/security/manager/ssl/tests/unit/tlsserver/generate_certs.sh @@ -267,6 +267,7 @@ export_cert localhostAndExampleCom default-ee.der make_EE ocspOtherEndEntity 'CN=Other Cert' testCA "localhost,*.example.com" make_INT testINT 'CN=Test Intermediate' testCA +export_cert testINT test-int.der make_EE ocspEEWithIntermediate 'CN=Test End-entity with Intermediate' testINT "localhost,*.example.com" make_EE expired 'CN=Expired Test End-entity' testCA "expired.example.com" "-w -400" export_cert expired expired-ee.der @@ -279,6 +280,7 @@ make_EE selfsigned 'CN=Self-signed Test End-entity' testCA "selfsigned.example.c # get regenerated. Either way, deletedINT will then be removed again. make_INT deletedINT 'CN=Test Intermediate to delete' testCA make_EE unknownissuer 'CN=Test End-entity from unknown issuer' deletedINT "unknownissuer.example.com" +export_cert unknownissuer unknown-issuer.der $RUN_MOZILLA $CERTUTIL -d $DB_ARGUMENT -D -n deletedINT @@ -320,4 +322,8 @@ make_V1 v1Cert 'CN=V1 Cert' testCA export_cert v1Cert v1Cert.der make_EE eeIssuedByV1Cert 'CN=EE Issued by V1 Cert' v1Cert "localhost,*.example.com" +# Make a valid EE using testINT to test OneCRL revocation of testINT +make_EE eeIssuedByIntermediate 'CN=EE issued by intermediate' testINT "localhost" +export_cert eeIssuedByIntermediate test-int-ee.der + cleanup diff --git a/security/manager/ssl/tests/unit/tlsserver/key4.db b/security/manager/ssl/tests/unit/tlsserver/key4.db index 5598271c5a31..81e70d293046 100644 Binary files a/security/manager/ssl/tests/unit/tlsserver/key4.db and b/security/manager/ssl/tests/unit/tlsserver/key4.db differ diff --git a/security/manager/ssl/tests/unit/tlsserver/test-int-ee.der b/security/manager/ssl/tests/unit/tlsserver/test-int-ee.der new file mode 100644 index 000000000000..f534b74c9920 Binary files /dev/null and b/security/manager/ssl/tests/unit/tlsserver/test-int-ee.der differ diff --git a/security/manager/ssl/tests/unit/tlsserver/test-int.der b/security/manager/ssl/tests/unit/tlsserver/test-int.der new file mode 100644 index 000000000000..72318b65f026 Binary files /dev/null and b/security/manager/ssl/tests/unit/tlsserver/test-int.der differ diff --git a/security/manager/ssl/tests/unit/tlsserver/unknown-issuer.der b/security/manager/ssl/tests/unit/tlsserver/unknown-issuer.der new file mode 100644 index 000000000000..59518ea3f16a Binary files /dev/null and b/security/manager/ssl/tests/unit/tlsserver/unknown-issuer.der differ diff --git a/security/manager/ssl/tests/unit/xpcshell.ini b/security/manager/ssl/tests/unit/xpcshell.ini index d61c6325e10a..5b3427c02be3 100644 --- a/security/manager/ssl/tests/unit/xpcshell.ini +++ b/security/manager/ssl/tests/unit/xpcshell.ini @@ -18,6 +18,7 @@ support-files = test_ocsp_fetch_method/** test_keysize/** test_pinning_dynamic/** + test_onecrl/** [test_datasignatureverifier.js] [test_hash_algorithms.js] @@ -46,6 +47,7 @@ skip-if = buildapp == "b2g" && processor == "arm" run-sequentially = hardcoded ports # Bug 1009158: this test times out on Android skip-if = os == "android" +[test_cert_blocklist.js] [test_ocsp_stapling_expired.js] run-sequentially = hardcoded ports # Bug 1009158: this test times out on Android diff --git a/toolkit/mozapps/extensions/nsBlocklistService.js b/toolkit/mozapps/extensions/nsBlocklistService.js index 6f5d0ab656ea..bd931e950a29 100644 --- a/toolkit/mozapps/extensions/nsBlocklistService.js +++ b/toolkit/mozapps/extensions/nsBlocklistService.js @@ -74,6 +74,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"); +XPCOMUtils.defineLazyServiceGetter(this, "gCertBlocklistService", + "@mozilla.org/security/certblocklist;1", + "nsICertBlocklist"); + XPCOMUtils.defineLazyGetter(this, "gPref", function bls_gPref() { return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService). QueryInterface(Ci.nsIPrefBranch); @@ -725,6 +729,13 @@ Blocklist.prototype = { # # # +# +# +# +# +# AkHVNA== +# +# # */ @@ -862,12 +873,17 @@ Blocklist.prototype = { this._pluginEntries = this._processItemNodes(element.childNodes, "plugin", this._handlePluginItemNode); break; + case "certItems": + this._processItemNodes(element.childNodes, "cert", + this._handleCertItemNode.bind(this)); + break; default: Services.obs.notifyObservers(element, "blocklist-data-" + element.localName, null); } } + gCertBlocklistService.saveEntries(); } catch (e) { LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e); @@ -889,6 +905,22 @@ Blocklist.prototype = { return result; }, + _handleCertItemNode: function Blocklist_handleCertItemNode(blocklistElement, + result) { + let issuer = blocklistElement.getAttribute("issuerName"); + for (let snElement of blocklistElement.children) { + try { + if (issuer) { + gCertBlocklistService.addRevokedCert(issuer, snElement.textContent); + } + } catch (e) { + // we want to keep trying other elements since missing all items + // is worse than missing one + LOG("Blocklist::_handleCertItemNode: Error adding revoked cert " + e); + } + } + }, + _handleEmItemNode: function Blocklist_handleEmItemNode(blocklistElement, result) { if (!matchesOSABI(blocklistElement)) return;