CNG Architecture: BCrypt, NCrypt, KSPs, and How Windows Picks Its Algorithms
A guided tour of the Cryptography API: Next Generation -- the two-tier API, the Key Storage Provider model, the FIPS toggle, and how PQC slots in.
Permalink1. From CAPI to CNG: why Microsoft started over
In the late 1990s, Microsoft shipped its first general cryptographic API. The original Cryptographic Service Providers (CAPI) model arrived in Windows NT 4.0 Service Pack 4 in 1998 and defined a plug-in unit called a Cryptographic Service Provider, or CSP. A CSP was a monolithic DLL: it owned the algorithm implementations, the key storage, and the export-control posture all at once. If you wanted to add hardware-backed RSA on Windows NT, you wrote a CSP. If you wanted to add a new hash function, you also wrote a CSP. The model worked for the algorithms Microsoft had in mind when it designed it.
Then the algorithms changed.
AES was standardized in 2001, after CAPI's design was already frozen. Microsoft retrofitted AES into the original architecture by shipping the Microsoft Enhanced RSA and AES Cryptographic Provider as a separate CSP, sitting alongside the original Microsoft Base Cryptographic Provider. Elliptic-curve cryptography was even more awkward: CAPI's algorithm identifiers and key-blob formats had no place for ECC curves. Every new algorithm required a new CSP or a new release of an existing one. The plug-in surface was rigid, the FIPS validation story was painful, and the API was relentlessly C-shaped in ways that made auditing hard.
Microsoft was not alone. The same era produced Intel's Common Data Security Architecture (CDSA) and several short-lived crypto frameworks for OS/2 and other platforms. Most of them disappeared. CAPI's longevity owed more to Windows market share than to its design.By 2005, Microsoft started over. The result was the Cryptography API: Next Generation, or CNG, which shipped with Windows Vista and Windows Server 2008 in January 2007. CNG was not a refactor. It was a clean second system, designed from a different set of assumptions: algorithms would keep arriving, key storage needed to be a separate concern, FIPS validation had to be a first-class output, and the same API had to work in user mode and kernel mode.
The Windows cryptographic API introduced in Vista (2007) as the long-term replacement for CAPI. CNG splits cryptography into a primitives layer (bcrypt.h, bcryptprimitives.dll) and a key-storage layer (ncrypt.h, ncrypt.dll), each pluggable through registered providers. Used by every modern Windows component that touches cryptography.
The plug-in unit of the legacy CAPI architecture (1998-onward). A CSP bundled algorithms, key storage, and FIPS posture into a single DLL. Largely superseded by CNG providers, but still present on the system for backwards compatibility.
The three design pillars Microsoft committed to in the CNG portal documentation were modularity, cryptographic agility, and FIPS-compliance readiness. All three would matter twenty years later when post-quantum cryptography arrived without warning the protocol authors. We will get to that.
2. BCrypt: the symmetric stack and the ephemeral key
Open a Visual Studio project, include <bcrypt.h>, link bcrypt.lib, and you have access to almost every cryptographic primitive Windows ships. AES in CBC, CFB, ECB, GCM, and CCM modes. SHA-1, SHA-256, SHA-384, SHA-512, the SHA-3 family, and the cSHAKE128 and cSHAKE256 extendable-output functions added in Windows 11 24H2. HMAC over any of those hashes. PBKDF2. The NIST SP 800-108 key-derivation construction. The DRBG-based random number generator drawn from NIST SP 800-90. Ephemeral asymmetric operations -- RSA encrypt, ECDSA sign, ECDH key agreement -- on key handles that vanish when the process exits.
The canonical BCrypt opening dance is four calls.
// Pseudocode mirroring the BCryptOpenAlgorithmProvider flow.
// In real C: NTSTATUS values, BCRYPT_ALG_HANDLE, etc.
const algId = "AES"; // wide string
const impl = null; // null -> walk the priority list
const flags = 0;
const hAlg = BCryptOpenAlgorithmProvider(algId, impl, flags);
BCryptSetProperty(hAlg, "ChainingMode", "ChainingModeGCM");
const hKey = BCryptGenerateSymmetricKey(hAlg, keyBytes);
const ciphertext = BCryptEncrypt(hKey, plaintext, authInfo);
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0); Press Run to execute.
The interesting parameter is impl. When it is NULL, BCryptOpenAlgorithmProvider "attempts to open each registered provider, in order of priority, for the algorithm specified by the pszAlgId parameter and returns the handle of the first provider that is successfully opened". That sentence is the whole story of CNG provider priority in nineteen words.
Algorithm identifiers are wide strings. L"AES", L"SHA256", L"RSA", L"ML-KEM", L"ML-DSA", L"CHACHA20_POLY1305", L"CSHAKE128". Each string is registered in CNG's configuration store under HKLM\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\, with a per-algorithm ordered list of providers that claim to implement it. Add a new algorithm and you add a new string. Add a new provider and you append to its priority list. The API surface does not change.
Underneath the API is a single implementation library. Microsoft's SymCrypt has been the actual workhorse since Windows 10 version 1703: "SymCrypt is the core cryptographic function library currently used by Windows... Since the 1703 release of Windows 10, SymCrypt has been the primary crypto library for all algorithms in Windows." SymCrypt is open source. It carries hand-tuned assembly for AES-NI, VAES, SHA-NI, and PCLMULQDQ on x64, plus ARM64 SHA and AES intrinsics. On a modern Xeon, AES-GCM throughput from BCrypt routinely sits in the 4 to 8 GB/s range per core.
SymCrypt's open-source release in 2019 was a quiet event for a Microsoft library: the algorithms that protect Windows are reviewable by anyone willing to read C and ARM/x64 assembly.BCrypt keys are ephemeral by construction. A BCRYPT_KEY_HANDLE lives in your process and dies with it. If you want to keep a private key around between processes, between reboots, or between machines, you do not use BCrypt. You use NCrypt.
That distinction is the first thing developers get wrong when they meet CNG. The second thing they get wrong is forgetting that BCrypt's GCM API does not allocate nonces for you. The NIST SP 800-38D specification of Galois/Counter Mode is famously brittle under nonce reuse: a single repeated nonce under the same key destroys both confidentiality (XOR of plaintexts leaks) and authenticity (the GHASH authentication key becomes recoverable). With 96-bit random nonces the birthday bound limits safe usage to roughly invocations per key before collision probability becomes meaningful. Counter-based nonces sidestep the birthday bound entirely but require persistent state. CNG does neither for you. That part is your problem.
Windows 10 added pseudo-handles -- pre-baked handle constants like BCRYPT_AES_ALG_HANDLE and BCRYPT_SHA256_ALG_HANDLE -- that skip the provider lookup for the built-in algorithms. The 24H2 release extended that list to include BCRYPT_MLKEM_ALG_HANDLE and the cSHAKE handles. Microsoft now recommends pseudo-handles over BCryptOpenAlgorithmProvider for new code when the algorithm is built in. The motivation is performance: pseudo-handles bypass the per-call provider walk and the configuration-store lookup.
That covers the primitives. Now we need a place to keep the keys.
3. NCrypt: where the long-lived secrets live
The ncrypt.h header opens a different door. Every function in the NCrypt API surface -- NCryptOpenStorageProvider, NCryptCreatePersistedKey, NCryptOpenKey, NCryptSignHash, NCryptDecrypt, NCryptKeyDerivation, NCryptExportKey, NCryptProtectSecret -- begins by routing the call through ncrypt.dll, which acts as a router rather than an implementation. The router decides which Key Storage Provider handles the operation and forwards the call.
That routing layer is the architectural distinction Microsoft has insisted on for two decades. Microsoft's Key Storage and Retrieval documentation describes it like this: the NCrypt router "conceals details, such as key isolation, from both the application and the storage provider itself." Translation: the application calls NCryptSignHash and gets back a signature. It does not know -- and should not need to know -- whether the key lives in %APPDATA%, inside a TPM chip on the motherboard, on a smart card halfway across the room, or in a network-attached hardware security module in a data center on a different continent.
A registered plug-in DLL that owns persistent private-key material and exposes it through the NCrypt API. Microsoft ships four built-in KSPs (Software, Platform/TPM, Smart Card, and the CNG-DPAPI provider); third parties ship KSPs for HSM appliances, USB security keys, and cloud key services. Selecting a KSP is a matter of passing the right name string to NCryptOpenStorageProvider.
The mechanical flow for creating a persisted key looks like this.
Diagram source
sequenceDiagram
participant App as Application
participant Router as ncrypt.dll (NCrypt router)
participant KSP as Microsoft Software KSP
participant LSA as LSA key-isolation process
participant Disk as %APPDATA%\Microsoft\Crypto\Keys\
App->>Router: NCryptOpenStorageProvider("Microsoft Software Key Storage Provider")
Router-->>App: hProvider
App->>Router: NCryptCreatePersistedKey(hProvider, "RSA", "MyKey", 2048, ...)
Router->>KSP: dispatch via registered KSP entry points
KSP->>LSA: LRPC: generate key, return handle
LSA->>Disk: write DPAPI-wrapped private blob
LSA-->>KSP: ok
KSP-->>Router: hKey
Router-->>App: hKey
App->>Router: NCryptSignHash(hKey, digest)
Router->>KSP: forward
KSP->>LSA: LRPC: sign with isolated key
LSA-->>KSP: signature
KSP-->>Router: signature
Router-->>App: signature Two facts about that diagram matter. First, the private key bits never enter the calling process. They are generated inside the LSA process and the calling application only ever receives a handle and the eventual signature. Second, the LRPC hop is real: it costs roughly 30 to 100 microseconds per call on modern hardware. For bulk symmetric encryption you would not want this overhead, which is why CNG's design pushes you toward BCrypt for symmetric work and reserves NCrypt for the rarer, smaller, and more sensitive operations on long-lived asymmetric keys.
The LSA key-isolation process islsaiso.exe on systems with Credential Guard enabled, hosted inside the Virtualization-Based Security (VBS) trustlet boundary. On systems without VBS, the role is played by lsass.exe itself. Either way, key material does not enter the application's address space.
NCrypt is also where the asymmetric algorithms live in their persistent form. The Microsoft Software Key Storage Provider claims RSA keys from 512 to 16384 bits in 64-bit increments, DSA, DH, and ECDSA/ECDH on the NIST P-256, P-384, and P-521 curves. Windows 11 24H2 added ML-KEM at the 512, 768, and 1024 parameter sets and ML-DSA at the 44, 65, and 87 parameter sets to the Software KSP's repertoire.
The split between BCrypt and NCrypt is sometimes confusing because there is overlap. You can sign with BCrypt's BCryptSignHash if you generated an ephemeral key pair. You can also sign with NCrypt's NCryptSignHash if the key is persisted in a KSP. The rule of thumb is: if the key needs to survive the process, use NCrypt; if it does not, use BCrypt. Real-world Windows code skews heavily toward NCrypt for asymmetric operations because almost every interesting asymmetric key has an associated certificate, and certificates outlive processes.
The router lets the application speak one language and have the storage backend vary. That makes the KSP plug-in model the most interesting piece of the architecture, and it deserves its own section.
4. The KSP model: one API, many places to keep keys
A KSP is a DLL on disk and an entry in the registry. The DLL exports a fixed set of function pointers that mirror NCrypt's API. The registry entry under HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Microsoft Software Key Storage Provider (and its siblings) tells ncrypt.dll which DLL to load when an application asks for a provider by name. That is the whole interface contract. If you can produce a DLL that implements the entry points and you can install a registry entry, you have a CNG KSP.
The platform comes with four. They sit on a spectrum from "your operating system is the entire trust boundary" to "the keys live on a separate piece of silicon and only signatures come back."
Diagram source
flowchart LR
A["Microsoft Software KSP — private keys on disk — (DPAPI-wrapped)"] --> B["Microsoft Platform Crypto Provider — TPM 2.0 or Pluton — on-CPU silicon"]
B --> C["Microsoft Smart Card KSP — removable hardware token — (PIV, CAC, Yubikey)"]
C --> D["Third-party HSM KSP — Thales Luna, Entrust nShield, — YubiHSM 2, AWS CloudHSM"]
A -.-> A1["~10^4 RSA-2048 sign/sec — FIPS 140-2 L1"]
B -.-> B1["~1-10 sign/sec — TPM vendor cert"]
C -.-> C1["~1-5 sign/sec — card vendor cert"]
D -.-> D1["~10^2-10^4 sign/sec — FIPS 140-2/-3 L3 typical"] 4.1 The Microsoft Software KSP
The default. If you pass NULL for the provider name in NCryptOpenStorageProvider, you get this one. It stores per-user private keys at %APPDATA%\Microsoft\Crypto\Keys\ and per-machine keys at %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\SystemKeys\, with each file-level blob further protected by DPAPI under either the user master key or the LocalSystem (S-1-5-18) master key. The private-key operations dispatch through LRPC into the LSA key-isolation process so that even with administrator privileges on the machine, naive code-injection into the application's address space does not yield key bits.
The Microsoft Software KSP is also the only KSP that runs inside the LSA key-isolation process. Third-party KSPs run in the calling application's process. That difference matters enormously for the threat model. Microsoft notes this explicitly: third-party KSPs "do not run inside the LSA process". If you are a third-party KSP that talks to remote HSM hardware, the isolation comes from the HSM itself, not from any Windows process boundary.
4.2 The Microsoft Platform Crypto Provider (TPM and Pluton)
The KSP that answers to MS_PLATFORM_KEY_STORAGE_PROVIDER is the TPM's face to CNG. When you call NCryptCreatePersistedKey against it, the TPM 2.0 chip itself generates the key under the protection of its Storage Root Key. The private bits never leave the chip. The application gets back a handle whose only operations are sign, decrypt, and key derivation -- the private key cannot be exported, and that property is enforced by physics, not by software policy.
The Platform Crypto Provider is the place where CNG stops trusting the operating system and starts trusting a separate piece of silicon. Every TPM-backed key in Windows -- BitLocker's Volume Master Key wrapping, Windows Hello credentials, AD CS attestation-enrolled machine identities -- enters and exits through this single KSP name.
Microsoft Pluton, the security processor that shipped in 2022 on AMD Ryzen 6000, Snapdragon 8cx Gen 3, and Intel Core Ultra Series 2 silicon, is exposed to Windows as a TPM 2.0 device behind the same Platform Crypto Provider name. Application code that worked against a discrete TPM works against Pluton with no changes. Pluton's wins are at the supply-chain layer (no SPI bus to physically tap between the chip and the CPU) and the firmware-update layer (Pluton firmware ships via Windows Update). The Windows-facing API is intentionally identical.
4.3 The Microsoft Smart Card KSP
MS_SMART_CARD_KEY_STORAGE_PROVIDER is a single KSP that routes to whichever vendor minidriver claims the inserted card. The minidriver model is Microsoft's plug-in layer below the KSP layer: smart-card vendors do not write CNG KSPs, they write minidrivers, and Microsoft's single KSP fans the calls out to them via the APDU protocol. Cards that follow Microsoft's Generic Identity Device Specification (GIDS) work without a vendor minidriver. Cards that do not, including most US federal PIV cards before about 2015, ship vendor-specific minidrivers.
This is the layer that powers Windows Hello for Business "virtual smart card" credentials, which present a TPM-backed key through the smart-card path because so much enterprise software already knew how to talk to PIV-style cards.
4.4 Third-party HSM and security-key KSPs
YubiHSM 2, Thales Luna, Entrust nShield, AWS CloudHSM Client for Windows, and various cloud-KMS bridges all ship CNG KSPs. The KSP DLL pretends to be a local provider and proxies operations across whatever transport the device uses -- USB for a YubiHSM, PCIe or TCP for a Luna, HTTPS for a cloud HSM. Latency varies from microseconds for a USB device to a few milliseconds for a network HSM. The application code that calls NCryptSignHash does not change.
5. The TPM KSP, attestation, and the hardware boundary
A TPM-bound key is a useful key, but a TPM-bound key with an attestation statement is a different kind of asset entirely. The Trusted Platform Module supports a primitive called key attestation: the TPM can sign a statement that says, "this key was generated inside me, I will never let it out, and here is a chain of trust back to my Endorsement Key that proves I am a real TPM made by a real vendor." A certificate authority that requires this attestation can refuse to issue a certificate for any key that did not come from inside a TPM.
Active Directory Certificate Services supports exactly this flow as "TPM key attestation". The flow involves three keys: an Endorsement Key (EK) burned into the TPM at manufacture, an Attestation Identity Key (AIK) derived from the EK and certified by Microsoft or by the enterprise PKI, and the application key being attested. The AIK signs a statement covering the application key's properties; the CA verifies the AIK certificate chain and the statement, and only then issues a certificate.
Diagram source
flowchart TD
EK["Endorsement Key (EK) — burned into TPM at manufacture — vendor cert from Intel/AMD/etc."]
AIK["Attestation Identity Key (AIK) — generated in TPM, certified by — Microsoft EK CA or enterprise PKI"]
APPK["Application key — generated in TPM via — NCryptCreatePersistedKey"]
STMT["Attestation statement — signed by AIK"]
CA["Enterprise CA (AD CS) — verifies AIK chain — and attestation"]
CERT["X.509 certificate — issued to application key"]
EK --> AIK
AIK --> STMT
APPK --> STMT
STMT --> CA
CA --> CERT The CNG-facing API for this is the property bag on a NCRYPT_KEY_HANDLE. After creating the key, the application calls NCryptGetProperty with NCRYPT_KEY_ATTESTATION_PROPERTY (and friends) to retrieve the attestation blob. The CA receives the blob in the certificate request and validates it against Microsoft's published EK CA roots. The whole protocol fits inside the standard certificate-enrollment flow.
A software KSP can promise that a key is non-exportable. A TPM KSP can prove it.
Throughput is the price. A typical TPM 2.0 chip performs single-digit RSA-2048 signatures per second. Pluton-based platforms are in the same neighborhood. Any architecture that wants to do a TPM signature on every HTTP request will fall over almost immediately. The TPM is the right home for one signature per session, per boot, or per logon -- not one per packet.
Key migration between TPMs is essentially impossible by design. Replace a motherboard, and any keys that were sealed to the old TPM's Storage Root Key are gone. This is the same property that makes BitLocker safe against motherboard theft (the recovery key, escrowed elsewhere, is the only way back) and the same property that makes TPM-bound device identities a key-management headache during hardware refresh cycles.There is a deeper, more philosophical reason to use the TPM that the API does not advertise. Software keys are bounded by the kernel's process-isolation guarantees. Any kernel-level attacker, any user with SeDebugPrivilege, or any code injected into lsass.exe can in principle reach key material. The provably stronger bound -- keys that no OS-level code can ever read -- requires an off-CPU hardware boundary. CNG's own design notes acknowledge this when they say CNG "is designed to be usable as a component in a FIPS level 2 validated system": software-only isolation maps to FIPS 140-2 Levels 1 and 2; hardware boundaries are required for Level 3 and above.
6. FIPS 140 mode, compliance, and the one-bit toggle
There is a registry value at HKLM\SYSTEM\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy\Enabled. When it is set to 1 (or when the equivalent Group Policy "System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing" is enabled), Schannel and CNG callers refuse to use algorithms that fall outside the FIPS-approved set. RC4 disappears. MD5 disappears. SHA-1 disappears for new signatures (though not for legacy verification). TLS suites that rely on any of those are removed from the negotiation list.
The toggle is a runtime gate, not a code path. The underlying modules -- bcryptprimitives.dll and cng.sys -- are the same modules either way. They have been submitted to the Cryptographic Module Validation Program and validated against the FIPS 140-2 standard. The toggle simply tells those modules that the calling environment expects FIPS-mode behavior, and the modules then refuse the non-approved algorithms.
A US federal certification program (Federal Information Processing Standard 140) that subjects a cryptographic module to laboratory testing and NIST review. Validated modules receive a public CMVP certificate. Federal agencies, FedRAMP/CMMC contractors, and most regulated industries can only use validated modules in approved configurations. FIPS 140-2 and the newer FIPS 140-3 differ mainly in test methodology and the standard's own ISO/IEC alignment.
Two current Windows 11 certificate numbers are worth memorizing. CMVP certificate #4825 covers bcryptprimitives.dll. CMVP certificate #4766 covers cng.sys, the kernel-mode primitives. Both are FIPS 140-2 Level 1 modules with a sunset date of September 21, 2026 under the CMVP's transition rules. Microsoft maintains the per-version FIPS validation portal for Windows 11, which lists the active certificates per build and the algorithms each one covers.
The cadence mismatch is the open story here. Windows ships H1 and H2 feature updates roughly every six months. CMVP validation of a new build's primitives DLL and kernel module typically takes 12 to 24 months. Federal customers, FedRAMP-bound cloud tenants, and CMMC contractors cannot run a Windows build that does not have an active FIPS certificate covering its cryptographic modules. Microsoft submits 140-3 evidence for newer modules, but as of mid-2026 no public 140-3 certificate is visible on CMVP for the bcryptprimitives.dll shipping in Windows 11 24H2.
The toggle also does not change the SymCrypt implementations. AES-GCM is still AES-GCM. What changes is which APIs the caller is allowed to reach. From the application's point of view, the symptom of FIPS mode is STATUS_NOT_SUPPORTED on BCryptOpenAlgorithmProvider(L"RC4", ...). From an auditor's point of view, the symptom is the absence of any disallowed primitive call in the binary.
7. The post-quantum slide: ML-KEM, ML-DSA, and the agility test
The piece of CNG that earns its "agility" billing is the post-quantum transition.
NIST opened the Post-Quantum Cryptography standardization process in 2016 and ran four rounds of public evaluation before issuing the first final standards in August 2024. FIPS 203 standardizes ML-KEM (formerly CRYSTALS-Kyber), a module-lattice key encapsulation mechanism. FIPS 204 standardizes ML-DSA (formerly CRYSTALS-Dilithium), a module-lattice digital signature algorithm. Microsoft Research had been working on lattice cryptography for years, and the public CNG implementations followed quickly: Windows 11 24H2 ships ML-KEM and ML-DSA as first-class CNG algorithms.
Here is the surprising part: the CNG API surface did not change. Adding ML-KEM was a matter of registering new algorithm identifier strings -- BCRYPT_MLKEM_ALGORITHM, the parameter sets BCRYPT_MLKEM_PARAMETER_SET_512, BCRYPT_MLKEM_PARAMETER_SET_768, BCRYPT_MLKEM_PARAMETER_SET_1024 -- in the CNG algorithm-identifier registry. The opening dance for an ML-KEM key encapsulation looks exactly like the opening dance for an ECDH key agreement, except for the string.
// Mirrors the BCrypt pattern shown in the Microsoft sample
// "Using ML-KEM with CNG for Key Exchange"
const hAlg = BCryptOpenAlgorithmProvider("ML-KEM", null, 0);
const hKeyPair = BCryptGenerateKeyPair(hAlg, 0, 0);
BCryptSetProperty(hKeyPair, "ParameterSetName", "ML-KEM-768");
BCryptFinalizeKeyPair(hKeyPair, 0);
const pubBlob = BCryptExportKey(hKeyPair, "MLKEMPUBLICBLOB");
// Sender side: encapsulate to recipient's public key
const recipPub = BCryptImportKeyPair(hAlg, "MLKEMPUBLICBLOB", pubBlob);
const { ciphertext, sharedSecret: ssA } = BCryptEncapsulate(recipPub);
// Recipient side: decapsulate with the matching private key
const ssB = BCryptDecapsulate(hKeyPair, ciphertext);
// ssA === ssB Press Run to execute.
That code is structurally identical to a 2007-era ECDH session. The string changes, the blob format changes, and the wire-format sizes change considerably. ML-KEM ciphertexts at the 512, 768, and 1024 parameter sets are 768, 1088, and 1568 bytes respectively, with public keys of 800, 1184, and 1568 bytes per FIPS 203. ML-DSA signatures at parameter sets 44, 65, and 87 are 2420, 3309, and 4627 bytes per FIPS 204. For comparison, an ECDSA P-256 signature is 64 bytes and an X25519 public key is 32 bytes. The PQC blowup is roughly an order of magnitude, and that has knock-on consequences for every protocol that carries certificates or handshakes on the wire.
The wire-format problem is why most TLS-PQ deployments use hybrid groups: classical X25519 combined with ML-KEM-768, with the shared secret derived from both. If either component breaks, the other one still holds. The IETF draft draft-kwiatkowski-tls-ecdhe-mlkem defines the X25519MLKEM768 group with IANA codepoint 0x11EC, and Chrome, Cloudflare, and AWS shipped support in production in 2024. OpenJDK JEP 527 tracks the equivalent work for Java's TLS stack. Schannel in Windows 11 24H2 can negotiate ML-KEM through CNG, but Microsoft has not publicly committed to a default-on hybrid group at the Schannel layer as of mid-2026.
Enumerate the CNG algorithms your system supports
On a Windows 11 24H2 machine, the following PowerShell snippet asks CNG for its registered algorithms:
[System.Security.Cryptography.CngAlgorithm]::new("ML-KEM")
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\Default\0010'The first line forces a CngAlgorithm lookup. The second walks the configuration store. If the keys ML-KEM and ML-DSA appear, your kernel-mode and user-mode primitives are 24H2-current.
The bigger structural lesson is that two decades of "cryptographic agility" claims actually paid off. The PQC transition required a 24H2 update, not a CNG redesign.
8. Where CNG actually shows up: TLS, BitLocker, and friends
The argument for an OS-level cryptographic API stands or falls on what runs on top of it. Every modern Windows component that touches cryptography is a CNG consumer.
The Windows implementation of TLS and DTLS, exposed through the SSPI (Security Support Provider Interface). Schannel handles the TLS protocol state machine, certificate validation, and cipher-suite negotiation, then delegates the actual cryptography to BCrypt and NCrypt. The cipher-suite priority list and protocol-version controls are configured per Windows version, often via Group Policy.
Schannel, the Windows TLS stack, sits directly above CNG. The Schannel cipher-suite list is its own per-version object, documented at the Schannel cipher-suites portal. For TLS 1.2 and earlier, the order is administered via the registry key HKLM\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\SSL\00010002 (the "Functions" value) or the Group Policy "SSL Cipher Suite Order." For TLS 1.3, the three suites (TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256) are not user-orderable; Schannel hard-codes the priority. TLS 1.0 and TLS 1.1 are off by default in Windows 11 23H2 and later, per Microsoft's August 2023 deprecation announcement.
Diagram source
flowchart TD
App["Application — (WinHTTP, HttpClient, browser, ...)"]
SSPI["SSPI / CredSSP layer"]
Schannel["Schannel — protocol state machine — cipher-suite negotiation"]
BCrypt["BCrypt — AES-GCM, SHA-2/3, HKDF, RNG"]
NCrypt["NCrypt — server cert private key sign — client cert auth"]
KSP["KSP (Software / TPM / — Smart Card / HSM)"]
App --> SSPI
SSPI --> Schannel
Schannel --> BCrypt
Schannel --> NCrypt
NCrypt --> KSP BitLocker is the canonical NCrypt-and-TPM consumer. The Full Volume Encryption Key (FVEK) is generated and stored encrypted on disk. The Volume Master Key (VMK) wraps the FVEK and is itself wrapped by one or more "protectors": the TPM, a recovery password, a startup PIN, a USB startup key. The TPM protector is an NCrypt-style operation against the Platform Crypto Provider, sealed to a set of Platform Configuration Register (PCR) measurements that capture the boot state. If anything in the early boot chain changes, the PCRs do not match, the TPM refuses to unwrap the VMK, and BitLocker falls back to recovery.
Authenticode, the signature format on Windows binaries, is a NCrypt-driven workflow at signing time and a BCrypt-driven workflow at verification time. The Windows kernel verifies driver signatures, the Windows loader verifies binary signatures, and WinVerifyTrust exposes the same machinery to applications. The hash algorithm in modern Authenticode is SHA-256, which means every signed executable on the system has a SHA-256 digest computed by BCrypt at some point during validation.
Credential Guard runs the LSA isolated process (lsaiso.exe) inside the Virtualization-Based Security trustlet boundary on systems with VBS enabled. Credential Guard does not replace CNG; it relocates the Microsoft Software KSP into a stronger isolation boundary. NTLM password hashes and Kerberos TGT session keys live inside that boundary, accessible only through the standard CNG calls dispatched into the trustlet.
Windows Hello for Business uses the Platform Crypto Provider as the home for the user's gesture-protected authentication key. The biometric (or PIN) unlocks a key in the TPM; that key signs an attestation that is consumed by Azure AD or AD FS. The biometric never leaves the device.
DPAPI and DPAPI-NG are themselves built on CNG, and they deserve their own section because they are the easiest place to see how the layering pays off.
"Schannel, BitLocker, EFS, Authenticode, Credential Guard, Windows Hello, DPAPI-NG, IPsec, SMB encryption, Kerberos PKINIT -- every modern Windows component is a CNG consumer."
9. DPAPI-NG: a worked example of the NCrypt model
The original Data Protection API (DPAPI), shipped with Windows 2000, was a per-user secret-protection mechanism. An application called CryptProtectData, passed a blob of secret data, and got back an encrypted blob that only the same user on the same machine could later unwrap. The mechanism was anchored in the user's logon credentials, with a master key per user and a complex backup mechanism for password resets. It worked. It also locked the secret to a single machine, which became a problem the moment users started living on more than one device.
DPAPI-NG, introduced in Windows 8 and Windows Server 2012, is the cloud-era rebuild. The CNG DPAPI documentation describes the three calls: NCryptCreateProtectionDescriptor, NCryptProtectSecret, and NCryptUnprotectSecret. The protection descriptor is a small string that names who can unwrap the data. Examples include SID=S-1-5-21-... for an Active Directory user or group, LOCAL=user for the legacy single-user behavior, WEBCREDENTIALS=... for a credential vault entry, and combinations connected by AND or OR operators.
Diagram source
flowchart LR
Plain["plaintext secret"] --> Protect["NCryptProtectSecret(descriptor, plain)"]
Desc["descriptor: — SID=group GUID — OR — LOCAL=user"] --> Protect
Protect --> Blob["opaque blob"]
Blob --> Unprotect["NCryptUnprotectSecret(blob)"]
Unprotect -.->|"resolves descriptor — via AD DC backup keys"| AD["Active Directory DC — (DPAPI backup keys)"]
Unprotect --> Out["plaintext secret — on any authorized machine"] The architectural win is that DPAPI-NG is just NCrypt with a particular protection-descriptor schema. Any KSP that can serve the key referenced by the descriptor can satisfy the unwrap. In an Active-Directory-joined environment, the AD domain controller's DPAPI backup keys allow any machine where the user (or any member of the named group) authenticates to recover the secret. The application that called NCryptProtectSecret does not need to know about backup keys, replication topology, or recovery flows. It calls NCrypt; the router and the relevant KSP do the rest.
This is the design payoff of the two-tier model. A new key-management capability (cross-machine recovery via AD-stored backup keys) becomes a new descriptor type, not a new API. The Windows team has used the same descriptor extensibility to add web-credential descriptors, container-bound descriptors, and the descriptors that protect Group Managed Service Account passwords. Each one is a private key-management concern; none of them broke the public API.
The DPAPI-NG descriptor language is small enough to read in one sitting and powerful enough to express "any member of this AD group, on any machine where that member can authenticate." That is the cloud-era access-control story that the original DPAPI never had.10. Engineering takeaways: choosing the right tool
The decision tree for CNG usage in production code is short.
Diagram source
flowchart TD
Q1{"Need persistent — private key?"}
Q1 -- No --> B["BCrypt — (ephemeral key, pseudo-handle)"]
Q1 -- Yes --> Q2{,"Threat model?"},
Q2 -- "Machine identity, — hardware-rooted" --> P["Microsoft Platform — Crypto Provider — (TPM / Pluton)"]
Q2 -- "User-bound PKI, — removable hardware" --> S["Microsoft Smart Card KSP — (PIV / virtual smart card)"]
Q2 -- "High signing rate, — regulated custody" --> H["Third-party HSM KSP — (YubiHSM / Luna / nShield)"]
Q2 -- "Default, — portable, fast" --> SW["Microsoft Software KSP"] For algorithm choice in mid-2026, the defensible defaults look like this. Symmetric encryption: ChaCha20-Poly1305 or AES-256-GCM. Hashing: SHA-256 or SHA-3 family. Signatures: ECDSA P-256 or P-384 today, with ML-DSA-65 in the back pocket for the inevitable hybrid transition. Key encapsulation: X25519 today, with X25519+ML-KEM-768 hybrid as soon as your peers support it. RSA-2048 only for legacy interoperability. RC4, 3DES, and SHA-1 only behind explicit deprecation policy, and only for verification of historical artifacts.
The hardest thing about CNG is not learning the API. It is choosing the right KSP. That single decision -- where the private key actually lives -- determines almost everything about your threat model, your throughput, your compliance posture, and your operational complexity.
A few engineering rules survive in any setting.
Do not put persistent keys in BCrypt. Every BCrypt key handle dies with the process. The architectural separation exists for a reason. If the key needs to survive a reboot, it belongs in NCrypt under a named KSP.
Do not assume the Software KSP. Code that calls NCryptOpenStorageProvider(NULL) ends up with whatever the default is. On a server with an HSM KSP configured as the default, this might be what you want; on a developer workstation, it might be the Microsoft Software KSP. Be explicit. Pass the name string. Test the negative case where the KSP you named is not registered.
Audit which KSP your certificates actually use. A certificate enrolled with the Platform Crypto Provider behaves identically to a certificate enrolled with the Software KSP from certutil's point of view. The difference is invisible until you ask. Use certutil -store -v My to dump certificate properties, and look for the provider field.
Treat FIPS mode as a deployment fact, not a development toggle. Code that works fine on a developer workstation can break in surprising ways on a FIPS-enabled production server. Run your CI on a FIPS-toggled image periodically. Catch the STATUS_NOT_SUPPORTED returns before customers do.
Watch the PQC roadmap. The ML-KEM and ML-DSA primitives are in 24H2. Hybrid TLS in Schannel is not on by default at the OS level as of mid-2026 (the most recent Microsoft public posture in the cipher-suite documentation does not yet list a default-on hybrid group), but downstream protocol updates will come. Code that uses the BCrypt and NCrypt patterns shown here picks up the new algorithms with a string change.
The story of CNG is the story of two architectural bets that paid off. The first bet was that algorithms would keep arriving, so the API should be a registry of strings rather than a hard-coded set of functions. The second bet was that key storage was a separate concern from algorithm implementation, so the same primitives could run against software, TPM, smart cards, and HSMs without changing the application. In 2007 those bets looked over-engineered. In 2026, with ML-KEM shipping behind the same BCryptEncapsulate call that an ECDH consumer would have used, they look like exactly the right design.
Frequently asked questions
Frequently asked questions about CNG
Is CNG's BCrypt the same thing as the bcrypt password-hashing function?
No. Microsoft's BCrypt is the bcrypt.h primitives header in CNG, providing AES, SHA, HMAC, RNG, and related primitives. The Provos-Mazieres bcrypt is a password-hashing function based on the Blowfish cipher, with no connection to Windows. The naming collision is unfortunate but firmly entrenched. When in doubt, BCrypt with a capital "B" usually means Microsoft's CNG header; lowercase bcrypt usually means the password-hashing function.
Can I use CNG from .NET, Go, Rust, or Python?
On Windows, yes. .NET's System.Security.Cryptography namespace wraps CNG directly: RSACng, ECDsaCng, AesGcm, SHA256.HashData(), CngKey. Go, Rust, and Python bindings exist as third-party crates and packages (the Rust windows crate exposes both BCrypt and NCrypt, for example). OpenSSL on Windows does not transparently use CNG; you need the openssl-cng provider or direct CNG calls if you want the OS-validated primitives to do the work.
What is the difference between BCrypt and NCrypt for asymmetric keys?
Both can do RSA, ECDSA, and (in 24H2) ML-DSA signatures. The difference is lifetime. BCrypt key handles are ephemeral: they live in your process and disappear when it exits. NCrypt keys are persisted in a KSP and survive process exit, reboots, and (for AD-replicated descriptors via DPAPI-NG) the loss of a single machine. Use BCrypt for one-shot ephemeral operations (signing a single message, deriving a session key); use NCrypt for anything with a certificate attached or anything that has to be around tomorrow.
If I enable FIPS mode, will my application break?
Possibly, depending on what algorithms it calls. Setting HKLM\SYSTEM\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy\Enabled = 1 causes CNG to refuse RC4, MD5, SHA-1 for new signatures, and a handful of other non-approved algorithms. Anything that relied on those returns STATUS_NOT_SUPPORTED. The fix is to switch to approved algorithms (AES, SHA-2 family, RSA, ECDSA, ML-KEM, ML-DSA), not to disable the toggle. The toggle is also necessary but not sufficient for FIPS compliance: you also need a Windows build with an active CMVP certificate covering the cryptographic modules.
When does Windows 11 add hybrid PQ TLS by default?
As of mid-2026, the public Schannel documentation does not list a default-on hybrid group like X25519MLKEM768. The ML-KEM primitive is in CNG in 24H2, and Schannel can use it through the standard cipher-suite negotiation, but Microsoft has not publicly committed to enabling a hybrid group out of the box at the OS level. Chrome, Cloudflare, and AWS have already shipped hybrid PQ TLS in production at the application layer. Expect Schannel to follow once IETF standardization stabilizes and CMVP validation of the new modules catches up.
How can I tell whether a private key is in the TPM or on disk?
For a certificate in the user or machine store, run certutil -store -v My (or My replaced with the store name) and look at the "Provider" field of each certificate. Microsoft Software Key Storage Provider means the key is on disk under %APPDATA% or %ALLUSERSPROFILE%. Microsoft Platform Crypto Provider means the key lives inside the TPM (or Pluton). Microsoft Smart Card Key Storage Provider means the key is on a card. Third-party HSM KSPs will show the vendor's provider name. For a freshly-created key via NCryptCreatePersistedKey, the provider name you passed to NCryptOpenStorageProvider is the source of truth.
Why does NCrypt have an LRPC hop on every call?
Because private keys do not live in the calling process. For the Microsoft Software KSP, key material lives in the LSA key-isolation process (lsaiso.exe under VBS, lsass.exe otherwise), and every operation that touches private bits has to cross that process boundary. The cost is around 30 to 100 microseconds per call. That is acceptable for signing or key derivation (operations that happen a handful of times per session); it would be punishing for bulk symmetric encryption. The architectural answer is to keep bulk crypto in BCrypt and let only the persistent-key operations pay the LRPC cost.
Study guide
Key terms
- CAPI (Cryptographic Application Programming Interface)
- The original Windows cryptographic API (1998-onward). Plug-in unit was the CSP. Superseded by CNG starting in 2007 but still present for backwards compatibility.
- CNG (Cryptography API: Next Generation)
- The Windows cryptographic API since Vista (2007). Two-tier split: BCrypt for primitives, NCrypt for key storage. The basis for all modern Windows cryptography.
- CSP (Cryptographic Service Provider)
- The CAPI-era plug-in unit. Monolithic DLL bundling algorithms, key storage, and FIPS posture.
- KSP (Key Storage Provider)
- The CNG-era plug-in unit for persistent key storage. Microsoft ships four; third parties ship many more. Selected by name string passed to NCryptOpenStorageProvider.
- Microsoft Software Key Storage Provider
- The default KSP. Stores DPAPI-wrapped keys on disk and dispatches operations through the LSA key-isolation process via LRPC.
- Microsoft Platform Crypto Provider
- The TPM-and-Pluton-backed KSP. Keys are generated and used inside the TPM chip; private bits never leave the silicon.
- TPM key attestation
- A three-key chain (EK -> AIK -> application key) that lets a CA verify a key was generated inside a real TPM. Supported by Active Directory Certificate Services since Windows Server 2012 R2.
- FIPS 140
- US federal certification program for cryptographic modules. Validated modules receive a public CMVP certificate. Windows 11's bcryptprimitives.dll holds CMVP certificate #4825, cng.sys holds #4766.
- ML-KEM (FIPS 203)
- Module-Lattice Key Encapsulation Mechanism. The NIST-standardized post-quantum KEM, formerly known as CRYSTALS-Kyber. Shipped in Windows 11 24H2.
- ML-DSA (FIPS 204)
- Module-Lattice Digital Signature Algorithm. The NIST-standardized post-quantum signature scheme, formerly known as CRYSTALS-Dilithium. Shipped in Windows 11 24H2.
- DPAPI-NG
- The CNG-era rebuild of the original Data Protection API. Uses NCrypt protection descriptors to bind protected data to AD principals (users, groups, web credentials) rather than to a single machine.
- SymCrypt
- Microsoft's open-source cryptographic implementation library. The actual workhorse behind BCrypt and NCrypt since Windows 10 version 1703 (2017).