惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Know Your Adversary
Know Your Adversary
N
News and Events Feed by Topic
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
P
Privacy International News Feed
Recent Announcements
Recent Announcements
T
Threatpost
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Last Week in AI
Last Week in AI
博客园 - 叶小钗
AWS News Blog
AWS News Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyberwarzone
Cyberwarzone
月光博客
月光博客
The Cloudflare Blog
罗磊的独立博客
P
Palo Alto Networks Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Tailwind CSS Blog
PCI Perspectives
PCI Perspectives
Forbes - Security
Forbes - Security
Latest news
Latest news
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
爱范儿
爱范儿
L
Lohrmann on Cybersecurity
Cisco Talos Blog
Cisco Talos Blog
I
Intezer
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
C
CERT Recently Published Vulnerability Notes
TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
H
Hacker News: Front Page
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V2EX - 技术
V2EX - 技术
Microsoft Azure Blog
Microsoft Azure Blog
O
OpenAI News
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Webroot Blog
Webroot Blog

Duende Software Official Site

Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Stop AI Bots from Wasting Your Server How Duende IdentityServer Filters Claims (And Why It Matters) Core vs Extended Protocols in Duende IdentityServer v8: What You Get and When You Need More Your IdentityServer v8 Upgrade Checklist: A Quick Pre-Flight Guide Setting Up SAML Single Sign-On in ASP.NET with Duende IdentityServer Your Identity, Your Terms: Duende's Modular Identity Infrastructure and v8.x Release Duende Spring Launch '26: Identity Infrastructure That Expands With You SAML and OpenID Connect (OIDC): Coexistence, Not Competition The 9 Components of SAML You Need to Know, Ranked by Importance The Cost of NOT Implementing Financial-Grade Security Token Issuer Isolation: Why It Matters for Security and Compliance The Composable Identity Pattern: Build What You Need, Skip What You Don't Multi-Brand Identity: When Your Company Needs More Than One Face The field Keyword in C# 14: Write Less, Validate More The Real Cost of Build vs. Buy for Identity OAuth 2.1 Made Simple: The Only Flows You Need Beyond localhost: Multi-Instance ASP.NET Core Deployment with .NET 10 Harden Your .NET JSON Deserialization with System.Text.Json and JsonSerializerOptions.Strict ASP.NET Core Cookie Size Limits in Production: Causes and Fixes The Emergency Stop Button - Implementing Immediate Token Revocation in .NET 10 The 2025 OWASP Top 10 and IdentityServer Update Guidance for CVE-2026-40372 - ASP.NET Data Protection Why a Standard JWT Access Token Matters The Identity Governance Checklist You Wish You Had Six Months Ago The History and Future of SAML: Why a 20-Year-Old Protocol Still Matters The Cookie Apocalypse Already Happened Verify - Open Source Sponsorship Why Identity Is Infrastructure, Not a Feature Extending Duende IdentityServer Server-Side Sessions with Dynamic User Metadata Give Your AI Coding Assistant Duende Expertise with Agent Skills and MCP Server Triggering User Registration via OpenID Connect with Duende IdentityServer Improving .NET Security Code with C# 14 Property Extensions Developing Audit Logs with Duende IdentityServer Events Patch Releases: Addressing CVE-2026-26127 in Microsoft.BCL.Memory Client-Initiated Backchannel Authentication (CIBA) in ASP.NET Core 10 with Duende Identity Server Rate Limiting IdentityServer Endpoints It's Probably DNS - Can You Dig It? Security Lingo Explained: Encode vs Encrypt vs Hash Implementing Zero Trust with Resource Isolation Security Lingo Explained: JWT DPoP Security for .NET APIs with JwtBearer Extensions v1.0.0 Announcing the Duende IdentityServer4 Migration Analysis Tool BenchmarkDotNet - Open Source Sponsorship Security Lingo Explained: PAR Why Signing Key Rotation Matters in OpenID Connect and Duende IdentityServer Security Lingo Explained: OP Duende Year-End Review 2025 Security Lingo Explained: BCP Security Lingo Explained: DPoP Security Lingo Explained: Auth Secure frontend apps with the BFF Pattern Scaling with Duende IdentityServer, MCP, and AI Duende IdentityServer v7.4 is now available Duende BFFv4 is now available Securing OpenAPI and Swagger UI with OAuth in .NET 10 Building a Federation Gateway with Duende IdentityServer: Strategies and Considerations for Identity Orchestration
Post-Quantum Cryptography in .NET 10: A Practical Guide
Khalid Abuhakmeh · 2026-05-14 · via Duende Software Official Site

