ML-KEM and ML-DSA Post-Quantum Cryptography in Windows

Β· 1082 words Β· 6 minutes to read

Following up on my recent posts about ML-KEM and ML-DSA post-quantum cryptography in .NET using BouncyCastle.NET, I wanted to share an interesting development on the Windows side. Microsoft has recently announced post-quantum cryptography support in Windows through their Cryptography API: Next Generation (CNG) libraries.

This development provides an alternative to third-party libraries for quantum-resistant cryptography on Windows systems. The implementation offers direct OS integration and follows standard .NET patterns for cryptographic operations.

Background and standardization efforts πŸ”—

As a quick reminder - in August of 2024, the first three PQC (post-quantum cryptography) standards were officially formalized:

  • πŸ” ML-KEM (Kyber) β†’ Module-Lattice-Based Key-Encapsulation Mechanism Standard β†’ FIPS-203
  • ✍️ ML-DSA (Dilithium) β†’ Module-Lattice-Based Digital Signature Standard β†’ FIPS-204
  • ✍️ SLH-DSA (SPHINCS+) β†’ Stateless Hash-Based Digital Signature Standard β†’ FIPS-205

They provide quantum-resistant alternatives to widely used public-key algorithms such as RSA and ECC (Elliptic Curve Cryptography). The goal is to ensure that our digital communications and data remain secure even in light of potential computational speed-up provided by quantum computers via Shor’s algorithm, which could potentially break traditional cryptographic schemes.

Native Windows Support πŸ”—

Microsoft announced post-quantum cryptography support in Windows in May this year. The implementation is available through Windows Insiders Canary Channel Build 27852 and higher. As this is an early preview implementation, it’s intended for experimentation and testing rather than production use.

The implementation includes the same ML-KEM and ML-DSA algorithms through the Windows CNG libraries. Additionally, .NET 10 Preview 6 introduced native MLKem and MLDsa classes that utilize this Windows support (some small changes came in .NET 10 Preview 7, which is the latest version at the time this post is being written).

This finally provides a viable alternative to the BouncyCastle implementations for .NET developers, with native .NET APIs that follow standard framework patterns.

API Comparison πŸ”—

The BouncyCastle ML-KEM implementation from my previous post used this approach:

// BouncyCastle approach
var random = new SecureRandom();
var keyGenParameters = new MLKemKeyGenerationParameters(random, MLKemParameters.ml_kem_768);
var kyberKeyPairGenerator = new MLKemKeyPairGenerator();
kyberKeyPairGenerator.Init(keyGenParameters);
var aliceKeyPair = kyberKeyPairGenerator.GenerateKeyPair();

The native .NET APIs simplify this to:

// Native .NET approach
using var aliceKeyPair = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);

The native APIs use standard .NET patterns including disposable resources, enum parameters, and integration with the existing cryptography namespace. BouncyCastle maintains the (rather obvious) advantage of having cross-platform compatibility, while the native APIs provide Windows-specific integrations.

Implementation Examples πŸ”—

The following code demonstrates both ML-KEM and ML-DSA using the native Windows APIs. .NET 10 Preview 6 or later is required. This is one-to-one port of the code that I used to implement the BouncyCastle examples in my previous post, so it should serve as a good comparison for the style and feeling of the two implementations.

#pragma warning disable SYSLIB5006 // ML-DSA is experimental
using System.Text;
using System.Security.Cryptography;
using System.Linq;
using Spectre.Console;

public static class WindowsDemo
{
    public static void RunMlKem()
    {
        if (!MLKem.IsSupported)
        {
            PrintPanel("Error", [$":broken_heart: ML-KEM is not supported on your Windows. PQC capabilities are available for Windows Insiders, Canary Channel Build 27852 and higher only."]);
            return;
        }

        Console.WriteLine("***************** ML-KEM *******************");

        // Generate key pair for Alice using ML-KEM 768
        using var aliceKeyPair = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);

        // Get and view the keys
        var pubEncoded = aliceKeyPair.ExportEncapsulationKey();
        var privateEncoded = aliceKeyPair.ExportDecapsulationKey();
        PrintPanel("Alice's keys", [$":unlocked: Public: {pubEncoded.PrettyPrint()}", $":locked: Private: {privateEncoded.PrettyPrint()}"]);

