Hybrid post-quantum cryptography for .NET — in your browser.

This page is the gold-standard reference for PostQuantum.Hybrid. It leads with the recommended high-level API (HybridEnvelope.Seal / Open), then peels back the layers so you can see what production code should — and should not — look like.

Native .NET 10 cryptography when supported by the host's OpenSSL; BouncyCastle fallback otherwise (see ADR 0012). This container reports BouncyCastle fallback.

Why hybrid?

Today's TLS protects key exchange with elliptic-curve Diffie-Hellman (X25519) and authentication with Ed25519 signatures. Both are safe against the attackers we actually have — but a sufficiently large quantum computer would break both in polynomial time (Shor's algorithm). NIST's ML-KEM-768 (FIPS 203) and ML-DSA-65 (FIPS 204) are post-quantum replacements that no known quantum algorithm breaks — but they are new, and the cryptographic community has had decades to scrutinize X25519 / Ed25519 that ML-KEM and ML-DSA have not yet had.

Hybrid construction sidesteps that asymmetry of confidence. PostQuantum.Hybrid runs both primitives in parallel and combines their outputs so the resulting shared secret (or signature) is safe as long as either primitive holds:

Hybrid KEM

X25519 + ML-KEM-768

Two key encapsulations run side by side. The classical and post-quantum shared secrets are mixed through HKDF-SHA256 with the KEM ciphertexts bound into the transcript, so an attacker breaking just one primitive learns nothing.

Hybrid signatures

Ed25519 + ML-DSA-65

Two signatures are concatenated and BOTH must verify. A forger needs to defeat both schemes simultaneously, which is strictly harder than defeating either one alone.

The library is fail-closed: every parse, verify, and decapsulate failure throws or returns false. There is no silent fallback to "best effort" mode. ML-KEM uses FIPS-203 implicit rejection, which means malformed ciphertexts yield a pseudorandom shared secret — the combined hybrid secret then diverges and the downstream AEAD authentically rejects.

Live demo

Three depths of API. The default — Recommended — is what new code should look like. The other tabs peel layers off so you can see exactly what the library does for you.

Default for new code. HybridEnvelope.Seal does the entire pipeline in one call: KEM encapsulation (X25519 + ML-KEM-768), HKDF-SHA256 key derivation, AES-256-GCM with the KEM ciphertext bound into associated data, nonce generation, and shared-secret zeroization on exit.

The crypto runs on the server. The Blazor Server interactive render mode means clicks call into HybridEnvelope directly without exposing private keys to your browser. If you want to drive the same operations from the shell, the JSON endpoints at /swagger are wired through the same recommended path.

Key rotation

Production-grade key handling rotates KEM keys on a schedule so a single key compromise doesn't leak forever. The library exposes IRotatingHybridKemKeyProvider in PostQuantum.Hybrid.AspNetCore for this — version counter, Rotated event, atomic in-memory swap on file change. Below is a live demonstration of that contract.

Current key version
v1

Bumps every time you click Rotate. Fires the IRotatingHybridKemKeyProvider.Rotated event in the library — applications subscribe to it to invalidate caches or kick a key-distribution refresh.

Probe envelope

Seal a small payload under the current key. You can then try to open it after rotating to see fail-closed behavior.

Recent rotations

No rotations yet. Click Rotate now above.

This page rotates in-process on click for clarity. The library's shipping implementation reloads from disk via FileSystemWatcher; see samples/KeyRotationDemo for the file-driven flow with the SimulatedSidecarService.

Security hygiene

Five Roslyn analyzers ship in PostQuantum.Hybrid.Analyzers and turn the most common misuse patterns into build-time errors. Each rule below shows the shape of bad code that fires it, the exact diagnostic the compiler will print, and the analyzer-clean replacement that production code in this repo follows.

PQH001

PostQuantum.Hybrid disposable not declared with `using`

Warning

Hybrid private-key and encapsulation-result types zero their buffers on Dispose. A local of one of those types declared without `using` leaks the buffers until GC (potentially forever for shared statics).

Bad — fires PQH001
var pair = HybridKem.GenerateKeyPair();
var enc  = HybridKem.Encapsulate(pair.PublicKey);
// ... enc.SharedSecret used ...
// Buffers stay in memory until GC fires.
Good — analyzer-clean
using var pair = HybridKem.GenerateKeyPair();
using var enc  = HybridKem.Encapsulate(pair.PublicKey);
// Buffers are zeroed on scope exit, even if we throw.
Diagnostic 'enc' holds sensitive key material; declare with `using` so its buffers are zeroed on dispose
PQH002