Quantum computers capable of breaking RSA and elliptic-curve cryptography don't exist yet, but the threat is real enough that NIST finalized three post-quantum cryptography (PQC) standards in 2024: FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA). The concern isn't just future decryption. "Harvest now, decrypt later" attacks mean that encrypted data captured today could be broken once quantum hardware catches up.

.NET 10 ships first-class support for all three FIPS-standardized PQC algorithms, plus a hybrid approach called Composite ML-DSA that combines classical and post-quantum signatures. No hand-waving. Let's write code.

Platform Support and Runtime Detection

Before diving into each algorithm, a quick note on where these APIs actually work. PQC support in .NET 10 requires one of:

  • Linux with OpenSSL 3.5 or newer
  • Windows with CNG PQC support
Note: PQC is not supported on macOS in .NET 10. .NET uses Apple's native security framework on macOS, not OpenSSL, so having OpenSSL 3.5+ installed via Homebrew won't help. Apple's frameworks gained PQC support in Platform 26, but that shipped after .NET 10. CryptoKit-based PQC support is tracked for .NET 11. If you're on a Mac, the easiest path is Docker. See the Try It Yourself section at the end of this post.

Each type exposes a static IsSupported property you should check before using the algorithm:

Csharp

using System.Security.Cryptography;

Console.WriteLine($"ML-KEM:          {MLKem.IsSupported}");
Console.WriteLine($"ML-DSA:          {MLDsa.IsSupported}");
Console.WriteLine($"SLH-DSA:         {SlhDsa.IsSupported}");
Console.WriteLine($"Composite ML-DSA:{CompositeMLDsa.IsSupported}");

if (!MLKem.IsSupported)
{
    Console.WriteLine("PQC requires OpenSSL 3.5+ (Linux) or Windows CNG.");
    return;
}
// ... run demos

One important design note: these new types don't inherit from AsymmetricAlgorithm. Instead of creating an object and then importing a key into it, you use static methods to generate or import keys directly. This is a cleaner pattern that the .NET team adopted for all PQC types.

ML-KEM: Post-Quantum Key Encapsulation (FIPS 203)

ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) replaces classical key exchange algorithms like Diffie-Hellman and ECDH. It lets two parties establish a shared secret over an insecure channel.

The flow works like this: Alice generates a key pair and shares her public key. Bob uses that public key to produce both a ciphertext and a shared secret. Alice then decapsulates the ciphertext with her private key to recover the same shared secret.

Csharp

using System.Security.Cryptography;

// Alice generates a key pair
using MLKem aliceKey = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);

// Alice shares her public (encapsulation) key with Bob
byte[] alicePublicKey = aliceKey.ExportEncapsulationKey();
Console.WriteLine($"Alice's public key: {alicePublicKey.Length} bytes");

// Bob uses Alice's public key to create a shared secret + ciphertext
using MLKem bobKey = MLKem.ImportEncapsulationKey(
    MLKemAlgorithm.MLKem768, alicePublicKey);

bobKey.Encapsulate(out byte[] ciphertext, out byte[] bobSharedSecret);

Console.WriteLine($"Bob's shared secret: {Convert.ToHexString(bobSharedSecret[..16])}...");

// Alice decapsulates the ciphertext to get the same shared secret
byte[] aliceSharedSecret = aliceKey.Decapsulate(ciphertext);

Console.WriteLine($"Alice's shared secret: {Convert.ToHexString(aliceSharedSecret[..16])}...");

// Both sides now have the same shared secret
bool match = aliceSharedSecret.AsSpan().SequenceEqual(bobSharedSecret);
Console.WriteLine($"Shared secrets match: {match}");

MLKemAlgorithm offers three parameter sets:

Parameter Set Security Level Public Key Size Ciphertext Size
MLKem512 NIST Level 1 800 bytes 768 bytes
MLKem768 NIST Level 3 1,184 bytes 1,088 bytes
MLKem1024 NIST Level 5 1,568 bytes 1,568 bytes

MLKem768 is a reasonable default for most applications, balancing security and performance.

MLKem is the only PQC type in .NET 10 that is not marked as [Experimental]. Its API surface is considered stable.

ML-DSA: Post-Quantum Digital Signatures (FIPS 204)

ML-DSA (Module-Lattice-Based Digital Signature Algorithm) is the post-quantum replacement for RSA and ECDSA signatures. If you sign tokens, documents, or messages today, you'll eventually migrate to ML-DSA.

