Post-Quantum Cryptography in .NET 10: New Algorithms and API Design

Cryptography

Explore .NET 10's Post-Quantum Cryptography (PQC) implementation, featuring new algorithms like ML-KEM and ML-DSA. This details the API redesign for enhanced security and quantum resistance.

Post-Quantum Cryptography in .NET 10: New Algorithms and API Design

.NET 10 introduces robust support for Post-Quantum Cryptography (PQC), a critical development given the evolving threat landscape from quantum computing. The "Post" in PQC refers to algorithms designed to withstand attacks by sufficiently powerful quantum computers, particularly those that pose a threat to existing asymmetric cryptography like RSA and ECC (e.g., EC-DSA, EC-Diffie-Hellman). This initiative focuses on new algorithms that will ensure cryptographic security in a quantum era.

The transition to PQC is urgent, often summarized by the strategy "Harvest now, decrypt later," emphasizing the need to adopt these new algorithms before cryptographically-relevant quantum computers (CRQCs) become a reality. .NET 10 aims to provide these capabilities early, aligning with the standardization of initial PQC specifications.

PQC Algorithms in .NET 10

.NET 10 focuses on four key PQC algorithms, designed to replace or enhance existing cryptographic functionalities:

AlgorithmKindSpecification.NET Class
ML-KEMKey EncapsulationNIST FIPS 203MLKem
ML-DSASignatureNIST FIPS 204MLDsa
SLH-DSASignatureNIST FIPS 205SlhDsa
Composite ML-DSASignatureIETF Draft "Composite ML-DSA for use in X.509 Public Key Infrastructure"CompositeMLDsa
  • ML-DSA, SLH-DSA, and Composite ML-DSA are designed as replacements for signatures currently handled by RSA and EC-DSA.
  • ML-KEM logically replaces RSA "Key Transport" and EC-Diffie-Hellman "Key Agreement," though its usage is not a direct drop-in replacement. There is no direct PQC replacement for RSA "Data Encryption," as this was never a recommended use of RSA.

Evolving from the AsymmetricAlgorithm Design

Historically, .NET's asymmetric cryptography has followed a consistent pattern: algorithm types deriving from AsymmetricAlgorithm, implementation types deriving from these, and a static Create() method for platform-agnostic instantiation. Keys could be imported, explicitly generated, or implicitly generated.

namespace System.Security.Cryptography;

public partial class AsymmetricAlgorithm : IDisposable { }
public partial class RSA : AsymmetricAlgorithm
{
    public static RSA Create();
}
// ...
public partial class RSACng : RSA { }
// etc

However, this design encountered challenges, particularly with the KeySize property of AsymmetricAlgorithm:

public partial class AsymmetricAlgorithm
{
    public virtual int KeySize { get; set; }
}

Initially, KeySize made sense for RSA and DSA, representing the modulus size. With the introduction of elliptic curve cryptography (ECC), KeySize was adapted to represent the number of bits required for the prime modulus p of a curve. As more elliptic curves were supported, the setter for KeySize became ambiguous, still limited to a few specific NIST curves, while the getter might report sizes for other curves (e.g., Brainpool).

With PQC algorithms like ML-DSA, the concept of a singular KeySize becomes even more problematic. For an ML-DSA-65 key, "65" is an arbitrary parameter set name, not a cryptographic size. A public key might be 1952 bytes (15616 bits), making KeySize an ill-fitting abstraction. This fundamental mismatch prompted a significant redesign away from AsymmetricAlgorithm.

Identifying Design Flaws in AsymmetricAlgorithm

The decision to move away from AsymmetricAlgorithm was driven by several identified issues:

  • Redundant Validation: Heavy use of public virtual methods forced state and argument validation to be repeated in every derived type, leading to potential inconsistencies and errors.
  • Capability Query: Instances had to be created to query capabilities (e.g., LegalKeySizes[]), which is inefficient.
  • Key Generation: The Create() method didn't always generate a key, causing performance surprises when keys were implicitly generated later.
  • Disposal Semantics: Dispose() didn't consistently mean an object was unusable; sometimes it implied a key could be regenerated.
  • Lack of Direct Usability: AsymmetricAlgorithm couldn't be used directly; it always required casting to a specific algorithm type.
  • Irrelevant KeySize: The KeySize property no longer made sense for the new PQC algorithms.
  • Layer Violation: Properties like KeyExchangeAlgorithm, SignatureAlgorithm, ToXmlString(bool), and FromXmlString(string) were tied to higher-level XML cryptography components (SignedXml, EncryptedXml) and felt out of place.
  • Ambiguous ExportParameters(bool): This method complicated consistent flow analysis for private vs. public key data.

The only significant advantage of the old design was a consistent mechanism for key import/export.

Goals for the New API Design