Hybrid KEM SharedSecret used as a key without HKDF

Warning

A hybrid KEM shared secret is uniform 32 bytes — coincidentally what AES-256 needs — but using it directly loses domain separation across uses of the same exchange. Feed it through HKDF.Expand with a purpose-specific `info` parameter.

Bad — fires PQH002
using var aes = new AesGcm(enc.SharedSecret, tagSizeInBytes: 16);
// Direct use of the shared secret as the AES key — no domain
// separation. Two uses of the same exchange share state.
Good — analyzer-clean
var aesKey = new byte[32];
try
{
    HKDF.Expand(
        HashAlgorithmName.SHA256,
        enc.Secret,             // typed wrapper (not raw byte[])
        aesKey,
        info: AeadLabel);       // purpose-specific
    using var aes = new AesGcm(aesKey, tagSizeInBytes: 16);
    // ... use aes ...
}
finally
{
    CryptographicOperations.ZeroMemory(aesKey);
}
Diagnostic 'enc.SharedSecret' is being passed directly to a symmetric primitive constructor; feed it through HKDF.Expand with a purpose-specific 'info' first
PQH003

HybridKem.Decapsulate called before HybridSignature.Verify

Warning

In sign-then-encrypt flows, the signature must be verified before any cryptographic operation runs on the carried material. Decapsulating first derives a shared secret from unauthenticated input and widens the attack surface (and can leak key-related side channels).

Bad — fires PQH003
// Decap runs on attacker-controlled bytes before authenticity is checked.
var secret = HybridKem.Decapsulate(kemPriv, kemCt);
if (!HybridSignature.Verify(sigPub, signedBody, sig))
    throw new CryptographicException("bad signature");
Good — analyzer-clean
// Verify FIRST. Only authenticated input reaches Decapsulate.
if (!HybridSignature.Verify(sigPub, signedBody, sig))
    throw new CryptographicException("bad signature");
var secret = HybridKem.Decapsulate(kemPriv, kemCt);
Diagnostic Decapsulating before verifying derives a shared secret from unauthenticated input; call HybridSignature.Verify first and abort on failure
PQH004

HybridSignature.Verify result discarded

Warning

HybridSignature.Verify returns a bool. Discarding it lets the caller treat unauthenticated input as authenticated — exactly the failure mode signatures exist to prevent.

Bad — fires PQH004
// Result thrown away. Forgery is now indistinguishable from valid input.
HybridSignature.Verify(sigPub, data, sig);
ProcessAuthenticatedMessage(data);
Good — analyzer-clean
if (!HybridSignature.Verify(sigPub, data, sig))
    throw new CryptographicException("bad signature");
ProcessAuthenticatedMessage(data);
Diagnostic The return value of HybridSignature.Verify must be checked; ignoring it is equivalent to skipping signature verification
PQH005

AES-GCM call inside a hybrid KEM flow without associatedData binding

Warning

When AES-GCM is keyed from a hybrid KEM exchange, binding the KEM ciphertext into associatedData prevents an attacker from swapping ciphertexts to re-key the AEAD against a different exchange.

Bad — fires PQH005
using var aes = new AesGcm(aesKey, tagSizeInBytes: 16);
aes.Encrypt(nonce, plaintext, ciphertext, tag);
// No associatedData. Swapping kemCt for a different exchange's
// ciphertext can re-key the AEAD without detection.
Good — analyzer-clean
using var aes = new AesGcm(aesKey, tagSizeInBytes: 16);
aes.Encrypt(
    nonce, plaintext, ciphertext, tag,
    associatedData: kemCt);     // binds the KEM exchange in
Diagnostic AesGcm.Encrypt is called without 'associatedData' but the same method uses hybrid KEM; bind the KEM ciphertext as associatedData

These analyzers are enforced as build errors across the library, tests, samples, and benchmarks (TreatWarningsAsErrors=true + the analyzer reference). The samples in this repo serve as a living regression test: any drift from analyzer-clean patterns fails the build.

Code you can copy

The three patterns this page demonstrates, ready to paste. Each block follows the analyzer-clean conventions — using on every disposable, KEM ciphertext bound into AAD where AES-GCM is wired by hand, verify before decapsulate.

Anonymous envelope — one call per direction

HybridEnvelope.Seal / Open
using PostQuantum.Hybrid;
using PostQuantum.Hybrid.Envelopes;

// Recipient holds the KEM key pair. `using` is mandatory — PQH001
// catches any local of these types declared without it.
using var recipient = HybridKem.GenerateKeyPair();

