bug 1592111 - add the preference "security.osclientcerts.autoload" to control auto-loading the OS client certs module r=jcj
authorDana Keeler <dkeeler@mozilla.com>
Wed, 13 Nov 2019 21:19:57 +0000
changeset 501827 5127c3b7168486c70e084bc45c324c9443cffc92
parent 501826 7284aee6b994ea181160cecee018ce30506988de
child 501828 8265027727efb5904ceb3efd7c20d07f28119f2b
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjcj
bugs1592111
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1592111 - add the preference "security.osclientcerts.autoload" to control auto-loading the OS client certs module r=jcj Differential Revision: https://phabricator.services.mozilla.com/D52288
security/certverifier/CertVerifier.cpp
security/certverifier/NSSCertDBTrustDomain.cpp
security/certverifier/NSSCertDBTrustDomain.h
security/manager/ssl/PKCS11ModuleDB.cpp
security/manager/ssl/nsINSSComponent.idl
security/manager/ssl/nsNSSCertificateDB.cpp
security/manager/ssl/nsNSSComponent.cpp
security/manager/ssl/nsNSSComponent.h
security/manager/ssl/tests/unit/head_psm.js
security/manager/ssl/tests/unit/test_osclientcerts_module.js
--- a/security/certverifier/CertVerifier.cpp
+++ b/security/certverifier/CertVerifier.cpp
@@ -173,17 +173,17 @@ Result IsDelegatedCredentialAcceptable(c
   return Result::Success;
 }
 
 // The term "builtin root" traditionally refers to a root CA certificate that
 // has been added to the NSS trust store, because it has been approved
 // for inclusion according to the Mozilla CA policy, and might be accepted
 // by Mozilla applications as an issuer for certificates seen on the public web.
 Result IsCertBuiltInRoot(CERTCertificate* cert, bool& result) {
-  if (NS_FAILED(BlockUntilLoadableRootsLoaded())) {
+  if (NS_FAILED(BlockUntilLoadableCertsLoaded())) {
     return Result::FATAL_ERROR_LIBRARY_FAILURE;
   }
 
   result = false;
 #ifdef DEBUG
   nsCOMPtr<nsINSSComponent> component(do_GetService(PSM_COMPONENT_CONTRACTID));
   if (!component) {
     return Result::FATAL_ERROR_LIBRARY_FAILURE;
@@ -487,17 +487,17 @@ Result CertVerifier::VerifyCert(
     /*optional out*/ CertificateTransparencyInfo* ctInfo) {
   MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Top of VerifyCert\n"));
 
   MOZ_ASSERT(cert);
   MOZ_ASSERT(usage == certificateUsageSSLServer || !(flags & FLAG_MUST_BE_EV));
   MOZ_ASSERT(usage == certificateUsageSSLServer || !keySizeStatus);
   MOZ_ASSERT(usage == certificateUsageSSLServer || !sha1ModeResult);
 
-  if (NS_FAILED(BlockUntilLoadableRootsLoaded())) {
+  if (NS_FAILED(BlockUntilLoadableCertsLoaded())) {
     return Result::FATAL_ERROR_LIBRARY_FAILURE;
   }
   if (NS_FAILED(CheckForSmartCardChanges())) {
     return Result::FATAL_ERROR_LIBRARY_FAILURE;
   }
 
   if (evOidPolicy) {
     *evOidPolicy = SEC_OID_UNKNOWN;
--- a/security/certverifier/NSSCertDBTrustDomain.cpp
+++ b/security/certverifier/NSSCertDBTrustDomain.cpp
@@ -1298,17 +1298,17 @@ void NSSCertDBTrustDomain::NoteAuxiliary
   }
 }
 
 SECStatus InitializeNSS(const nsACString& dir, NSSDBConfig nssDbConfig,
                         PKCS11DBConfig pkcs11DbConfig) {
   MOZ_ASSERT(NS_IsMainThread());
 
   // The NSS_INIT_NOROOTINIT flag turns off the loading of the root certs
-  // module by NSS_Initialize because we will load it in InstallLoadableRoots
+  // module by NSS_Initialize because we will load it in LoadLoadableRoots
   // later.  It also allows us to work around a bug in the system NSS in
   // Ubuntu 8.04, which loads any nonexistent "<configdir>/libnssckbi.so" as
   // "/usr/lib/nss/libnssckbi.so".
   uint32_t flags = NSS_INIT_NOROOTINIT | NSS_INIT_OPTIMIZESPACE;
   if (nssDbConfig == NSSDBConfig::ReadOnly) {
     flags |= NSS_INIT_READONLY;
   }
   if (pkcs11DbConfig == PKCS11DBConfig::DoNotLoadModules) {
@@ -1350,64 +1350,82 @@ void DisableMD5() {
   NSS_SetAlgorithmPolicy(
       SEC_OID_PKCS1_MD5_WITH_RSA_ENCRYPTION, 0,
       NSS_USE_ALG_IN_CERT_SIGNATURE | NSS_USE_ALG_IN_CMS_SIGNATURE);
   NSS_SetAlgorithmPolicy(
       SEC_OID_PKCS5_PBE_WITH_MD5_AND_DES_CBC, 0,
       NSS_USE_ALG_IN_CERT_SIGNATURE | NSS_USE_ALG_IN_CMS_SIGNATURE);
 }
 
-bool LoadLoadableRoots(const nsCString& dir) {
+bool LoadUserModuleAt(const char* moduleName, const char* libraryName,
+                      const nsCString& dir) {
   // If a module exists with the same name, make a best effort attempt to delete
   // it. Note that it isn't possible to delete the internal module, so checking
   // the return value would be detrimental in that case.
   int unusedModType;
-  Unused << SECMOD_DeleteModule(kRootModuleName, &unusedModType);
-  // Some NSS command-line utilities will load a roots module under the name
-  // "Root Certs" if there happens to be a `MOZ_DLL_PREFIX "nssckbi"
-  // MOZ_DLL_SUFFIX` file in the directory being operated on. In some cases this
-  // can cause us to fail to load our roots module. In these cases, deleting the
-  // "Root Certs" module allows us to load the correct one. See bug 1406396.
-  Unused << SECMOD_DeleteModule("Root Certs", &unusedModType);
+  Unused << SECMOD_DeleteModule(moduleName, &unusedModType);
 
   nsAutoCString fullLibraryPath;
   if (!dir.IsEmpty()) {
     fullLibraryPath.Assign(dir);
     fullLibraryPath.AppendLiteral(FILE_PATH_SEPARATOR);
   }
-  fullLibraryPath.Append(MOZ_DLL_PREFIX "nssckbi" MOZ_DLL_SUFFIX);
+  fullLibraryPath.Append(MOZ_DLL_PREFIX);
+  fullLibraryPath.Append(libraryName);
+  fullLibraryPath.Append(MOZ_DLL_SUFFIX);
   // Escape the \ and " characters.
   fullLibraryPath.ReplaceSubstring("\\", "\\\\");
   fullLibraryPath.ReplaceSubstring("\"", "\\\"");
 
   nsAutoCString pkcs11ModuleSpec("name=\"");
-  pkcs11ModuleSpec.Append(kRootModuleName);
+  pkcs11ModuleSpec.Append(moduleName);
   pkcs11ModuleSpec.AppendLiteral("\" library=\"");
   pkcs11ModuleSpec.Append(fullLibraryPath);
   pkcs11ModuleSpec.AppendLiteral("\"");
 
-  UniqueSECMODModule rootsModule(SECMOD_LoadUserModule(
+  UniqueSECMODModule userModule(SECMOD_LoadUserModule(
       const_cast<char*>(pkcs11ModuleSpec.get()), nullptr, false));
-  if (!rootsModule) {
+  if (!userModule) {
     return false;
   }
 
-  if (!rootsModule->loaded) {
+  if (!userModule->loaded) {
     return false;
   }
 
   return true;
 }
 
-void UnloadLoadableRoots() {
+const char* kOSClientCertsModuleName = "OS Client Cert Module";
+
+bool LoadOSClientCertsModule(const nsCString& dir) {
+  return LoadUserModuleAt(kOSClientCertsModuleName, "osclientcerts", dir);
+}
+
+bool LoadLoadableRoots(const nsCString& dir) {
+  // Some NSS command-line utilities will load a roots module under the name
+  // "Root Certs" if there happens to be a `MOZ_DLL_PREFIX "nssckbi"
+  // MOZ_DLL_SUFFIX` file in the directory being operated on. In some cases this
+  // can cause us to fail to load our roots module. In these cases, deleting the
+  // "Root Certs" module allows us to load the correct one. See bug 1406396.
+  int unusedModType;
+  Unused << SECMOD_DeleteModule("Root Certs", &unusedModType);
+  return LoadUserModuleAt(kRootModuleName, "nssckbi", dir);
+}
+
+void UnloadUserModules() {
   UniqueSECMODModule rootsModule(SECMOD_FindModule(kRootModuleName));
-
   if (rootsModule) {
     SECMOD_UnloadUserModule(rootsModule.get());
   }
+  UniqueSECMODModule osClientCertsModule(
+      SECMOD_FindModule(kOSClientCertsModuleName));
+  if (osClientCertsModule) {
+    SECMOD_UnloadUserModule(osClientCertsModule.get());
+  }
 }
 
 nsresult DefaultServerNicknameForCert(const CERTCertificate* cert,
                                       /*out*/ nsCString& nickname) {
   MOZ_ASSERT(cert);
   NS_ENSURE_ARG_POINTER(cert);
 
   UniquePORTString baseName(CERT_GetCommonName(&cert->subject));
--- a/security/certverifier/NSSCertDBTrustDomain.h
+++ b/security/certverifier/NSSCertDBTrustDomain.h
@@ -63,17 +63,32 @@ void DisableMD5();
  * @param dir
  *        The path to the directory containing the NSS builtin roots module.
  *        Usually the same as the path to the other NSS shared libraries.
  *        If empty, the (library) path will be searched.
  * @return true if the roots were successfully loaded, false otherwise.
  */
 bool LoadLoadableRoots(const nsCString& dir);
 
-void UnloadLoadableRoots();
+/**
+ * Loads the OS client certs module.
+ *
+ * @param dir
+ *        The path to the directory containing the module. This should be the
+ *        same as where all of the other gecko libraries live.
+ * @return true if the module was successfully loaded, false otherwise.
+ */
+bool LoadOSClientCertsModule(const nsCString& dir);
+
+extern const char* kOSClientCertsModuleName;
+
+/**
+ * Unloads the loadable roots module and os client certs module, if loaded.
+ */
+void UnloadUserModules();
 
 nsresult DefaultServerNicknameForCert(const CERTCertificate* cert,
                                       /*out*/ nsCString& nickname);
 
 #ifdef MOZ_NEW_CERT_STORAGE
 /**
  * Build nsTArray<uint8_t>s out of the issuer, serial, subject and public key
  * data from the supplied certificate for use in revocation checks.
@@ -269,19 +284,19 @@ class NSSCertDBTrustDomain : public mozi
   ValidityCheckingMode mValidityCheckingMode;
   CertVerifier::SHA1Mode mSHA1Mode;
   NetscapeStepUpPolicy mNetscapeStepUpPolicy;
   DistrustedCAPolicy mDistrustedCAPolicy;
   bool mSawDistrustedCAByPolicyError;
   const OriginAttributes& mOriginAttributes;
   const Vector<mozilla::pkix::Input>& mThirdPartyRootInputs;  // non-owning
   const Vector<mozilla::pkix::Input>&
-      mThirdPartyIntermediateInputs;  // non-owning
+      mThirdPartyIntermediateInputs;                             // non-owning
   const Maybe<nsTArray<nsTArray<uint8_t>>>& mExtraCertificates;  // non-owning
-  UniqueCERTCertList& mBuiltChain;    // non-owning
+  UniqueCERTCertList& mBuiltChain;                               // non-owning
   PinningTelemetryInfo* mPinningTelemetryInfo;
   const char* mHostname;  // non-owning - only used for pinning checks
 #ifdef MOZ_NEW_CERT_STORAGE
   nsCOMPtr<nsICertStorage> mCertStorage;
 #else
   nsCOMPtr<nsICertBlocklist> mCertBlocklist;
 #endif
   CertVerifier::OCSPStaplingStatus mOCSPStaplingStatus;
--- a/security/manager/ssl/PKCS11ModuleDB.cpp
+++ b/security/manager/ssl/PKCS11ModuleDB.cpp
@@ -78,17 +78,17 @@ PKCS11ModuleDB::AddModule(const nsAStrin
   // Certs". We should prevent the user from adding an unrelated module named
   // "Root Certs" in the first place so PSM doesn't delete it. See bug 1406396.
   if (aModuleName.EqualsLiteral("Root Certs")) {
     return NS_ERROR_ILLEGAL_VALUE;
   }
 
   // There appears to be a deadlock if we try to load modules concurrently, so
   // just wait until the loadable roots module has been loaded.
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   nsAutoCString moduleNameNormalized;
   rv = NormalizeModuleNameIn(aModuleName, moduleNameNormalized);
   if (NS_FAILED(rv)) {
     return rv;
@@ -105,17 +105,17 @@ PKCS11ModuleDB::AddModule(const nsAStrin
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PKCS11ModuleDB::ListModules(nsISimpleEnumerator** _retval) {
   NS_ENSURE_ARG_POINTER(_retval);
 
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   nsCOMPtr<nsIMutableArray> array = do_CreateInstance(NS_ARRAY_CONTRACTID);
   if (!array) {
     return NS_ERROR_FAILURE;
   }
--- a/security/manager/ssl/nsINSSComponent.idl
+++ b/security/manager/ssl/nsINSSComponent.idl
@@ -59,17 +59,17 @@ interface nsINSSComponent : nsISupports 
    */
   Array<Array<octet> > getEnterpriseIntermediates();
 
   /**
    * For performance reasons, the builtin roots module is loaded on a background
    * thread. When any code that depends on the builtin roots module runs, it
    * must first wait for the module to be loaded.
    */
-  [noscript] void blockUntilLoadableRootsLoaded();
+  [noscript] void blockUntilLoadableCertsLoaded();
 
   /**
    * In theory a token on a PKCS#11 module can be inserted or removed at any
    * time. Operations that may depend on resources on external tokens should
    * call this to ensure they have a recent view of the token.
    */
   [noscript] void checkForSmartCardChanges();
 
--- a/security/manager/ssl/nsNSSCertificateDB.cpp
+++ b/security/manager/ssl/nsNSSCertificateDB.cpp
@@ -62,17 +62,17 @@ nsNSSCertificateDB::FindCertByDBKey(cons
                                     /*out*/ nsIX509Cert** _cert) {
   NS_ENSURE_ARG_POINTER(_cert);
   *_cert = nullptr;
 
   if (aDBKey.IsEmpty()) {
     return NS_ERROR_INVALID_ARG;
   }
 
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   UniqueCERTCertificate cert;
   rv = FindCertByDBKey(aDBKey, cert);
   if (NS_FAILED(rv)) {
     return rv;
@@ -682,17 +682,17 @@ nsNSSCertificateDB::SetCertTrust(nsIX509
 }
 
 NS_IMETHODIMP
 nsNSSCertificateDB::IsCertTrusted(nsIX509Cert* cert, uint32_t certType,
                                   uint32_t trustType, bool* _isTrusted) {
   NS_ENSURE_ARG_POINTER(_isTrusted);
   *_isTrusted = false;
 
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   SECStatus srv;
   UniqueCERTCertificate nsscert(cert->GetCert());
   CERTCertTrust nsstrust;
   srv = CERT_GetCertTrust(nsscert.get(), &nsstrust);
@@ -779,17 +779,17 @@ nsNSSCertificateDB::ImportCertsFromFile(
 }
 
 NS_IMETHODIMP
 nsNSSCertificateDB::ImportPKCS12File(nsIFile* aFile, const nsAString& aPassword,
                                      uint32_t* aError) {
   if (!NS_IsMainThread()) {
     return NS_ERROR_NOT_SAME_THREAD;
   }
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   NS_ENSURE_ARG(aFile);
   nsPKCS12Blob blob;
   rv = blob.ImportFromFile(aFile, aPassword, *aError);
   nsCOMPtr<nsIObserverService> observerService =
@@ -804,17 +804,17 @@ nsNSSCertificateDB::ImportPKCS12File(nsI
 
 NS_IMETHODIMP
 nsNSSCertificateDB::ExportPKCS12File(
     nsIFile* aFile, const nsTArray<RefPtr<nsIX509Cert>>& aCerts,
     const nsAString& aPassword, uint32_t* aError) {
   if (!NS_IsMainThread()) {
     return NS_ERROR_NOT_SAME_THREAD;
   }
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   NS_ENSURE_ARG(aFile);
   if (aCerts.IsEmpty()) {
     return NS_OK;
   }
@@ -874,17 +874,17 @@ nsNSSCertificateDB::ConstructX509(const 
 
 void nsNSSCertificateDB::get_default_nickname(CERTCertificate* cert,
                                               nsIInterfaceRequestor* ctx,
                                               nsCString& nickname) {
   nickname.Truncate();
 
   CK_OBJECT_HANDLE keyHandle;
 
-  if (NS_FAILED(BlockUntilLoadableRootsLoaded())) {
+  if (NS_FAILED(BlockUntilLoadableCertsLoaded())) {
     return;
   }
 
   CERTCertDBHandle* defaultcertdb = CERT_GetDefaultCertDB();
   nsAutoCString username;
   UniquePORTString tempCN(CERT_GetCommonName(&cert->subject));
   if (tempCN) {
     username = tempCN.get();
@@ -1129,17 +1129,17 @@ NS_IMETHODIMP nsNSSCertificateDB::AsPKCS
 
   _retval.Assign(nsDependentCSubstring(
       reinterpret_cast<const char*>(certP7.data), certP7.len));
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNSSCertificateDB::GetCerts(nsTArray<RefPtr<nsIX509Cert>>& _retval) {
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   rv = CheckForSmartCardChanges();
   if (NS_FAILED(rv)) {
     return rv;
   }
--- a/security/manager/ssl/nsNSSComponent.cpp
+++ b/security/manager/ssl/nsNSSComponent.cpp
@@ -194,22 +194,22 @@ static void GetRevocationBehaviorFromPre
   hardTimeoutMillis =
       std::min(hardTimeoutMillis, OCSP_TIMEOUT_MILLISECONDS_HARD_MAX);
   hardTimeout = TimeDuration::FromMilliseconds(hardTimeoutMillis);
 
   SSL_ClearSessionCache();
 }
 
 nsNSSComponent::nsNSSComponent()
-    : mLoadableRootsLoadedMonitor("nsNSSComponent.mLoadableRootsLoadedMonitor"),
-      mLoadableRootsLoaded(false),
-      mLoadableRootsLoadedResult(NS_ERROR_FAILURE),
+    : mLoadableCertsLoadedMonitor("nsNSSComponent.mLoadableCertsLoadedMonitor"),
+      mLoadableCertsLoaded(false),
+      mLoadableCertsLoadedResult(NS_ERROR_FAILURE),
       mMutex("nsNSSComponent.mMutex"),
       mMitmDetecionEnabled(false),
-      mLoadLoadableRootsTaskDispatched(false) {
+      mLoadLoadableCertsTaskDispatched(false) {
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("nsNSSComponent::ctor\n"));
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
   MOZ_ASSERT(mInstanceCount == 0,
              "nsNSSComponent is a singleton, but instantiated multiple times!");
   ++mInstanceCount;
 }
 
@@ -484,16 +484,17 @@ void nsNSSComponent::UnloadEnterpriseRoo
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("UnloadEnterpriseRoots"));
   MutexAutoLock lock(mMutex);
   mEnterpriseCerts.clear();
   setValidationOptions(false, lock);
 }
 
 static const char* kEnterpriseRootModePref =
     "security.enterprise_roots.enabled";
+static const char* kOSClientCertsModulePref = "security.osclientcerts.autoload";
 
 class BackgroundImportEnterpriseCertsTask final : public CryptoTask {
  public:
   explicit BackgroundImportEnterpriseCertsTask(nsNSSComponent* nssComponent)
       : mNSSComponent(nssComponent) {}
 
  private:
   virtual nsresult CalculateResult() override {
@@ -548,17 +549,17 @@ void nsNSSComponent::ImportEnterpriseRoo
     mEnterpriseCerts = std::move(enterpriseCerts);
   } else {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("failed gathering enterprise roots"));
   }
 }
 
 nsresult nsNSSComponent::CommonGetEnterpriseCerts(
     nsTArray<nsTArray<uint8_t>>& enterpriseCerts, bool getRoots) {
-  nsresult rv = BlockUntilLoadableRootsLoaded();
+  nsresult rv = BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   MutexAutoLock nsNSSComponentLock(mMutex);
   enterpriseCerts.Clear();
   for (const auto& cert : mEnterpriseCerts) {
     nsTArray<uint8_t> certCopy;
@@ -583,62 +584,65 @@ nsNSSComponent::GetEnterpriseRoots(
 }
 
 NS_IMETHODIMP
 nsNSSComponent::GetEnterpriseIntermediates(
     nsTArray<nsTArray<uint8_t>>& enterpriseIntermediates) {
   return CommonGetEnterpriseCerts(enterpriseIntermediates, false);
 }
 
-class LoadLoadableRootsTask final : public Runnable {
+class LoadLoadableCertsTask final : public Runnable {
  public:
-  LoadLoadableRootsTask(nsNSSComponent* nssComponent,
+  LoadLoadableCertsTask(nsNSSComponent* nssComponent,
                         bool importEnterpriseRoots, uint32_t familySafetyMode,
-                        Vector<nsCString>&& possibleLoadableRootsLocations)
-      : Runnable("LoadLoadableRootsTask"),
+                        Vector<nsCString>&& possibleLoadableRootsLocations,
+                        Maybe<nsCString>&& osClientCertsModuleLocation)
+      : Runnable("LoadLoadableCertsTask"),
         mNSSComponent(nssComponent),
         mImportEnterpriseRoots(importEnterpriseRoots),
         mFamilySafetyMode(familySafetyMode),
         mPossibleLoadableRootsLocations(
-            std::move(possibleLoadableRootsLocations)) {
+            std::move(possibleLoadableRootsLocations)),
+        mOSClientCertsModuleLocation(std::move(osClientCertsModuleLocation)) {
     MOZ_ASSERT(nssComponent);
   }
 
-  ~LoadLoadableRootsTask() = default;
+  ~LoadLoadableCertsTask() = default;
 
   nsresult Dispatch();
 
  private:
   NS_IMETHOD Run() override;
   nsresult LoadLoadableRoots();
   RefPtr<nsNSSComponent> mNSSComponent;
   bool mImportEnterpriseRoots;
   uint32_t mFamilySafetyMode;
   Vector<nsCString> mPossibleLoadableRootsLocations;
+  Maybe<nsCString> mOSClientCertsModuleLocation;
 };
 
-nsresult LoadLoadableRootsTask::Dispatch() {
+nsresult LoadLoadableCertsTask::Dispatch() {
   // The stream transport service (note: not the socket transport service) can
   // be used to perform background tasks or I/O that would otherwise block the
   // main thread.
   nsCOMPtr<nsIEventTarget> target(
       do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID));
   if (!target) {
     return NS_ERROR_FAILURE;
   }
   return target->Dispatch(this, NS_DISPATCH_NORMAL);
 }
 
 NS_IMETHODIMP
-LoadLoadableRootsTask::Run() {
+LoadLoadableCertsTask::Run() {
   nsresult loadLoadableRootsResult = LoadLoadableRoots();
   if (NS_WARN_IF(NS_FAILED(loadLoadableRootsResult))) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Error, ("LoadLoadableRoots failed"));
     // We don't return loadLoadableRootsResult here because then
-    // BlockUntilLoadableRootsLoaded will just wait forever. Instead we'll save
+    // BlockUntilLoadableCertsLoaded will just wait forever. Instead we'll save
     // its value (below) so we can inform code that relies on the roots module
     // being present that loading it failed.
   }
 
   // Loading EV information will only succeed if we've successfully loaded the
   // loadable roots module.
   if (NS_SUCCEEDED(loadLoadableRootsResult)) {
     if (NS_FAILED(LoadExtendedValidationInfo())) {
@@ -653,36 +657,123 @@ LoadLoadableRootsTask::Run() {
   if (mNSSComponent->ShouldEnableEnterpriseRootsForFamilySafety(
           mFamilySafetyMode)) {
     mImportEnterpriseRoots = true;
   }
   if (mImportEnterpriseRoots) {
     mNSSComponent->ImportEnterpriseRoots();
     mNSSComponent->UpdateCertVerifierWithEnterpriseRoots();
   }
+  if (mOSClientCertsModuleLocation.isSome()) {
+    bool success = LoadOSClientCertsModule(*mOSClientCertsModuleLocation);
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("loading OS client certs module %s",
+             success ? "succeeded" : "failed"));
+  }
   {
-    MonitorAutoLock rootsLoadedLock(mNSSComponent->mLoadableRootsLoadedMonitor);
-    mNSSComponent->mLoadableRootsLoaded = true;
-    // Cache the result of LoadLoadableRoots so BlockUntilLoadableRootsLoaded
-    // can return it to all callers later.
-    mNSSComponent->mLoadableRootsLoadedResult = loadLoadableRootsResult;
-    nsresult rv = mNSSComponent->mLoadableRootsLoadedMonitor.NotifyAll();
+    MonitorAutoLock rootsLoadedLock(mNSSComponent->mLoadableCertsLoadedMonitor);
+    mNSSComponent->mLoadableCertsLoaded = true;
+    // Cache the result of LoadLoadableRoots so BlockUntilLoadableCertsLoaded
+    // can return it to all callers later (we use that particular result because
+    // if that operation fails, it's unlikely that any TLS connection will
+    // succeed whereas the browser may still be able to operate if the other
+    // tasks fail).
+    mNSSComponent->mLoadableCertsLoadedResult = loadLoadableRootsResult;
+    nsresult rv = mNSSComponent->mLoadableCertsLoadedMonitor.NotifyAll();
     if (NS_WARN_IF(NS_FAILED(rv))) {
       MOZ_LOG(gPIPNSSLog, LogLevel::Error,
-              ("failed to notify loadable roots loaded monitor"));
+              ("failed to notify loadable certs loaded monitor"));
     }
   }
   return NS_OK;
 }
 
+// Returns by reference the path to the desired directory, based on the current
+// settings in the directory service.
+static nsresult GetDirectoryPath(const char* directoryKey, nsCString& result) {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIProperties> directoryService(
+      do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID));
+  if (!directoryService) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("could not get directory service"));
+    return NS_ERROR_FAILURE;
+  }
+  nsCOMPtr<nsIFile> directory;
+  nsresult rv = directoryService->Get(directoryKey, NS_GET_IID(nsIFile),
+                                      getter_AddRefs(directory));
+  if (NS_FAILED(rv)) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("could not get '%s' from directory service", directoryKey));
+    return rv;
+  }
+#ifdef XP_WIN
+  // Native path will drop Unicode characters that cannot be mapped to system's
+  // codepage, using short (canonical) path as workaround.
+  nsCOMPtr<nsILocalFileWin> directoryWin = do_QueryInterface(directory);
+  if (!directoryWin) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get nsILocalFileWin"));
+    return NS_ERROR_FAILURE;
+  }
+  return directoryWin->GetNativeCanonicalPath(result);
+#else
+  return directory->GetNativePath(result);
+#endif
+}
+
+class BackgroundLoadOSClientCertsModuleTask final : public CryptoTask {
+ public:
+  explicit BackgroundLoadOSClientCertsModuleTask(const nsCString&& libraryDir)
+      : mLibraryDir(std::move(libraryDir)) {}
+
+ private:
+  virtual nsresult CalculateResult() override {
+    bool success = LoadOSClientCertsModule(mLibraryDir);
+    return success ? NS_OK : NS_ERROR_FAILURE;
+  }
+
+  virtual void CallCallback(nsresult rv) override {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("loading OS client certs module %s",
+             NS_SUCCEEDED(rv) ? "succeeded" : "failed"));
+    nsCOMPtr<nsIObserverService> observerService =
+        mozilla::services::GetObserverService();
+    if (observerService) {
+      observerService->NotifyObservers(
+          nullptr, "psm:load-os-client-certs-module-task-ran", nullptr);
+    }
+  }
+
+  nsCString mLibraryDir;
+};
+
+void AsyncLoadOrUnloadOSClientCertsModule(bool load) {
+  if (load) {
+    nsCString libraryDir;
+    nsresult rv = GetDirectoryPath(NS_GRE_BIN_DIR, libraryDir);
+    if (NS_FAILED(rv)) {
+      return;
+    }
+    RefPtr<BackgroundLoadOSClientCertsModuleTask> task =
+        new BackgroundLoadOSClientCertsModuleTask(std::move(libraryDir));
+    Unused << task->Dispatch();
+  } else {
+    UniqueSECMODModule osClientCertsModule(
+        SECMOD_FindModule(kOSClientCertsModuleName));
+    if (osClientCertsModule) {
+      SECMOD_UnloadUserModule(osClientCertsModule.get());
+    }
+  }
+}
+
 NS_IMETHODIMP
 nsNSSComponent::HasActiveSmartCards(bool* result) {
   NS_ENSURE_ARG_POINTER(result);
 
-  BlockUntilLoadableRootsLoaded();
+  BlockUntilLoadableCertsLoaded();
 
 #ifndef MOZ_NO_SMART_CARDS
   AutoSECMODListReadLock secmodLock;
   SECMODModuleList* list = SECMOD_GetDefaultModuleList();
   while (list) {
     SECMODModule* module = list->module;
     if (SECMOD_HasRemovableSlots(module)) {
       *result = true;
@@ -700,34 +791,34 @@ nsNSSComponent::HasActiveSmartCards(bool
   *result = false;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNSSComponent::HasUserCertsInstalled(bool* result) {
   NS_ENSURE_ARG_POINTER(result);
 
-  BlockUntilLoadableRootsLoaded();
+  BlockUntilLoadableCertsLoaded();
 
   // FindNonCACertificatesWithPrivateKeys won't ever return an empty list, so
   // all we need to do is check if this is null or not.
   UniqueCERTCertList certList(FindNonCACertificatesWithPrivateKeys());
   *result = !!certList;
 
   return NS_OK;
 }
 
-nsresult nsNSSComponent::BlockUntilLoadableRootsLoaded() {
-  MonitorAutoLock rootsLoadedLock(mLoadableRootsLoadedMonitor);
-  while (!mLoadableRootsLoaded) {
+nsresult nsNSSComponent::BlockUntilLoadableCertsLoaded() {
+  MonitorAutoLock rootsLoadedLock(mLoadableCertsLoadedMonitor);
+  while (!mLoadableCertsLoaded) {
     rootsLoadedLock.Wait();
   }
-  MOZ_ASSERT(mLoadableRootsLoaded);
+  MOZ_ASSERT(mLoadableCertsLoaded);
 
-  return mLoadableRootsLoadedResult;
+  return mLoadableCertsLoadedResult;
 }
 
 nsresult nsNSSComponent::CheckForSmartCardChanges() {
 #ifndef MOZ_NO_SMART_CARDS
   // SECMOD_UpdateSlotList attempts to acquire the list lock as well,
   // so we have to do this in two steps. The lock protects the list itself, so
   // if we get our own owned references to the modules we're interested in,
   // there's no thread safety concern here.
@@ -788,59 +879,26 @@ static nsresult GetNSS3Directory(nsCStri
   if (NS_FAILED(rv)) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get parent directory?"));
     return rv;
   }
 #ifdef XP_WIN
   // Native path will drop Unicode characters that cannot be mapped to system's
   // codepage, using short (canonical) path as workaround.
   nsCOMPtr<nsILocalFileWin> nss3DirectoryWin = do_QueryInterface(nss3Directory);
-  if (NS_FAILED(rv)) {
+  if (!nss3DirectoryWin) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get nsILocalFileWin"));
-    return rv;
+    return NS_ERROR_FAILURE;
   }
   return nss3DirectoryWin->GetNativeCanonicalPath(result);
 #else
   return nss3Directory->GetNativePath(result);
 #endif
 }
 
-// Returns by reference the path to the desired directory, based on the current
-// settings in the directory service.
-static nsresult GetDirectoryPath(const char* directoryKey, nsCString& result) {
-  MOZ_ASSERT(NS_IsMainThread());
-
-  nsCOMPtr<nsIProperties> directoryService(
-      do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID));
-  if (!directoryService) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("could not get directory service"));
-    return NS_ERROR_FAILURE;
-  }
-  nsCOMPtr<nsIFile> directory;
-  nsresult rv = directoryService->Get(directoryKey, NS_GET_IID(nsIFile),
-                                      getter_AddRefs(directory));
-  if (NS_FAILED(rv)) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-            ("could not get '%s' from directory service", directoryKey));
-    return rv;
-  }
-#ifdef XP_WIN
-  // Native path will drop Unicode characters that cannot be mapped to system's
-  // codepage, using short (canonical) path as workaround.
-  nsCOMPtr<nsILocalFileWin> directoryWin = do_QueryInterface(directory);
-  if (NS_FAILED(rv)) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get nsILocalFileWin"));
-    return rv;
-  }
-  return directoryWin->GetNativeCanonicalPath(result);
-#else
-  return directory->GetNativePath(result);
-#endif
-}
-
 // The loadable roots library is probably in the same directory we loaded the
 // NSS shared library from, but in some cases it may be elsewhere. This function
 // enumerates and returns the possible locations as nsCStrings.
 static nsresult ListPossibleLoadableRootsLocations(
     Vector<nsCString>& possibleLoadableRootsLocations) {
   MOZ_ASSERT(NS_IsMainThread());
   if (!NS_IsMainThread()) {
     return NS_ERROR_NOT_SAME_THREAD;
@@ -884,17 +942,17 @@ static nsresult ListPossibleLoadableRoot
   nsAutoCString emptyString;
   if (!possibleLoadableRootsLocations.append(std::move(emptyString))) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   return NS_OK;
 }
 
-nsresult LoadLoadableRootsTask::LoadLoadableRoots() {
+nsresult LoadLoadableCertsTask::LoadLoadableRoots() {
   for (const auto& possibleLocation : mPossibleLoadableRootsLocations) {
     if (mozilla::psm::LoadLoadableRoots(possibleLocation)) {
       MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
               ("loaded CKBI from %s", possibleLocation.get()));
       return NS_OK;
     }
   }
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("could not load loadable roots"));
@@ -1818,50 +1876,61 @@ nsresult nsNSSComponent::InitializeNSS()
     uint32_t familySafetyMode =
         Preferences::GetUint(kFamilySafetyModePref, kFamilySafetyModeDefault);
     Vector<nsCString> possibleLoadableRootsLocations;
     rv = ListPossibleLoadableRootsLocations(possibleLoadableRootsLocations);
     MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
     if (NS_FAILED(rv)) {
       return rv;
     }
-    RefPtr<LoadLoadableRootsTask> loadLoadableRootsTask(
-        new LoadLoadableRootsTask(this, importEnterpriseRoots, familySafetyMode,
-                                  std::move(possibleLoadableRootsLocations)));
-    rv = loadLoadableRootsTask->Dispatch();
+
+    bool loadOSClientCertsModule =
+        Preferences::GetBool(kOSClientCertsModulePref, false);
+    Maybe<nsCString> maybeOSClientCertsModuleLocation;
+    if (loadOSClientCertsModule) {
+      nsAutoCString libraryDir;
+      if (NS_SUCCEEDED(GetDirectoryPath(NS_GRE_BIN_DIR, libraryDir))) {
+        maybeOSClientCertsModuleLocation.emplace(libraryDir);
+      }
+    }
+    RefPtr<LoadLoadableCertsTask> loadLoadableCertsTask(
+        new LoadLoadableCertsTask(this, importEnterpriseRoots, familySafetyMode,
+                                  std::move(possibleLoadableRootsLocations),
+                                  std::move(maybeOSClientCertsModuleLocation)));
+    rv = loadLoadableCertsTask->Dispatch();
     MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
     if (NS_FAILED(rv)) {
       return rv;
     }
 
-    mLoadLoadableRootsTaskDispatched = true;
+    mLoadLoadableCertsTaskDispatched = true;
     return NS_OK;
   }
 }
 
 void nsNSSComponent::ShutdownNSS() {
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("nsNSSComponent::ShutdownNSS\n"));
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
-  bool loadLoadableRootsTaskDispatched;
+  bool loadLoadableCertsTaskDispatched;
   {
     MutexAutoLock lock(mMutex);
-    loadLoadableRootsTaskDispatched = mLoadLoadableRootsTaskDispatched;
+    loadLoadableCertsTaskDispatched = mLoadLoadableCertsTaskDispatched;
   }
-  // We have to block until the load loadable roots task has completed, because
-  // otherwise we might try to unload the loadable roots while the loadable
-  // roots loading thread is setting up EV information, which can cause
+  // We have to block until the load loadable certs task has completed, because
+  // otherwise we might try to unload the loaded modules while the loadable
+  // certs loading thread is setting up EV information, which can cause
   // it to fail to find the roots it is expecting. However, if initialization
-  // failed, we won't have dispatched the load loadable roots background task.
+  // failed, we won't have dispatched the load loadable certs background task.
   // In that case, we don't want to block on an event that will never happen.
-  if (loadLoadableRootsTaskDispatched) {
-    Unused << BlockUntilLoadableRootsLoaded();
+  if (loadLoadableCertsTaskDispatched) {
+    Unused << BlockUntilLoadableCertsLoaded();
   }
 
-  ::mozilla::psm::UnloadLoadableRoots();
+  ::mozilla::psm::UnloadUserModules();
 
   PK11_SetPasswordFunc((PK11PasswordFunc) nullptr);
 
   Preferences::RemoveObserver(this, "security.");
 
   // Release the default CertVerifier. This will cause any held NSS resources
   // to be released.
   MutexAutoLock lock(mMutex);
@@ -1992,16 +2061,20 @@ nsNSSComponent::Observe(nsISupports* aSu
       MutexAutoLock lock(mMutex);
       mContentSigningRootHash.Truncate();
       Preferences::GetString("security.content.signature.root_hash",
                              mContentSigningRootHash);
     } else if (prefName.Equals(kEnterpriseRootModePref) ||
                prefName.Equals(kFamilySafetyModePref)) {
       UnloadEnterpriseRoots();
       MaybeImportEnterpriseRoots();
+    } else if (prefName.Equals(kOSClientCertsModulePref)) {
+      bool loadOSClientCertsModule =
+          Preferences::GetBool(kOSClientCertsModulePref, false);
+      AsyncLoadOrUnloadOSClientCertsModule(loadOSClientCertsModule);
     } else if (prefName.EqualsLiteral("security.pki.mitm_canary_issuer")) {
       MutexAutoLock lock(mMutex);
       mMitmCanaryIssuer.Truncate();
       Preferences::GetString("security.pki.mitm_canary_issuer",
                              mMitmCanaryIssuer);
     } else if (prefName.EqualsLiteral(
                    "security.pki.mitm_canary_issuer.enabled")) {
       MutexAutoLock lock(mMutex);
@@ -2159,17 +2232,17 @@ namespace psm {
 
 already_AddRefed<SharedCertVerifier> GetDefaultCertVerifier() {
   static NS_DEFINE_CID(kNSSComponentCID, NS_NSSCOMPONENT_CID);
 
   nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(kNSSComponentCID));
   if (!nssComponent) {
     return nullptr;
   }
-  nsresult rv = nssComponent->BlockUntilLoadableRootsLoaded();
+  nsresult rv = nssComponent->BlockUntilLoadableCertsLoaded();
   if (NS_FAILED(rv)) {
     return nullptr;
   }
   RefPtr<SharedCertVerifier> result;
   rv = nssComponent->GetDefaultCertVerifier(getter_AddRefs(result));
   if (NS_FAILED(rv)) {
     return nullptr;
   }
--- a/security/manager/ssl/nsNSSComponent.h
+++ b/security/manager/ssl/nsNSSComponent.h
@@ -49,19 +49,19 @@ UniqueCERTCertList FindNonCACertificates
     }                                                \
   }
 
 extern bool EnsureNSSInitializedChromeOrContent();
 
 // Implementation of the PSM component interface.
 class nsNSSComponent final : public nsINSSComponent, public nsIObserver {
  public:
-  // LoadLoadableRootsTask updates mLoadableRootsLoaded and
-  // mLoadableRootsLoadedResult and then signals mLoadableRootsLoadedMonitor.
-  friend class LoadLoadableRootsTask;
+  // LoadLoadableCertsTask updates mLoadableCertsLoaded and
+  // mLoadableCertsLoadedResult and then signals mLoadableCertsLoadedMonitor.
+  friend class LoadLoadableCertsTask;
   // BackgroundImportEnterpriseCertsTask calls ImportEnterpriseRoots and
   // UpdateCertVerifierWithEnterpriseRoots.
   friend class BackgroundImportEnterpriseCertsTask;
 
   nsNSSComponent();
 
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSINSSCOMPONENT
@@ -91,20 +91,20 @@ class nsNSSComponent final : public nsIN
   void MaybeImportEnterpriseRoots();
   void ImportEnterpriseRoots();
   void UnloadEnterpriseRoots();
   nsresult CommonGetEnterpriseCerts(
       nsTArray<nsTArray<uint8_t>>& enterpriseCerts, bool getRoots);
 
   bool ShouldEnableEnterpriseRootsForFamilySafety(uint32_t familySafetyMode);
 
-  // mLoadableRootsLoadedMonitor protects mLoadableRootsLoaded.
-  mozilla::Monitor mLoadableRootsLoadedMonitor;
-  bool mLoadableRootsLoaded;
-  nsresult mLoadableRootsLoadedResult;
+  // mLoadableCertsLoadedMonitor protects mLoadableCertsLoaded.
+  mozilla::Monitor mLoadableCertsLoadedMonitor;
+  bool mLoadableCertsLoaded;
+  nsresult mLoadableCertsLoadedResult;
 
   // mMutex protects all members that are accessed from more than one thread.
   mozilla::Mutex mMutex;
 
   // The following members are accessed from more than one thread:
 
 #ifdef DEBUG
   nsString mTestBuiltInRootHash;
@@ -113,30 +113,31 @@ class nsNSSComponent final : public nsIN
   RefPtr<mozilla::psm::SharedCertVerifier> mDefaultCertVerifier;
   nsString mMitmCanaryIssuer;
   bool mMitmDetecionEnabled;
   mozilla::Vector<EnterpriseCert> mEnterpriseCerts;
 
   // The following members are accessed only on the main thread:
   static int mInstanceCount;
   // If InitializeNSS succeeds, then we have dispatched an event to load the
-  // loadable roots module on a background thread. We must wait for it to
-  // complete before attempting to unload the module again in ShutdownNSS. If we
-  // never dispatched the event, then we can't wait for it to complete (because
-  // it will never complete) so we use this boolean to keep track of if we
-  // should wait.
-  bool mLoadLoadableRootsTaskDispatched;
+  // loadable roots module, enterprise certificates (if enabled), and the os
+  // client certs module (if enabled) on a background thread. We must wait for
+  // it to complete before attempting to unload the modules again in
+  // ShutdownNSS. If we never dispatched the event, then we can't wait for it
+  // to complete (because it will never complete) so we use this boolean to keep
+  // track of if we should wait.
+  bool mLoadLoadableCertsTaskDispatched;
 };
 
-inline nsresult BlockUntilLoadableRootsLoaded() {
+inline nsresult BlockUntilLoadableCertsLoaded() {
   nsCOMPtr<nsINSSComponent> component(do_GetService(PSM_COMPONENT_CONTRACTID));
   if (!component) {
     return NS_ERROR_FAILURE;
   }
-  return component->BlockUntilLoadableRootsLoaded();
+  return component->BlockUntilLoadableCertsLoaded();
 }
 
 inline nsresult CheckForSmartCardChanges() {
 #ifndef MOZ_NO_SMART_CARDS
   nsCOMPtr<nsINSSComponent> component(do_GetService(PSM_COMPONENT_CONTRACTID));
   if (!component) {
     return NS_ERROR_FAILURE;
   }
--- a/security/manager/ssl/tests/unit/head_psm.js
+++ b/security/manager/ssl/tests/unit/head_psm.js
@@ -1112,17 +1112,17 @@ function checkPKCS11ModuleNotPresent(mod
   ok(
     modules.hasMoreElements(),
     "One or more modules should be present with test module not present"
   );
   for (let module of modules) {
     notEqual(
       module.name,
       moduleName,
-      "Non-test module name shouldn't equal 'PKCS11 Test Module'"
+      `Non-test module name shouldn't equal '${moduleName}'`
     );
     ok(
       !(module.libName && module.libName.includes(libraryName)),
       `Non-test module lib name should not include '${libraryName}'`
     );
   }
 }
 
--- a/security/manager/ssl/tests/unit/test_osclientcerts_module.js
+++ b/security/manager/ssl/tests/unit/test_osclientcerts_module.js
@@ -3,26 +3,30 @@
 // http://creativecommons.org/publicdomain/zero/1.0/
 "use strict";
 
 // Tests that the platform can load the osclientcerts module.
 
 // Ensure that the appropriate initialization has happened.
 do_get_profile();
 
-function run_test() {
-  // Check that if we have never added the osclientcerts module, that we don't
-  // find it in the module list.
+const { TestUtils } = ChromeUtils.import(
+  "resource://testing-common/TestUtils.jsm"
+);
+
+add_task(async function run_test() {
+  // Check that if we haven't loaded the osclientcerts module, we don't find it
+  // in the module list.
   checkPKCS11ModuleNotPresent("OS Client Cert Module", "osclientcerts");
 
-  // Check that adding the osclientcerts module makes it appear in the module
-  // list.
-  let libraryFile = Services.dirsvc.get("GreBinD", Ci.nsIFile);
-  libraryFile.append(ctypes.libraryName("osclientcerts"));
-  loadPKCS11Module(libraryFile, "OS Client Cert Module", true);
+  // Check that enabling the pref that loads the osclientcerts module makes it
+  // appear in the module list.
+  Services.prefs.setBoolPref("security.osclientcerts.autoload", true);
+  // Loading happens asynchronously, so we have to wait for the notification.
+  await TestUtils.topicObserved("psm:load-os-client-certs-module-task-ran");
   let testModule = checkPKCS11ModuleExists(
     "OS Client Cert Module",
     "osclientcerts"
   );
 
   // Check that listing the slots for the osclientcerts module works.
   let testModuleSlotNames = Array.from(
     testModule.listSlots(),
@@ -31,16 +35,13 @@ function run_test() {
   testModuleSlotNames.sort();
   const expectedSlotNames = ["OS Client Cert Slot"];
   deepEqual(
     testModuleSlotNames,
     expectedSlotNames,
     "Actual and expected slot names should be equal"
   );
 
-  // Check that deleting the osclientcerts module makes it disappear from the
-  // module list.
-  let pkcs11ModuleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
-    Ci.nsIPKCS11ModuleDB
-  );
-  pkcs11ModuleDB.deleteModule("OS Client Cert Module");
+  // Check that disabling the pref that loads the osclientcerts module (thus
+  // unloading the module) makes it disappear from the module list.
+  Services.prefs.setBoolPref("security.osclientcerts.autoload", false);
   checkPKCS11ModuleNotPresent("OS Client Cert Module", "osclientcerts");
-}
+});