The redesign aimed to address these issues with clear objectives:

  • Key-Centric Instances: Each instance should unequivocally represent a specific key or keypair.
  • Strict Disposal: Once an object is disposed, it should be permanently unusable.
  • Minimal Base Classes: Avoid common base classes unless a truly shared set of functionalities exists.
  • Reduced Derived Code: Minimize code in derived types to reduce the surface area for errors.
  • Precise Terminology: Use existing terminology where appropriate, and introduce new terms when existing ones are misleading.
  • Span-Based Design: Optimize for Span<byte> to enhance performance and memory safety.

The New MLDsa Class Design

The new design for PQC algorithms, exemplified by the MLDsa class, reflects these goals:

namespace System.Security.Cryptography;

public abstract partial class MLDsa : System.IDisposable
{
    public static bool IsSupported { get; }

    protected MLDsa(MLDsaAlgorithm algorithm);
    public MLDsaAlgorithm Algorithm { get; }

    public void Dispose();
    protected virtual void Dispose(bool disposing);

    // Static key generation and import methods
    public static MLDsa GenerateKey(MLDsaAlgorithm algorithm);
    public static MLDsa ImportMLDsaPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan<byte> source);
    // ... various other Import methods ...

    // Key export methods
    public void ExportMLDsaPublicKey(Span<byte> destination);
    // ... various other Export methods ...

    // Operations the algorithm can perform
    public void SignData(ReadOnlySpan<byte> data, Span<byte> destination, ReadOnlySpan<byte> context = default);
    public void SignMu(ReadOnlySpan<byte> externalMu, Span<byte> destination);
    public void SignPreHash(ReadOnlySpan<byte> hash, Span<byte> destination, string hashAlgorithmOid, ReadOnlySpan<byte> context = default);
    public bool VerifyData(ReadOnlySpan<byte> data, ReadOnlySpan<byte> signature, ReadOnlySpan<byte> context = default);
    public bool VerifyMu(ReadOnlySpan<byte> externalMu, ReadOnlySpan<byte> signature);
    public bool VerifyPreHash(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> signature, string hashAlgorithmOid, ReadOnlySpan<byte> context = default);

    // Protected abstract core methods for implementation specifics
    protected abstract void ExportMLDsaPrivateSeedCore(Span<byte> destination);
    // ... other protected abstract methods ...
}

This design achieves several objectives:

  • Key-Centric Instances: All instance methods operate on the key/keypair; key generation and import are static methods.
  • Strict Disposal: The base class manages disposal, preventing virtual member calls on disposed objects.
  • Reduced Base Class Overload: New PQC algorithms (ML-DSA, ML-KEM, SLH-DSA, Composite ML-DSA) directly extend object rather than AsymmetricAlgorithm, as they share minimal commonality.
  • Template Method Pattern: Argument and state validation are handled in the public base class methods, leaving only the final operation to protected abstract derived methods.
  • Consistent Terminology: Methods like SignData align with existing RSA/ECDsa patterns, while new operations like SignPreHash or SignMu are given distinct names to avoid ambiguity when functionality differs.
  • Span-Based Operations: The design heavily utilizes Span<byte> for performance, particularly for fixed-size algorithm responses, where callers must provide correctly sized buffers.

To guide buffer sizing, the MLDsaAlgorithm class provides necessary parameter set information:

namespace System.Security.Cryptography;

public sealed partial class MLDsaAlgorithm : IEquatable<MLDsaAlgorithm>
{
    public static MLDsaAlgorithm MLDsa44 { get; }
    public static MLDsaAlgorithm MLDsa65 { get; }
    public static MLDsaAlgorithm MLDsa87 { get; }

    public string Name { get; }

    public int MuSizeInBytes { get; }
    public int PrivateKeySizeInBytes { get; }
    public int PrivateSeedSizeInBytes { get; }
    public int PublicKeySizeInBytes { get; }
    public int SignatureSizeInBytes { get; }
}

Interop with Underlying Providers

Provider-specific implementations like MLDsaCng (for Windows CNG) and MLDsaOpenSsl (for OpenSSL) still exist but are significantly leaner. They focus on exposing provider-specific details (e.g., CngKey or SafeEvpPKeyHandle) while delegating core cryptographic operations to the MLDsa base class. Key generation and import are typically performed via the base class to promote platform-independent code.

Advantages for Implementers

This new design offers significant benefits, especially for developers extending cryptographic key types:

  • Reduced Code Complexity: Derived types (e.g., MLDsaOpenSsl) are greatly simplified, often delegating argument validation, disposal checks, and buffer management to the base class. For example, MLDsaOpenSsl.SignDataCore primarily makes a direct call to the underlying OpenSSL interop function.
  • Improved Consistency and Testability: By centralizing validation and common logic in the base class, inconsistencies across derived types are eliminated. This reduces the number of tests required, making the overall testing phase faster and more efficient without sacrificing coverage.