        // Bob encapsulates a new shared secret using Alice's public key
        using var bobKey = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, pubEncoded);
        bobKey.Encapsulate(out byte[] cipherText, out byte[] bobSecret);

        // Alice decapsulates a new shared secret using Alice's private key
        byte[] aliceSecret = aliceKeyPair.Decapsulate(cipherText);
        PrintPanel("Key encapsulation", [$":man: Bob's secret: {bobSecret.PrettyPrint()}", $":locked_with_key: Cipher text (Bob -> Alice): {cipherText.PrettyPrint()}", $":woman: Alice's secret: {aliceSecret.PrettyPrint()}"]);

        // Compare secrets
        var equal = bobSecret.SequenceEqual(aliceSecret);
        PrintPanel("Verification", [$"{(equal ? ":check_mark_button:" : ":cross_mark:")} Secrets equal!"]);
    }

    public static void RunMlDsa()
    {
        if (!MLDsa.IsSupported)
        {
            PrintPanel("Error", [$":broken_heart: ML-DSA is not supported on your Windows. PQC capabilities are available for Windows Insiders, Canary Channel Build 27852 and higher only."]);
            return;
        }

        Console.WriteLine("***************** ML-DSA *******************");

        var raw = "Hello, ML-DSA!";
        Console.WriteLine($"Raw Message: {raw}");

        var data = Encoding.ASCII.GetBytes(raw);
        PrintPanel("Message", [$"Raw: {raw}", $"Encoded: {data.PrettyPrint()}"]);

        // Generate key pair using ML-DSA 65 (equivalent to Dilithium3)
        using var mldsaKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);

        // Export keys for demonstration
        var publicKeyBytes = mldsaKey.ExportSubjectPublicKeyInfo();
        var privateKeyBytes = mldsaKey.ExportPkcs8PrivateKey();
        PrintPanel("Keys", [$":unlocked: Public: {publicKeyBytes.PrettyPrint()}", $":locked: Private: {privateKeyBytes.PrettyPrint()}"]);

        // Sign the data
        var signature = mldsaKey.SignData(data);
        PrintPanel("Signature", [$":pen: {signature.PrettyPrint()}"]);

        // Verify signature with the same key
        bool verified = mldsaKey.VerifyData(data, signature);
        PrintPanel("Verification", [$"{(verified ? ":check_mark_button:" : ":cross_mark:")} Verified!"]);

        // Demonstrate key import/export - recreate key from exported private key
        using var recoveredKey = MLDsa.ImportPkcs8PrivateKey(privateKeyBytes);
        var signature2 = recoveredKey.SignData(data);
        PrintPanel("Signature (from recovered key)", [$":pen: {signature2.PrettyPrint()}"]);

        // Verify second signature with a public-key-only instance
        using var publicOnlyKey = MLDsa.ImportSubjectPublicKeyInfo(publicKeyBytes);
        bool verifiedWithPublicKey = publicOnlyKey.VerifyData(data, signature2);
        PrintPanel("Reverification", [$"{(verifiedWithPublicKey ? ":check_mark_button:" : ":cross_mark:")} Verified!"]);
    }

    static void PrintPanel(string header, string[] data)
    {
        var content = string.Join(Environment.NewLine, data);
        var panel = new Panel(content)
        {
            Header = new PanelHeader(header)
        };
        AnsiConsole.Write(panel);
    }
}

The implementation follows standard .NET cryptography patterns. The MLKem class provides key encapsulation functionality, while MLDsa handles digital signatures using familiar .NET conventions.

The size implications of PQC algorithms remain unchanged from the BouncyCastle implementations. ML-KEM 768 public keys are approximately 1.2KB, and ML-DSA 65 signatures are around ~3.3KB. The native implementation utilizes Windows CNG directly rather than managed code implementations. While both approaches provide adequate performance for most use cases, native implementations may offer benefits in high-throughput scenarios, as well as lower-level optimizations available through the OS. One would also expect lower memory usage due to the absence of managed code overhead and rigid security practices enforced by the OS.

Future Development πŸ”—

Microsoft’s roadmap includes additional algorithm support (SLH-DSA), integration with the Windows TLS stack (Schannel) for hybrid key exchange, and support in Active Directory Certificate Services for PQC certificates.

This development represents a step in the evolution from third-party library implementations to native platform support. The availability of both BouncyCastle for cross-platform scenarios and native Windows support provides developers with different implementation options depending on their requirements.

PQC in .NET πŸ”—

The development path for post-quantum cryptography in .NET has progressed from the early excellent experimental BouncyCastle implementations of Kyber and DIlithium, to standardized ML-KEM and ML-DSA in BouncyCastle 2.5.0, and now to (super early) native Windows support.

This follows the typical progression for new cryptographic standards: research implementations, third-party library support, and eventual native platform integration. BouncyCastle continues to provide cross-platform compatibility, while native implementations offer platform-specific optimizations. The open issue for .NET 11 indicates plans for broader native support in the core .NET runtime.

You can find the full source code for this post on GitHub.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP