Bug 1630434 - de-duplicate preloaded intermediates that may have been cached in cert9.db r=kjacobs,bbeurdouche

In general, PSM caches intermediates from verified certificate chains in the
NSS certdb. Before bug 1619021, this would include preloaded intermediates,
which is unnecessary because cert_storage has a copy of those certificates, and
so they don't need to take up time and space in the NSS certdb. This patch
introduces the intermediate preloading healer, which periodically runs on a
background thread, looks for these duplicate intermediates, and removes them
from the NSS certdb.

Differential Revision: https://phabricator.services.mozilla.com/D77152
This commit is contained in:
Dana Keeler 2020-06-09 18:02:52 +00:00
Родитель cfbc528f9c
Коммит 63919c509b
4 изменённых файлов: 286 добавлений и 2 удалений

Просмотреть файл

@ -194,9 +194,12 @@ pref("security.pki.mitm_detected", false);
// Intermediate CA Preloading settings
#if defined(MOZ_NEW_CERT_STORAGE) && !defined(MOZ_WIDGET_ANDROID)
pref("security.remote_settings.intermediates.enabled", true);
pref("security.intermediate_preloading_healer.enabled", true);
#else
pref("security.remote_settings.intermediates.enabled", false);
pref("security.intermediate_preloading_healer.enabled", false);
#endif
pref("security.intermediate_preloading_healer.timer_interval_ms", 300000);
pref("security.remote_settings.intermediates.bucket", "security-state");
pref("security.remote_settings.intermediates.collection", "intermediates");
pref("security.remote_settings.intermediates.checked", 0);

Просмотреть файл

