Keys and key storage
graph TD
NEW["Newly issued certificate"]
PEM["Certificate loaded from PEM or DER"]
PFX["PFX file, PBES2 with AES-256"]
STORE["Key file next to the certificate store"]
OS["Operating system key store: DPAPI or Secret Service"]
BAK["Backup file, Argon2id and AES-256-GCM"]
TABLE["Process key table, by thumbprint"]
NEW -- "automatic" --> TABLE
PEM -- "Attach method" --> TABLE
PFX -- "ImportPfx" --> TABLE
TABLE -- "ExportPfx" --> PFX
TABLE -- "X509Store.Add with CryptoMode" --> STORE
STORE -- "X509Store.Find with restorePqKeys" --> TABLE
OS -. "protects" .-> STORE
STORE -- "BackupAllPqKeys" --> BAK
BAK -- "RestoreFromBackup" --> STOREFigure: At run time, private PQ keys live in a table inside the process. For long-term storage, they go into a PFX file or into a key file next to the certificate store, protected by the operating system's key store. A backup bundles all key files under one password.
The .NET X509Certificate2 class doesn't know ML-DSA or ML-KEM keys, so it can't carry them. That's why
the library keeps private PQ keys outside the certificate and associates them with it through its
thumbprint.
Key features
- Automatic association at issuance: A newly issued certificate finds its private PQ key without any further call.
- Later association with the Attach methods, for certificates loaded from files without a key.
- Transport as a PFX file with
ExportPfxandImportPfx, encrypted with PBES2 and AES-256. - Storage next to the certificate store through the
X509Storeextensions, protected by the operating system's key store on Windows and Linux. - Backup of all stored keys to a password-protected file with
PqKeyStoreManager, which you can restore on a different machine and under a different account.
Core concepts
The process key table
When the library issues a certificate with a PQ part, it puts the private PQ key into a table that applies
to the whole process. The table is keyed by the certificate's thumbprint, so every X509Certificate2
object with the same content finds the same key, including a freshly loaded copy. The entries remain until
the process ends.
A certificate that another process issued and that you load from a PEM or DER file has no key in the table. Associate the key with the method that matches the key type:
| Key | Read | Associate |
|---|---|---|
| ML-DSA key of a hybrid certificate | GetAlternativePrivateMLDsaKey | AttachAlternativePrivateMLDsaKey |
| ML-DSA key of a pure ML-DSA certificate | GetPrivateMLDsaKey | AttachPrivateMLDsaKey |
| ML-KEM key | GetPrivateMLKemKey | AttachPrivateMLKemKey |
| Composite ML-KEM key | GetPrivateCompositeMLKemKey | AttachPrivateCompositeMLKemKey |
The read methods return null if the process doesn't know a key. The .NET HasPrivateKey property only
refers to classical keys. For a pure ML-DSA certificate, it stays false even after you associate the key.
Like the .NET method, CopyWithPrivateKey(privateKey, mode) only attaches the classical key. The copy has
the same thumbprint, so it finds a PQ key that's already associated. The method doesn't generate a PQ key.
With CryptoMode.Hybrid, it only checks that the certificate carries an alternative ML-DSA key.
PFX files
ExportPfx writes the certificate, the classical key, and the PQ key to a password-protected file.
X509Certificate2ExportExtensions.ImportPfx reads the file and associates the PQ key again. This is how
you move keys between processes and machines.
With a PQ key, ExportPfx encrypts every part of the file with PBES2 (RFC 8018): AES-256-CBC with a key
from PBKDF2 using HMAC-SHA-256 and 2,048 iterations. SHA-256 computes the file's integrity checksum (MAC).
That's the OpenSSL 3.6 default. An overload that takes PbeParameters lets you choose the algorithm and the
iteration count. For details, see
Transfer certificates with PQ keys as PFX files.
Storage next to the certificate store
The operating system's certificate store doesn't accept PQ keys. So X509Store.Add(certificate, mode)
adds the certificate to the store as usual and also writes the PQ key to a file named
<thumbprint>.pqkey in the WKL.Crypto/PqKeys folder under the account's local application data folder
(%LOCALAPPDATA% on Windows, ~/.local/share on Linux).
X509Store.Find(findType, findValue, validOnly, restorePqKeys: true) reads the file during the search and
associates the key again.
The operating system's key store protects the file's content for the signed-in account:
| Operating system | Protection |
|---|---|
| Windows | Windows Data Protection API (DPAPI) for the signed-in account |
| Linux with Secret Service (for example GNOME Keyring or KWallet) | AES-256-GCM with a random wrapping key that the account's Secret Service holds; the file is bound to its certificate's thumbprint |
| macOS, Linux without Secret Service | no storage: X509Store.Add in a PQ mode and PersistPqKeys throw PlatformNotSupportedException, and the certificate store stays unchanged |
On systems without a key store, keep the keys in a PFX file.
RestorePqKeys still reads key files that an earlier version of the library wrote outside Windows. If a
key store is available, the method immediately rewrites such a file in the new format.
Backup
PqKeyStoreManager.BackupAllPqKeys(backupPath, password) decrypts all key files of the account and writes
the keys to a backup file:
- Argon2id (RFC 9106) derives the key from the password, with 3 passes, 64 MiB of memory, and a parallelism of 4.
- AES-256-GCM encrypts and protects the content, so a modified file is rejected on restore.
RestoreFromBackup(backupPath, password) creates the key files under the protection of the account it runs
in. That's why you can restore the backup on a different machine and under a different account. The method
still reads backups from an earlier version of the library, but those restore only under the original
account on the original machine.
CleanupOrphanedKeys deletes key files whose certificate is no longer in the certificate store.
Limitations
- Protection against other accounts, not against your own. DPAPI and Secret Service hand the key to any program that runs under the same account. Anyone who can run code under the account can read the key files.
- Storage only on Windows and on Linux with Secret Service. On other systems, and in Linux sessions without Secret Service, such as services and containers without a login session, the library doesn't create key files.
- The password alone protects the PFX file and the backup. Whoever has the file and the password has the keys. Use a long, random password, and send it through a different channel than the file.
- Older import programs. Programs that can't read PBES2, such as older Windows versions, can't import the PFX file. For them,
ExportPfxwithPbeEncryptionAlgorithm.TripleDes3KeyPkcs12writes the weaker Triple DES variant. - Keys in memory. Private PQ keys stay unencrypted in the process's memory until the process ends.
- Argon2id requires OpenSSL 3.2 or later. Backup and restore compute Argon2id in OpenSSL. Any version with ML-DSA, that is, 3.5 or later, meets this requirement. If the loaded installation doesn't provide Argon2id, the backup throws a
CryptographicExceptionthat namesARGON2ID.