Quantum computing has been a theoretical threat to modern cryptography for decades, but that threat is getting a lot more concrete. NIST finalised three post-quantum cryptography (PQC) standards in 2024 — FIPS 203, 204, and 205 — and .NET 10, which shipped as an LTS release on 2 April 2026, is the first version with built-in support for all three. If you've ever wondered when you'd need to care about quantum-safe cryptography, the answer is now: the APIs are in the box, the standards are ratified, and the migration conversation has already started in regulated industries.
This isn't about ripping out RSA tomorrow. It's about understanding what's available, how the new APIs differ from what you're used to, and where the rough edges are.
The three algorithms
.NET 10 adds three new abstract classes in System.Security.Cryptography, each mapping to a NIST standard:
| Class | Standard | Purpose | Use case |
|---|---|---|---|
MLKem |
FIPS 203 | Key encapsulation | Establishing shared secrets (replaces key exchange) |
MLDsa |
FIPS 204 | Digital signatures | Signing data and verifying authenticity |
SlhDsa |
FIPS 205 | Digital signatures | Stateless hash-based signatures for long-term, conservative use |
There's also a fourth — CompositeMLDsa — which pairs ML-DSA with a classical algorithm (like ECDSA) in a hybrid signature for transitional deployments. We'll focus on the three core algorithms here.
Each algorithm comes with multiple parameter sets that trade off key size, signature size, and security level. You don't pick these based on vibes — the parameter set determines whether your keys are 1 KB or 5 KB, and in a world of constrained IoT devices or high-throughput APIs, that matters.
ML-DSA: signing and verifying data
ML-DSA (Module-Lattice-Based Digital Signature Algorithm) is the workhorse of the three. If you need to sign and verify data — API request payloads, licence tokens, software packages — this is the one you'll reach for first.
The API follows a pattern that will feel familiar if you've used RSA or ECDsa, but with a key difference: instances represent a key or key pair, not an algorithm. There's no KeySize property to wrestle with. You generate or import, then sign or verify.
using System.Security.Cryptography;
using System.Text;
public class DocumentSigner
{
public (byte[] Signature, byte[] PublicKey) SignDocument(string content)
{
if (!MLDsa.IsSupported)
throw new PlatformNotSupportedException(
"ML-DSA is not supported on this platform.");
byte[] data = Encoding.UTF8.GetBytes(content);
using MLDsa key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
byte[] signature = key.SignData(data);
byte[] publicKey = key.ExportMLDsaPublicKey();
return (signature, publicKey);
}
public bool VerifyDocument(string content, byte[] signature, byte[] publicKey)
{
byte[] data = Encoding.UTF8.GetBytes(content);
using MLDsa key = MLDsa.ImportMLDsaPublicKey(
MLDsaAlgorithm.MLDsa65, publicKey);
return key.VerifyData(data, signature);
}
}
Choosing a parameter set
MLDsaAlgorithm offers three parameter sets:
| Parameter set | Private key | Public key | Signature | Security level |
|---|---|---|---|---|
MLDsa44 |
2,560 bytes | 1,312 bytes | 2,420 bytes | NIST Level 2 (roughly AES-128) |
MLDsa65 |
4,032 bytes | 1,952 bytes | 3,309 bytes | NIST Level 3 (roughly AES-192) |
MLDsa87 |
4,896 bytes | 2,592 bytes | 4,627 bytes | NIST Level 5 (roughly AES-256) |
Compare that to ECDSA P-256: a 32-byte private key, 64-byte public key, and ~72-byte signature. Post-quantum keys are dramatically larger. ML-DSA-65 is the recommended default for most applications — it provides a solid security margin without the size penalty of ML-DSA-87.
Context strings
ML-DSA supports an optional context parameter on SignData and VerifyData. Context strings are domain separation labels — they ensure a signature produced for one purpose can't be replayed in another:
byte[] context = Encoding.UTF8.GetBytes("invoice-signing-v1");
key.SignData(data, destination, context);
key.VerifyData(data, signature, context);
// TIP
Use context strings whenever the same key might sign data for different purposes. They're cheap to add and prevent a whole class of cross-protocol attacks.
ML-KEM: key encapsulation
ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) replaces traditional key exchange. Rather than Diffie-Hellman or ECDH, ML-KEM uses an encapsulate/decapsulate pattern: one party generates a shared secret and a ciphertext from a public key, and the other party recovers the same shared secret from the ciphertext using their private key.
using System.Security.Cryptography;
public class SecureChannelSetup
{
public static (byte[] SharedSecret, byte[] Ciphertext, byte[] PublicKey)
InitiateKeyExchange()
{
if (!MLKem.IsSupported)
throw new PlatformNotSupportedException(
"ML-KEM is not supported on this platform.");
// Alice generates a key pair
using MLKem aliceKey = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
byte[] publicKey = aliceKey.ExportEncapsulationKey();
// Bob encapsulates a shared secret using Alice's public key
using MLKem bobKey = MLKem.ImportEncapsulationKey(
MLKemAlgorithm.MLKem768, publicKey);
bobKey.Encapsulate(out byte[] ciphertext, out byte[] bobSecret);
// Alice decapsulates to get the same shared secret
byte[] aliceSecret = aliceKey.Decapsulate(ciphertext);
// aliceSecret and bobSecret are identical
return (aliceSecret, ciphertext, publicKey);
}
}
ML-KEM-768 is the recommended parameter set for general use. ML-KEM-512 is faster but offers a lower security margin, and ML-KEM-1024 is for high-security environments.
// WARNING
ML-KEM is a key encapsulation mechanism, not a key agreement protocol. The shared secret is generated by the encapsulating party, not negotiated. Don't try to use it like ECDH — the flow is fundamentally different.
SLH-DSA: the conservative option
SLH-DSA (Stateless Hash-Based Digital Signature Algorithm) is a different beast. Where ML-DSA is based on lattice mathematics, SLH-DSA is built entirely on hash functions. That makes it mathematically simpler and more conservative — the security proof rests on well-understood hash function properties rather than lattice assumptions.
The trade-off is performance. SLH-DSA signatures are significantly larger and slower to generate than ML-DSA signatures. Use it when you need the strongest possible assurance and can tolerate the overhead — think root CA certificates, firmware signing, or long-lived document signatures that must remain valid for decades.
using System.Security.Cryptography;
public class FirmwareSigner
{
public byte[] SignFirmware(byte[] firmwareImage)
{
if (!SlhDsa.IsSupported)
throw new PlatformNotSupportedException(
"SLH-DSA is not supported on this platform.");
using SlhDsa key = SlhDsa.GenerateKey(
SlhDsaAlgorithm.SlhDsaSha2_128s);
return key.SignData(firmwareImage);
}
}
// NOTE
SLH-DSA has many parameter sets — varying by hash function (SHA-2 or SHAKE), security level (128, 192, 256), and performance profile (s for small signatures, f for fast signing). Choose based on your constraints, not habit.
Key export and interoperability
All three classes support standard key formats: PKCS#8 for private keys, X.509 SubjectPublicKeyInfo for public keys, and PEM encoding for both. This means they integrate with the same certificate and key management infrastructure you already have.
using System.Security.Cryptography;
public class KeyManager
{
public void ExportAndReimport()
{
using MLDsa original = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
// Export as PEM — easy to store in config or secrets management
string publicPem = original.ExportSubjectPublicKeyInfoPem();
string privatePem = original.ExportPkcs8PrivateKeyPem();
// Re-import from PEM
using MLDsa reimported = MLDsa.ImportFromPem(privatePem);
// Export with password protection
byte[] encrypted = original.ExportEncryptedPkcs8PrivateKey(
"my-secure-passphrase",
new PbeParameters(
PbeEncryptionAlgorithm.Aes256Cbc,
HashAlgorithmName.SHA256,
iterationCount: 100_000));
}
}
The PQC keys also work with CertificateRequest for generating self-signed certificates, SignedCms for CMS/PKCS#7 signatures, and SslStream for TLS — though TLS support depends on the underlying OS and TLS library supporting PQC cipher suites.
Platform requirements
This is where it gets tricky. The .NET PQC classes are abstract — they delegate to the operating system's cryptographic library. That means platform support is uneven:
| Platform | Provider | Requirement |
|---|---|---|
| Windows | CNG (Cryptography API: Next Generation) | Windows 11 / Server 2025 with PQC update |
| Linux | OpenSSL | OpenSSL 3.5 or newer |
| macOS | OpenSSL (via Homebrew) | macOS native crypto doesn't support PQC yet |
Each class has a static IsSupported property that tells you at runtime whether the underlying OS can handle it. Always check this before attempting to generate keys or verify signatures — a PlatformNotSupportedException in production is not the way to discover your container's base image ships OpenSSL 3.2.
if (!MLDsa.IsSupported)
{
// Fall back to classical signature, log a warning,
// or fail loudly — depending on your security posture
}
There are also platform-specific derived classes for interop: MLDsaCng and MLKemCng on Windows, and MLDsaOpenSsl and MLKemOpenSsl on Linux. Use the base classes unless you need direct access to CNG key handles or OpenSSL EVP_PKEY pointers.
Experimental status
Not everything is fully stable yet. Here's the current state in .NET 10:
MLDsa: Stable. Not marked as[Experimental].MLKem: The class itself is marked[Experimental("SYSLIB5006")]. Some individual methods carry the experimental attribute even onMLDsa.SlhDsa: Marked[Experimental("SYSLIB5006")].CompositeMLDsa: Marked[Experimental("SYSLIB5006")].
To use experimental APIs, you need to suppress the diagnostic in your project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<NoWarn>$(NoWarn);SYSLIB5006</NoWarn>
</PropertyGroup>
</Project>
// IMPORTANT
The [Experimental] attribute means the API surface may change in future releases. ML-DSA is stable enough for prototyping and internal tooling. For production systems with long-term key commitments, track the dotnet/runtime repository for stabilisation updates.
Common pitfalls
Assuming key sizes are similar to classical algorithms. An ML-DSA-65 public key is 1,952 bytes — nearly 31 times larger than an ECDSA P-256 public key. If you're embedding keys in JWTs, storing them in databases, or transmitting them over constrained networks, size matters. Plan your storage and wire formats accordingly.
Ignoring platform support at deployment time. Your development machine running Windows 11 might support ML-DSA, but your Alpine Linux container running OpenSSL 3.2 won't. Test IsSupported in your CI pipeline against your actual deployment targets, not just your dev box.
Using SLH-DSA where ML-DSA would do. SLH-DSA is the conservative choice, but it's also dramatically slower and produces larger signatures. Unless you have a specific reason to prefer hash-based signatures (extremely long-lived keys, regulatory requirements), ML-DSA is the better default.
Forgetting that ML-KEM is not key agreement. If you're used to ECDH, the mental model is different. ML-KEM doesn't produce a shared secret from two public keys — one party encapsulates, the other decapsulates. Trying to bolt it into a DH-shaped protocol will lead to incorrect implementations.
Not planning for hybrid deployments. During the transition period, many systems will need to support both classical and post-quantum algorithms. CompositeMLDsa exists for exactly this purpose — it pairs ML-DSA with a classical algorithm so that a signature is valid only if both components verify. Don't skip the hybrid step if your counterparties aren't ready for pure PQC.
Summary
- .NET 10 ships with built-in support for ML-KEM (FIPS 203), ML-DSA (FIPS 204), and SLH-DSA (FIPS 205) via
System.Security.Cryptography. - ML-DSA is the general-purpose digital signature algorithm — use
MLDsa65as your default parameter set. - ML-KEM replaces key exchange with an encapsulate/decapsulate pattern — the mental model is different from ECDH.
- SLH-DSA is the conservative, hash-based option for long-lived or high-assurance signatures.
- Platform support depends on the OS cryptographic library — check
IsSupportedbefore use and test against your deployment targets. - Key and signature sizes are dramatically larger than classical algorithms — plan storage, transport, and wire formats accordingly.
- Some APIs are still marked
[Experimental]— track stabilisation in the dotnet/runtime repository before committing to production use with long-lived keys.