Skip to main content

Command Palette

Search for a command to run...

Post-Quantum Cryptography in .NET 10

Preparing for the Keys That Outlive Your Algorithms

Updated
22 min readView as Markdown
Post-Quantum Cryptography in .NET 10
P
Senior Software Engineer specialising in cloud architecture, distributed systems, and modern .NET development, with over two decades of experience designing and delivering enterprise platforms in financial, insurance, and high-scale commercial environments. My focus is on building systems that are reliable, scalable, and maintainable over the long term. I’ve led modernisation initiatives moving legacy platforms to cloud-native Azure architectures, designed high-throughput streaming solutions to eliminate performance bottlenecks, and implemented secure microservices environments using container-based deployment models and event-driven integration patterns. From an architecture perspective, I have strong practical experience applying approaches such as Vertical Slice Architecture, Domain-Driven Design, Clean Architecture, and Hexagonal Architecture. I’m particularly interested in modular system design that balances delivery speed with long-term sustainability, and I enjoy solving complex problems involving distributed workflows, performance optimisation, and system reliability. I enjoy mentoring engineers, contributing to architectural decisions, and helping teams simplify complex systems into clear, maintainable designs. I’m always open to connecting with other engineers, architects, and technology leaders working on modern cloud and distributed system challenges.

Most applications do not have a quantum-computing problem today. They may already have a cryptographic migration problem.

An application that stores signed contracts, insurance documents, medical records, software packages or encrypted archives is making assumptions about how long its cryptography will remain trustworthy. Those assumptions are often hidden behind a few calls to RSA, ECDsa or SslStream. The code works, the tests pass and the data is protected, but there may be no record of which algorithm protected which item, no way to introduce another algorithm and no plan for verifying old signatures after the current key infrastructure changes.

.NET 10 adds first-class APIs for the post-quantum algorithms ML-KEM, ML-DSA and SLH-DSA, together with support for composite ML-DSA signatures. These algorithms are designed around a future in which a sufficiently capable quantum computer could break the public key systems that much of today's infrastructure uses. The useful part for most C# developers is not the underlying lattice mathematics. It is understanding what each algorithm does, where it fits into an application and how to make cryptographic choices replaceable before a forced migration arrives.

This article builds a small document protection flow in .NET 10. It signs documents with ML-DSA and uses ML-KEM with AES-GCM to encrypt them. It also shows why crypto agility, key ownership and envelope versioning are just as important as choosing a new algorithm.

What quantum computing threatens

Most public key cryptography in current applications depends on mathematical problems that are impractical for conventional computers to solve at the required scale. RSA relies on the difficulty of factoring large integers. Elliptic-curve cryptography relies on the difficulty of the elliptic-curve discrete logarithm problem. A sufficiently capable cryptographically relevant quantum computer running Shor's algorithm would change those assumptions. RSA, ECDSA and ECDH would no longer provide the security properties on which we currently depend.

That does not mean every cryptographic primitive suddenly becomes useless. Symmetric encryption and hashing are affected differently. AES and the SHA-2 and SHA-3 families are not exposed to Shor's algorithm in the same way. Grover's algorithm provides a theoretical quadratic speed up against brute force search, which can be addressed by using suitable symmetric key sizes. AES-256 therefore remains part of post quantum designs.

The boundary is broadly this:

Purpose Common current choice Post-quantum direction
Establish a shared secret RSA key transport or ECDH ML-KEM
Create a digital signature RSA, ECDSA or EdDSA ML-DSA or SLH-DSA
Encrypt application data AES-GCM AES-GCM with an appropriately sized key
Hash data SHA-256 or SHA-3 Continue using approved hash functions

Post quantum migration is primarily a public key cryptography problem. Replacing every use of AES with a post quantum algorithm would misunderstand both the threat and the new APIs.

Why waiting for a quantum computer is too late

The immediate concern is often described as harvest now, decrypt later. An attacker can collect encrypted traffic or stored ciphertext today without being able to read it. If the key agreement or key transport used to protect the encryption key later becomes breakable, the attacker may be able to recover the historical plaintext. That risk depends on the lifetime of the information. A short lived access token that expires in minutes has a different exposure from a confidential acquisition document that must remain secret for twenty years. Migration lead time matters as well. Large organisations do not replace certificate infrastructure, hardware security modules, protocols, partner integrations and archived formats in a weekend.