Csharp

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

// Generate a signing key
using MLDsa signingKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);

// Sign some data
byte[] data = Encoding.UTF8.GetBytes("Transfer $1,000 to account 12345");
byte[] signature = signingKey.SignData(data);
Console.WriteLine($"Data: {Encoding.UTF8.GetString(data)}");
Console.WriteLine($"Signature: {signature.Length} bytes");

// Verify with the same key (has both private and public)
bool valid = signingKey.VerifyData(data, signature);
Console.WriteLine($"Signature valid: {valid}");

// Export the public key and verify with a separate key instance
string publicKeyPem = signingKey.ExportSubjectPublicKeyInfoPem();
using MLDsa verifyKey = MLDsa.ImportFromPem(publicKeyPem);
bool validFromPem = verifyKey.VerifyData(data, signature);
Console.WriteLine($"Verified from exported PEM: {validFromPem}");

// Tamper with the data and verify again
byte[] tamperedData = Encoding.UTF8.GetBytes("Transfer $9,999 to account 12345");
bool tamperedValid = verifyKey.VerifyData(tamperedData, signature);
Console.WriteLine($"Tampered data valid: {tamperedValid}");

Notice how SignData returns a byte[] directly. Earlier previews required you to pre-allocate a buffer and pass it in. The .NET team added these convenience overloads to simplify common patterns.

MLDsaAlgorithm offers three parameter sets:

Parameter Set Security Level Public Key Size Signature Size
MLDsa44 NIST Level 2 1,312 bytes 2,420 bytes
MLDsa65 NIST Level 3 1,952 bytes 3,309 bytes
MLDsa87 NIST Level 5 2,592 bytes 4,627 bytes

HashML-DSA (Pre-Hash Mode)

ML-DSA also supports a "pre-hash" mode where you hash the data first, then sign the hash. This is useful when the data is large or when you need to sign with a specific hash algorithm. The API takes the hash algorithm's OID as a string. One catch: the hash algorithm must meet or exceed the security level of the ML-DSA parameter set. ML-DSA-65 is NIST Level 3, so SHA-384, SHA-512, SHA3-384, SHA3-512, or SHAKE256 work. SHA-256 and SHA3-256 are too weak.

Csharp

using System.Security.Cryptography;

byte[] SignPreHashSha512(MLDsa signingKey, ReadOnlySpan<byte> data)
{
    const string Sha512Oid = "2.16.840.1.101.3.4.2.3";
    return signingKey.SignPreHash(SHA512.HashData(data), Sha512Oid);
}

MLDsa is marked as [Experimental] under diagnostic SYSLIB5006 until the underlying standards work is complete.

SLH-DSA: Stateless Hash-Based Signatures (FIPS 205)

SLH-DSA (Stateless Hash-Based Digital Signature Algorithm) takes a different approach than ML-DSA. Where ML-DSA relies on lattice mathematics, SLH-DSA is built entirely on hash functions. The trade-off: signatures are larger, but the security argument rests on hash function properties that cryptographers have studied for decades.

Csharp

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

// SLH-DSA uses hash-based cryptography (no lattice math)
// Larger signatures, but based on well-understood hash functions
using SlhDsa signingKey = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128f);

byte[] data = Encoding.UTF8.GetBytes("Critical audit log entry #4821");
byte[] signature = signingKey.SignData(data);
Console.WriteLine($"Data: {Encoding.UTF8.GetString(data)}");
Console.WriteLine($"Signature: {signature.Length} bytes (larger than ML-DSA)");

bool valid = signingKey.VerifyData(data, signature);
Console.WriteLine($"Signature valid: {valid}");

The f and s suffixes in algorithm names like SlhDsaSha2_128f indicate speed trade-offs:

  • f (fast): Faster signing, larger signatures
  • s (small): Slower signing, smaller signatures

Choose SLH-DSA when you want defense-in-depth. If lattice-based assumptions (used by ML-DSA) are ever broken, SLH-DSA still stands because it relies only on hash functions.

SlhDsa is also marked as [Experimental] under SYSLIB5006.

Composite ML-DSA: The Migration-Safe Approach

Composite ML-DSA combines a classical signature algorithm (such as RSA) with a post-quantum algorithm (ML-DSA) into a single signature scheme. If either algorithm is broken, the other still protects you. This is the pragmatic choice for systems that need quantum resistance today but can't afford to bet everything on a single new algorithm.

Csharp

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

// Combine ML-DSA with RSA for a migration-safe approach.
// If either algorithm is broken, the other still protects you.
var algorithm = CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss;
using CompositeMLDsa privateKey = CompositeMLDsa.GenerateKey(algorithm);

byte[] data = Encoding.UTF8.GetBytes("High-value token payload");
byte[] signature = privateKey.SignData(data);
Console.WriteLine($"Data: {Encoding.UTF8.GetString(data)}");
Console.WriteLine($"Composite signature: {signature.Length} bytes");

// Export the composite public key and verify
byte[] compositePublicKey = privateKey.ExportCompositeMLDsaPublicKey();
using CompositeMLDsa publicKey = CompositeMLDsa.ImportCompositeMLDsaPublicKey(
    algorithm, compositePublicKey);

bool valid = publicKey.VerifyData(data, signature);
Console.WriteLine($"Composite signature valid: {valid}");

// Tamper with the signature
signature[0] ^= 1;
bool tamperedValid = publicKey.VerifyData(data, signature);
Console.WriteLine($"Tampered signature valid: {tamperedValid}");

The CompositeMLDsaAlgorithm type provides RSA-based composite options. .NET 10 ships with the RSA variants implemented:

Algorithm Components
MLDsa44WithRSA2048Pss ML-DSA-44 + RSA-2048-PSS
MLDsa65WithRSA3072Pss ML-DSA-65 + RSA-3072-PSS
MLDsa65WithRSA4096Pss ML-DSA-65 + RSA-4096-PSS

This implements draft-ietf-lamps-pq-composite-sigs. The draft is still evolving (draft 15 at the time of writing), so expect the .NET implementation to keep pace with updates in future releases.

CompositeMLDsa is marked as [Experimental] under SYSLIB5006.

Choosing the Right Algorithm

Scenario Recommended Why
Key exchange / key agreement ML-KEM (768) Replaces ECDH; stable API, not experimental
General signing (new systems) ML-DSA (65) Smallest signatures of the PQC options; best for most use cases
Maximum hash-based confidence SLH-DSA No reliance on lattice math; conservative choice
Migration from RSA/ECDSA Composite ML-DSA Both classical and PQC protection during transition
Signing where backward compatibility matters Composite ML-DSA Classical verifiers can still validate the RSA component

What This Means for Identity Infrastructure

Token signing is at the heart of modern identity systems. Every JWT, every SAML assertion, every OpenID Connect (OIDC) ID token carries a digital signature that clients verify. Today, those signatures use RSA or ECDSA. Eventually, they will need to support post-quantum algorithms.

That transition won't happen overnight. Certificate authorities are beginning PQC trials. Standards bodies are working on PQC-aware token formats. But the building blocks are now available in .NET 10, and the time to start experimenting is now.

Key rotation strategies become especially important during algorithm migration. A system that can smoothly transition from RSA to ML-DSA (or use Composite ML-DSA during the transition) is better positioned than one with hardcoded assumptions about the algorithm. Duende IdentityServer's automatic key management is designed to be algorithm-agnostic, handling signing key lifecycle regardless of the underlying algorithm. As PQC support stabilizes across the ecosystem, that flexibility matters.

Production Considerations

The code in this post is for learning and experimentation. If you adapt these patterns for production use, keep in mind:

  • Never log or display shared secrets or private key material. The ML-KEM demo prints truncated shared secrets to show that both sides match. In a real system, those values go straight into a KDF or symmetric cipher.
  • Zero sensitive byte arrays when you're done with them. Call CryptographicOperations.ZeroMemory() on shared secrets, plaintext key material, and signature buffers that contain sensitive data.
  • Three of the four PQC APIs are experimental. ML-DSA, SLH-DSA, and Composite ML-DSA are all marked [Experimental]. Their API surface may change before stabilizing. ML-KEM is the only stable PQC type in .NET 10.

Try It Yourself

Everything you need to run these demos is right here. Create three files in an empty directory:

PostQuantumCrypto.csproj

Markup

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    
    <NoWarn>SYSLIB5006</NoWarn>
  </PropertyGroup>

</Project>

Program.cs

Csharp

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

Console.WriteLine("=== Post-Quantum Cryptography in .NET 10 ===\n");

// Check platform support before running any demos
Console.WriteLine("Platform support:");
Console.WriteLine($"  ML-KEM:          {MLKem.IsSupported}");
Console.WriteLine($"  ML-DSA:          {MLDsa.IsSupported}");
Console.WriteLine($"  SLH-DSA:         {SlhDsa.IsSupported}");
Console.WriteLine($"  Composite ML-DSA:{CompositeMLDsa.IsSupported}");
Console.WriteLine();

if (!MLKem.IsSupported)
{
    Console.WriteLine("PQC algorithms require OpenSSL 3.5+ (Linux) or Windows CNG with PQC support.");
    Console.WriteLine("Exiting.");
    return;
}

MlKemDemo();
MlDsaDemo();
HashMlDsaDemo();
SlhDsaDemo();
CompositeMlDsaDemo();

// ──────────────────────────────────────────────
// ML-KEM: Post-Quantum Key Encapsulation
// ──────────────────────────────────────────────

void MlKemDemo()
{
    Console.WriteLine("── ML-KEM (FIPS 203): Key Encapsulation ──\n");

    // Alice generates a key pair
    using MLKem aliceKey = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);

    // Alice shares her public (encapsulation) key with Bob
    byte[] alicePublicKey = aliceKey.ExportEncapsulationKey();
    Console.WriteLine($"Alice's public key: {alicePublicKey.Length} bytes");

    // Bob uses Alice's public key to create a shared secret + ciphertext
    using MLKem bobKey = MLKem.ImportEncapsulationKey(
        MLKemAlgorithm.MLKem768, alicePublicKey);

    bobKey.Encapsulate(out byte[] ciphertext, out byte[] bobSharedSecret);

    Console.WriteLine($"Bob's shared secret: {Convert.ToHexString(bobSharedSecret[..16])}...");

    // Alice decapsulates the ciphertext to get the same shared secret
    byte[] aliceSharedSecret = aliceKey.Decapsulate(ciphertext);

    Console.WriteLine($"Alice's shared secret: {Convert.ToHexString(aliceSharedSecret[..16])}...");

    // Both sides now have the same shared secret
    bool match = aliceSharedSecret.AsSpan().SequenceEqual(bobSharedSecret);
    Console.WriteLine($"Shared secrets match: {match}\n");
}

// ──────────────────────────────────────────────
// ML-DSA: Post-Quantum Digital Signatures
// ──────────────────────────────────────────────

void MlDsaDemo()
{
    Console.WriteLine("── ML-DSA (FIPS 204): Digital Signatures ──\n");

    // Generate a signing key
    using MLDsa signingKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);

    // Sign some data
    byte[] data = Encoding.UTF8.GetBytes("Transfer $1,000 to account 12345");
    byte[] signature = signingKey.SignData(data);
    Console.WriteLine($"Data: {Encoding.UTF8.GetString(data)}");
    Console.WriteLine($"Signature: {signature.Length} bytes");

    // Verify with the same key (has both private and public)
    bool valid = signingKey.VerifyData(data, signature);
    Console.WriteLine($"Signature valid: {valid}");

    // Export the public key and verify with a separate key instance
    string publicKeyPem = signingKey.ExportSubjectPublicKeyInfoPem();
    using MLDsa verifyKey = MLDsa.ImportFromPem(publicKeyPem);
    bool validFromPem = verifyKey.VerifyData(data, signature);
    Console.WriteLine($"Verified from exported PEM: {validFromPem}");

    // Tamper with the data and verify again
    byte[] tamperedData = Encoding.UTF8.GetBytes("Transfer $9,999 to account 12345");
    bool tamperedValid = verifyKey.VerifyData(tamperedData, signature);
    Console.WriteLine($"Tampered data valid: {tamperedValid}\n");
}

// ──────────────────────────────────────────────
// HashML-DSA: Pre-Hash Mode for ML-DSA
// ──────────────────────────────────────────────

void HashMlDsaDemo()
{
    Console.WriteLine("── HashML-DSA: Pre-Hash Mode ──\n");

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

    byte[] data = Encoding.UTF8.GetBytes("Large document content that benefits from pre-hashing");

    // Pre-hash the data with SHA-512, then sign the hash.
    // The hash algorithm must meet or exceed the ML-DSA security level.
    // ML-DSA-65 is NIST Level 3, so SHA-384, SHA-512, SHA3-384, SHA3-512, or SHAKE256 work.
    // SHA-256 and SHA3-256 are too weak for ML-DSA-65.
    byte[] signature = SignPreHashSha512(signingKey, data);
    Console.WriteLine($"Pre-hash signature: {signature.Length} bytes\n");
}

byte[] SignPreHashSha512(MLDsa signingKey, ReadOnlySpan<byte> data)
{
    const string Sha512Oid = "2.16.840.1.101.3.4.2.3";
    return signingKey.SignPreHash(SHA512.HashData(data), Sha512Oid);
}

// ──────────────────────────────────────────────
// SLH-DSA: Stateless Hash-Based Signatures
// ──────────────────────────────────────────────

void SlhDsaDemo()
{
    Console.WriteLine("── SLH-DSA (FIPS 205): Hash-Based Signatures ──\n");

    // SLH-DSA uses hash-based cryptography (no lattice math)
    // Larger signatures, but based on well-understood hash functions
    using SlhDsa signingKey = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128f);

    byte[] data = Encoding.UTF8.GetBytes("Critical audit log entry #4821");
    byte[] signature = signingKey.SignData(data);
    Console.WriteLine($"Data: {Encoding.UTF8.GetString(data)}");
    Console.WriteLine($"Signature: {signature.Length} bytes (larger than ML-DSA)");

    bool valid = signingKey.VerifyData(data, signature);
    Console.WriteLine($"Signature valid: {valid}\n");
}

// ──────────────────────────────────────────────
// Composite ML-DSA: Hybrid Classical + PQC
// ──────────────────────────────────────────────

void CompositeMlDsaDemo()
{
    Console.WriteLine("── Composite ML-DSA: Hybrid Signatures ──\n");

    // Combine ML-DSA with RSA for a migration-safe approach.
    // If either algorithm is broken, the other still protects you.
    var algorithm = CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss;
    using CompositeMLDsa privateKey = CompositeMLDsa.GenerateKey(algorithm);

    byte[] data = Encoding.UTF8.GetBytes("High-value token payload");
    byte[] signature = privateKey.SignData(data);
    Console.WriteLine($"Data: {Encoding.UTF8.GetString(data)}");
    Console.WriteLine($"Composite signature: {signature.Length} bytes");

    // Export the composite public key and verify
    byte[] compositePublicKey = privateKey.ExportCompositeMLDsaPublicKey();
    using CompositeMLDsa publicKey = CompositeMLDsa.ImportCompositeMLDsaPublicKey(
        algorithm, compositePublicKey);

    bool valid = publicKey.VerifyData(data, signature);
    Console.WriteLine($"Composite signature valid: {valid}");

    // Tamper with the signature
    signature[0] ^= 1;
    bool tamperedValid = publicKey.VerifyData(data, signature);
    Console.WriteLine($"Tampered signature valid: {tamperedValid}\n");
}

Dockerfile

The Alpine-based .NET images ship with OpenSSL 3.5, so PQC works out of the box. This is the easiest way to run the demos on macOS or any system without OpenSSL 3.5+.

FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY *.cs ./
RUN dotnet build -c Release --no-restore

FROM mcr.microsoft.com/dotnet/runtime:10.0-alpine
WORKDIR /app
COPY --from=build /app/bin/Release/net10.0/ ./

ENTRYPOINT ["dotnet", "PostQuantumCrypto.dll"]

Build and run with Docker:

Bash

docker build -t pqc-demo .
docker run --rm pqc-demo

Sample Output

=== Post-Quantum Cryptography in .NET 10 ===

Platform support:
  ML-KEM:          True
  ML-DSA:          True
  SLH-DSA:         True
  Composite ML-DSA:True

── ML-KEM (FIPS 203): Key Encapsulation ──

Alice's public key: 1184 bytes
Bob's shared secret: AE8446438B3840C519100D63F95B9484...
Alice's shared secret: AE8446438B3840C519100D63F95B9484...
Shared secrets match: True

── ML-DSA (FIPS 204): Digital Signatures ──

Data: Transfer $1,000 to account 12345
Signature: 3309 bytes
Signature valid: True
Verified from exported PEM: True
Tampered data valid: False

── HashML-DSA: Pre-Hash Mode ──

Pre-hash signature: 3309 bytes

── SLH-DSA (FIPS 205): Hash-Based Signatures ──

Data: Critical audit log entry #4821
Signature: 17088 bytes (larger than ML-DSA)
Signature valid: True

── Composite ML-DSA: Hybrid Signatures ──

Data: High-value token payload
Composite signature: 3821 bytes
Composite signature valid: True
Tampered signature valid: False

Shared secret hex values will differ on each run because the keys are generated anew.

Conclusion

Post-quantum cryptography is no longer theoretical. The APIs are here, NIST has standardized the algorithms, and .NET 10 gives you a clean, idiomatic way to start using them. The question isn't whether to adopt PQC, it's when. Starting now gives you time to build expertise before it becomes urgent.