// Sender side — one call. No HKDF, no AesGcm, no nonce generation,
// no associatedData binding, no manual zeroization.
byte[] envelope = HybridEnvelope.Seal(recipient.PublicKey, plaintext);

// Recipient side — one call. Throws CryptographicException on any
// tamper / parse failure (fail-closed).
byte[] recovered = HybridEnvelope.Open(recipient.PrivateKey, envelope);

Source: samples/EnvelopesDemo.

Signed envelope — sender identity baked in

SignedHybridEnvelope.Seal / Open
using PostQuantum.Hybrid;
using PostQuantum.Hybrid.Envelopes;

using var sender    = HybridSignature.GenerateKeyPair();
using var recipient = HybridKem.GenerateKeyPair();

// Sign + seal in one call. The signature covers the full inner
// envelope; tamper anywhere fails verification before decryption.
byte[] envelope = SignedHybridEnvelope.Seal(
    sender.PrivateKey, recipient.PublicKey, plaintext);

// Verify (PQH003) THEN decapsulate. Open throws on any failure.
byte[] recovered = SignedHybridEnvelope.Open(
    sender.PublicKey, recipient.PrivateKey, envelope);

Verify-before-decapsulate (PQH003) is enforced internally — the recipient never sees plaintext that hasn't been authenticated.

ASP.NET Core wiring with rotating keys

AddPostQuantumHybrid + AddRotatingHybridKemKeys
using PostQuantum.Hybrid.AspNetCore;
using PostQuantum.Hybrid.Envelopes;

var builder = WebApplication.CreateBuilder(args);

// Wire static keys from configuration / KMS / IDataProtector.
builder.Services.AddPostQuantumHybrid(options =>
{
    options.KemPublicKeyPem        = File.ReadAllText("kem.pub.pem");
    options.KemPrivateKeyPem       = File.ReadAllText("kem.priv.pem");
    options.SignaturePublicKeyPem  = File.ReadAllText("sig.pub.pem");
    options.SignaturePrivateKeyPem = File.ReadAllText("sig.priv.pem");
});

// Or — rotating KEM keys, hot-swapped from disk by a sidecar:
// builder.Services.AddRotatingHybridKemKeys("kem.pub.pem", "kem.priv.pem");

var app = builder.Build();

app.MapPost("/seal", (string plaintext, IHybridKemKeyProvider keys) =>
{
    byte[] envelope = HybridEnvelope.Seal(
        keys.PublicKey, Encoding.UTF8.GetBytes(plaintext));
    return Results.Json(new { envelope = Convert.ToBase64String(envelope) });
});

app.Run();

Source: samples/KeyRotationDemo.

Use this in your project

Three packages cover the vast majority of usage. Install the base library, the recommended high-level API, and the analyzers; reach for the AspNetCore / TestingSupport / Templates packages only when you need them.

Add the packages

dotnet add package
dotnet add package PostQuantum.Hybrid
dotnet add package PostQuantum.Hybrid.Envelopes      # recommended high-level API
dotnet add package PostQuantum.Hybrid.Analyzers      # always — catches misuse at build time
dotnet add package PostQuantum.Hybrid.AspNetCore     # if you're in ASP.NET Core

Or start from a template

dotnet new
dotnet new install PostQuantum.Hybrid.Templates

# Console app pre-wired with PostQuantum.Hybrid + Analyzers.
dotnet new pqhybrid-app -n MyApp

# ASP.NET Core Minimal API with the AspNetCore wiring + analyzers.
dotnet new pqhybrid-webapi -n MyApi

# Console app showing the Envelopes one-call pattern.
dotnet new pqhybrid-envelope -n MyEnvelope

Which package do I want?

Package
Use it for
PostQuantum.Hybrid
Always. The core HybridKem + HybridSignature primitives. Direct use is the advanced path.
PostQuantum.Hybrid.Envelopes
Default for new code. One-call HybridEnvelope.Seal / Open and SignedHybridEnvelope.Seal / Open.
PostQuantum.Hybrid.Analyzers
Always install alongside the base library. Catches PQH001-PQH005 at build time. Skipping it is a hostile choice for your future self.
PostQuantum.Hybrid.AspNetCore
If you're in ASP.NET Core: DI extensions, IRotatingHybridKemKeyProvider, HybridEnvelopeDataProtector.
PostQuantum.Hybrid.TestingSupport
If you're writing tests against hybrid keys: cached deterministic key pairs, tamper-injection helpers, fake providers.
PostQuantum.Hybrid.Templates
If you want a fresh dotnet new scaffold instead of copying snippets.