This is why post quantum preparation starts before the hardware exists. For many teams, the first useful step is not enabling ML-KEM in production. It is discovering where RSA and elliptic curve algorithms are used, recording the algorithm with every protected payload and removing assumptions that a key or signature will always have one fixed format.

The algorithms available in .NET 10

.NET 10 exposes four relevant algorithm families through System.Security.Cryptography.

ML-KEM

MLKem implements the Module Lattice Based Key Encapsulation Mechanism standard defined by NIST FIPS 203. It is derived from CRYSTALS Kyber. It lets two parties establish the same shared secret over a public channel. One party has an encapsulation key, which can be distributed publicly, and a decapsulation key, which must remain private. A sender uses the public key to produce a ciphertext and a shared secret. The recipient uses the private key to decapsulate that ciphertext and recover the same shared secret. It does not encrypt the document. The shared secret is used as input to a symmetric encryption operation.

ML-DSA

MLDsa implements the Module Lattice Based Digital Signature Algorithm standard defined by NIST FIPS 204. It is derived from CRYSTALS-Dilithium. It creates and verifies digital signatures. A signature proves that the holder of the private key signed a specific sequence of bytes and that those bytes have not been altered. It doesnt make the signed data confidential.

SLH-DSA

SlhDsa implements the Stateless Hash Based Digital Signature Algorithm from NIST FIPS 205, derived from SPHINCS+. Its security is based on hash functions rather than the lattice assumptions used by ML-DSA. That makes it a useful alternative rather than a drop-in performance equivalent. SLH-DSA signatures are much larger and the performance characteristics differ. In .NET 10 the API is marked experimental, so it deserves evaluation rather than casual production adoption.

Composite ML-DSA

CompositeMLDsa combines an ML-DSA signature with a traditional signature algorithm. The verifier requires the composite signature to validate as a whole. This supports a transitional model in which security does not depend solely on either the classical or the post-quantum algorithm. It can preserve compatibility with existing trust decisions while adding protection against a future quantum attack. Composite ML-DSA in .NET 10 follows an evolving IETF draft and is marked experimental. Microsoft explicitly advises against depending on the current class in production because the underlying specification can change. It is worth understanding as a migration pattern, but it should not be treated as a permanently stable storage format yet.

The names are similar enough to invite confusion. The simplest distinction is that ML-DSA signs, while ML-KEM agrees a secret.

Platform support is part of the design

The .NET APIs rely on cryptographic implementations supplied by the underlying platform. Deploying on .NET 10 does not by itself guarantee that every post quantum algorithm is available. Linux requires OpenSSL 3.5 or later for the built-in implementations. Windows support depends on a version of Windows CNG that provides the corresponding algorithms. Apple platforms, Android and browser based .NET do not currently provide the same support.

Every new algorithm type exposes an IsSupported property. Check it when the application starts, not halfway through processing an important request.

using System.Security.Cryptography;

var capabilities = new
{
    MlKem = MLKem.IsSupported,
    MlDsa = MLDsa.IsSupported,
    SlhDsa = SlhDsa.IsSupported,
    CompositeMlDsa = CompositeMLDsa.IsSupported
};

Console.WriteLine(
    "ML-KEM: {0}, ML-DSA: {1}, SLH-DSA: {2}, Composite ML-DSA: {3}",
    capabilities.MlKem,
    capabilities.MlDsa,
    capabilities.SlhDsa,
    capabilities.CompositeMlDsa);

A production service should convert this into a startup health check. If a workload is configured to require ML-DSA, silently falling back to RSA changes the security contract. Failing startup with a clear diagnostic is usually safer than discovering the missing capability after accepting a document.

using System.Security.Cryptography;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public sealed class PostQuantumCryptographyHealthCheck
    : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken stopToken = default)
    {
        if (!MLDsa.IsSupported || !MLKem.IsSupported)
        {
            return Task.FromResult(
                HealthCheckResult.Unhealthy(
                    "The configured platform does not support ML-DSA and ML-KEM."));
        }

        return Task.FromResult(
            HealthCheckResult.Healthy(
                "Required post-quantum algorithms are available."));
    }
}

The registration is conventional:

builder.Services
    .AddHealthChecks()
    .AddCheck<PostQuantumCryptographyHealthCheck>("post-quantum-cryptography");

This also makes platform support visible to deployment automation. A container image built against an older OpenSSL version can be rejected before it begins serving traffic.

Signing a document with ML-DSA

The first example signs a document using ML-DSA-65. The parameter sets MLDsa44, MLDsa65 and MLDsa87 represent increasing security categories, with corresponding increases in public key, private key and signature sizes.

The private key signs the document. The public key is enough to verify it.

using System.Security.Cryptography;
using System.Text;

if (!MLDsa.IsSupported)
{
    throw new PlatformNotSupportedException(
        "ML-DSA is not supported on this platform.");
}

byte[] document = Encoding.UTF8.GetBytes(
    "Policy 4821 is approved with a limit of EUR 5,000,000.");

byte[] context = Encoding.UTF8.GetBytes("policy-document:v1");

using MLDsa signingKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);

byte[] signature = signingKey.SignData(document, context);
string publicKeyPem = signingKey.ExportSubjectPublicKeyInfoPem();

using MLDsa verificationKey = MLDsa.ImportFromPem(publicKeyPem);

bool isValid = verificationKey.VerifyData(
    document,
    signature,
    context);

Console.WriteLine($"Signature valid: {isValid}");

The context is not decorative metadata. ML-DSA allows a context value of up to 255 bytes to separate signatures created for different purposes. Using policy-document:v1 means that the same key can sign bytes in more than one protocol without every signature being treated as interchangeable.

The verifier must use the same context:

byte[] wrongContext = Encoding.UTF8.GetBytes("software-package:v1");

bool isValidForWrongPurpose = verificationKey.VerifyData(
    document,
    signature,
    wrongContext);

Console.WriteLine(isValidForWrongPurpose); 

Domain separation should be stable, documented and versioned. It should not contain per-request information such as a correlation ID. The document bytes already provide the changing input.

Sign exact bytes, not an abstract object

A signature applies to bytes. Serialising an object twice does not necessarily guarantee the same byte representation if formatting, property ordering, normalisation or serialiser settings change. For JSON documents, define a canonical representation or sign the immutable payload exactly as stored. A practical envelope can record the media type and a hash alongside the signature:

public sealed record DocumentSignatureEnvelope(
    int FormatVersion,
    string Algorithm,
    string KeyId,
    string Context,
    string MediaType,
    byte[] ContentHash,
    byte[] Signature,
    DateTimeOffset SignedAtUtc);

The Algorithm should contain a specific identifier such as ML-DSA-65, not merely PQC. The key ID identifies the private key that created the signature without placing the private key in the envelope. The format version describes the envelope structure and canonicalisation rules, independently of the cryptographic algorithm.

In a real service, the private key should normally remain inside a managed key provider, hardware backed store or isolated signing service. Exporting it into an application configuration file would undo much of the value of selecting a stronger algorithm.

ML-KEM establishes a secret, AES-GCM encrypts the data

The next example protects a document for a recipient. The recipient creates an ML-KEM key pair and distributes the encapsulation key. The sender uses that public key to create two outputs:

  1. an ML-KEM ciphertext that can safely travel with the encrypted document;

  2. a shared secret known to the sender.

The recipient combines the ciphertext with the private decapsulation key to recover the same shared secret. Both sides then derive an AES key from it.

The envelope needs all the non-secret values required by the recipient:

public sealed record EncryptedDocumentEnvelope(
    int FormatVersion,
    string KeyEncapsulationAlgorithm,
    string ContentEncryptionAlgorithm,
    string RecipientKeyId,
    byte[] EncapsulatedSecret,
    byte[] Salt,
    byte[] Nonce,
    byte[] Tag,
    byte[] Ciphertext);

The shared secret and derived AES key are deliberately absent.

Creating an ML-KEM recipient key

This example uses ML-KEM-768, the middle parameter set:

using System.Security.Cryptography;

if (!MLKem.IsSupported)
{
    throw new PlatformNotSupportedException(
        "ML-KEM is not supported on this platform.");
}