@ -41,6 +41,8 @@
#include "nsIOService.h"
#include "nsIPrompt.h"
#include "nsIProperties.h"
#include "nsISerialEventTarget.h"
#include "nsITimer.h"
#include "nsITokenPasswordDialogs.h"
#include "nsIWindowWatcher.h"
#include "nsIXULRuntime.h"
@ -65,6 +67,10 @@
#include "prmem.h"
#include "GeckoProfiler.h"
#ifdef MOZ_NEW_CERT_STORAGE
# include "cert_storage/src/cert_storage.h"
#endif
#if defined(XP_LINUX) && !defined(ANDROID)
# include <linux/magic.h>
# include <sys/vfs.h>
@ -2115,6 +2121,11 @@ void nsNSSComponent::ShutdownNSS() {
Preferences::RemoveObserver(this, "security.");
if (mIntermediatePreloadingHealerTimer) {
mIntermediatePreloadingHealerTimer->Cancel();
mIntermediatePreloadingHealerTimer = nullptr;
}
// Release the default CertVerifier. This will cause any held NSS resources
// to be released.
MutexAutoLock lock(mMutex);
@ -2124,6 +2135,138 @@ void nsNSSComponent::ShutdownNSS() {
// be any XPCOM objects holding NSS resources).
}
#ifdef MOZ_NEW_CERT_STORAGE
// The aim of the intermediate preloading healer is to remove intermediates
// that were previously cached by PSM in the NSS certdb that are now preloaded
// in cert_storage. When cached by PSM, these certificates will have no
// particular trust set - they are intended to inherit their trust. If, upon
// examination, these certificates do have trust bits set that affect
// certificate validation, they must have been modified by the user, so we want
// to leave them alone.
bool CertHasDefaultTrust(CERTCertificate* cert) {
CERTCertTrust trust;
if (CERT_GetCertTrust(cert, &trust) != SECSuccess) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("CERT_GetCertTrust failed"));
return false;
}
// This is the active distrust test for CA certificates (this is expected to
// be an intermediate).
if ((trust.sslFlags & (CERTDB_TRUSTED_CA | CERTDB_TERMINAL_RECORD)) ==
CERTDB_TERMINAL_RECORD) {
return false;
}
// This is the trust anchor test.
if (trust.sslFlags & CERTDB_TRUSTED_CA) {
return false;
}
// This is the active distrust test for CA certificates (this is expected to
// be an intermediate).
if ((trust.emailFlags & (CERTDB_TRUSTED_CA | CERTDB_TERMINAL_RECORD)) ==
CERTDB_TERMINAL_RECORD) {
return false;
}
// This is the trust anchor test.
if (trust.emailFlags & CERTDB_TRUSTED_CA) {
return false;
}
return true;
}
void IntermediatePreloadingHealerCallback(nsITimer*, void*) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("IntermediatePreloadingHealerCallback"));
// Get the slot corresponding to the NSS certdb.
UniquePK11SlotInfo softokenSlot(PK11_GetInternalKeySlot());
if (!softokenSlot) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("PK11_GetInternalKeySlot failed"));
return;
}
// List the certificates in the NSS certdb.
UniqueCERTCertList softokenCertificates(
PK11_ListCertsInSlot(softokenSlot.get()));
if (!softokenCertificates) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("PK11_ListCertsInSlot failed"));
return;
}
nsCOMPtr<nsICertStorage> certStorage(do_GetService(NS_CERT_STORAGE_CID));
if (!certStorage) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get cert_storage"));
return;
}
Vector<UniqueCERTCertificate> certsToDelete;
// For each certificate, look it up in cert_storage. If there's a match, this
// is a preloaded intermediate.
for (CERTCertListNode* n = CERT_LIST_HEAD(softokenCertificates);
!CERT_LIST_END(n, softokenCertificates); n = CERT_LIST_NEXT(n)) {
nsTArray<uint8_t> subject;
subject.AppendElements(n->cert->derSubject.data, n->cert->derSubject.len);
nsTArray<nsTArray<uint8_t>> certs;
nsresult rv = certStorage->FindCertsBySubject(subject, certs);
if (NS_FAILED(rv)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("FindCertsBySubject failed"));
break;
}
for (const auto& encodedCert : certs) {
if (encodedCert.Length() != n->cert->derCert.len) {
continue;
}
if (memcmp(encodedCert.Elements(), n->cert->derCert.data,
encodedCert.Length()) != 0) {
continue;
}
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("found preloaded intermediate in certdb"));
if (!CertHasDefaultTrust(n->cert)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("certificate doesn't have default trust - skipping"));
continue;
}
UniqueCERTCertificate certCopy(CERT_DupCertificate(n->cert));
if (!certCopy) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("CERT_DupCertificate failed"));
continue;
}
// Note that we want to remove this certificate from the NSS certdb
// because it also exists in preloaded intermediate storage and is thus
// superfluous.
if (!certsToDelete.append(std::move(certCopy))) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("append failed - out of memory?"));
return;
}
break;
}
// Only delete 20 at a time.
if (certsToDelete.length() >= 20) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("found limit of 20 preloaded intermediates in certdb"));
break;
}
}
for (const auto& certToDelete : certsToDelete) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("attempting to delete preloaded intermediate '%s'",
certToDelete->subjectName));
if (SEC_DeletePermCertificate(certToDelete.get()) != SECSuccess) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("SEC_DeletePermCertificate failed"));
}
}
// This is for tests - notify that this ran.
nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
"IntermediatePreloadingHealerCallbackDone", []() -> void {
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService) {
observerService->NotifyObservers(
nullptr, "psm:intermediate-preloading-healer-ran", nullptr);
}
}));
Unused << NS_DispatchToMainThread(runnable.forget());
}
#endif // MOZ_NEW_CERT_STORAGE
nsresult nsNSSComponent::Init() {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
if (!NS_IsMainThread()) {
@ -2147,7 +2290,64 @@ nsresult nsNSSComponent::Init() {
return rv;
}
return RegisterObservers();
rv = RegisterObservers();
if (NS_FAILED(rv)) {
return rv;
}
rv = MaybeEnableIntermediatePreloadingHealer();
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
nsresult nsNSSComponent::MaybeEnableIntermediatePreloadingHealer() {
#ifdef MOZ_NEW_CERT_STORAGE
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("nsNSSComponent::MaybeEnableIntermediatePreloadingHealer"));
MOZ_ASSERT(NS_IsMainThread());
if (!NS_IsMainThread()) {
return NS_ERROR_NOT_SAME_THREAD;
}
if (mIntermediatePreloadingHealerTimer) {
mIntermediatePreloadingHealerTimer->Cancel();
mIntermediatePreloadingHealerTimer = nullptr;
}
if (!Preferences::GetBool("security.intermediate_preloading_healer.enabled",
false)) {
return NS_OK;
}
if (!mIntermediatePreloadingHealerTaskQueue) {
nsresult rv = NS_CreateBackgroundTaskQueue(
"IntermediatePreloadingHealer",
getter_AddRefs(mIntermediatePreloadingHealerTaskQueue));
if (NS_FAILED(rv)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Error,
("NS_CreateBackgroundTaskQueue failed"));
return rv;
}
}
uint32_t timerDelayMS = Preferences::GetUint(
"security.intermediate_preloading_healer.timer_interval_ms",
5 * 60 * 1000);
nsresult rv = NS_NewTimerWithFuncCallback(
getter_AddRefs(mIntermediatePreloadingHealerTimer),
IntermediatePreloadingHealerCallback, nullptr, timerDelayMS,
nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY,
"IntermediatePreloadingHealer", mIntermediatePreloadingHealerTaskQueue);
if (NS_FAILED(rv)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Error,
("NS_NewTimerWithFuncCallback failed"));
return rv;
}
#endif // MOZ_NEW_CERT_STORAGE
return NS_OK;
}
// nsISupports Implementation for the class
@ -2230,6 +2430,13 @@ nsNSSComponent::Observe(nsISupports* aSubject, const char* aTopic,
if (clearSessionCache) {
ClearSSLExternalAndInternalSessionCacheNative();
}
// Preferences that don't affect certificate verification.
if (prefName.Equals("security.intermediate_preloading_healer.enabled") ||
prefName.Equals(
"security.intermediate_preloading_healer.timer_interval_ms")) {
MaybeEnableIntermediatePreloadingHealer();
}
}
return NS_OK;