[Experimental] API Considerations

.NET Cryptography generally adheres to a "rule-of-two," meaning an algorithm is usually added only if at least two supported operating systems (Windows, OpenSSL, macOS) offer it. This minimizes risks associated with incomplete platform support.

  • SlhDsa and CompositeMLDsa Classes: These are marked [Experimental] because Windows currently lacks support for SLH-DSA, and neither Windows nor OpenSSL fully support Composite ML-DSA as a first-class algorithm. This indicates potential for breaking structural changes if OS support evolves.
  • Methods in MLKem and MLDsa: While MLKem and MLDsa classes are no longer experimental, certain methods (e.g., ExportEncryptedPkcs8PrivateKey, ImportFromPem) retain the [Experimental] attribute. This is primarily due to their reliance on draft specifications (e.g., "draft-ietf-lamps-kyber-certificates") for key formats, which might still undergo breaking changes or have interoperability concerns.
  • SignPreHash and VerifyPreHash in MLDsa: These methods are also [Experimental], despite being from NIST FIPS 204. The concern here lies in representing hash algorithms by name (or OID) and potential ambiguities regarding fixed output lengths for eXtendable Output Functions (XOFs) like SHAKE-128. Further guidance from the ecosystem (e.g., OpenSSL's more polished support) is awaited.

Integration Across .NET APIs

These new PQC algorithms are integrated into several areas of the System.Security.Cryptography namespaces:

  • CertificateRequest: Supports MLDsa, SlhDsa, and CompositeMLDsa.
  • SignedCms' CmsSigner: Supports MLDsa and SlhDsa.
  • COSE: CoseSigner can accept an MLDsa instance via the new CoseKey type. SLH-DSA support for COSE is an expired draft but may appear in future .NET versions.
  • X509Certificate2: Can manage private keys for MLDsa, SlhDsa, CompositeMLDsa, and MLKem.
  • SslStreamCertificateContext and SslStream: Certificates with ML-DSA public keys can generally be used for TLS 1.3 (or newer) connections, provided the OS and the other connection endpoint also support it. ML-KEM private keys, however, cannot be used for TLS signing.
  • Kestrel: Required specific changes (e.g., CertificateConfigLoader) to support ML-DSA and SLH-DSA.

Getting Started

To begin using Post-Quantum Cryptography in .NET 10:

  1. Obtain .NET 10: Ensure you have the latest version.
  2. OS Support: Verify your operating system supports the algorithms. You can check this via static properties like System.Security.Cryptography.MLDsa.IsSupported.
    • Linux: Requires OpenSSL 3.5 or newer.
    • Windows: Support was added recently; Windows 11 with the latest Patch Tuesday updates should work.
  3. Target .NET Standard 2.0: If targeting .NET Standard 2.0, reference a 10.0 version of Microsoft.Bcl.Cryptography.

Here's an example demonstrating MLKem key generation, encapsulation, and decapsulation:

using System.Security.Cryptography;

if (!MLKem.IsSupported)
{
    Console.WriteLine("ML-KEM isn't supported :(");
    return;
}

MLKemAlgorithm alg = MLKemAlgorithm.MLKem768;

using (MLKem privateKey = MLKem.GenerateKey(alg))
using (MLKem publicKey = MLKem.ImportEncapsulationKey(alg, privateKey.ExportEncapsulationKey()))
{
    publicKey.Encapsulate(out byte[] ciphertext, out byte[] sharedSecret1);
    byte[] sharedSecret2 = privateKey.Decapsulate(ciphertext);

    if (sharedSecret1.AsSpan().SequenceEqual(sharedSecret2))
    {
        Console.WriteLine($"Same answer, yay math! {Convert.ToHexString(sharedSecret1)}");
    }
    else
    {
        Console.WriteLine("You just got the one in 2^165 failure. There's probably a prize for that.");
        Console.WriteLine($"sharedSecret1: {Convert.ToHexString(sharedSecret1)}");
        Console.WriteLine($"sharedSecret2: {Convert.ToHexString(sharedSecret2)}");
        Console.WriteLine($"MLKEM768 seed: {Convert.ToHexString(privateKey.ExportPrivateSeed())}");
    }
}

Should you encounter any unexpected behavior, please provide feedback to the development team.

Acknowledgments

The successful implementation of Post-Quantum Cryptography in .NET 10 was a collaborative effort. Special thanks are extended to the various teams and communities that contributed through design input, development, CI/CD setup, rapid adoption of standards, feedback on Windows Insider builds, and responses to specification questions. Their support was instrumental in achieving the quality and timely delivery of this significant security feature.