using MLKem recipientKey = MLKem.GenerateKey(
    MLKemAlgorithm.MLKem768);

byte[] publicEncapsulationKey =
    recipientKey.ExportEncapsulationKey();

byte[] privateDecapsulationKey =
    recipientKey.ExportDecapsulationKey();

The two byte arrays have very different handling requirements. The encapsulation key can be published. The decapsulation key must be protected like any other high value private key and should not normally be exported from its secure provider. The export is shown here to make the roles visible. It is not a recommendation to store the private key beside the application.

Encrypting for the recipient

The sender imports only the public encapsulation key:

using System.Security.Cryptography;

public static partial class PostQuantumDocumentEncryption
{
    private const int AesKeySize = 32;
    private const int SaltSize = 32;
    private const int NonceSize = 12;
    private const int TagSize = 16;

    public static EncryptedDocumentEnvelope Encrypt(
        ReadOnlySpan<byte> plaintext,
        ReadOnlySpan<byte> recipientEncapsulationKey,
        string recipientKeyId)
    {
        using MLKem recipient = MLKem.ImportEncapsulationKey(
            MLKemAlgorithm.MLKem768,
            recipientEncapsulationKey);

        recipient.Encapsulate(
            out byte[] encapsulatedSecret,
            out byte[] sharedSecret);

        byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
        byte[] encryptionKey = HKDF.DeriveKey(
            HashAlgorithmName.SHA256,
            sharedSecret,
            AesKeySize,
            salt,
            "document-encryption:v1"u8);

        byte[] nonce = RandomNumberGenerator.GetBytes(NonceSize);
        byte[] ciphertext = new byte[plaintext.Length];
        byte[] tag = new byte[TagSize];

        byte[] associatedData =
            "document-envelope:v1"u8.ToArray();

        try
        {
            using var aes = new AesGcm(
                encryptionKey,
                TagSize);

            aes.Encrypt(
                nonce,
                plaintext,
                ciphertext,
                tag,
                associatedData);

            return new EncryptedDocumentEnvelope(
                FormatVersion: 1,
                KeyEncapsulationAlgorithm: "ML-KEM-768",
                ContentEncryptionAlgorithm: "AES-256-GCM",
                RecipientKeyId: recipientKeyId,
                EncapsulatedSecret: encapsulatedSecret,
                Salt: salt,
                Nonce: nonce,
                Tag: tag,
                Ciphertext: ciphertext);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(sharedSecret);
            CryptographicOperations.ZeroMemory(encryptionKey);
        }
    }
}

The output from ML-KEM is passed through HKDF-SHA-256 before it becomes an AES key. This gives the derived key an explicit purpose, incorporates a random salt and avoids binding the application directly to the raw representation of the shared secret. AES-GCM provides authenticated encryption. It creates a tag that detects changes to the ciphertext and authenticates the associated data. A random 96-bit nonce is generated for every encryption. Reusing a nonce with the same AES key would seriously damage GCM's security. Here each encapsulation creates a new shared secret and therefore a new derived key, but generating a fresh nonce remains a clear and safe rule.

Decrypting with the recipient's private key

The recipient imports the private decapsulation key and reconstructs the AES key:

using System.Security.Cryptography;

public static partial class PostQuantumDocumentEncryption
{
    public static byte[] Decrypt(
        EncryptedDocumentEnvelope envelope,
        ReadOnlySpan<byte> recipientDecapsulationKey)
    {
        if (envelope.FormatVersion != 1 ||
            envelope.KeyEncapsulationAlgorithm != "ML-KEM-768" ||
            envelope.ContentEncryptionAlgorithm != "AES-256-GCM")
        {
            throw new CryptographicException(
                "The encrypted document format is not supported.");
        }

        using MLKem recipient = MLKem.ImportDecapsulationKey(
            MLKemAlgorithm.MLKem768,
            recipientDecapsulationKey);

        byte[] sharedSecret = recipient.Decapsulate(
            envelope.EncapsulatedSecret);

        byte[] encryptionKey = HKDF.DeriveKey(
            HashAlgorithmName.SHA256,
            sharedSecret,
            AesKeySize,
            envelope.Salt,
            "document-encryption:v1"u8);

        byte[] plaintext = new byte[envelope.Ciphertext.Length];
        byte[] associatedData =
            "document-envelope:v1"u8.ToArray();

        try
        {
            using var aes = new AesGcm(
                encryptionKey,
                TagSize);

            aes.Decrypt(
                envelope.Nonce,
                envelope.Ciphertext,
                envelope.Tag,
                plaintext,
                associatedData);

            return plaintext;
        }
        catch
        {
            CryptographicOperations.ZeroMemory(plaintext);
            throw;
        }
        finally
        {
            CryptographicOperations.ZeroMemory(sharedSecret);
            CryptographicOperations.ZeroMemory(encryptionKey);
        }
    }
}

