diff --git a/Cargo.lock b/Cargo.lock
index ca3c9268985caf8b15bc14d57298328ca0b0dafd..e5b38b48b73e7f95a6a61ac59de85fb84ad12641 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2437,6 +2437,19 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "ipcclientcerts-static"
+version = "0.1.0"
+dependencies = [
+ "byteorder",
+ "env_logger",
+ "lazy_static",
+ "log",
+ "pkcs11",
+ "rsclientcerts",
+ "sha2",
+]
+
 [[package]]
 name = "itertools"
 version = "0.8.2"
diff --git a/Cargo.toml b/Cargo.toml
index e0f622a411ed76146780a89d21df3c9c31e6242e..2ea112cc1a02ed8bcf16176e7d40a93d19cc427f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ members = [
   "js/src/rust",
   "js/src/wasm/cranelift",
   "netwerk/test/http3server",
+  "security/manager/ssl/ipcclientcerts",
   "security/manager/ssl/osclientcerts",
   "testing/geckodriver",
   "toolkit/crashreporter/rust_minidump_writer_linux",
diff --git a/browser/components/extensions/parent/ext-pkcs11.js b/browser/components/extensions/parent/ext-pkcs11.js
index 697cd85d2775f71ea9c76ff2084376874d9212d3..334adaa2c3ed1e5cc38977ecedb23bfb4813db09 100644
--- a/browser/components/extensions/parent/ext-pkcs11.js
+++ b/browser/components/extensions/parent/ext-pkcs11.js
@@ -49,7 +49,8 @@ this.pkcs11 = class extends ExtensionAPI {
         }
         if (
           manifestLib !== ctypes.libraryName("nssckbi") &&
-          manifestLib !== ctypes.libraryName("osclientcerts")
+          manifestLib !== ctypes.libraryName("osclientcerts") &&
+          manifestLib !== ctypes.libraryName("ipcclientcerts")
         ) {
           return hostInfo.manifest;
         }
diff --git a/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
index ffd510fe6543ad31205d50e3c65b2349d6ebe13c..6463c5964870def34ad9e064c04e76fb54a9a789 100644
--- a/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
@@ -204,6 +204,26 @@ add_task(async function test_pkcs11() {
         /No such PKCS#11 module osclientcerts/,
         "getModuleSlots should not work on the built-in osclientcerts module"
       );
+      await browser.test.assertRejects(
+        browser.pkcs11.installModule("ipcclientcerts", 0),
+        /No such PKCS#11 module ipcclientcerts/,
+        "installModule should not work on the built-in ipcclientcerts module"
+      );
+      await browser.test.assertRejects(
+        browser.pkcs11.uninstallModule("ipcclientcerts"),
+        /No such PKCS#11 module ipcclientcerts/,
+        "uninstallModule should not work on the built-in ipcclientcerts module"
+      );
+      await browser.test.assertRejects(
+        browser.pkcs11.isModuleInstalled("ipcclientcerts"),
+        /No such PKCS#11 module ipcclientcerts/,
+        "isModuleLoaded should not work on the built-in ipcclientcerts module"
+      );
+      await browser.test.assertRejects(
+        browser.pkcs11.getModuleSlots("ipcclientcerts"),
+        /No such PKCS#11 module ipcclientcerts/,
+        "getModuleSlots should not work on the built-in ipcclientcerts module"
+      );
       browser.test.notifyPass("pkcs11");
     } catch (e) {
       browser.test.fail(`Error: ${String(e)} :: ${e.stack}`);
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index 4d7a4810a7f27167c2c2c23dd3bfdbb7709fd87c..0d2ff9dff23213b8599e597946ef164f31a3223c 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -372,6 +372,8 @@ bin/libfreebl_64int_3.so
 #endif
 #endif
 
+@BINPATH@/@DLL_PREFIX@ipcclientcerts@DLL_SUFFIX@
+
 ; For process sandboxing
 #if defined(MOZ_SANDBOX)
 #if defined(XP_LINUX)
diff --git a/dom/media/webrtc/transport/dtlsidentity.cpp b/dom/media/webrtc/transport/dtlsidentity.cpp
index 25e22d1c974427350f82c71324de3e69b64a60fc..e4ecee32101300be6e4289eb3f8a27764f697ea5 100644
--- a/dom/media/webrtc/transport/dtlsidentity.cpp
+++ b/dom/media/webrtc/transport/dtlsidentity.cpp
@@ -15,6 +15,7 @@
 #include "ssl.h"
 #include "mozpkix/nss_scoped_ptrs.h"
 #include "secerr.h"
+#include "sslerr.h"
 
 #include "mozilla/Sprintf.h"
 #include "mozilla/dom/CryptoBuffer.h"
@@ -24,10 +25,103 @@
 
 namespace mozilla {
 
+SECItem* WrapPrivateKeyInfoWithEmptyPassword(
+    SECKEYPrivateKey* pk) /* encrypt this private key */
+{
+  if (!pk) {
+    PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
+    return nullptr;
+  }
+
+  UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+  if (!slot) {
+    return nullptr;
+  }
+
+  // For private keys, NSS cannot export anything other than RSA, but we need EC
+  // also. So, we use the private key encryption function to serialize instead,
+  // using a hard-coded dummy password; this is not intended to provide any
+  // additional security, it just works around a limitation in NSS.
+  SECItem dummyPassword = {siBuffer, nullptr, 0};
+  UniqueSECKEYEncryptedPrivateKeyInfo epki(PK11_ExportEncryptedPrivKeyInfo(
+      slot.get(), SEC_OID_AES_128_CBC, &dummyPassword, pk, 1, nullptr));
+
+  if (!epki) {
+    return nullptr;
+  }
+
+  return SEC_ASN1EncodeItem(
+      nullptr, nullptr, epki.get(),
+      NSS_Get_SECKEY_EncryptedPrivateKeyInfoTemplate(nullptr, false));
+}
+
+SECStatus UnwrapPrivateKeyInfoWithEmptyPassword(
+    SECItem* derPKI, const UniqueCERTCertificate& aCert,
+    SECKEYPrivateKey** privk) {
+  if (!derPKI || !aCert || !privk) {
+    PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
+    return SECFailure;
+  }
+
+  UniqueSECKEYPublicKey publicKey(CERT_ExtractPublicKey(aCert.get()));
+  // This is a pointer to data inside publicKey
+  SECItem* publicValue = nullptr;
+  switch (publicKey->keyType) {
+    case dsaKey:
+      publicValue = &publicKey->u.dsa.publicValue;
+      break;
+    case dhKey:
+      publicValue = &publicKey->u.dh.publicValue;
+      break;
+    case rsaKey:
+      publicValue = &publicKey->u.rsa.modulus;
+      break;
+    case ecKey:
+      publicValue = &publicKey->u.ec.publicValue;
+      break;
+    default:
+      MOZ_ASSERT(false);
+      PR_SetError(SSL_ERROR_BAD_CERTIFICATE, 0);
+      return SECFailure;
+  }
+
+  UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+  if (!slot) {
+    return SECFailure;
+  }
+
+  UniquePLArenaPool temparena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
+  if (!temparena) {
+    return SECFailure;
+  }
+
+  SECKEYEncryptedPrivateKeyInfo* epki =
+      PORT_ArenaZNew(temparena.get(), SECKEYEncryptedPrivateKeyInfo);
+  if (!epki) {
+    return SECFailure;
+  }
+
+  SECStatus rv = SEC_ASN1DecodeItem(
+      temparena.get(), epki,
+      NSS_Get_SECKEY_EncryptedPrivateKeyInfoTemplate(nullptr, false), derPKI);
+  if (rv != SECSuccess) {
+    // If SEC_ASN1DecodeItem fails, we cannot assume anything about the
+    // validity of the data in epki. The best we can do is free the arena
+    // and return.
+    return rv;
+  }
+
+  // See comment in WrapPrivateKeyInfoWithEmptyPassword about this
+  // dummy password stuff.
+  SECItem dummyPassword = {siBuffer, nullptr, 0};
+  return PK11_ImportEncryptedPrivateKeyInfoAndReturnKey(
+      slot.get(), epki, &dummyPassword, nullptr, publicValue, false, false,
+      publicKey->keyType, KU_ALL, privk, nullptr);
+}
+
 nsresult DtlsIdentity::Serialize(nsTArray<uint8_t>* aKeyDer,
                                  nsTArray<uint8_t>* aCertDer) {
-  ScopedSECItem derPki(
-      psm::WrapPrivateKeyInfoWithEmptyPassword(private_key_.get()));
+  ScopedSECItem derPki(WrapPrivateKeyInfoWithEmptyPassword(private_key_.get()));
   if (!derPki) {
     return NS_ERROR_FAILURE;
   }
@@ -50,7 +144,7 @@ RefPtr<DtlsIdentity> DtlsIdentity::Deserialize(
                     static_cast<unsigned int>(aKeyDer.Length())};
 
   SECKEYPrivateKey* privateKey;
-  if (psm::UnwrapPrivateKeyInfoWithEmptyPassword(&derPKI, cert, &privateKey) !=
+  if (UnwrapPrivateKeyInfoWithEmptyPassword(&derPKI, cert, &privateKey) !=
       SECSuccess) {
     MOZ_ASSERT(false);
     return nullptr;
diff --git a/ipc/glue/BackgroundChildImpl.cpp b/ipc/glue/BackgroundChildImpl.cpp
index e10c90b5300ef1ebae3ea904001fe05355ed1261..54728c6200e72522ff38c1a55e0c369c81ebaa97 100644
--- a/ipc/glue/BackgroundChildImpl.cpp
+++ b/ipc/glue/BackgroundChildImpl.cpp
@@ -57,6 +57,7 @@
 #include "mozilla/dom/WebAuthnTransactionChild.h"
 #include "mozilla/dom/MIDIPortChild.h"
 #include "mozilla/dom/MIDIManagerChild.h"
+#include "mozilla/psm/IPCClientCertsChild.h"
 #include "mozilla/RemoteLazyInputStreamChild.h"
 #include "nsID.h"
 #include "nsTraceRefcnt.h"
diff --git a/ipc/glue/BackgroundParentImpl.cpp b/ipc/glue/BackgroundParentImpl.cpp
index 32e125f8d8dcb01c4fd6abd587f19fbae16e449f..d6f189c03ef8660472123650e6437afafb1054df 100644
--- a/ipc/glue/BackgroundParentImpl.cpp
+++ b/ipc/glue/BackgroundParentImpl.cpp
@@ -71,6 +71,8 @@
 #include "mozilla/net/HttpBackgroundChannelParent.h"
 #include "mozilla/net/HttpConnectionMgrParent.h"
 #include "mozilla/net/WebSocketConnectionParent.h"
+#include "mozilla/psm/IPCClientCertsChild.h"
+#include "mozilla/psm/IPCClientCertsParent.h"
 #include "mozilla/psm/VerifySSLServerCertParent.h"
 #include "nsIHttpChannelInternal.h"
 #include "nsIPrincipal.h"
@@ -1047,6 +1049,21 @@ mozilla::ipc::IPCResult BackgroundParentImpl::RecvPMessagePortConstructor(
   return IPC_OK();
 }
 
+already_AddRefed<psm::PIPCClientCertsParent>
+BackgroundParentImpl::AllocPIPCClientCertsParent() {
+  // This should only be called in the parent process with the socket process
+  // as the child process, not any content processes, hence the check that the
+  // child ID be 0.
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(mozilla::ipc::BackgroundParent::GetChildID(this) == 0);
+  if (!XRE_IsParentProcess() ||
+      mozilla::ipc::BackgroundParent::GetChildID(this) != 0) {
+    return nullptr;
+  }
+  RefPtr<psm::IPCClientCertsParent> result = new psm::IPCClientCertsParent();
+  return result.forget();
+}
+
 bool BackgroundParentImpl::DeallocPMessagePortParent(
     PMessagePortParent* aActor) {
   AssertIsInMainOrSocketProcess();
diff --git a/ipc/glue/BackgroundParentImpl.h b/ipc/glue/BackgroundParentImpl.h
index 4923deffc68fe23c9579a2a42900e9f9512db6f4..5865d241e75f2eddd38c49246633844e429bce1e 100644
--- a/ipc/glue/BackgroundParentImpl.h
+++ b/ipc/glue/BackgroundParentImpl.h
@@ -295,6 +295,8 @@ class BackgroundParentImpl : public PBackgroundParent,
       PMessagePortParent* aActor, const nsID& aUUID,
       const nsID& aDestinationUUID, const uint32_t& aSequenceID) override;
 
+  already_AddRefed<PIPCClientCertsParent> AllocPIPCClientCertsParent() override;
+
   bool DeallocPMessagePortParent(PMessagePortParent* aActor) override;
 
   mozilla::ipc::IPCResult RecvMessagePortForceClose(
diff --git a/ipc/glue/PBackground.ipdl b/ipc/glue/PBackground.ipdl
index 019d6f0d9237af546e74d4c5c8087d3cac268677..03dbbb98a04b22c11e320c195441ba2d8e314c1a 100644
--- a/ipc/glue/PBackground.ipdl
+++ b/ipc/glue/PBackground.ipdl
@@ -25,6 +25,7 @@ include protocol PFileSystemRequest;
 include protocol PGamepadEventChannel;
 include protocol PGamepadTestChannel;
 include protocol PHttpBackgroundChannel;
+include protocol PIPCClientCerts;
 include protocol PIdleScheduler;
 include protocol PRemoteLazyInputStream;
 include protocol PMediaTransport;
@@ -105,6 +106,7 @@ sync protocol PBackground
   manages PGamepadEventChannel;
   manages PGamepadTestChannel;
   manages PHttpBackgroundChannel;
+  manages PIPCClientCerts;
   manages PIdleScheduler;
   manages PRemoteLazyInputStream;
   manages PLockManager;
@@ -287,6 +289,8 @@ parent:
 
   async PLockManager(ContentPrincipalInfo aPrincipalInfo, nsID aClientId);
 
+  async PIPCClientCerts();
+
 child:
   async PCache();
   async PCacheStreamControl();
diff --git a/ipc/ipdl/sync-messages.ini b/ipc/ipdl/sync-messages.ini
index 0ba1651a7cc51d23da5de21ffbcf37cd60c67f64..60f909afef27b9a4e8cf8cec725f1b1b9c017d96 100644
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -1070,6 +1070,10 @@ description = Reflection is cold code, but synchronous by spec.
 # -
 [PSocketProcess::GetTLSClientCert]
 description = Synchronously get client certificate and key from parent process. Once bug 696976 has been fixed, this can be removed.
+[PIPCClientCerts::FindObjects]
+description = Finds certificates and private keys in the parent process. As this is called from PKCS#11, there is no way to make this asynchronous.
+[PIPCClientCerts::Sign]
+description = Performs a signature on given data with a key corresponding to the given identifier. This is called from PKCS#11, so there is no way to make this asynchronous.
 
 #############################################################
 #          AVOID ADDING NEW MESSAGES TO THIS FILE           #
diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in
index 95eaa5107f2f52d4c171d35e508c56d8921ad657..3149f3ac9c6bf03b16fd8bf972f8f6cf618ccb9e 100644
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -66,6 +66,8 @@
 @BINPATH@/@DLL_PREFIX@softokn3.chk
 #endif
 
+@BINPATH@/@DLL_PREFIX@ipcclientcerts@DLL_SUFFIX@
+
 #ifndef MOZ_FOLD_LIBS
 @BINPATH@/@DLL_PREFIX@mozsqlite3@DLL_SUFFIX@
 #endif
diff --git a/netwerk/ipc/PSocketProcess.ipdl b/netwerk/ipc/PSocketProcess.ipdl
index 11988f4976e9fcb62b50abec84ecaae75b087f02..2a332136c2565fbb612999630ceb8e3c77d06386 100644
--- a/netwerk/ipc/PSocketProcess.ipdl
+++ b/netwerk/ipc/PSocketProcess.ipdl
@@ -128,7 +128,7 @@ parent:
                         ByteArray aServerCert,
                         ByteArray? aClientCert,
                         ByteArray[] aCollectedCANames)
-    returns (bool aSucceeded, ByteArray aOutCert, ByteArray aOutKey, ByteArray[] aBuiltChain);
+    returns (bool aSucceeded, ByteArray aOutCert, ByteArray[] aBuiltChain);
   async PProxyConfigLookup(nsIURI aUri, uint32_t aFlags);
   async CachePushCheck(nsIURI aPushedURL,
                        OriginAttributes aOriginAttributes,
diff --git a/netwerk/ipc/SocketProcessChild.cpp b/netwerk/ipc/SocketProcessChild.cpp
index e6ea9ea56b95a2d5fac42ea37e35de82086e6e70..ee4a736372410b728e7bdbf1888381066d9b8793 100644
--- a/netwerk/ipc/SocketProcessChild.cpp
+++ b/netwerk/ipc/SocketProcessChild.cpp
@@ -165,7 +165,15 @@ bool SocketProcessChild::Init(base::ProcessId aParentPid,
   // Initialize DNS Service here, since it needs to be done in main thread.
   nsCOMPtr<nsIDNSService> dns =
       do_GetService("@mozilla.org/network/dns-service;1", &rv);
-  return NS_SUCCEEDED(rv);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  if (!EnsureNSSInitializedChromeOrContent()) {
+    return false;
+  }
+
+  return true;
 }
 
 void SocketProcessChild::ActorDestroy(ActorDestroyReason aWhy) {
@@ -214,6 +222,7 @@ mozilla::ipc::IPCResult SocketProcessChild::RecvInit(
   if (aAttributes.mInitSandbox()) {
     Unused << RecvInitLinuxSandbox(aAttributes.mSandboxBroker());
   }
+
   return IPC_OK();
 }
 
@@ -472,9 +481,7 @@ SocketProcessChild::GetAndRemoveDataBridge(uint64_t aChannelId) {
 }
 
 mozilla::ipc::IPCResult SocketProcessChild::RecvClearSessionCache() {
-  if (EnsureNSSInitializedChromeOrContent()) {
-    nsNSSComponent::DoClearSSLExternalAndInternalSessionCache();
-  }
+  nsNSSComponent::DoClearSSLExternalAndInternalSessionCache();
   return IPC_OK();
 }
 
diff --git a/netwerk/ipc/SocketProcessImpl.cpp b/netwerk/ipc/SocketProcessImpl.cpp
index 6f55eb85845f9c2d59ee4252765a9ff77f3b9d2b..6a0739622f1697b411bb6b1f74f25754060338fc 100644
--- a/netwerk/ipc/SocketProcessImpl.cpp
+++ b/netwerk/ipc/SocketProcessImpl.cpp
@@ -49,11 +49,13 @@ bool SocketProcessImpl::Init(int aArgc, char* aArgv[]) {
   LoadLibraryW(L"nss3.dll");
   LoadLibraryW(L"softokn3.dll");
   LoadLibraryW(L"freebl3.dll");
+  LoadLibraryW(L"ipcclientcerts.dll");
   mozilla::SandboxTarget::Instance()->StartSandbox();
 #elif defined(__OpenBSD__) && defined(MOZ_SANDBOX)
   PR_LoadLibrary("libnss3.so");
   PR_LoadLibrary("libsoftokn3.so");
   PR_LoadLibrary("libfreebl3.so");
+  PR_LoadLibrary("libipcclientcerts.so");
   StartOpenBSDSandbox(GeckoProcessType_Socket);
 #endif
 
diff --git a/netwerk/ipc/SocketProcessParent.cpp b/netwerk/ipc/SocketProcessParent.cpp
index 92f1fb2fc752aef8c99aa2f0d1b57489d4feb871..6a69209be231acb611dfe0dbceb2e94682e321a8 100644
--- a/netwerk/ipc/SocketProcessParent.cpp
+++ b/netwerk/ipc/SocketProcessParent.cpp
@@ -25,6 +25,7 @@
 #include "nsIConsoleService.h"
 #include "nsIHttpActivityObserver.h"
 #include "nsIObserverService.h"
+#include "nsNSSComponent.h"
 #include "nsNSSIOLayer.h"
 #include "nsIOService.h"
 #include "nsHttpHandler.h"
@@ -311,8 +312,7 @@ mozilla::ipc::IPCResult SocketProcessParent::RecvGetTLSClientCert(
     const int32_t& aPort, const uint32_t& aProviderFlags,
     const uint32_t& aProviderTlsFlags, const ByteArray& aServerCert,
     Maybe<ByteArray>&& aClientCert, nsTArray<ByteArray>&& aCollectedCANames,
-    bool* aSucceeded, ByteArray* aOutCert, ByteArray* aOutKey,
-    nsTArray<ByteArray>* aBuiltChain) {
+    bool* aSucceeded, ByteArray* aOutCert, nsTArray<ByteArray>* aBuiltChain) {
   *aSucceeded = false;
 
   SECItem serverCertItem = {
@@ -342,16 +342,15 @@ mozilla::ipc::IPCResult SocketProcessParent::RecvGetTLSClientCert(
   }
 
   UniqueCERTCertificate cert;
-  UniqueSECKEYPrivateKey key;
   UniqueCERTCertList builtChain;
   SECStatus status =
       DoGetClientAuthData(std::move(info), serverCert,
-                          std::move(collectedCANames), cert, key, builtChain);
+                          std::move(collectedCANames), cert, builtChain);
   if (status != SECSuccess) {
     return IPC_OK();
   }
 
-  SerializeClientCertAndKey(cert, key, *aOutCert, *aOutKey);
+  aOutCert->data().AppendElements(cert->derCert.data, cert->derCert.len);
 
   if (builtChain) {
     for (CERTCertListNode* n = CERT_LIST_HEAD(builtChain);
diff --git a/netwerk/ipc/SocketProcessParent.h b/netwerk/ipc/SocketProcessParent.h
index c51124d991e8a75bc16f6e76ace0b260e12e276a..3628a0ee449d313a47fb892b3531d6b692535521 100644
--- a/netwerk/ipc/SocketProcessParent.h
+++ b/netwerk/ipc/SocketProcessParent.h
@@ -99,8 +99,14 @@ class SocketProcessParent final
       const int32_t& aPort, const uint32_t& aProviderFlags,
       const uint32_t& aProviderTlsFlags, const ByteArray& aServerCert,
       Maybe<ByteArray>&& aClientCert, nsTArray<ByteArray>&& aCollectedCANames,
-      bool* aSucceeded, ByteArray* aOutCert, ByteArray* aOutKey,
-      nsTArray<ByteArray>* aBuiltChain);
+      bool* aSucceeded, ByteArray* aOutCert, nsTArray<ByteArray>* aBuiltChain);
+
+  mozilla::ipc::IPCResult RecvFindIPCClientCertObjects(
+      nsTArray<IPCClientCertObject>* aObjects);
+  mozilla::ipc::IPCResult RecvIPCClientCertSign(ByteArray aCert,
+                                                ByteArray aData,
+                                                ByteArray aParams,
+                                                ByteArray* aSignature);
 
   already_AddRefed<PProxyConfigLookupParent> AllocPProxyConfigLookupParent(
       nsIURI* aURI, const uint32_t& aProxyResolveFlags);
diff --git a/security/certverifier/NSSCertDBTrustDomain.cpp b/security/certverifier/NSSCertDBTrustDomain.cpp
index a26121054c5a7f3b201a9a47f11357dc75521d02..9f16736e817aafc93db3dc9db7613c91b5d75a27 100644
--- a/security/certverifier/NSSCertDBTrustDomain.cpp
+++ b/security/certverifier/NSSCertDBTrustDomain.cpp
@@ -35,6 +35,7 @@
 #include "nsNSSCertHelper.h"
 #include "nsNSSCertificate.h"
 #include "nsNSSCertificateDB.h"
+#include "nsNSSIOLayer.h"
 #include "nsPrintfCString.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
@@ -1604,8 +1605,12 @@ void DisableMD5() {
       NSS_USE_ALG_IN_CERT_SIGNATURE | NSS_USE_ALG_IN_CMS_SIGNATURE);
 }
 
+// Load a given PKCS#11 module located in the given directory. It will be named
+// the given module name. Optionally pass some string parameters to it via
+// 'params'. This argument will be provided to C_Initialize when called on the
+// module.
 bool LoadUserModuleAt(const char* moduleName, const char* libraryName,
-                      const nsCString& dir) {
+                      const nsCString& dir, /* optional */ const char* params) {
   // 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.
@@ -1629,6 +1634,11 @@ bool LoadUserModuleAt(const char* moduleName, const char* libraryName,
   pkcs11ModuleSpec.AppendLiteral("\" library=\"");
   pkcs11ModuleSpec.Append(fullLibraryPath);
   pkcs11ModuleSpec.AppendLiteral("\"");
+  if (params) {
+    pkcs11ModuleSpec.AppendLiteral("\" parameters=\"");
+    pkcs11ModuleSpec.Append(params);
+    pkcs11ModuleSpec.AppendLiteral("\"");
+  }
 
   UniqueSECMODModule userModule(SECMOD_LoadUserModule(
       const_cast<char*>(pkcs11ModuleSpec.get()), nullptr, false));
@@ -1643,6 +1653,19 @@ bool LoadUserModuleAt(const char* moduleName, const char* libraryName,
   return true;
 }
 
+const char* kIPCClientCertsModuleName = "IPC Client Cert Module";
+
+bool LoadIPCClientCertsModule(const nsCString& dir) {
+  // The IPC client certs module needs to be able to call back into gecko to be
+  // able to communicate with the parent process over IPC. This is achieved by
+  // serializing the addresses of the relevant functions and passing them as an
+  // extra string parameter that will be available when C_Initialize is called
+  // on IPC client certs.
+  nsPrintfCString addrs("%p,%p", DoFindObjects, DoSign);
+  return LoadUserModuleAt(kIPCClientCertsModuleName, "ipcclientcerts", dir,
+                          addrs.get());
+}
+
 const char* kOSClientCertsModuleName = "OS Client Cert Module";
 
 bool LoadOSClientCertsModule(const nsCString& dir) {
@@ -1652,7 +1675,8 @@ bool LoadOSClientCertsModule(const nsCString& dir) {
     return false;
   }
 #endif
-  return LoadUserModuleAt(kOSClientCertsModuleName, "osclientcerts", dir);
+  return LoadUserModuleAt(kOSClientCertsModuleName, "osclientcerts", dir,
+                          nullptr);
 }
 
 bool LoadLoadableRoots(const nsCString& dir) {
@@ -1663,7 +1687,7 @@ bool LoadLoadableRoots(const nsCString& dir) {
   // "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);
+  return LoadUserModuleAt(kRootModuleName, "nssckbi", dir, nullptr);
 }
 
 nsresult DefaultServerNicknameForCert(const CERTCertificate* cert,
diff --git a/security/certverifier/NSSCertDBTrustDomain.h b/security/certverifier/NSSCertDBTrustDomain.h
index fa24b85f2e249404753f51beed6d268496f1835d..8e354fe0184fd3fb9970b92dab7cf26bdf9a9e43 100644
--- a/security/certverifier/NSSCertDBTrustDomain.h
+++ b/security/certverifier/NSSCertDBTrustDomain.h
@@ -76,6 +76,23 @@ bool LoadOSClientCertsModule(const nsCString& dir);
 
 extern const char* kOSClientCertsModuleName;
 
+/**
+ * Loads the IPC 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 LoadIPCClientCertsModule(const nsCString& dir);
+
+extern const char* kIPCClientCertsModuleName;
+
+/**
+ * Unloads the loadable roots module and os client certs module, if loaded.
+ */
+void UnloadUserModules();
+
 nsresult DefaultServerNicknameForCert(const CERTCertificate* cert,
                                       /*out*/ nsCString& nickname);
 
diff --git a/security/manager/ssl/IPCClientCertsChild.cpp b/security/manager/ssl/IPCClientCertsChild.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bea0e27dffc71556f9b7ff97e832a2db16261d18
--- /dev/null
+++ b/security/manager/ssl/IPCClientCertsChild.cpp
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IPCClientCertsChild.h"
+
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+
+namespace mozilla::psm {
+
+IPCClientCertsChild::IPCClientCertsChild() = default;
+
+}  // namespace mozilla::psm
diff --git a/security/manager/ssl/IPCClientCertsChild.h b/security/manager/ssl/IPCClientCertsChild.h
new file mode 100644
index 0000000000000000000000000000000000000000..17020fce2ed3b504d1e3ddb0a1f0426078108b68
--- /dev/null
+++ b/security/manager/ssl/IPCClientCertsChild.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_psm_IPCClientCertsChild_h__
+#define mozilla_psm_IPCClientCertsChild_h__
+
+#include "mozilla/psm/PIPCClientCertsChild.h"
+
+namespace mozilla {
+
+namespace ipc {
+class BackgroundChildImpl;
+}  // namespace ipc
+
+namespace psm {
+
+class IPCClientCertsChild final : public PIPCClientCertsChild {
+  friend class mozilla::ipc::BackgroundChildImpl;
+
+ public:
+  IPCClientCertsChild();
+
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(IPCClientCertsChild);
+
+ private:
+  ~IPCClientCertsChild() = default;
+};
+
+}  // namespace psm
+}  // namespace mozilla
+
+#endif
diff --git a/security/manager/ssl/IPCClientCertsParent.cpp b/security/manager/ssl/IPCClientCertsParent.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8058dbdd8365fb87dbcb5fcc3417efb770685bd8
--- /dev/null
+++ b/security/manager/ssl/IPCClientCertsParent.cpp
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IPCClientCertsParent.h"
+
+#include "mozilla/ipc/BackgroundParent.h"
+
+namespace mozilla::psm {
+
+IPCClientCertsParent::IPCClientCertsParent() = default;
+
+// When the IPC client certs module needs to find certificate and key objects
+// in the socket process, it will cause this function to be called in the
+// parent process. The parent process needs to find all certificates with
+// private keys (because these are potential client certificates).
+mozilla::ipc::IPCResult IPCClientCertsParent::RecvFindObjects(
+    nsTArray<IPCClientCertObject>* aObjects) {
+  UniqueCERTCertList certList(psm::FindClientCertificatesWithPrivateKeys());
+  if (!certList) {
+    return IPC_OK();
+  }
+  CERTCertListNode* n = CERT_LIST_HEAD(certList);
+  while (!CERT_LIST_END(n, certList)) {
+    nsTArray<uint8_t> certDER(n->cert->derCert.data, n->cert->derCert.len);
+    uint32_t slotType;
+    UniqueSECKEYPublicKey pubkey(CERT_ExtractPublicKey(n->cert));
+    if (!pubkey) {
+      return IPC_OK();
+    }
+    switch (SECKEY_GetPublicKeyType(pubkey.get())) {
+      case rsaKey:
+      case rsaPssKey: {
+        slotType = PK11_DoesMechanism(n->cert->slot, CKM_RSA_PKCS_PSS)
+                       ? kIPCClientCertsSlotTypeModern
+                       : kIPCClientCertsSlotTypeLegacy;
+        nsTArray<uint8_t> modulus(pubkey->u.rsa.modulus.data,
+                                  pubkey->u.rsa.modulus.len);
+        RSAKey rsakey(modulus, certDER, slotType);
+        aObjects->AppendElement(std::move(rsakey));
+        break;
+      }
+      case ecKey: {
+        slotType = kIPCClientCertsSlotTypeModern;
+        nsTArray<uint8_t> params(pubkey->u.ec.DEREncodedParams.data,
+                                 pubkey->u.ec.DEREncodedParams.len);
+        ECKey eckey(params, certDER, slotType);
+        aObjects->AppendElement(std::move(eckey));
+        break;
+      }
+      default:
+        n = CERT_LIST_NEXT(n);
+        continue;
+    }
+    Certificate cert(certDER, slotType);
+    aObjects->AppendElement(std::move(cert));
+
+    n = CERT_LIST_NEXT(n);
+  }
+  return IPC_OK();
+}
+
+// When the IPC client certs module needs to sign data using a key managed by
+// the parent process, it will cause this function to be called in the parent
+// process. The parent process needs to find the key corresponding to the given
+// certificate and sign the given data with the given parameters.
+mozilla::ipc::IPCResult IPCClientCertsParent::RecvSign(ByteArray aCert,
+                                                       ByteArray aData,
+                                                       ByteArray aParams,
+                                                       ByteArray* aSignature) {
+  SECItem certItem = {siBuffer, const_cast<uint8_t*>(aCert.data().Elements()),
+                      static_cast<unsigned int>(aCert.data().Length())};
+  aSignature->data().Clear();
+
+  UniqueCERTCertificate cert(CERT_NewTempCertificate(
+      CERT_GetDefaultCertDB(), &certItem, nullptr, false, true));
+  if (!cert) {
+    return IPC_OK();
+  }
+  UniqueSECKEYPrivateKey key(PK11_FindKeyByAnyCert(cert.get(), nullptr));
+  if (!key) {
+    return IPC_OK();
+  }
+  SECItem params = {siBuffer, aParams.data().Elements(),
+                    static_cast<unsigned int>(aParams.data().Length())};
+  SECItem* paramsPtr = aParams.data().Length() > 0 ? &params : nullptr;
+  CK_MECHANISM_TYPE mechanism;
+  switch (key->keyType) {
+    case ecKey:
+      mechanism = CKM_ECDSA;
+      break;
+    case rsaKey:
+      mechanism = aParams.data().Length() > 0 ? CKM_RSA_PKCS_PSS : CKM_RSA_PKCS;
+      break;
+    default:
+      return IPC_OK();
+  }
+  uint32_t len = PK11_SignatureLen(key.get());
+  UniqueSECItem sig(::SECITEM_AllocItem(nullptr, nullptr, len));
+  SECItem hash = {siBuffer, aData.data().Elements(),
+                  static_cast<unsigned int>(aData.data().Length())};
+  SECStatus srv =
+      PK11_SignWithMechanism(key.get(), mechanism, paramsPtr, sig.get(), &hash);
+  if (srv != SECSuccess) {
+    return IPC_OK();
+  }
+  aSignature->data().AppendElements(sig->data, sig->len);
+  return IPC_OK();
+}
+
+}  // namespace mozilla::psm
diff --git a/security/manager/ssl/IPCClientCertsParent.h b/security/manager/ssl/IPCClientCertsParent.h
new file mode 100644
index 0000000000000000000000000000000000000000..09ad589aca053ec7bc525f045b45341d3d5b5d46
--- /dev/null
+++ b/security/manager/ssl/IPCClientCertsParent.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_psm_IPCClientCertsParent_h__
+#define mozilla_psm_IPCClientCertsParent_h__
+
+#include "mozilla/psm/PIPCClientCertsParent.h"
+
+namespace mozilla {
+
+namespace ipc {
+class BackgroundParentImpl;
+}  // namespace ipc
+
+namespace psm {
+
+class IPCClientCertsParent final : public PIPCClientCertsParent {
+  friend class mozilla::ipc::BackgroundParentImpl;
+
+ public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(IPCClientCertsParent)
+
+  mozilla::ipc::IPCResult RecvFindObjects(
+      nsTArray<IPCClientCertObject>* aObjects);
+  mozilla::ipc::IPCResult RecvSign(ByteArray aCert, ByteArray aData,
+                                   ByteArray aParams, ByteArray* aSignature);
+
+ private:
+  IPCClientCertsParent();
+  ~IPCClientCertsParent() = default;
+};
+
+}  // namespace psm
+}  // namespace mozilla
+
+#endif
diff --git a/security/manager/ssl/PIPCClientCerts.ipdl b/security/manager/ssl/PIPCClientCerts.ipdl
new file mode 100644
index 0000000000000000000000000000000000000000..6e9d7bad61e57d8f9f87fed5b705b6e9a58c1ce4
--- /dev/null
+++ b/security/manager/ssl/PIPCClientCerts.ipdl
@@ -0,0 +1,35 @@
+/* -*- 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 protocol PBackground;
+
+include PSMIPCTypes;
+
+using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h";
+
+namespace mozilla {
+namespace psm {
+
+[RefCounted] sync protocol PIPCClientCerts
+{
+  manager PBackground;
+
+parent:
+  // Called from the socket process to the parent process to find client
+  // certificates and associated keys.
+  sync FindObjects() returns (IPCClientCertObject[] aObjects);
+
+  // Called from the socket process to the parent process to sign the given
+  // data with the given parameters using the key associated with the given
+  // certificate. Used when a TLS server requests a client authentication
+  // certificate.
+  sync Sign(ByteArray aCert, ByteArray aData, ByteArray aParams)
+    returns (ByteArray aSignature);
+
+  async __delete__();
+};
+
+} // namespace psm
+} // namespace mozilla
diff --git a/security/manager/ssl/PSMIPCCommon.cpp b/security/manager/ssl/PSMIPCCommon.cpp
index 6e0ed62302b36852b858e82c894fbafd1a8ba968..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/security/manager/ssl/PSMIPCCommon.cpp
+++ b/security/manager/ssl/PSMIPCCommon.cpp
@@ -1,161 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set sw=2 ts=8 et tw=80 : */
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "CTVerifyResult.h"
-#include "PSMIPCCommon.h"
-
-namespace mozilla {
-namespace psm {
-
-SECItem* WrapPrivateKeyInfoWithEmptyPassword(
-    SECKEYPrivateKey* pk) /* encrypt this private key */
-{
-  if (!pk) {
-    PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
-    return nullptr;
-  }
-
-  UniquePK11SlotInfo slot(PK11_GetInternalSlot());
-  if (!slot) {
-    return nullptr;
-  }
-
-  // For private keys, NSS cannot export anything other than RSA, but we need EC
-  // also. So, we use the private key encryption function to serialize instead,
-  // using a hard-coded dummy password; this is not intended to provide any
-  // additional security, it just works around a limitation in NSS.
-  SECItem dummyPassword = {siBuffer, nullptr, 0};
-  UniqueSECKEYEncryptedPrivateKeyInfo epki(PK11_ExportEncryptedPrivKeyInfo(
-      slot.get(), SEC_OID_AES_128_CBC, &dummyPassword, pk, 1, nullptr));
-
-  if (!epki) {
-    return nullptr;
-  }
-
-  return SEC_ASN1EncodeItem(
-      nullptr, nullptr, epki.get(),
-      NSS_Get_SECKEY_EncryptedPrivateKeyInfoTemplate(nullptr, false));
-}
-
-SECStatus UnwrapPrivateKeyInfoWithEmptyPassword(
-    SECItem* derPKI, const UniqueCERTCertificate& aCert,
-    SECKEYPrivateKey** privk) {
-  if (!derPKI || !aCert || !privk) {
-    PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
-    return SECFailure;
-  }
-
-  UniqueSECKEYPublicKey publicKey(CERT_ExtractPublicKey(aCert.get()));
-  // This is a pointer to data inside publicKey
-  SECItem* publicValue = nullptr;
-  switch (publicKey->keyType) {
-    case dsaKey:
-      publicValue = &publicKey->u.dsa.publicValue;
-      break;
-    case dhKey:
-      publicValue = &publicKey->u.dh.publicValue;
-      break;
-    case rsaKey:
-      publicValue = &publicKey->u.rsa.modulus;
-      break;
-    case ecKey:
-      publicValue = &publicKey->u.ec.publicValue;
-      break;
-    default:
-      MOZ_ASSERT(false);
-      PR_SetError(SSL_ERROR_BAD_CERTIFICATE, 0);
-      return SECFailure;
-  }
-
-  UniquePK11SlotInfo slot(PK11_GetInternalSlot());
-  if (!slot) {
-    return SECFailure;
-  }
-
-  UniquePLArenaPool temparena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
-  if (!temparena) {
-    return SECFailure;
-  }
-
-  SECKEYEncryptedPrivateKeyInfo* epki =
-      PORT_ArenaZNew(temparena.get(), SECKEYEncryptedPrivateKeyInfo);
-  if (!epki) {
-    return SECFailure;
-  }
-
-  SECStatus rv = SEC_ASN1DecodeItem(
-      temparena.get(), epki,
-      NSS_Get_SECKEY_EncryptedPrivateKeyInfoTemplate(nullptr, false), derPKI);
-  if (rv != SECSuccess) {
-    // If SEC_ASN1DecodeItem fails, we cannot assume anything about the
-    // validity of the data in epki. The best we can do is free the arena
-    // and return.
-    return rv;
-  }
-
-  // See comment in WrapPrivateKeyInfoWithEmptyPassword about this
-  // dummy password stuff.
-  SECItem dummyPassword = {siBuffer, nullptr, 0};
-  return PK11_ImportEncryptedPrivateKeyInfoAndReturnKey(
-      slot.get(), epki, &dummyPassword, nullptr, publicValue, false, false,
-      publicKey->keyType, KU_ALL, privk, nullptr);
-}
-
-void SerializeClientCertAndKey(const UniqueCERTCertificate& aCert,
-                               const UniqueSECKEYPrivateKey& aKey,
-                               ByteArray& aOutSerializedCert,
-                               ByteArray& aOutSerializedKey) {
-  if (!aCert || !aKey) {
-    return;
-  }
-
-  UniqueSECItem derPki(WrapPrivateKeyInfoWithEmptyPassword(aKey.get()));
-  if (!derPki) {
-    return;
-  }
-
-  aOutSerializedCert.data().AppendElements(aCert->derCert.data,
-                                           aCert->derCert.len);
-  aOutSerializedKey.data().AppendElements(derPki->data, derPki->len);
-}
-
-void DeserializeClientCertAndKey(const ByteArray& aSerializedCert,
-                                 const ByteArray& aSerializedKey,
-                                 UniqueCERTCertificate& aOutCert,
-                                 UniqueSECKEYPrivateKey& aOutKey) {
-  if (aSerializedCert.data().IsEmpty() || aSerializedKey.data().IsEmpty()) {
-    return;
-  }
-
-  SECItem item = {siBuffer,
-                  const_cast<uint8_t*>(aSerializedCert.data().Elements()),
-                  static_cast<unsigned int>(aSerializedCert.data().Length())};
-
-  UniqueCERTCertificate cert(CERT_NewTempCertificate(
-      CERT_GetDefaultCertDB(), &item, nullptr, false, true));
-
-  if (!cert) {
-    return;
-  }
-
-  SECItem derPKI = {siBuffer,
-                    const_cast<uint8_t*>(aSerializedKey.data().Elements()),
-                    static_cast<unsigned int>(aSerializedKey.data().Length())};
-
-  SECKEYPrivateKey* privateKey;
-  if (UnwrapPrivateKeyInfoWithEmptyPassword(&derPKI, cert, &privateKey) !=
-      SECSuccess) {
-    MOZ_ASSERT(false);
-    return;
-  }
-
-  aOutCert = std::move(cert);
-  aOutKey.reset(privateKey);
-}
-
-}  // namespace psm
-}  // namespace mozilla
diff --git a/security/manager/ssl/PSMIPCCommon.h b/security/manager/ssl/PSMIPCCommon.h
index 7d5455dd4b3f87d25ae1c5293829d91da4d91bcb..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/security/manager/ssl/PSMIPCCommon.h
+++ b/security/manager/ssl/PSMIPCCommon.h
@@ -1,34 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set sw=2 ts=8 et tw=80 : */
-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#ifndef PSMIPCCommon_h__
-#define PSMIPCCommon_h__
-
-#include "mozilla/psm/PSMIPCTypes.h"
-#include "seccomon.h"
-#include "ScopedNSSTypes.h"
-
-namespace mozilla {
-namespace psm {
-
-SECItem* WrapPrivateKeyInfoWithEmptyPassword(SECKEYPrivateKey* pk);
-SECStatus UnwrapPrivateKeyInfoWithEmptyPassword(
-    SECItem* derPKI, const UniqueCERTCertificate& aCert,
-    SECKEYPrivateKey** privk);
-void SerializeClientCertAndKey(const UniqueCERTCertificate& aCert,
-                               const UniqueSECKEYPrivateKey& aKey,
-                               ByteArray& aOutSerializedCert,
-                               ByteArray& aOutSerializedKey);
-void DeserializeClientCertAndKey(const ByteArray& aSerializedCert,
-                                 const ByteArray& aSerializedKey,
-                                 UniqueCERTCertificate& aOutCert,
-                                 UniqueSECKEYPrivateKey& aOutKey);
-
-}  // namespace psm
-}  // namespace mozilla
-
-#endif  // PSMIPCCommon_h__
diff --git a/security/manager/ssl/PSMIPCTypes.ipdlh b/security/manager/ssl/PSMIPCTypes.ipdlh
index 73e8ca04f525ba6129ee1dc0eb8a5148306e6b7e..54bce317c2039706d475f5e444238cf5b06b6502 100644
--- a/security/manager/ssl/PSMIPCTypes.ipdlh
+++ b/security/manager/ssl/PSMIPCTypes.ipdlh
@@ -12,6 +12,36 @@ struct ByteArray{
   uint8_t[] data;
 };
 
+// For ECKey, RSAKey, and Certificate, slotType indicates which slot this object
+// should exist on:
+// 1: modern (supports EC, RSA-PSS)
+// 2: legacy (only supports RSA PKCS#1v1.5)
+
+struct ECKey{
+  uint8_t[] params; // the EC point representing this key
+  uint8_t[] cert; // the encoded certificate containing this key
+  uint32_t slotType;
+};
+
+struct RSAKey{
+  uint8_t[] modulus; // the modulus of this RSA key
+  uint8_t[] cert; // the encoded certificate containing this key
+  uint32_t slotType;
+};
+
+struct Certificate{
+  uint8_t[] der; // the encoding of this certificate
+  uint32_t slotType;
+};
+
+// Helper type for sending keys and certificates over IPC for use by IPC client
+// certs.
+union IPCClientCertObject{
+  ECKey;
+  RSAKey;
+  Certificate;
+};
+
 struct DelegatedCredentialInfoArg {
   uint32_t scheme;
   uint32_t authKeyBits;
diff --git a/security/manager/ssl/ipcclientcerts/Cargo.toml b/security/manager/ssl/ipcclientcerts/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..8ef5bf5839d79b24bc8bb1a8c70211d2ecac4d7f
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "ipcclientcerts-static"
+version = "0.1.0"
+authors = ["Dana Keeler <dkeeler@mozilla.com>"]
+edition = "2018"
+
+[dependencies]
+byteorder = "1.3"
+env_logger = {version = "0.8", default-features = false } # disable `regex` to reduce code size
+lazy_static = "1"
+log = "0.4"
+pkcs11 = "0.4"
+rsclientcerts = { path = "../rsclientcerts" }
+sha2 = "0.8"
+
+[lib]
+crate-type = ["staticlib"]
diff --git a/security/manager/ssl/ipcclientcerts/dynamic-library/ipcclientcerts.symbols b/security/manager/ssl/ipcclientcerts/dynamic-library/ipcclientcerts.symbols
new file mode 100644
index 0000000000000000000000000000000000000000..562ecea21d43acd090e6c3f8f098125bd724206e
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/dynamic-library/ipcclientcerts.symbols
@@ -0,0 +1 @@
+C_GetFunctionList
diff --git a/security/manager/ssl/ipcclientcerts/dynamic-library/moz.build b/security/manager/ssl/ipcclientcerts/dynamic-library/moz.build
new file mode 100644
index 0000000000000000000000000000000000000000..afc265aef942c29e145862eac9787092e9d7ae88
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/dynamic-library/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+USE_LIBS += ["ipcclientcerts-static"]
+
+SOURCES += [
+    "stub.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+    OS_LIBS += [
+        "userenv",
+        "ws2_32",
+    ]
+
+SharedLibrary("ipcclientcerts")
+
+NoVisibilityFlags()
+SYMBOLS_FILE = "ipcclientcerts.symbols"
diff --git a/security/manager/ssl/ipcclientcerts/dynamic-library/stub.cpp b/security/manager/ssl/ipcclientcerts/dynamic-library/stub.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e50f8675741d2171eaab798fb65532f51a15a277
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/dynamic-library/stub.cpp
@@ -0,0 +1,29 @@
+/* -*- 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 "pkcs11.h"
+
+// The build system builds the rust library ipcclientcerts as a static library
+// called ipcclientcerts_static. On macOS and Windows, that static library can
+// be linked with an empty file and turned into a shared library with the
+// function C_GetFunctionList exposed. This allows that shared library to be
+// used as a PKCS#11 module (see osclientcerts).
+// Unfortunately, on Linux, exposing the C_GetFunctionList in the static
+// library doesn't work for some unknown reason. As a workaround, this file
+// declares its own C_GetFunctionList that can be exposed in the shared
+// library. It then calls the function IPCCC_GetFunctionList exposed
+// (internally to the linkage in question) by ipcclientcerts. This enables
+// the build system to ultimately turn ipcclientcerts into a shared library
+// that exposes a C_GetFunctionList function, meaning it can be used as a
+// PKCS#11 module.
+
+extern "C" {
+
+CK_RV IPCCC_GetFunctionList(CK_FUNCTION_LIST_PTR_PTR ppFunctionList);
+
+CK_RV C_GetFunctionList(CK_FUNCTION_LIST_PTR_PTR ppFunctionList) {
+  return IPCCC_GetFunctionList(ppFunctionList);
+}
+}
diff --git a/security/manager/ssl/ipcclientcerts/moz.build b/security/manager/ssl/ipcclientcerts/moz.build
new file mode 100644
index 0000000000000000000000000000000000000000..1d24bdea31dab3894e013ee26a4c9cdf88219c4d
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += ["dynamic-library"]
+
+RustLibrary("ipcclientcerts-static")
diff --git a/security/manager/ssl/ipcclientcerts/src/backend.rs b/security/manager/ssl/ipcclientcerts/src/backend.rs
new file mode 100644
index 0000000000000000000000000000000000000000..46742f71e94f836a1e6d217fed5468f367ac600d
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/src/backend.rs
@@ -0,0 +1,380 @@
+/* -*- Mode: rust; rust-indent-offset: 4 -*- */
+/* 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/. */
+
+use pkcs11::types::*;
+use rsclientcerts::error::{Error, ErrorType};
+use rsclientcerts::manager::{ClientCertsBackend, CryptokiObject, Sign, SlotType};
+use rsclientcerts::util::*;
+use sha2::{Digest, Sha256};
+use std::ffi::c_void;
+use std::thread;
+
+use crate::FindObjectsFunction;
+use crate::SignFunction;
+
+pub struct Cert {
+    class: Vec<u8>,
+    token: Vec<u8>,
+    id: Vec<u8>,
+    label: Vec<u8>,
+    value: Vec<u8>,
+    issuer: Vec<u8>,
+    serial_number: Vec<u8>,
+    subject: Vec<u8>,
+    slot_type: SlotType,
+}
+
+impl Cert {
+    fn new(der: &[u8], slot_type: SlotType) -> Result<Cert, Error> {
+        let (serial_number, issuer, subject) = read_encoded_certificate_identifiers(der)?;
+        let id = Sha256::digest(der).to_vec();
+        Ok(Cert {
+            class: serialize_uint(CKO_CERTIFICATE)?,
+            token: serialize_uint(CK_TRUE)?,
+            id,
+            label: b"IPC certificate".to_vec(),
+            value: der.to_vec(),
+            issuer,
+            serial_number,
+            subject,
+            slot_type,
+        })
+    }
+
+    fn class(&self) -> &[u8] {
+        &self.class
+    }
+
+    fn token(&self) -> &[u8] {
+        &self.token
+    }
+
+    fn id(&self) -> &[u8] {
+        &self.id
+    }
+
+    fn label(&self) -> &[u8] {
+        &self.label
+    }
+
+    fn value(&self) -> &[u8] {
+        &self.value
+    }
+
+    fn issuer(&self) -> &[u8] {
+        &self.issuer
+    }
+
+    fn serial_number(&self) -> &[u8] {
+        &self.serial_number
+    }
+
+    fn subject(&self) -> &[u8] {
+        &self.subject
+    }
+}
+
+impl CryptokiObject for Cert {
+    fn matches(&self, slot_type: SlotType, attrs: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)]) -> bool {
+        if self.slot_type != slot_type {
+            return false;
+        }
+        for (attr_type, attr_value) in attrs {
+            let comparison = match *attr_type {
+                CKA_CLASS => self.class(),
+                CKA_TOKEN => self.token(),
+                CKA_LABEL => self.label(),
+                CKA_ID => self.id(),
+                CKA_VALUE => self.value(),
+                CKA_ISSUER => self.issuer(),
+                CKA_SERIAL_NUMBER => self.serial_number(),
+                CKA_SUBJECT => self.subject(),
+                _ => return false,
+            };
+            if attr_value.as_slice() != comparison {
+                return false;
+            }
+        }
+        true
+    }
+
+    fn get_attribute(&self, attribute: CK_ATTRIBUTE_TYPE) -> Option<&[u8]> {
+        let result = match attribute {
+            CKA_CLASS => self.class(),
+            CKA_TOKEN => self.token(),
+            CKA_LABEL => self.label(),
+            CKA_ID => self.id(),
+            CKA_VALUE => self.value(),
+            CKA_ISSUER => self.issuer(),
+            CKA_SERIAL_NUMBER => self.serial_number(),
+            CKA_SUBJECT => self.subject(),
+            _ => return None,
+        };
+        Some(result)
+    }
+}
+
+pub struct Key {
+    cert: Vec<u8>,
+    class: Vec<u8>,
+    token: Vec<u8>,
+    id: Vec<u8>,
+    private: Vec<u8>,
+    key_type: Vec<u8>,
+    modulus: Option<Vec<u8>>,
+    ec_params: Option<Vec<u8>>,
+    slot_type: SlotType,
+    sign: SignFunction,
+}
+
+impl Key {
+    fn new(
+        modulus: Option<&[u8]>,
+        ec_params: Option<&[u8]>,
+        cert: &[u8],
+        slot_type: SlotType,
+        sign: SignFunction,
+    ) -> Result<Key, Error> {
+        let id = Sha256::digest(cert).to_vec();
+        let key_type = if modulus.is_some() { CKK_RSA } else { CKK_EC };
+        Ok(Key {
+            cert: cert.to_vec(),
+            class: serialize_uint(CKO_PRIVATE_KEY)?,
+            token: serialize_uint(CK_TRUE)?,
+            id,
+            private: serialize_uint(CK_TRUE)?,
+            key_type: serialize_uint(key_type)?,
+            modulus: modulus.map(|b| b.to_vec()),
+            ec_params: ec_params.map(|b| b.to_vec()),
+            slot_type,
+            sign,
+        })
+    }
+
+    fn class(&self) -> &[u8] {
+        &self.class
+    }
+
+    fn token(&self) -> &[u8] {
+        &self.token
+    }
+
+    pub fn id(&self) -> &[u8] {
+        &self.id
+    }
+
+    fn private(&self) -> &[u8] {
+        &self.private
+    }
+
+    fn key_type(&self) -> &[u8] {
+        &self.key_type
+    }
+
+    fn modulus(&self) -> Option<&[u8]> {
+        match &self.modulus {
+            Some(modulus) => Some(modulus.as_slice()),
+            None => None,
+        }
+    }
+
+    fn ec_params(&self) -> Option<&[u8]> {
+        match &self.ec_params {
+            Some(ec_params) => Some(ec_params.as_slice()),
+            None => None,
+        }
+    }
+}
+
+impl CryptokiObject for Key {
+    fn matches(&self, slot_type: SlotType, attrs: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)]) -> bool {
+        if self.slot_type != slot_type {
+            return false;
+        }
+        for (attr_type, attr_value) in attrs {
+            let comparison = match *attr_type {
+                CKA_CLASS => self.class(),
+                CKA_TOKEN => self.token(),
+                CKA_ID => self.id(),
+                CKA_PRIVATE => self.private(),
+                CKA_KEY_TYPE => self.key_type(),
+                CKA_MODULUS => {
+                    if let Some(modulus) = self.modulus() {
+                        modulus
+                    } else {
+                        return false;
+                    }
+                }
+                CKA_EC_PARAMS => {
+                    if let Some(ec_params) = self.ec_params() {
+                        ec_params
+                    } else {
+                        return false;
+                    }
+                }
+                _ => return false,
+            };
+            if attr_value.as_slice() != comparison {
+                return false;
+            }
+        }
+        true
+    }
+
+    fn get_attribute(&self, attribute: CK_ATTRIBUTE_TYPE) -> Option<&[u8]> {
+        match attribute {
+            CKA_CLASS => Some(self.class()),
+            CKA_TOKEN => Some(self.token()),
+            CKA_ID => Some(self.id()),
+            CKA_PRIVATE => Some(self.private()),
+            CKA_KEY_TYPE => Some(self.key_type()),
+            CKA_MODULUS => self.modulus(),
+            CKA_EC_PARAMS => self.ec_params(),
+            _ => None,
+        }
+    }
+}
+
+impl Sign for Key {
+    fn get_signature_length(
+        &mut self,
+        data: &[u8],
+        params: &Option<CK_RSA_PKCS_PSS_PARAMS>,
+    ) -> Result<usize, Error> {
+        // Unfortunately we don't have a way of getting the length of a signature without creating
+        // one.
+        let dummy_signature_bytes = self.sign(data, params)?;
+        Ok(dummy_signature_bytes.len())
+    }
+
+    fn sign(
+        &mut self,
+        data: &[u8],
+        params: &Option<CK_RSA_PKCS_PSS_PARAMS>,
+    ) -> Result<Vec<u8>, Error> {
+        let mut signature = Vec::new();
+        let (params_len, params) = match params {
+            Some(params) => (
+                std::mem::size_of::<CK_RSA_PKCS_PSS_PARAMS>(),
+                params as *const _ as *const u8,
+            ),
+            None => (0, std::ptr::null()),
+        };
+        (self.sign)(
+            self.cert.len(),
+            self.cert.as_ptr(),
+            data.len(),
+            data.as_ptr(),
+            params_len,
+            params,
+            Some(sign_callback),
+            &mut signature as *mut _ as *mut c_void,
+        );
+        if signature.len() > 0 {
+            Ok(signature)
+        } else {
+            Err(error_here!(ErrorType::LibraryFailure))
+        }
+    }
+}
+
+unsafe extern "C" fn sign_callback(data_len: usize, data: *const u8, ctx: *mut c_void) {
+    let signature: &mut Vec<u8> = std::mem::transmute(ctx);
+    signature.clear();
+    signature.extend_from_slice(std::slice::from_raw_parts(data, data_len));
+}
+
+unsafe extern "C" fn find_objects_callback(
+    typ: u8,
+    data_len: usize,
+    data: *const u8,
+    extra_len: usize,
+    extra: *const u8,
+    slot_type: u32,
+    ctx: *mut c_void,
+) {
+    let data = std::slice::from_raw_parts(data, data_len);
+    let extra = std::slice::from_raw_parts(extra, extra_len);
+    let slot_type = match slot_type {
+        1 => SlotType::Modern,
+        2 => SlotType::Legacy,
+        _ => return,
+    };
+    let find_objects_context: &mut FindObjectsContext = std::mem::transmute(ctx);
+    match typ {
+        1 => match Cert::new(data, slot_type) {
+            Ok(cert) => find_objects_context.certs.push(cert),
+            Err(e) => {
+                log_with_thread_id!(error, "find_objects_callback: couldn't create Cert: {}", e)
+            }
+        },
+        2 => match Key::new(
+            Some(data),
+            None,
+            extra,
+            slot_type,
+            find_objects_context.sign,
+        ) {
+            Ok(key) => find_objects_context.keys.push(key),
+            Err(e) => {
+                log_with_thread_id!(error, "find_objects_callback: couldn't create Key: {}", e)
+            }
+        },
+        3 => match Key::new(
+            None,
+            Some(data),
+            extra,
+            slot_type,
+            find_objects_context.sign,
+        ) {
+            Ok(key) => find_objects_context.keys.push(key),
+            Err(e) => {
+                log_with_thread_id!(error, "find_objects_callback: couldn't create Key: {}", e)
+            }
+        },
+        _ => log_with_thread_id!(error, "find_objects_callback: unknown type {}", typ),
+    }
+}
+
+struct FindObjectsContext {
+    certs: Vec<Cert>,
+    keys: Vec<Key>,
+    sign: SignFunction,
+}
+
+impl FindObjectsContext {
+    fn new(sign: SignFunction) -> FindObjectsContext {
+        FindObjectsContext {
+            certs: Vec::new(),
+            keys: Vec::new(),
+            sign,
+        }
+    }
+}
+
+pub struct Backend {
+    find_objects: FindObjectsFunction,
+    sign: SignFunction,
+}
+
+impl Backend {
+    pub fn new(find_objects: FindObjectsFunction, sign: SignFunction) -> Backend {
+        Backend { find_objects, sign }
+    }
+}
+
+impl ClientCertsBackend for Backend {
+    type Cert = Cert;
+    type Key = Key;
+
+    fn find_objects(&self) -> Result<(Vec<Cert>, Vec<Key>), Error> {
+        let mut find_objects_context = FindObjectsContext::new(self.sign);
+        (self.find_objects)(
+            Some(find_objects_callback),
+            &mut find_objects_context as *mut _ as *mut c_void,
+        );
+        Ok((find_objects_context.certs, find_objects_context.keys))
+    }
+}
diff --git a/security/manager/ssl/ipcclientcerts/src/lib.rs b/security/manager/ssl/ipcclientcerts/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..93eb97171e7cd6892e7502c831418e708d161412
--- /dev/null
+++ b/security/manager/ssl/ipcclientcerts/src/lib.rs
@@ -0,0 +1,1251 @@
+/* -*- Mode: rust; rust-indent-offset: 4 -*- */
+/* 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/. */
+
+#![allow(non_snake_case)]
+
+extern crate byteorder;
+extern crate env_logger;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+extern crate pkcs11;
+#[macro_use]
+extern crate rsclientcerts;
+extern crate sha2;
+
+use pkcs11::types::*;
+use rsclientcerts::manager::{Manager, SlotType};
+use std::ffi::{c_void, CStr};
+use std::sync::Mutex;
+use std::thread;
+
+// Helper macro to prefix log messages with the current thread ID.
+macro_rules! log_with_thread_id {
+    ($log_level:ident, $($message:expr),*) => {{
+        $log_level!("{:?} {}", thread::current().id(), format_args!($($message),*));
+    }};
+}
+
+mod backend;
+
+use backend::Backend;
+
+type FindObjectsCallback = Option<
+    unsafe extern "C" fn(
+        typ: u8,
+        data_len: usize,
+        data: *const u8,
+        extra_len: usize,
+        extra: *const u8,
+        slot_type: u32,
+        ctx: *mut c_void,
+    ),
+>;
+
+type FindObjectsFunction = extern "C" fn(callback: FindObjectsCallback, ctx: *mut c_void);
+
+type SignCallback =
+    Option<unsafe extern "C" fn(data_len: usize, data: *const u8, ctx: *mut c_void)>;
+
+type SignFunction = extern "C" fn(
+    cert_len: usize,
+    cert: *const u8,
+    data_len: usize,
+    data: *const u8,
+    params_len: usize,
+    params: *const u8,
+    callback: SignCallback,
+    ctx: *mut c_void,
+);
+
+lazy_static! {
+    /// The singleton `Manager` that handles state with respect to PKCS #11. Only one thread
+    /// may use it at a time, but there is no restriction on which threads may use it.
+    static ref MANAGER: Mutex<Option<Manager<Backend>>> = Mutex::new(None);
+}
+
+// Obtaining a handle on the manager is a two-step process. First the mutex must be locked, which
+// (if successful), results in a mutex guard object. We must then get a mutable refence to the
+// underlying manager (if set - otherwise we return an error). This can't happen all in one macro
+// without dropping a reference that needs to live long enough for this to be safe. In
+// practice, this looks like:
+//   let mut manager_guard = try_to_get_manager_guard!();
+//   let manager = manager_guard_to_manager!(manager_guard);
+macro_rules! try_to_get_manager_guard {
+    () => {
+        match MANAGER.lock() {
+            Ok(maybe_manager) => maybe_manager,
+            Err(poison_error) => {
+                log_with_thread_id!(
+                    error,
+                    "previous thread panicked acquiring manager lock: {}",
+                    poison_error
+                );
+                return CKR_DEVICE_ERROR;
+            }
+        }
+    };
+}
+
+macro_rules! manager_guard_to_manager {
+    ($manager_guard:ident) => {
+        match $manager_guard.as_mut() {
+            Some(manager) => manager,
+            None => {
+                log_with_thread_id!(error, "manager expected to be set, but it is not");
+                return CKR_DEVICE_ERROR;
+            }
+        }
+    };
+}
+
+/// This gets called to initialize the module. For this implementation, this consists of
+/// instantiating the `Manager`.
+extern "C" fn C_Initialize(pInitArgs: CK_C_INITIALIZE_ARGS_PTR) -> CK_RV {
+    // This will fail if this has already been called, but this isn't a problem because either way,
+    // logging has been initialized.
+    let _ = env_logger::try_init();
+
+    // pInitArgs.pReserved will be a c-string containing the base-16
+    // stringification of the addresses of the functions to call to communicate
+    // with the main process.
+    if pInitArgs.is_null() {
+        log_with_thread_id!(error, "pInitArgs is null?");
+        return CKR_DEVICE_ERROR;
+    }
+    let serialized_addresses_ptr = unsafe { (*pInitArgs).pReserved };
+    if serialized_addresses_ptr.is_null() {
+        log_with_thread_id!(error, "pInitArgs.pReserved is null?");
+        return CKR_DEVICE_ERROR;
+    }
+    let serialized_addresses_cstr =
+        unsafe { CStr::from_ptr(serialized_addresses_ptr as *mut std::os::raw::c_char) };
+    let serialized_addresses = match serialized_addresses_cstr.to_str() {
+        Ok(serialized_addresses) => serialized_addresses,
+        Err(_) => {
+            log_with_thread_id!(error, "pInitArgs.pReserved isn't a valid utf-8 string?");
+            return CKR_DEVICE_ERROR;
+        }
+    };
+    let function_addresses: Vec<usize> = serialized_addresses
+        .split(',')
+        .filter_map(|serialized_address| usize::from_str_radix(serialized_address, 16).ok())
+        .collect();
+    if function_addresses.len() != 2 {
+        log_with_thread_id!(
+            error,
+            "expected 2 hex addresses, found {}",
+            function_addresses.len()
+        );
+        return CKR_DEVICE_ERROR;
+    }
+    let find_objects: FindObjectsFunction = unsafe { std::mem::transmute(function_addresses[0]) };
+    let sign: SignFunction = unsafe { std::mem::transmute(function_addresses[1]) };
+    let mut manager_guard = try_to_get_manager_guard!();
+    match manager_guard.replace(Manager::new(Backend::new(find_objects, sign))) {
+        Some(_unexpected_previous_manager) => {
+            #[cfg(target_os = "macos")]
+            {
+                log_with_thread_id!(info, "C_Initialize: manager previously set (this is expected on macOS - replacing it)");
+            }
+            #[cfg(target_os = "windows")]
+            {
+                log_with_thread_id!(warn, "C_Initialize: manager unexpectedly previously set (bravely continuing by replacing it)");
+            }
+        }
+        None => {}
+    }
+    log_with_thread_id!(debug, "C_Initialize: CKR_OK");
+    CKR_OK
+}
+
+extern "C" fn C_Finalize(_pReserved: CK_VOID_PTR) -> CK_RV {
+    log_with_thread_id!(debug, "C_Finalize: CKR_OK");
+    CKR_OK
+}
+
+// The specification mandates that these strings be padded with spaces to the appropriate length.
+// Since the length of fixed-size arrays in rust is part of the type, the compiler enforces that
+// these byte strings are of the correct length.
+const MANUFACTURER_ID_BYTES: &[u8; 32] = b"Mozilla Corporation             ";
+const LIBRARY_DESCRIPTION_BYTES: &[u8; 32] = b"IPC Client Cert Module          ";
+
+/// This gets called to gather some information about the module. In particular, this implementation
+/// supports (portions of) cryptoki (PKCS #11) version 2.2.
+extern "C" fn C_GetInfo(pInfo: CK_INFO_PTR) -> CK_RV {
+    if pInfo.is_null() {
+        log_with_thread_id!(error, "C_GetInfo: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    log_with_thread_id!(debug, "C_GetInfo: CKR_OK");
+    let mut info = CK_INFO::default();
+    info.cryptokiVersion.major = 2;
+    info.cryptokiVersion.minor = 2;
+    info.manufacturerID = *MANUFACTURER_ID_BYTES;
+    info.libraryDescription = *LIBRARY_DESCRIPTION_BYTES;
+    unsafe {
+        *pInfo = info;
+    }
+    CKR_OK
+}
+
+/// This module has two slots.
+const SLOT_COUNT: CK_ULONG = 2;
+/// The slot with ID 1 supports modern mechanisms like RSA-PSS.
+const SLOT_ID_MODERN: CK_SLOT_ID = 1;
+/// The slot with ID 2 only supports legacy mechanisms.
+const SLOT_ID_LEGACY: CK_SLOT_ID = 2;
+
+/// This gets called twice: once with a null `pSlotList` to get the number of slots (returned via
+/// `pulCount`) and a second time to get the ID for each slot.
+extern "C" fn C_GetSlotList(
+    _tokenPresent: CK_BBOOL,
+    pSlotList: CK_SLOT_ID_PTR,
+    pulCount: CK_ULONG_PTR,
+) -> CK_RV {
+    if pulCount.is_null() {
+        log_with_thread_id!(error, "C_GetSlotList: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    if !pSlotList.is_null() {
+        if unsafe { *pulCount } < SLOT_COUNT {
+            log_with_thread_id!(error, "C_GetSlotList: CKR_BUFFER_TOO_SMALL");
+            return CKR_BUFFER_TOO_SMALL;
+        }
+        unsafe {
+            *pSlotList = SLOT_ID_MODERN;
+            *pSlotList.offset(1) = SLOT_ID_LEGACY;
+        }
+    };
+    unsafe {
+        *pulCount = SLOT_COUNT;
+    }
+    log_with_thread_id!(debug, "C_GetSlotList: CKR_OK");
+    CKR_OK
+}
+
+const SLOT_DESCRIPTION_MODERN_BYTES: &[u8; 64] =
+    b"IPC Client Cert Slot (Modern)                                   ";
+const SLOT_DESCRIPTION_LEGACY_BYTES: &[u8; 64] =
+    b"IPC Client Cert Slot (Legacy)                                   ";
+
+/// This gets called to obtain information about slots. In this implementation, the tokens are
+/// always present in the slots.
+extern "C" fn C_GetSlotInfo(slotID: CK_SLOT_ID, pInfo: CK_SLOT_INFO_PTR) -> CK_RV {
+    if (slotID != SLOT_ID_MODERN && slotID != SLOT_ID_LEGACY) || pInfo.is_null() {
+        log_with_thread_id!(error, "C_GetSlotInfo: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let description = if slotID == SLOT_ID_MODERN {
+        SLOT_DESCRIPTION_MODERN_BYTES
+    } else {
+        SLOT_DESCRIPTION_LEGACY_BYTES
+    };
+    let slot_info = CK_SLOT_INFO {
+        slotDescription: *description,
+        manufacturerID: *MANUFACTURER_ID_BYTES,
+        flags: CKF_TOKEN_PRESENT,
+        hardwareVersion: CK_VERSION::default(),
+        firmwareVersion: CK_VERSION::default(),
+    };
+    unsafe {
+        *pInfo = slot_info;
+    }
+    log_with_thread_id!(debug, "C_GetSlotInfo: CKR_OK");
+    CKR_OK
+}
+
+const TOKEN_LABEL_MODERN_BYTES: &[u8; 32] = b"IPC Client Cert Token (Modern)  ";
+const TOKEN_LABEL_LEGACY_BYTES: &[u8; 32] = b"IPC Client Cert Token (Legacy)  ";
+const TOKEN_MODEL_BYTES: &[u8; 16] = b"ipcclientcerts  ";
+const TOKEN_SERIAL_NUMBER_BYTES: &[u8; 16] = b"0000000000000000";
+
+/// This gets called to obtain some information about tokens. This implementation has two slots,
+/// so it has two tokens. This information is primarily for display purposes.
+extern "C" fn C_GetTokenInfo(slotID: CK_SLOT_ID, pInfo: CK_TOKEN_INFO_PTR) -> CK_RV {
+    if (slotID != SLOT_ID_MODERN && slotID != SLOT_ID_LEGACY) || pInfo.is_null() {
+        log_with_thread_id!(error, "C_GetTokenInfo: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mut token_info = CK_TOKEN_INFO::default();
+    let label = if slotID == SLOT_ID_MODERN {
+        TOKEN_LABEL_MODERN_BYTES
+    } else {
+        TOKEN_LABEL_LEGACY_BYTES
+    };
+    token_info.label = *label;
+    token_info.manufacturerID = *MANUFACTURER_ID_BYTES;
+    token_info.model = *TOKEN_MODEL_BYTES;
+    token_info.serialNumber = *TOKEN_SERIAL_NUMBER_BYTES;
+    unsafe {
+        *pInfo = token_info;
+    }
+    log_with_thread_id!(debug, "C_GetTokenInfo: CKR_OK");
+    CKR_OK
+}
+
+/// This gets called to determine what mechanisms a slot supports. The modern slot supports ECDSA,
+/// RSA PKCS, and RSA PSS. The legacy slot only supports RSA PKCS.
+extern "C" fn C_GetMechanismList(
+    slotID: CK_SLOT_ID,
+    pMechanismList: CK_MECHANISM_TYPE_PTR,
+    pulCount: CK_ULONG_PTR,
+) -> CK_RV {
+    if (slotID != SLOT_ID_MODERN && slotID != SLOT_ID_LEGACY) || pulCount.is_null() {
+        log_with_thread_id!(error, "C_GetMechanismList: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mechanisms = if slotID == SLOT_ID_MODERN {
+        vec![CKM_ECDSA, CKM_RSA_PKCS, CKM_RSA_PKCS_PSS]
+    } else {
+        vec![CKM_RSA_PKCS]
+    };
+    if !pMechanismList.is_null() {
+        if unsafe { *pulCount as usize } < mechanisms.len() {
+            log_with_thread_id!(error, "C_GetMechanismList: CKR_ARGUMENTS_BAD");
+            return CKR_ARGUMENTS_BAD;
+        }
+        for i in 0..mechanisms.len() {
+            unsafe {
+                *pMechanismList.offset(i as isize) = mechanisms[i];
+            }
+        }
+    }
+    unsafe {
+        *pulCount = mechanisms.len() as CK_ULONG;
+    }
+    log_with_thread_id!(debug, "C_GetMechanismList: CKR_OK");
+    CKR_OK
+}
+
+extern "C" fn C_GetMechanismInfo(
+    _slotID: CK_SLOT_ID,
+    _type: CK_MECHANISM_TYPE,
+    _pInfo: CK_MECHANISM_INFO_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_GetMechanismInfo: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_InitToken(
+    _slotID: CK_SLOT_ID,
+    _pPin: CK_UTF8CHAR_PTR,
+    _ulPinLen: CK_ULONG,
+    _pLabel: CK_UTF8CHAR_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_InitToken: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_InitPIN(
+    _hSession: CK_SESSION_HANDLE,
+    _pPin: CK_UTF8CHAR_PTR,
+    _ulPinLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_InitPIN: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SetPIN(
+    _hSession: CK_SESSION_HANDLE,
+    _pOldPin: CK_UTF8CHAR_PTR,
+    _ulOldLen: CK_ULONG,
+    _pNewPin: CK_UTF8CHAR_PTR,
+    _ulNewLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SetPIN: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+/// This gets called to create a new session. This module defers to the `ManagerProxy` to implement
+/// this.
+extern "C" fn C_OpenSession(
+    slotID: CK_SLOT_ID,
+    _flags: CK_FLAGS,
+    _pApplication: CK_VOID_PTR,
+    _Notify: CK_NOTIFY,
+    phSession: CK_SESSION_HANDLE_PTR,
+) -> CK_RV {
+    if (slotID != SLOT_ID_MODERN && slotID != SLOT_ID_LEGACY) || phSession.is_null() {
+        log_with_thread_id!(error, "C_OpenSession: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    let slot_type = if slotID == SLOT_ID_MODERN {
+        SlotType::Modern
+    } else {
+        SlotType::Legacy
+    };
+    let session_handle = match manager.open_session(slot_type) {
+        Ok(session_handle) => session_handle,
+        Err(e) => {
+            log_with_thread_id!(error, "C_OpenSession: open_session failed: {}", e);
+            return CKR_DEVICE_ERROR;
+        }
+    };
+    unsafe {
+        *phSession = session_handle;
+    }
+    log_with_thread_id!(debug, "C_OpenSession: CKR_OK");
+    CKR_OK
+}
+
+/// This gets called to close a session. This is handled by the `ManagerProxy`.
+extern "C" fn C_CloseSession(hSession: CK_SESSION_HANDLE) -> CK_RV {
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    if manager.close_session(hSession).is_err() {
+        log_with_thread_id!(error, "C_CloseSession: CKR_SESSION_HANDLE_INVALID");
+        return CKR_SESSION_HANDLE_INVALID;
+    }
+    log_with_thread_id!(debug, "C_CloseSession: CKR_OK");
+    CKR_OK
+}
+
+/// This gets called to close all open sessions at once. This is handled by the `ManagerProxy`.
+extern "C" fn C_CloseAllSessions(slotID: CK_SLOT_ID) -> CK_RV {
+    if slotID != SLOT_ID_MODERN && slotID != SLOT_ID_LEGACY {
+        log_with_thread_id!(error, "C_CloseAllSessions: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    let slot_type = if slotID == SLOT_ID_MODERN {
+        SlotType::Modern
+    } else {
+        SlotType::Legacy
+    };
+    match manager.close_all_sessions(slot_type) {
+        Ok(()) => {
+            log_with_thread_id!(debug, "C_CloseAllSessions: CKR_OK");
+            CKR_OK
+        }
+        Err(e) => {
+            log_with_thread_id!(
+                error,
+                "C_CloseAllSessions: close_all_sessions failed: {}",
+                e
+            );
+            CKR_DEVICE_ERROR
+        }
+    }
+}
+
+extern "C" fn C_GetSessionInfo(_hSession: CK_SESSION_HANDLE, _pInfo: CK_SESSION_INFO_PTR) -> CK_RV {
+    log_with_thread_id!(error, "C_GetSessionInfo: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_GetOperationState(
+    _hSession: CK_SESSION_HANDLE,
+    _pOperationState: CK_BYTE_PTR,
+    _pulOperationStateLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_GetOperationState: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SetOperationState(
+    _hSession: CK_SESSION_HANDLE,
+    _pOperationState: CK_BYTE_PTR,
+    _ulOperationStateLen: CK_ULONG,
+    _hEncryptionKey: CK_OBJECT_HANDLE,
+    _hAuthenticationKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SetOperationState: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_Login(
+    _hSession: CK_SESSION_HANDLE,
+    _userType: CK_USER_TYPE,
+    _pPin: CK_UTF8CHAR_PTR,
+    _ulPinLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_Login: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+/// This gets called to log out and drop any authenticated resources. Because this module does not
+/// hold on to authenticated resources, this module "implements" this by doing nothing and
+/// returning a success result.
+extern "C" fn C_Logout(_hSession: CK_SESSION_HANDLE) -> CK_RV {
+    log_with_thread_id!(debug, "C_Logout: CKR_OK");
+    CKR_OK
+}
+
+extern "C" fn C_CreateObject(
+    _hSession: CK_SESSION_HANDLE,
+    _pTemplate: CK_ATTRIBUTE_PTR,
+    _ulCount: CK_ULONG,
+    _phObject: CK_OBJECT_HANDLE_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_CreateObject: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_CopyObject(
+    _hSession: CK_SESSION_HANDLE,
+    _hObject: CK_OBJECT_HANDLE,
+    _pTemplate: CK_ATTRIBUTE_PTR,
+    _ulCount: CK_ULONG,
+    _phNewObject: CK_OBJECT_HANDLE_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_CopyObject: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DestroyObject(_hSession: CK_SESSION_HANDLE, _hObject: CK_OBJECT_HANDLE) -> CK_RV {
+    log_with_thread_id!(error, "C_DestroyObject: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_GetObjectSize(
+    _hSession: CK_SESSION_HANDLE,
+    _hObject: CK_OBJECT_HANDLE,
+    _pulSize: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_GetObjectSize: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+/// This gets called to obtain the values of a number of attributes of an object identified by the
+/// given handle. This module implements this by requesting that the `ManagerProxy` find the object
+/// and attempt to get the value of each attribute. If a specified attribute is not defined on the
+/// object, the length of that attribute is set to -1 to indicate that it is not available.
+/// This gets called twice: once to obtain the lengths of the attributes and again to get the
+/// values.
+extern "C" fn C_GetAttributeValue(
+    _hSession: CK_SESSION_HANDLE,
+    hObject: CK_OBJECT_HANDLE,
+    pTemplate: CK_ATTRIBUTE_PTR,
+    ulCount: CK_ULONG,
+) -> CK_RV {
+    if pTemplate.is_null() {
+        log_with_thread_id!(error, "C_GetAttributeValue: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mut attr_types = Vec::with_capacity(ulCount as usize);
+    for i in 0..ulCount {
+        let attr = unsafe { &*pTemplate.offset(i as isize) };
+        attr_types.push(attr.attrType);
+    }
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    let values = match manager.get_attributes(hObject, attr_types) {
+        Ok(values) => values,
+        Err(e) => {
+            log_with_thread_id!(error, "C_GetAttributeValue: CKR_ARGUMENTS_BAD ({})", e);
+            return CKR_ARGUMENTS_BAD;
+        }
+    };
+    if values.len() != ulCount as usize {
+        log_with_thread_id!(
+            error,
+            "C_GetAttributeValue: manager.get_attributes didn't return the right number of values"
+        );
+        return CKR_DEVICE_ERROR;
+    }
+    for i in 0..ulCount as usize {
+        let mut attr = unsafe { &mut *pTemplate.offset(i as isize) };
+        // NB: the safety of this array access depends on the length check above
+        if let Some(attr_value) = &values[i] {
+            if attr.pValue.is_null() {
+                attr.ulValueLen = attr_value.len() as CK_ULONG;
+            } else {
+                let ptr: *mut u8 = attr.pValue as *mut u8;
+                if attr_value.len() != attr.ulValueLen as usize {
+                    log_with_thread_id!(error, "C_GetAttributeValue: incorrect attr size");
+                    return CKR_ARGUMENTS_BAD;
+                }
+                unsafe {
+                    std::ptr::copy_nonoverlapping(attr_value.as_ptr(), ptr, attr_value.len());
+                }
+            }
+        } else {
+            attr.ulValueLen = (0 - 1) as CK_ULONG;
+        }
+    }
+    log_with_thread_id!(debug, "C_GetAttributeValue: CKR_OK");
+    CKR_OK
+}
+
+extern "C" fn C_SetAttributeValue(
+    _hSession: CK_SESSION_HANDLE,
+    _hObject: CK_OBJECT_HANDLE,
+    _pTemplate: CK_ATTRIBUTE_PTR,
+    _ulCount: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SetAttributeValue: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+fn trace_attr(prefix: &str, attr: &CK_ATTRIBUTE) {
+    let typ = match unsafe_packed_field_access!(attr.attrType) {
+        CKA_CLASS => "CKA_CLASS".to_string(),
+        CKA_TOKEN => "CKA_TOKEN".to_string(),
+        CKA_LABEL => "CKA_LABEL".to_string(),
+        CKA_ID => "CKA_ID".to_string(),
+        CKA_VALUE => "CKA_VALUE".to_string(),
+        CKA_ISSUER => "CKA_ISSUER".to_string(),
+        CKA_SERIAL_NUMBER => "CKA_SERIAL_NUMBER".to_string(),
+        CKA_SUBJECT => "CKA_SUBJECT".to_string(),
+        CKA_PRIVATE => "CKA_PRIVATE".to_string(),
+        CKA_KEY_TYPE => "CKA_KEY_TYPE".to_string(),
+        CKA_MODULUS => "CKA_MODULUS".to_string(),
+        CKA_EC_PARAMS => "CKA_EC_PARAMS".to_string(),
+        _ => format!("0x{:x}", unsafe_packed_field_access!(attr.attrType)),
+    };
+    let value =
+        unsafe { std::slice::from_raw_parts(attr.pValue as *const u8, attr.ulValueLen as usize) };
+    log_with_thread_id!(
+        trace,
+        "{}CK_ATTRIBUTE {{ attrType: {}, pValue: {:?}, ulValueLen: {} }}",
+        prefix,
+        typ,
+        value,
+        unsafe_packed_field_access!(attr.ulValueLen)
+    );
+}
+
+/// This gets called to initialize a search for objects matching a given list of attributes. This
+/// module implements this by gathering the attributes and passing them to the `ManagerProxy` to
+/// start the search.
+extern "C" fn C_FindObjectsInit(
+    hSession: CK_SESSION_HANDLE,
+    pTemplate: CK_ATTRIBUTE_PTR,
+    ulCount: CK_ULONG,
+) -> CK_RV {
+    if pTemplate.is_null() {
+        log_with_thread_id!(error, "C_FindObjectsInit: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mut attrs = Vec::new();
+    log_with_thread_id!(trace, "C_FindObjectsInit:");
+    for i in 0..ulCount {
+        let attr = unsafe { &*pTemplate.offset(i as isize) };
+        trace_attr("  ", &attr);
+        let slice = unsafe {
+            std::slice::from_raw_parts(attr.pValue as *const u8, attr.ulValueLen as usize)
+        };
+        attrs.push((attr.attrType, slice.to_owned()));
+    }
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    match manager.start_search(hSession, attrs) {
+        Ok(()) => {}
+        Err(e) => {
+            log_with_thread_id!(error, "C_FindObjectsInit: CKR_ARGUMENTS_BAD: {}", e);
+            return CKR_ARGUMENTS_BAD;
+        }
+    }
+    log_with_thread_id!(debug, "C_FindObjectsInit: CKR_OK");
+    CKR_OK
+}
+
+/// This gets called after `C_FindObjectsInit` to get the results of a search. This module
+/// implements this by looking up the search in the `ManagerProxy` and copying out the matching
+/// object handles.
+extern "C" fn C_FindObjects(
+    hSession: CK_SESSION_HANDLE,
+    phObject: CK_OBJECT_HANDLE_PTR,
+    ulMaxObjectCount: CK_ULONG,
+    pulObjectCount: CK_ULONG_PTR,
+) -> CK_RV {
+    if phObject.is_null() || pulObjectCount.is_null() || ulMaxObjectCount == 0 {
+        log_with_thread_id!(error, "C_FindObjects: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    let handles = match manager.search(hSession, ulMaxObjectCount as usize) {
+        Ok(handles) => handles,
+        Err(e) => {
+            log_with_thread_id!(error, "C_FindObjects: CKR_ARGUMENTS_BAD: {}", e);
+            return CKR_ARGUMENTS_BAD;
+        }
+    };
+    log_with_thread_id!(debug, "C_FindObjects: found handles {:?}", handles);
+    if handles.len() > ulMaxObjectCount as usize {
+        log_with_thread_id!(error, "C_FindObjects: manager returned too many handles");
+        return CKR_DEVICE_ERROR;
+    }
+    unsafe {
+        *pulObjectCount = handles.len() as CK_ULONG;
+    }
+    for (index, handle) in handles.iter().enumerate() {
+        if index < ulMaxObjectCount as usize {
+            unsafe {
+                *(phObject.add(index)) = *handle;
+            }
+        }
+    }
+    log_with_thread_id!(debug, "C_FindObjects: CKR_OK");
+    CKR_OK
+}
+
+/// This gets called after `C_FindObjectsInit` and `C_FindObjects` to finish a search. The module
+/// tells the `ManagerProxy` to clear the search.
+extern "C" fn C_FindObjectsFinal(hSession: CK_SESSION_HANDLE) -> CK_RV {
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    // It would be an error if there were no search for this session, but we can be permissive here.
+    match manager.clear_search(hSession) {
+        Ok(()) => {
+            log_with_thread_id!(debug, "C_FindObjectsFinal: CKR_OK");
+            CKR_OK
+        }
+        Err(e) => {
+            log_with_thread_id!(error, "C_FindObjectsFinal: clear_search failed: {}", e);
+            CKR_DEVICE_ERROR
+        }
+    }
+}
+
+extern "C" fn C_EncryptInit(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_EncryptInit: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_Encrypt(
+    _hSession: CK_SESSION_HANDLE,
+    _pData: CK_BYTE_PTR,
+    _ulDataLen: CK_ULONG,
+    _pEncryptedData: CK_BYTE_PTR,
+    _pulEncryptedDataLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_Encrypt: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_EncryptUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pPart: CK_BYTE_PTR,
+    _ulPartLen: CK_ULONG,
+    _pEncryptedPart: CK_BYTE_PTR,
+    _pulEncryptedPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_EncryptUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_EncryptFinal(
+    _hSession: CK_SESSION_HANDLE,
+    _pLastEncryptedPart: CK_BYTE_PTR,
+    _pulLastEncryptedPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_EncryptFinal: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DecryptInit(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DecryptInit: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_Decrypt(
+    _hSession: CK_SESSION_HANDLE,
+    _pEncryptedData: CK_BYTE_PTR,
+    _ulEncryptedDataLen: CK_ULONG,
+    _pData: CK_BYTE_PTR,
+    _pulDataLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_Decrypt: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DecryptUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pEncryptedPart: CK_BYTE_PTR,
+    _ulEncryptedPartLen: CK_ULONG,
+    _pPart: CK_BYTE_PTR,
+    _pulPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DecryptUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DecryptFinal(
+    _hSession: CK_SESSION_HANDLE,
+    _pLastPart: CK_BYTE_PTR,
+    _pulLastPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DecryptFinal: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DigestInit(_hSession: CK_SESSION_HANDLE, _pMechanism: CK_MECHANISM_PTR) -> CK_RV {
+    log_with_thread_id!(error, "C_DigestInit: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_Digest(
+    _hSession: CK_SESSION_HANDLE,
+    _pData: CK_BYTE_PTR,
+    _ulDataLen: CK_ULONG,
+    _pDigest: CK_BYTE_PTR,
+    _pulDigestLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_Digest: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DigestUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pPart: CK_BYTE_PTR,
+    _ulPartLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DigestUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DigestKey(_hSession: CK_SESSION_HANDLE, _hKey: CK_OBJECT_HANDLE) -> CK_RV {
+    log_with_thread_id!(error, "C_DigestKey: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DigestFinal(
+    _hSession: CK_SESSION_HANDLE,
+    _pDigest: CK_BYTE_PTR,
+    _pulDigestLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DigestFinal: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+/// This gets called to set up a sign operation. The module essentially defers to the
+/// `ManagerProxy`.
+extern "C" fn C_SignInit(
+    hSession: CK_SESSION_HANDLE,
+    pMechanism: CK_MECHANISM_PTR,
+    hKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    if pMechanism.is_null() {
+        log_with_thread_id!(error, "C_SignInit: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    // Presumably we should validate the mechanism against hKey, but the specification doesn't
+    // actually seem to require this.
+    let mechanism = unsafe { *pMechanism };
+    log_with_thread_id!(debug, "C_SignInit: mechanism is {:?}", mechanism);
+    let mechanism_params = if mechanism.mechanism == CKM_RSA_PKCS_PSS {
+        if mechanism.ulParameterLen as usize != std::mem::size_of::<CK_RSA_PKCS_PSS_PARAMS>() {
+            log_with_thread_id!(
+                error,
+                "C_SignInit: bad ulParameterLen for CKM_RSA_PKCS_PSS: {}",
+                unsafe_packed_field_access!(mechanism.ulParameterLen)
+            );
+            return CKR_ARGUMENTS_BAD;
+        }
+        Some(unsafe { *(mechanism.pParameter as *const CK_RSA_PKCS_PSS_PARAMS) })
+    } else {
+        None
+    };
+    let mut manager_guard = try_to_get_manager_guard!();
+    let manager = manager_guard_to_manager!(manager_guard);
+    match manager.start_sign(hSession, hKey, mechanism_params) {
+        Ok(()) => {}
+        Err(e) => {
+            log_with_thread_id!(error, "C_SignInit: CKR_GENERAL_ERROR: {}", e);
+            return CKR_GENERAL_ERROR;
+        }
+    };
+    log_with_thread_id!(debug, "C_SignInit: CKR_OK");
+    CKR_OK
+}
+
+/// NSS calls this after `C_SignInit` (there are more ways in the PKCS #11 specification to sign
+/// data, but this is the only way supported by this module). The module essentially defers to the
+/// `ManagerProxy` and copies out the resulting signature.
+extern "C" fn C_Sign(
+    hSession: CK_SESSION_HANDLE,
+    pData: CK_BYTE_PTR,
+    ulDataLen: CK_ULONG,
+    pSignature: CK_BYTE_PTR,
+    pulSignatureLen: CK_ULONG_PTR,
+) -> CK_RV {
+    if pData.is_null() || pulSignatureLen.is_null() {
+        log_with_thread_id!(error, "C_Sign: CKR_ARGUMENTS_BAD");
+        return CKR_ARGUMENTS_BAD;
+    }
+    let data = unsafe { std::slice::from_raw_parts(pData, ulDataLen as usize) };
+    if pSignature.is_null() {
+        let mut manager_guard = try_to_get_manager_guard!();
+        let manager = manager_guard_to_manager!(manager_guard);
+        match manager.get_signature_length(hSession, data.to_vec()) {
+            Ok(signature_length) => unsafe {
+                *pulSignatureLen = signature_length as CK_ULONG;
+            },
+            Err(e) => {
+                log_with_thread_id!(error, "C_Sign: get_signature_length failed: {}", e);
+                return CKR_GENERAL_ERROR;
+            }
+        }
+    } else {
+        let mut manager_guard = try_to_get_manager_guard!();
+        let manager = manager_guard_to_manager!(manager_guard);
+        match manager.sign(hSession, data.to_vec()) {
+            Ok(signature) => {
+                let signature_capacity = unsafe { *pulSignatureLen } as usize;
+                if signature_capacity < signature.len() {
+                    log_with_thread_id!(error, "C_Sign: CKR_ARGUMENTS_BAD");
+                    return CKR_ARGUMENTS_BAD;
+                }
+                let ptr: *mut u8 = pSignature as *mut u8;
+                unsafe {
+                    std::ptr::copy_nonoverlapping(signature.as_ptr(), ptr, signature.len());
+                    *pulSignatureLen = signature.len() as CK_ULONG;
+                }
+            }
+            Err(e) => {
+                log_with_thread_id!(error, "C_Sign: sign failed: {}", e);
+                return CKR_GENERAL_ERROR;
+            }
+        }
+    }
+    log_with_thread_id!(debug, "C_Sign: CKR_OK");
+    CKR_OK
+}
+
+extern "C" fn C_SignUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pPart: CK_BYTE_PTR,
+    _ulPartLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SignUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SignFinal(
+    _hSession: CK_SESSION_HANDLE,
+    _pSignature: CK_BYTE_PTR,
+    _pulSignatureLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SignFinal: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SignRecoverInit(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SignRecoverInit: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SignRecover(
+    _hSession: CK_SESSION_HANDLE,
+    _pData: CK_BYTE_PTR,
+    _ulDataLen: CK_ULONG,
+    _pSignature: CK_BYTE_PTR,
+    _pulSignatureLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SignRecover: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_VerifyInit(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_VerifyInit: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_Verify(
+    _hSession: CK_SESSION_HANDLE,
+    _pData: CK_BYTE_PTR,
+    _ulDataLen: CK_ULONG,
+    _pSignature: CK_BYTE_PTR,
+    _ulSignatureLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_Verify: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_VerifyUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pPart: CK_BYTE_PTR,
+    _ulPartLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_VerifyUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_VerifyFinal(
+    _hSession: CK_SESSION_HANDLE,
+    _pSignature: CK_BYTE_PTR,
+    _ulSignatureLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_VerifyFinal: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_VerifyRecoverInit(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hKey: CK_OBJECT_HANDLE,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_VerifyRecoverInit: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_VerifyRecover(
+    _hSession: CK_SESSION_HANDLE,
+    _pSignature: CK_BYTE_PTR,
+    _ulSignatureLen: CK_ULONG,
+    _pData: CK_BYTE_PTR,
+    _pulDataLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_VerifyRecover: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DigestEncryptUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pPart: CK_BYTE_PTR,
+    _ulPartLen: CK_ULONG,
+    _pEncryptedPart: CK_BYTE_PTR,
+    _pulEncryptedPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DigestEncryptUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DecryptDigestUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pEncryptedPart: CK_BYTE_PTR,
+    _ulEncryptedPartLen: CK_ULONG,
+    _pPart: CK_BYTE_PTR,
+    _pulPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DecryptDigestUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SignEncryptUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pPart: CK_BYTE_PTR,
+    _ulPartLen: CK_ULONG,
+    _pEncryptedPart: CK_BYTE_PTR,
+    _pulEncryptedPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SignEncryptUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DecryptVerifyUpdate(
+    _hSession: CK_SESSION_HANDLE,
+    _pEncryptedPart: CK_BYTE_PTR,
+    _ulEncryptedPartLen: CK_ULONG,
+    _pPart: CK_BYTE_PTR,
+    _pulPartLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DecryptVerifyUpdate: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_GenerateKey(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _pTemplate: CK_ATTRIBUTE_PTR,
+    _ulCount: CK_ULONG,
+    _phKey: CK_OBJECT_HANDLE_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_GenerateKey: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_GenerateKeyPair(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _pPublicKeyTemplate: CK_ATTRIBUTE_PTR,
+    _ulPublicKeyAttributeCount: CK_ULONG,
+    _pPrivateKeyTemplate: CK_ATTRIBUTE_PTR,
+    _ulPrivateKeyAttributeCount: CK_ULONG,
+    _phPublicKey: CK_OBJECT_HANDLE_PTR,
+    _phPrivateKey: CK_OBJECT_HANDLE_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_GenerateKeyPair: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_WrapKey(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hWrappingKey: CK_OBJECT_HANDLE,
+    _hKey: CK_OBJECT_HANDLE,
+    _pWrappedKey: CK_BYTE_PTR,
+    _pulWrappedKeyLen: CK_ULONG_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_WrapKey: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_UnwrapKey(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hUnwrappingKey: CK_OBJECT_HANDLE,
+    _pWrappedKey: CK_BYTE_PTR,
+    _ulWrappedKeyLen: CK_ULONG,
+    _pTemplate: CK_ATTRIBUTE_PTR,
+    _ulAttributeCount: CK_ULONG,
+    _phKey: CK_OBJECT_HANDLE_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_UnwrapKey: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_DeriveKey(
+    _hSession: CK_SESSION_HANDLE,
+    _pMechanism: CK_MECHANISM_PTR,
+    _hBaseKey: CK_OBJECT_HANDLE,
+    _pTemplate: CK_ATTRIBUTE_PTR,
+    _ulAttributeCount: CK_ULONG,
+    _phKey: CK_OBJECT_HANDLE_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_DeriveKey: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_SeedRandom(
+    _hSession: CK_SESSION_HANDLE,
+    _pSeed: CK_BYTE_PTR,
+    _ulSeedLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_SeedRandom: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_GenerateRandom(
+    _hSession: CK_SESSION_HANDLE,
+    _RandomData: CK_BYTE_PTR,
+    _ulRandomLen: CK_ULONG,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_GenerateRandom: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_GetFunctionStatus(_hSession: CK_SESSION_HANDLE) -> CK_RV {
+    log_with_thread_id!(error, "C_GetFunctionStatus: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_CancelFunction(_hSession: CK_SESSION_HANDLE) -> CK_RV {
+    log_with_thread_id!(error, "C_CancelFunction: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+extern "C" fn C_WaitForSlotEvent(
+    _flags: CK_FLAGS,
+    _pSlot: CK_SLOT_ID_PTR,
+    _pRserved: CK_VOID_PTR,
+) -> CK_RV {
+    log_with_thread_id!(error, "C_WaitForSlotEvent: CKR_FUNCTION_NOT_SUPPORTED");
+    CKR_FUNCTION_NOT_SUPPORTED
+}
+
+/// To be a valid PKCS #11 module, this list of functions must be supported. At least cryptoki 2.2
+/// must be supported for this module to work in NSS.
+static mut FUNCTION_LIST: CK_FUNCTION_LIST = CK_FUNCTION_LIST {
+    version: CK_VERSION { major: 2, minor: 2 },
+    C_Initialize: Some(C_Initialize),
+    C_Finalize: Some(C_Finalize),
+    C_GetInfo: Some(C_GetInfo),
+    C_GetFunctionList: None,
+    C_GetSlotList: Some(C_GetSlotList),
+    C_GetSlotInfo: Some(C_GetSlotInfo),
+    C_GetTokenInfo: Some(C_GetTokenInfo),
+    C_GetMechanismList: Some(C_GetMechanismList),
+    C_GetMechanismInfo: Some(C_GetMechanismInfo),
+    C_InitToken: Some(C_InitToken),
+    C_InitPIN: Some(C_InitPIN),
+    C_SetPIN: Some(C_SetPIN),
+    C_OpenSession: Some(C_OpenSession),
+    C_CloseSession: Some(C_CloseSession),
+    C_CloseAllSessions: Some(C_CloseAllSessions),
+    C_GetSessionInfo: Some(C_GetSessionInfo),
+    C_GetOperationState: Some(C_GetOperationState),
+    C_SetOperationState: Some(C_SetOperationState),
+    C_Login: Some(C_Login),
+    C_Logout: Some(C_Logout),
+    C_CreateObject: Some(C_CreateObject),
+    C_CopyObject: Some(C_CopyObject),
+    C_DestroyObject: Some(C_DestroyObject),
+    C_GetObjectSize: Some(C_GetObjectSize),
+    C_GetAttributeValue: Some(C_GetAttributeValue),
+    C_SetAttributeValue: Some(C_SetAttributeValue),
+    C_FindObjectsInit: Some(C_FindObjectsInit),
+    C_FindObjects: Some(C_FindObjects),
+    C_FindObjectsFinal: Some(C_FindObjectsFinal),
+    C_EncryptInit: Some(C_EncryptInit),
+    C_Encrypt: Some(C_Encrypt),
+    C_EncryptUpdate: Some(C_EncryptUpdate),
+    C_EncryptFinal: Some(C_EncryptFinal),
+    C_DecryptInit: Some(C_DecryptInit),
+    C_Decrypt: Some(C_Decrypt),
+    C_DecryptUpdate: Some(C_DecryptUpdate),
+    C_DecryptFinal: Some(C_DecryptFinal),
+    C_DigestInit: Some(C_DigestInit),
+    C_Digest: Some(C_Digest),
+    C_DigestUpdate: Some(C_DigestUpdate),
+    C_DigestKey: Some(C_DigestKey),
+    C_DigestFinal: Some(C_DigestFinal),
+    C_SignInit: Some(C_SignInit),
+    C_Sign: Some(C_Sign),
+    C_SignUpdate: Some(C_SignUpdate),
+    C_SignFinal: Some(C_SignFinal),
+    C_SignRecoverInit: Some(C_SignRecoverInit),
+    C_SignRecover: Some(C_SignRecover),
+    C_VerifyInit: Some(C_VerifyInit),
+    C_Verify: Some(C_Verify),
+    C_VerifyUpdate: Some(C_VerifyUpdate),
+    C_VerifyFinal: Some(C_VerifyFinal),
+    C_VerifyRecoverInit: Some(C_VerifyRecoverInit),
+    C_VerifyRecover: Some(C_VerifyRecover),
+    C_DigestEncryptUpdate: Some(C_DigestEncryptUpdate),
+    C_DecryptDigestUpdate: Some(C_DecryptDigestUpdate),
+    C_SignEncryptUpdate: Some(C_SignEncryptUpdate),
+    C_DecryptVerifyUpdate: Some(C_DecryptVerifyUpdate),
+    C_GenerateKey: Some(C_GenerateKey),
+    C_GenerateKeyPair: Some(C_GenerateKeyPair),
+    C_WrapKey: Some(C_WrapKey),
+    C_UnwrapKey: Some(C_UnwrapKey),
+    C_DeriveKey: Some(C_DeriveKey),
+    C_SeedRandom: Some(C_SeedRandom),
+    C_GenerateRandom: Some(C_GenerateRandom),
+    C_GetFunctionStatus: Some(C_GetFunctionStatus),
+    C_CancelFunction: Some(C_CancelFunction),
+    C_WaitForSlotEvent: Some(C_WaitForSlotEvent),
+};
+
+/// This is the only function this module exposes. The C stub calls it when NSS
+/// calls its exposed C_GetFunctionList function to obtain the list of functions
+/// comprising this module.
+#[no_mangle]
+pub extern "C" fn IPCCC_GetFunctionList(ppFunctionList: CK_FUNCTION_LIST_PTR_PTR) -> CK_RV {
+    if ppFunctionList.is_null() {
+        return CKR_ARGUMENTS_BAD;
+    }
+    unsafe {
+        *ppFunctionList = &mut FUNCTION_LIST;
+    }
+    CKR_OK
+}
+
+#[cfg_attr(target_os = "macos", link(name = "Security", kind = "framework"))]
+extern "C" {}
diff --git a/security/manager/ssl/moz.build b/security/manager/ssl/moz.build
index cefed1a541443c088e509cc4562628f1a90521f5..07804ac59626887f5e5e59be9901e43677281e90 100644
--- a/security/manager/ssl/moz.build
+++ b/security/manager/ssl/moz.build
@@ -12,6 +12,8 @@ if (CONFIG["OS_ARCH"] == "WINNT" and CONFIG["CPU_ARCH"] != "aarch64") or CONFIG[
 ] == "Darwin":
     DIRS += ["osclientcerts"]
 
+DIRS += ["ipcclientcerts"]
+
 TEST_DIRS += ["tests"]
 
 XPIDL_SOURCES += [
@@ -93,6 +95,8 @@ EXPORTS.mozilla += [
 ]
 
 EXPORTS.mozilla.psm += [
+    "IPCClientCertsChild.h",
+    "IPCClientCertsParent.h",
     "PSMIPCCommon.h",
     "TransportSecurityInfo.h",
     "VerifySSLServerCertChild.h",
@@ -111,6 +115,8 @@ UNIFIED_SOURCES += [
     "CSTrustDomain.cpp",
     "DataStorage.cpp",
     "EnterpriseRoots.cpp",
+    "IPCClientCertsChild.cpp",
+    "IPCClientCertsParent.cpp",
     "LocalCertService.cpp",
     "nsCertOverrideService.cpp",
     "nsClientAuthRemember.cpp",
@@ -189,6 +195,7 @@ if CONFIG["OS_ARCH"] == "WINNT":
     ]
 
 IPDL_SOURCES += [
+    "PIPCClientCerts.ipdl",
     "PSMIPCTypes.ipdlh",
     "PVerifySSLServerCert.ipdl",
 ]
@@ -209,6 +216,7 @@ LOCAL_INCLUDES += [
     "/dom/crypto",
     "/netwerk/base",
     "/security/certverifier",
+    "/xpcom/build",
 ]
 
 LOCAL_INCLUDES += [
diff --git a/security/manager/ssl/nsNSSCallbacks.cpp b/security/manager/ssl/nsNSSCallbacks.cpp
index 1695b37be95f48e3e97879456741213c2b2c3a8c..7bb3ac3efafb7d2935b68e4e3cad0c4c064513a3 100644
--- a/security/manager/ssl/nsNSSCallbacks.cpp
+++ b/security/manager/ssl/nsNSSCallbacks.cpp
@@ -13,6 +13,7 @@
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Casting.h"
+#include "mozilla/Logging.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Span.h"
 #include "mozilla/Telemetry.h"
diff --git a/security/manager/ssl/nsNSSComponent.cpp b/security/manager/ssl/nsNSSComponent.cpp
index ec98414777c02de8d90907c0fd60799e33712943..0efd03fb0adeb306b6d037942a8a5e5f7c50993f 100644
--- a/security/manager/ssl/nsNSSComponent.cpp
+++ b/security/manager/ssl/nsNSSComponent.cpp
@@ -6,6 +6,7 @@
 
 #include "nsNSSComponent.h"
 
+#include "BinaryPath.h"
 #include "CryptoTask.h"
 #include "EnterpriseRoots.h"
 #include "ExtendedValidation.h"
@@ -21,6 +22,7 @@
 #include "mozilla/Assertions.h"
 #include "mozilla/Casting.h"
 #include "mozilla/EndianUtils.h"
+#include "mozilla/FilePreferences.h"
 #include "mozilla/PodOperations.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ProfilerLabels.h"
@@ -100,6 +102,38 @@ int nsNSSComponent::mInstanceCount = 0;
 // Forward declaration.
 nsresult CommonInit();
 
+// Take an nsIFile and get a c-string representation of the location of that
+// file (encapsulated in an nsACString). This function handles a
+// platform-specific issue on Windows where Unicode characters that cannot be
+// mapped to the system's codepage will be dropped, resulting in a c-string
+// that is useless to describe the location of the file in question.
+// This operation is generally to be avoided, except when interacting with
+// third-party or legacy libraries that cannot handle `nsIFile`s (such as NSS).
+nsresult FileToCString(const nsCOMPtr<nsIFile>& file, nsACString& result) {
+#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> fileWin = do_QueryInterface(file);
+  if (!fileWin) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get nsILocalFileWin"));
+    return NS_ERROR_FAILURE;
+  }
+  return fileWin->GetNativeCanonicalPath(result);
+#else
+  return file->GetNativePath(result);
+#endif
+}
+
+void TruncateFromLastDirectorySeparator(nsCString& path) {
+  static const nsAutoCString kSeparatorString(
+      mozilla::FilePreferences::kPathSeparator);
+  int32_t index = path.RFind(kSeparatorString);
+  if (index == kNotFound) {
+    return;
+  }
+  path.Truncate(index);
+}
+
 // This function can be called from chrome or content or socket processes
 // to ensure that NSS is initialized.
 bool EnsureNSSInitializedChromeOrContent() {
@@ -149,6 +183,36 @@ bool EnsureNSSInitializedChromeOrContent() {
     if (NS_FAILED(CommonInit())) {
       return false;
     }
+    // This returns the path to the binary currently running, which in most
+    // cases is "plugin-container".
+    UniqueFreePtr<char> pluginContainerPath(BinaryPath::Get());
+    if (!pluginContainerPath) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+              ("failed to get get plugin-container path"));
+      return false;
+    }
+    nsAutoCString ipcClientCertsDirString(pluginContainerPath.get());
+    // On most platforms, ipcclientcerts is in the same directory as
+    // plugin-container. To obtain the path to that directory, truncate from
+    // the last directory separator.
+    // On macOS, plugin-container is in
+    // Firefox.app/Contents/MacOS/plugin-container.app/Contents/MacOS/,
+    // whereas ipcclientcerts is in Firefox.app/Contents/MacOS/. Consequently,
+    // this truncation from the last directory separator has to happen 4 times
+    // total. Normally this would be done using nsIFile APIs, but due to when
+    // this is initialized in the socket process, those aren't available.
+    TruncateFromLastDirectorySeparator(ipcClientCertsDirString);
+#ifdef XP_MACOSX
+    TruncateFromLastDirectorySeparator(ipcClientCertsDirString);
+    TruncateFromLastDirectorySeparator(ipcClientCertsDirString);
+    TruncateFromLastDirectorySeparator(ipcClientCertsDirString);
+#endif
+    if (!LoadIPCClientCertsModule(ipcClientCertsDirString)) {
+      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+              ("failed to load ipcclientcerts from '%s'",
+               ipcClientCertsDirString.get()));
+      return false;
+    }
     initialized = true;
     return true;
   }
@@ -750,18 +814,7 @@ static nsresult GetDirectoryPath(const char* directoryKey, nsCString& result) {
             ("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
+  return FileToCString(directory, result);
 }
 
 class BackgroundLoadOSClientCertsModuleTask final : public CryptoTask {
@@ -841,8 +894,6 @@ NS_IMETHODIMP
 nsNSSComponent::HasUserCertsInstalled(bool* result) {
   NS_ENSURE_ARG_POINTER(result);
 
-  BlockUntilLoadableCertsLoaded();
-
   // FindClientCertificatesWithPrivateKeys won't ever return an empty list, so
   // all we need to do is check if this is null or not.
   UniqueCERTCertList certList(FindClientCertificatesWithPrivateKeys());
@@ -941,18 +992,7 @@ static nsresult GetNSS3Directory(nsCString& result) {
     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 (!nss3DirectoryWin) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("couldn't get nsILocalFileWin"));
-    return NS_ERROR_FAILURE;
-  }
-  return nss3DirectoryWin->GetNativeCanonicalPath(result);
-#else
-  return nss3Directory->GetNativePath(result);
-#endif
+  return FileToCString(nss3Directory, result);
 }
 
 // The loadable roots library is probably in the same directory we loaded the
@@ -2620,6 +2660,9 @@ UniqueCERTCertList FindClientCertificatesWithPrivateKeys() {
   });
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
           ("FindClientCertificatesWithPrivateKeys"));
+
+  BlockUntilLoadableCertsLoaded();
+
   UniqueCERTCertList certsWithPrivateKeys(CERT_NewCertList());
   if (!certsWithPrivateKeys) {
     return nullptr;
diff --git a/security/manager/ssl/nsNSSIOLayer.cpp b/security/manager/ssl/nsNSSIOLayer.cpp
index 81e06b2698267a07ff0aeae11764e24672328b4b..ab80a8e45c3edc75fc1aa94b2aa0f35cdecc6447 100644
--- a/security/manager/ssl/nsNSSIOLayer.cpp
+++ b/security/manager/ssl/nsNSSIOLayer.cpp
@@ -25,8 +25,12 @@
 #include "mozilla/Logging.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Telemetry.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
 #include "mozilla/net/SSLTokensCache.h"
 #include "mozilla/net/SocketProcessChild.h"
+#include "mozilla/psm/IPCClientCertsChild.h"
+#include "mozilla/psm/PIPCClientCertsChild.h"
 #include "mozpkix/pkixnss.h"
 #include "mozpkix/pkixtypes.h"
 #include "mozpkix/pkixutil.h"
@@ -54,6 +58,7 @@
 #include "sslproto.h"
 
 using namespace mozilla::psm;
+using namespace mozilla::ipc;
 
 //#define DEBUG_SSL_VERBOSE //Enable this define to get minimal
 // reports when doing SSL read/write
@@ -1826,8 +1831,7 @@ class ClientAuthDataRunnable : public SyncRunnableBase {
       : mInfo(std::move(info)),
         mServerCert(serverCert.get()),
         mCollectedCANames(std::move(collectedCANames)),
-        mSelectedCertificate(nullptr),
-        mSelectedKey(nullptr) {}
+        mSelectedCertificate(nullptr) {}
 
   virtual mozilla::pkix::Result BuildChainForCertificate(
       CERTCertificate* cert, UniqueCERTCertList& builtChain);
@@ -1837,10 +1841,6 @@ class ClientAuthDataRunnable : public SyncRunnableBase {
   UniqueCERTCertificate TakeSelectedCertificate() {
     return std::move(mSelectedCertificate);
   }
-  // Take the private key for the selected certificate. Will be null if no
-  // certificate was selected or an error prevented selecting one or getting
-  // the corresponding key.
-  UniqueSECKEYPrivateKey TakeSelectedKey() { return std::move(mSelectedKey); }
 
  protected:
   virtual void RunOnTargetThread() override;
@@ -1850,7 +1850,6 @@ class ClientAuthDataRunnable : public SyncRunnableBase {
   nsTArray<nsTArray<uint8_t>> mCollectedCANames;
   nsTArray<nsTArray<uint8_t>> mEnterpriseCertificates;
   UniqueCERTCertificate mSelectedCertificate;
-  UniqueSECKEYPrivateKey mSelectedKey;
 };
 
 class RemoteClientAuthDataRunnable : public ClientAuthDataRunnable {
@@ -1946,30 +1945,42 @@ SECStatus nsNSS_SSLGetClientAuthData(void* arg, PRFileDesc* socket,
   nsTArray<nsTArray<uint8_t>> collectedCANames(CollectCANames(caNames));
 
   UniqueCERTCertificate selectedCertificate;
-  UniqueSECKEYPrivateKey selectedKey;
   UniqueCERTCertList builtChain;
-  SECStatus status = DoGetClientAuthData(
-      std::move(authInfo), serverCert, std::move(collectedCANames),
-      selectedCertificate, selectedKey, builtChain);
+  SECStatus status = DoGetClientAuthData(std::move(authInfo), serverCert,
+                                         std::move(collectedCANames),
+                                         selectedCertificate, builtChain);
   if (status != SECSuccess) {
     return status;
   }
 
-  if (selectedCertificate && selectedKey) {
-    if (builtChain) {
-      info->SetClientCertChain(std::move(builtChain));
-    } else {
-      MOZ_LOG(
-          gPIPNSSLog, LogLevel::Debug,
-          ("[%p] couldn't determine chain for selected client cert", socket));
-    }
-    *pRetCert = selectedCertificate.release();
-    *pRetKey = selectedKey.release();
-    // Make joinConnection prohibit joining after we've sent a client cert
-    info->SetSentClientCert();
-    if (info->GetSSLVersionUsed() == nsISSLSocketControl::TLS_VERSION_1_3) {
-      Telemetry::Accumulate(Telemetry::TLS_1_3_CLIENT_AUTH_USES_PHA,
-                            info->IsHandshakeCompleted());
+  // Currently, the IPC client certs module only refreshes its view of
+  // available certificates and keys if the platform issues a search for all
+  // certificates or keys. In the socket process, such a search may not have
+  // happened, so this ensures it has.
+  if (XRE_IsSocketProcess()) {
+    UniqueCERTCertList certList(FindClientCertificatesWithPrivateKeys());
+    Unused << certList;
+  }
+
+  if (selectedCertificate) {
+    UniqueSECKEYPrivateKey selectedKey(
+        PK11_FindKeyByAnyCert(selectedCertificate.get(), nullptr));
+    if (selectedKey) {
+      if (builtChain) {
+        info->SetClientCertChain(std::move(builtChain));
+      } else {
+        MOZ_LOG(
+            gPIPNSSLog, LogLevel::Debug,
+            ("[%p] couldn't determine chain for selected client cert", socket));
+      }
+      *pRetCert = selectedCertificate.release();
+      *pRetKey = selectedKey.release();
+      // Make joinConnection prohibit joining after we've sent a client cert
+      info->SetSentClientCert();
+      if (info->GetSSLVersionUsed() == nsISSLSocketControl::TLS_VERSION_1_3) {
+        Telemetry::Accumulate(Telemetry::TLS_1_3_CLIENT_AUTH_USES_PHA,
+                              info->IsHandshakeCompleted());
+      }
     }
   }
 
@@ -1980,7 +1991,6 @@ SECStatus DoGetClientAuthData(ClientAuthInfo&& info,
                               const UniqueCERTCertificate& serverCert,
                               nsTArray<nsTArray<uint8_t>>&& collectedCANames,
                               UniqueCERTCertificate& outCert,
-                              UniqueSECKEYPrivateKey& outKey,
                               UniqueCERTCertList& outBuiltChain) {
   // XXX: This should be done asynchronously; see bug 696976
   RefPtr<ClientAuthDataRunnable> runnable =
@@ -1997,8 +2007,7 @@ SECStatus DoGetClientAuthData(ClientAuthInfo&& info,
   }
 
   outCert = runnable->TakeSelectedCertificate();
-  outKey = runnable->TakeSelectedKey();
-  if (outCert && outKey) {
+  if (outCert) {
     mozilla::pkix::Result result =
         runnable->BuildChainForCertificate(outCert.get(), outBuiltChain);
     if (result != Success) {
@@ -2033,7 +2042,7 @@ class ClientAuthCertNonverifyingTrustDomain final : public TrustDomain {
 
   virtual mozilla::pkix::Result CheckRevocation(
       EndEntityOrCA endEntityOrCA, const CertID& certID, Time time,
-      Duration validityDuration,
+      mozilla::pkix::Duration validityDuration,
       /*optional*/ const Input* stapledOCSPresponse,
       /*optional*/ const Input* aiaExtension,
       /*optional*/ const Input* sctExtension) override {
@@ -2309,8 +2318,6 @@ void ClientAuthDataRunnable::RunOnTargetThread() {
     if (NS_WARN_IF(!mSelectedCertificate)) {
       return;
     }
-    mSelectedKey.reset(
-        PK11_FindKeyByAnyCert(mSelectedCertificate.get(), nullptr));
     return;
   }
 
@@ -2360,7 +2367,6 @@ void ClientAuthDataRunnable::RunOnTargetThread() {
         } else {
           // this is a good cert to present
           mSelectedCertificate.reset(CERT_DupCertificate(node->cert));
-          mSelectedKey = std::move(tmpKey);
           return;
         }
       }
@@ -2372,8 +2378,6 @@ void ClientAuthDataRunnable::RunOnTargetThread() {
 
     if (lowPrioNonrepCert) {
       mSelectedCertificate = std::move(lowPrioNonrepCert);
-      mSelectedKey.reset(
-          PK11_FindKeyByAnyCert(mSelectedCertificate.get(), nullptr));
     }
     return;
   }
@@ -2422,8 +2426,6 @@ void ClientAuthDataRunnable::RunOnTargetThread() {
         if (NS_WARN_IF(!mSelectedCertificate)) {
           return;
         }
-        mSelectedKey.reset(
-            PK11_FindKeyByAnyCert(mSelectedCertificate.get(), nullptr));
         return;
       }
     }
@@ -2485,8 +2487,6 @@ void ClientAuthDataRunnable::RunOnTargetThread() {
     if (NS_WARN_IF(!mSelectedCertificate)) {
       return;
     }
-    mSelectedKey.reset(
-        PK11_FindKeyByAnyCert(mSelectedCertificate.get(), nullptr));
   }
 
   if (cars && wantRemember) {
@@ -2550,18 +2550,19 @@ void RemoteClientAuthDataRunnable::RunOnTargetThread() {
 
   bool succeeded = false;
   ByteArray cert;
-  ByteArray key;
   mozilla::net::SocketProcessChild::GetSingleton()->SendGetTLSClientCert(
       nsCString(mInfo.HostName()), mInfo.OriginAttributesRef(), mInfo.Port(),
       mInfo.ProviderFlags(), mInfo.ProviderTlsFlags(), serverCertSerialized,
-      clientCertSerialized, collectedCANames, &succeeded, &cert, &key,
-      &mBuiltChain);
+      clientCertSerialized, collectedCANames, &succeeded, &cert, &mBuiltChain);
 
   if (!succeeded) {
     return;
   }
 
-  DeserializeClientCertAndKey(cert, key, mSelectedCertificate, mSelectedKey);
+  SECItem certItem = {siBuffer, const_cast<uint8_t*>(cert.data().Elements()),
+                      static_cast<unsigned int>(cert.data().Length())};
+  mSelectedCertificate.reset(CERT_NewTempCertificate(
+      CERT_GetDefaultCertDB(), &certItem, nullptr, false, true));
 }
 
 static PRFileDesc* nsSSLIOLayerImportFD(PRFileDesc* fd,
@@ -2948,3 +2949,91 @@ loser:
   }
   return NS_ERROR_FAILURE;
 }
+
+already_AddRefed<IPCClientCertsChild> GetIPCClientCertsActor() {
+  PBackgroundChild* backgroundActor =
+      BackgroundChild::GetOrCreateForSocketParentBridgeForCurrentThread();
+  if (!backgroundActor) {
+    return nullptr;
+  }
+  RefPtr<PIPCClientCertsChild> actor =
+      SingleManagedOrNull(backgroundActor->ManagedPIPCClientCertsChild());
+  if (!actor) {
+    actor = backgroundActor->SendPIPCClientCertsConstructor(
+        new IPCClientCertsChild());
+    if (!actor) {
+      return nullptr;
+    }
+  }
+  return actor.forget().downcast<IPCClientCertsChild>();
+}
+
+extern "C" {
+
+const uint8_t kIPCClientCertsObjectTypeCert = 1;
+const uint8_t kIPCClientCertsObjectTypeRSAKey = 2;
+const uint8_t kIPCClientCertsObjectTypeECKey = 3;
+
+// This function is provided to the IPC client certs module so it can cause the
+// parent process to find certificates and keys and send identifying
+// information about them over IPC.
+void DoFindObjects(FindObjectsCallback cb, void* ctx) {
+  RefPtr<IPCClientCertsChild> ipcClientCertsActor(GetIPCClientCertsActor());
+  if (!ipcClientCertsActor) {
+    return;
+  }
+  nsTArray<IPCClientCertObject> objects;
+  if (!ipcClientCertsActor->SendFindObjects(&objects)) {
+    return;
+  }
+  for (const auto& object : objects) {
+    switch (object.type()) {
+      case IPCClientCertObject::TECKey:
+        cb(kIPCClientCertsObjectTypeECKey, object.get_ECKey().params().Length(),
+           object.get_ECKey().params().Elements(),
+           object.get_ECKey().cert().Length(),
+           object.get_ECKey().cert().Elements(), object.get_ECKey().slotType(),
+           ctx);
+        break;
+      case IPCClientCertObject::TRSAKey:
+        cb(kIPCClientCertsObjectTypeRSAKey,
+           object.get_RSAKey().modulus().Length(),
+           object.get_RSAKey().modulus().Elements(),
+           object.get_RSAKey().cert().Length(),
+           object.get_RSAKey().cert().Elements(),
+           object.get_RSAKey().slotType(), ctx);
+        break;
+      case IPCClientCertObject::TCertificate:
+        cb(kIPCClientCertsObjectTypeCert,
+           object.get_Certificate().der().Length(),
+           object.get_Certificate().der().Elements(), 0, nullptr,
+           object.get_Certificate().slotType(), ctx);
+        break;
+      default:
+        MOZ_ASSERT_UNREACHABLE("unhandled IPCClientCertObject type");
+        break;
+    }
+  }
+}
+
+// This function is provided to the IPC client certs module so it can cause the
+// parent process to sign the given data using the key corresponding to the
+// given certificate, using the given parameters.
+void DoSign(size_t cert_len, const uint8_t* cert, size_t data_len,
+            const uint8_t* data, size_t params_len, const uint8_t* params,
+            SignCallback cb, void* ctx) {
+  RefPtr<IPCClientCertsChild> ipcClientCertsActor(GetIPCClientCertsActor());
+  if (!ipcClientCertsActor) {
+    return;
+  }
+  ByteArray certBytes(nsTArray<uint8_t>(cert, cert_len));
+  ByteArray dataBytes(nsTArray<uint8_t>(data, data_len));
+  ByteArray paramsBytes(nsTArray<uint8_t>(params, params_len));
+  ByteArray signature;
+  if (!ipcClientCertsActor->SendSign(certBytes, dataBytes, paramsBytes,
+                                     &signature)) {
+    return;
+  }
+  cb(signature.data().Length(), signature.data().Elements(), ctx);
+}
+}  // extern "C"
diff --git a/security/manager/ssl/nsNSSIOLayer.h b/security/manager/ssl/nsNSSIOLayer.h
index ed36348cb52a4745053c29a2d54ff8effc733e9c..af5ca0a3c3e16b53953c527d13c9f51c8ab2969f 100644
--- a/security/manager/ssl/nsNSSIOLayer.h
+++ b/security/manager/ssl/nsNSSIOLayer.h
@@ -27,6 +27,9 @@ class SharedSSLState;
 }  // namespace psm
 }  // namespace mozilla
 
+const uint32_t kIPCClientCertsSlotTypeModern = 1;
+const uint32_t kIPCClientCertsSlotTypeLegacy = 2;
+
 using mozilla::OriginAttributes;
 
 class nsIObserver;
@@ -354,11 +357,22 @@ nsresult nsSSLIOLayerAddToSocket(int32_t family, const char* host, int32_t port,
                                  bool forSTARTTLS, uint32_t flags,
                                  uint32_t tlsFlags);
 
+extern "C" {
+using FindObjectsCallback = void (*)(uint8_t type, size_t id_len,
+                                     const uint8_t* id, size_t data_len,
+                                     const uint8_t* data, uint32_t slotType,
+                                     void* ctx);
+void DoFindObjects(FindObjectsCallback cb, void* ctx);
+using SignCallback = void (*)(size_t data_len, const uint8_t* data, void* ctx);
+void DoSign(size_t cert_len, const uint8_t* cert, size_t data_len,
+            const uint8_t* data, size_t params_len, const uint8_t* params,
+            SignCallback cb, void* ctx);
+}
+
 SECStatus DoGetClientAuthData(ClientAuthInfo&& info,
                               const mozilla::UniqueCERTCertificate& serverCert,
                               nsTArray<nsTArray<uint8_t>>&& collectedCANames,
                               mozilla::UniqueCERTCertificate& outCert,
-                              mozilla::UniqueSECKEYPrivateKey& outKey,
                               mozilla::UniqueCERTCertList& outBuiltChain);
 
 #endif  // nsNSSIOLayer_h
diff --git a/security/manager/ssl/osclientcerts/src/lib.rs b/security/manager/ssl/osclientcerts/src/lib.rs
index add9880c05e2790a37b43c2ad00cd3642f632371..a1986f66bb96675ccb24ff4a9dd0bd14b9aea0cc 100644
--- a/security/manager/ssl/osclientcerts/src/lib.rs
+++ b/security/manager/ssl/osclientcerts/src/lib.rs
@@ -88,8 +88,7 @@ macro_rules! manager_guard_to_manager {
 // Helper macro to prefix log messages with the current thread ID.
 macro_rules! log_with_thread_id {
     ($log_level:ident, $($message:expr),*) => {
-        let message = format!($($message),*);
-        $log_level!("{:?} {}", thread::current().id(), message);
+        $log_level!("{:?} {}", thread::current().id(), format_args!($($message),*));
     };
 }
 
diff --git a/security/manager/ssl/rsclientcerts/src/manager.rs b/security/manager/ssl/rsclientcerts/src/manager.rs
index 3ad026fec4f0abe1c22b6daade92db6896f5608a..b8b47dcc9b7b9de240edd379f1de740cba9f8262 100644
--- a/security/manager/ssl/rsclientcerts/src/manager.rs
+++ b/security/manager/ssl/rsclientcerts/src/manager.rs
@@ -146,7 +146,7 @@ impl ManagerProxy {
                         }
                         ManagerArguments::StartSearch(session, attrs) => {
                             ManagerReturnValue::StartSearch(
-                                real_manager.start_search(session, &attrs),
+                                real_manager.start_search(session, attrs),
                             )
                         }
                         ManagerArguments::Search(session, max_objects) => {
@@ -167,11 +167,11 @@ impl ManagerProxy {
                         }
                         ManagerArguments::GetSignatureLength(session, data) => {
                             ManagerReturnValue::GetSignatureLength(
-                                real_manager.get_signature_length(session, &data),
+                                real_manager.get_signature_length(session, data),
                             )
                         }
                         ManagerArguments::Sign(session, data) => {
-                            ManagerReturnValue::Sign(real_manager.sign(session, &data))
+                            ManagerReturnValue::Sign(real_manager.sign(session, data))
                         }
                         ManagerArguments::Stop => ManagerReturnValue::Stop(Ok(())),
                     };
@@ -402,23 +402,23 @@ impl<B: ClientCertsBackend> Object<B> {
 
     fn get_signature_length(
         &mut self,
-        data: &[u8],
+        data: Vec<u8>,
         params: &Option<CK_RSA_PKCS_PSS_PARAMS>,
     ) -> Result<usize, Error> {
         match self {
             Object::Cert(_) => Err(error_here!(ErrorType::InvalidArgument)),
-            Object::Key(key) => key.get_signature_length(data, params),
+            Object::Key(key) => key.get_signature_length(&data, params),
         }
     }
 
     fn sign(
         &mut self,
-        data: &[u8],
+        data: Vec<u8>,
         params: &Option<CK_RSA_PKCS_PSS_PARAMS>,
     ) -> Result<Vec<u8>, Error> {
         match self {
             Object::Cert(_) => Err(error_here!(ErrorType::InvalidArgument)),
-            Object::Key(key) => key.sign(data, params),
+            Object::Key(key) => key.sign(&data, params),
         }
     }
 }
@@ -426,7 +426,7 @@ impl<B: ClientCertsBackend> Object<B> {
 /// The `Manager` keeps track of the state of this module with respect to the PKCS #11
 /// specification. This includes what sessions are open, which search and sign operations are
 /// ongoing, and what objects are known and by what handle.
-struct Manager<B: ClientCertsBackend> {
+pub struct Manager<B: ClientCertsBackend> {
     /// A map of session to session type (modern or legacy). Sessions can be created (opened) and
     /// later closed.
     sessions: BTreeMap<CK_SESSION_HANDLE, SlotType>,
@@ -546,7 +546,7 @@ impl<B: ClientCertsBackend> Manager<B> {
     pub fn start_search(
         &mut self,
         session: CK_SESSION_HANDLE,
-        attrs: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)],
+        attrs: Vec<(CK_ATTRIBUTE_TYPE, Vec<u8>)>,
     ) -> Result<(), Error> {
         let slot_type = match self.sessions.get(&session) {
             Some(slot_type) => *slot_type,
@@ -554,7 +554,7 @@ impl<B: ClientCertsBackend> Manager<B> {
         };
         // If the search is for an attribute we don't support, no objects will match. This check
         // saves us having to look through all of our objects.
-        for (attr, _) in attrs {
+        for (attr, _) in &attrs {
             if !SUPPORTED_ATTRIBUTES.contains(attr) {
                 self.searches.insert(session, Vec::new());
                 return Ok(());
@@ -565,12 +565,12 @@ impl<B: ClientCertsBackend> Manager<B> {
         // indication for the backend to re-scan for new objects from tokens that may have been
         // inserted or certificates that may have been imported into the OS. Since these searches
         // are relatively rare, this minimizes the impact of doing these re-scans.
-        if search_is_for_all_certificates_or_keys(attrs)? {
+        if search_is_for_all_certificates_or_keys(&attrs)? {
             self.maybe_find_new_objects()?;
         }
         let mut handles = Vec::new();
         for (handle, object) in &self.objects {
-            if object.matches(slot_type, attrs) {
+            if object.matches(slot_type, &attrs) {
                 handles.push(*handle);
             }
         }
@@ -651,7 +651,7 @@ impl<B: ClientCertsBackend> Manager<B> {
     pub fn get_signature_length(
         &mut self,
         session: CK_SESSION_HANDLE,
-        data: &[u8],
+        data: Vec<u8>,
     ) -> Result<usize, Error> {
         let (key_handle, params) = match self.signs.get(&session) {
             Some((key_handle, params)) => (key_handle, params),
@@ -664,7 +664,7 @@ impl<B: ClientCertsBackend> Manager<B> {
         key.get_signature_length(data, params)
     }
 
-    pub fn sign(&mut self, session: CK_SESSION_HANDLE, data: &[u8]) -> Result<Vec<u8>, Error> {
+    pub fn sign(&mut self, session: CK_SESSION_HANDLE, data: Vec<u8>) -> Result<Vec<u8>, Error> {
         // Performing the signature (via C_Sign, which is the only way we support) finishes the sign
         // operation, so it needs to be removed here.
         let (key_handle, params) = match self.signs.remove(&session) {