Просмотреть файл

@ -29,7 +29,8 @@
class nsIDOMWindow;
class nsIPrompt;
class SmartCardThreadList;
class nsISerialEventTarget;
class nsITimer;
namespace mozilla {
namespace psm {
@ -107,6 +108,8 @@ class nsNSSComponent final : public nsINSSComponent, public nsIObserver {
bool ShouldEnableEnterpriseRootsForFamilySafety(uint32_t familySafetyMode);
nsresult MaybeEnableIntermediatePreloadingHealer();
// mLoadableCertsLoadedMonitor protects mLoadableCertsLoaded.
mozilla::Monitor mLoadableCertsLoadedMonitor;
bool mLoadableCertsLoaded;
@ -136,6 +139,13 @@ class nsNSSComponent final : public nsINSSComponent, public nsIObserver {
// to complete (because it will never complete) so we use this boolean to keep
// track of if we should wait.
bool mLoadLoadableCertsTaskDispatched;
// If the intermediate preloading healer is enabled, the following timer
// periodically dispatches events to the background task queue. Each of these
// events scans the NSS certdb for preloaded intermediates that are in
// cert_storage and thus can be removed. By default, the interval is 5
// minutes.
nsCOMPtr<nsISerialEventTarget> mIntermediatePreloadingHealerTaskQueue;
nsCOMPtr<nsITimer> mIntermediatePreloadingHealerTimer;
};
inline nsresult BlockUntilLoadableCertsLoaded() {

Просмотреть файл

@ -586,6 +586,70 @@ add_task(
}
);
function findCertByCommonName(certDB, commonName) {
for (let cert of certDB.getCerts()) {
if (cert.commonName == commonName) {
return cert;
}
}
return null;
}
add_task(
{
skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
},
async function test_healer() {
Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100);
let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
// Add an intermediate as if it had previously been cached.
addCertFromFile(certDB, "test_intermediate_preloads/int.pem", ",,");
// Add an intermediate with non-default trust settings as if it had been added by the user.
addCertFromFile(certDB, "test_intermediate_preloads/int2.pem", "CTu,,");
let syncResult = await syncAndDownload(["int.pem", "int2.pem"]);
equal(syncResult, "success", "Preloading update should have run");
equal(
(await locallyDownloaded()).length,
2,
"There should have been 2 downloads"
);
let healerRanPromise = TestUtils.topicObserved(
"psm:intermediate-preloading-healer-ran"
);
Services.prefs.setIntPref(
"security.intermediate_preloading_healer.timer_interval_ms",
500
);
Services.prefs.setBoolPref(
"security.intermediate_preloading_healer.enabled",
true
);
await healerRanPromise;
Services.prefs.setBoolPref(
"security.intermediate_preloading_healer.enabled",
false
);
let intermediate = findCertByCommonName(
certDB,
"intermediate-preloading-intermediate"
);
equal(intermediate, null, "should not find intermediate in NSS");
let intermediate2 = findCertByCommonName(
certDB,
"intermediate-preloading-intermediate2"
);
notEqual(intermediate2, null, "should find second intermediate in NSS");
}
);
function run_test() {
server = new HttpServer();
server.start(-1);