The method rejects unknown versions and algorithms before performing cryptographic work. In a larger system, dispatching to version specific handlers is cleaner than growing this method into a long series of conditionals. The example also clears secret material where the managed buffers are available. This reduces how long sensitive values remain in memory, although it does not replace process isolation or secure key management.

Encryption and signing solve different problems

An encrypted document is not automatically attributable to a sender. Anyone with the recipient's public ML-KEM encapsulation key can create a ciphertext for that recipient. A signed document is not confidential. Anyone who obtains it can read it, even if they cannot forge its signature.

Many document workflows need both:

A sensible order is usually sign first and then encrypt the document together with its signature envelope. The recipient decrypts the package and then verifies the signature. This keeps signer metadata private and ensures the signature remains attached to the exact document representation. The protocol should define the order rather than leaving it to individual callers. “Uses ML-DSA and ML-KEM” is not a complete interoperability specification.

Crypto-agility should be designed into the envelope

Replacing RSA.Create() with MLDsa.GenerateKey() does not make an application crypto-agile. The application is agile only if it can introduce another approved algorithm without rewriting every consumer or losing the ability to process historical data.

That requires explicit metadata and stable boundaries:

public enum SignatureAlgorithm
{
    RsaPssSha256,
    EcdsaP256Sha256,
    MlDsa65
}

public sealed record SignatureRequest(
    string KeyId,
    SignatureAlgorithm Algorithm,
    string Purpose,
    ReadOnlyMemory<byte> Content);

public sealed record SignatureResult(
    int EnvelopeVersion,
    string KeyId,
    SignatureAlgorithm Algorithm,
    string Purpose,
    byte[] Signature,
    DateTimeOffset CreatedAtUtc);

public interface IDocumentSignatureService
{
    ValueTask<SignatureResult> SignAsync(
        SignatureRequest request,
        CancellationToken stopToken);

    ValueTask<bool> VerifyAsync(
        ReadOnlyMemory<byte> content,
        SignatureResult signature,
        CancellationToken stopToken);
}

Callers request an approved algorithm identifier and purpose. They do not receive a private key or choose arbitrary curve names, padding modes and hash functions.

The implementation can route to a key provider by key ID:

Old verification paths stay available after new signing has moved to ML-DSA. Disabling the ability to create new RSA signatures is different from deleting the ability to verify existing ones.

Keep policy out of individual callers

An application should not let every endpoint decide which cryptography is acceptable. A policy layer can select the active signing key and algorithm according to document type, retention period and partner compatibility.

public sealed record CryptographyPolicy(
    string PolicyName,
    int Version,
    SignatureAlgorithm SigningAlgorithm,
    string SigningKeyId,
    TimeSpan RequiredConfidentialityLifetime);

public interface ICryptographyPolicyProvider
{
    CryptographyPolicy GetRequiredPolicy(
        string documentType);
}

Version the policy and record the applied version in audit data. Otherwise, six months later it may be impossible to explain why one document used ML-DSA and another still used RSA. Policy should come from controlled, reviewed configuration rather than arbitrary request input. Supporting several algorithms is useful. Allowing a caller to downgrade protection is not.

Key IDs matter more than embedding public keys everywhere

The earlier signature example exported the public key to make verification easy to follow. Production envelopes should normally reference a trusted key by ID. If an attacker can replace both the document and an embedded public key, a mathematically valid signature proves only that the attacker signed the replacement with the attacker's own key. Trust comes from binding the key ID to an authorised identity through a trusted key registry, certificate chain or equivalent mechanism.

A key record needs lifecycle information:

public sealed record VerificationKeyRecord(
    string KeyId,
    string Owner,
    SignatureAlgorithm Algorithm,
    byte[] SubjectPublicKeyInfo,
    DateTimeOffset ValidFromUtc,
    DateTimeOffset? ValidUntilUtc,
    DateTimeOffset? RevokedAtUtc,
    string Status);

Rotation creates a new key ID. Previously signed documents continue to reference the old verification key. Revocation does not necessarily mean deleting the public key; it means applying a documented decision about signatures created before and after the revocation time. Private keys need stricter controls. They should be non exportable where the platform and provider allow it, and signing operations should be auditable. The example's ExportDecapsulationKey() call is helpful for learning the API, but an enterprise implementation should prefer a provider that performs decapsulation without returning private key material to application code.

Key and signature sizes are no longer background details

Post quantum keys and signatures are significantly larger than familiar elliptic-curve equivalents. For ML-DSA, Microsoft documents the following raw sizes:

Parameter set Private key Public key Signature
ML-DSA-44 2,560 bytes 1,312 bytes 2,420 bytes
ML-DSA-65 4,032 bytes 1,952 bytes 3,309 bytes
ML-DSA-87 4,896 bytes 2,592 bytes 4,627 bytes

That can affect certificate chains, signed tokens, database columns, message size limits, API gateways and network handshakes. A column designed around a 256-byte RSA signature will not quietly accommodate ML-DSA. The effect becomes more noticeable when a message carries several signatures or a certificate chain. Composite signatures increase the size again because they contain classical and post-quantum components.

Before migration, inspect:

  • binary and text column limits;

  • maximum HTTP header and message sizes;

  • queue and event payload limits;

  • certificate storage assumptions;

  • logging that accidentally copies large signatures;

  • APIs that encode signatures as Base64 and expand them further.

This is one of the few places where a short inventory is more useful than an abstract architecture discussion. Size limits are concrete, and they tend to fail late if nobody measures them.

Certificates and TLS need both ends to participate

.NET 10 integrates post quantum signature keys with APIs including CertificateRequest, X509Certificate2, SignedCms and COSE signing. ML-DSA certificates can also participate in TLS through SslStream where the operating system, TLS version and remote peer all support the required algorithms. That final condition is important. Application support does not make a protocol interoperable by itself.

ML-KEM certificates are different. A KEM key cannot self sign, and existing TLS integration does not make an ML-KEM subject key a simple replacement for every certificate use. Avoid assuming that because ML-KEM can export a SubjectPublicKeyInfo value it can automatically replace the key exchange in an existing HTTPS deployment. Protocol support must be evaluated at the TLS stack and peer level, not inferred from the presence of a .NET class.

Hybrid migration without pretending experimental formats are permanent

A hybrid strategy combines a traditional algorithm with a post quantum one during transition. It protects against two uncertainties:

  1. the traditional algorithm may eventually be broken by a quantum computer;

  2. a newly standardised post quantum algorithm or implementation may later reveal a weakness.

Composite ML-DSA represents this idea within one signature format, but its current IETF specification is still evolving and the .NET 10 API is experimental. Persisting experimental composite signatures as thirty-year records creates its own migration problem. An application level dual signature envelope is easier to version while standards settle:

public sealed record SignatureEntry(
    string Algorithm,
    string KeyId,
    byte[] Signature);

public sealed record HybridSignatureEnvelope(
    int FormatVersion,
    string Purpose,
    IReadOnlyList<SignatureEntry> Signatures,
    DateTimeOffset SignedAtUtc);

The verification policy can require both an RSA-PSS signature and an ML-DSA signature:

bool isAccepted =
    rsaSignatureIsValid &&
    mlDsaSignatureIsValid;

The operator must define the rule explicitly. Accepting either signature provides compatibility, but it also permits the weaker path to determine acceptance. Requiring both offers a different security property and different failure behaviour. An application level envelope is not automatically superior to a standard composite format. It is simply easier to control and version while the standard remains experimental. Once ecosystem support stabilises, using the standard representation can improve interoperability.

Testing more than the happy path

Cryptographic tests should prove protocol behaviour, not attempt to prove the underlying algorithm's mathematics. The platform implementation already has algorithm test vectors. Application tests should cover the way keys, purposes, versions and envelopes are used.

For an ML-DSA document workflow, tests should confirm that an unchanged document verifies successfully while even a single byte modification invalidates the signature. Verification should also fail when the correct signature is presented with a different purpose context. The workflow must reject unknown algorithms and unsupported envelope versions, while retaining the ability to verify historical documents after keys have been rotated. Revoked keys should be handled according to the documented time based policy, and the application should fail during startup if the host platform doesn’t provide a required cryptographic capability.

The encryption path should test tampering with the ciphertext, nonce, tag, salt and encapsulated secret. Each change must fail decryption rather than return corrupted plaintext.

[Fact]
public void Modified_ciphertext_is_rejected()
{
    using MLKem recipient =
        MLKem.GenerateKey(MLKemAlgorithm.MLKem768);

    byte[] plaintext =
        "confidential document"u8.ToArray();

    EncryptedDocumentEnvelope envelope =
        PostQuantumDocumentEncryption.Encrypt(
            plaintext,
            recipient.ExportEncapsulationKey(),
            "recipient-key-2026-01");

    envelope.Ciphertext[0] ^= 0x01;

    Assert.Throws<CryptographicException>(() =>
        PostQuantumDocumentEncryption.Decrypt(
            envelope,
            recipient.ExportDecapsulationKey()));
}

Production readiness also needs known answer or interoperability tests against keys and payloads created outside the application. Two systems agreeing with themselves is weaker evidence than two independent implementations exchanging valid signatures and encapsulated secrets.

A practical migration plan

The first phase is discovery. Search for direct uses of RSA, ECDsa, ECDiffieHellman, certificate loading, CMS signing, JWT signing, custom encryption and any database fields that store keys or signatures. Include infrastructure such as TLS termination and message brokers, because not all cryptography lives in C#.

The second phase is classification. Record why each algorithm is used and how long the protected information must remain confidential or verifiable. A signature used to validate a deployment package for one week has different retention requirements from a signed legal agreement.

The third phase is abstraction and metadata. Introduce versioned envelopes, stable key IDs and centrally controlled policy. Preserve historical verification while preventing new code from reaching directly for a preferred algorithm.

The fourth phase is experimentation. Run .NET 10 proof of concept workloads on the exact operating systems and container images used in production. Measure key sizes, signature sizes, throughput, startup diagnostics and interoperability with partner systems.

The final phase is controlled rollout. New documents can adopt ML-DSA or a hybrid signature policy first, while old signatures remain verifiable. ML-KEM should be introduced only where both sides of the protocol understand the envelope and key lifecycle.

This sequence avoids turning a cryptographic upgrade into a flag day. It also produces value before PQC is enabled: centralised key ownership, explicit algorithms and testable versioning improve the current system.

What not to build yourself

The examples compose standard .NET cryptographic primitives into an application protocol. They do not implement ML-KEM, ML-DSA, AES-GCM, HKDF or random-number generation. Do not translate an academic description of a lattice algorithm into production C#. Do not replace RandomNumberGenerator with Random. Do not create a home grown key derivation function. Do not invent an unversioned binary layout and assume every consumer will interpret it identically.

Theres still application design work to do, but it sits around the cryptographic primitives. The application must define the exact bytes being protected and how content is canonicalised, use stable purpose contexts, and control which algorithms are permitted. It also needs a clear approach to key management and rotation, versioned envelope formats, interoperability testing and the continued verification of historical records after algorithms or keys have changed.

The real preparation is replaceability

.NET 10 makes post quantum cryptography tangible for C# developers. ML-DSA can sign application data. ML-KEM can establish a shared secret that feeds AES-GCM. The platform can export standard key formats, work with certificates and integrate selected algorithms with existing cryptographic APIs. None of that means every application should switch all cryptography immediately. Platform support remains uneven, partner protocols may not understand the algorithms and some migration formats are still experimental. The useful action today is to stop treating RSA, ECDSA or any other algorithm as an invisible permanent detail. Store the algorithm and version with the protected data. Give keys stable identities. Separate signing from verification policy. Keep old verification paths while changing how new records are produced. Test the operating-system cryptography actually present in production.

Quantum resistant algorithms are new. The need to make security mechanisms replaceable is not.