Post-quantum / hybrid key wrapping for ASP.NET Core Data Protection. One line in
Program.csand every persisted Data Protection key — cookie keys, antiforgery keys, session tickets, Blazor circuit tokens, everyIDataProtectorpayload at rest — is wrapped under an ML-KEM-768 (FIPS 203) + AES-256-GCM hybrid envelope.
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("keys"))
.ProtectKeysWithPostQuantum(o => o.KeyStorePath = "keys/pq-keystore.txt");That's it.
Seven packages, one CLI, four end-to-end samples. Mix and match.
| Package | Purpose |
|---|---|
PostQuantum.DataProtection |
The core. Encryptor / decryptor, key manager, file-backed key store, DI extensions, health check, scheduled rotation, retention/prune helper, metrics + tracing. Supports ML-KEM-512, ML-KEM-768, ML-KEM-1024 + HKDF and X-Wing combiners. |
PostQuantum.DataProtection.AzureKeyVault |
IPostQuantumKeyStore backed by Azure Key Vault Secrets. One line: services.AddPostQuantumDataProtectionAzureKeyVault(vaultUri). |
PostQuantum.DataProtection.Aws |
IPostQuantumKeyStore backed by AWS Secrets Manager. One line: services.AddPostQuantumDataProtectionAws(). |
PostQuantum.DataProtection.Redis |
IPostQuantumKeyStore backed by Redis. Natural pair with PersistKeysToStackExchangeRedis. |
PostQuantum.DataProtection.OpenTelemetry |
One-line OTel wiring. .AddPostQuantumDataProtectionInstrumentation() on a MeterProviderBuilder / TracerProviderBuilder. |
PostQuantum.DataProtection.Testing |
FakePostQuantumKeyStore + AddPostQuantumDataProtectionTesting() for consumer unit tests — no cloud, no disk. |
PostQuantum.DataProtection.Fips |
Deployment guidance + declarative tripwire for running under FIPS 140-3. Ships FipsDeploymentMarker to commit the process to a FIPS-only posture and fail fast when the active provider isn't validated. Not a FIPS-validated ML-KEM implementation — you bring the validated provider. |
PostQuantum.DataProtection.Cli (pq-dp) |
dotnet tool for inspecting persisted DP key XML files. No secrets emitted. |
Samples:
| Sample | What it shows |
|---|---|
AspNetCore.Sample |
Minimal-API host with cookie auth + antiforgery, both PQ-protected. |
WorkerService.Sample |
Worker Service using PQ outside ASP.NET Core, with scheduled rotation. |
Blazor.Sample |
Blazor Server with PQ-protected circuit + cookie + IDataProtector roundtrip. |
MultiReplica.Sample |
Two simulated replicas sharing one Key Vault — proves the multi-replica shape end to end. |
- Hybrid = belt-and-braces. The AES-256-GCM key that wraps each Data Protection element is
HKDF-derived from both the ML-KEM shared secret and a classical secret from your
IContentKeyProvider. An attacker has to defeat both layers to recover plaintext. A classically-broken passphrase still has ML-KEM in the way; a (hypothetical) quantum-broken ML-KEM still has the classical wrap in the way. This is the IETF hybrid-KEM pattern. - ML-KEM-768 is the general-purpose pick. NIST category 3 (≈ 192-bit classical strength) —
the level NIST recommends for general use. We default there; switch to 512 or 1024 when the
selectable parameter set lands (see
KNOWN-GAPS.md§C1). - Verified against FIPS 203. Our integration is pinned by a NIST-aligned KAT vector: given
the seed bytes from
post-quantum-cryptography/KAT, BouncyCastle's ML-KEM-768 produces the exactpk(1184 bytes),sk(2400 bytes), and decapsulates the published ciphertext to the published shared secret — byte for byte. The test runs on every PR.
| Situation | Verdict |
|---|---|
| You run ASP.NET Core, Blazor, or any .NET host with Data Protection. | ✅ Yes |
| Your threat model includes "harvest-now, decrypt-later" against the key store. | ✅ Especially |
| You want defense-in-depth without ripping out your existing Data Protection wiring. | ✅ One line |
| You need a FIPS 140-3 validated module today. | ❌ Roadmap — see §C6 |
| You want PQ session-key negotiation over the wire. | ❌ Different layer — TLS hybrid groups belong in the stack |
dotnet add package PostQuantum.DataProtection --prerelease
dotnet add package PostQuantum.KeyManagement --prereleaseusing Microsoft.AspNetCore.DataProtection;
using PostQuantum.DataProtection.Hybrid;
using PostQuantum.KeyManagement;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddPostQuantumKeyManagement(options =>
{
options.Passphrase = builder.Configuration["KeyManagement:Passphrase"]
?? throw new InvalidOperationException("Missing passphrase");
options.WorkFactor = KekWorkFactor.Moderate;
options.KeyringPath = "keys/host-keyring.bin";
});
builder.Services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("keys/data-protection"))
.ProtectKeysWithPostQuantum(options =>
{
options.KeyStorePath = "keys/pq-keystore.txt";
// Mode defaults to HybridKemMode.XWingHybrid (recommended). Set Hybrid or MlKemOnly to override.
options.RotationInterval = TimeSpan.FromDays(90); // optional auto-rotation; null disables
});
builder.Services.AddHealthChecks().AddPostQuantumDataProtection();One-call shortcut.
builder.Services.AddPostQuantumDataProtection("keys/pq-keystore.txt")callsAddDataProtection()andProtectKeysWithPostQuantum(...)for you and returns theIDataProtectionBuilderso you can still chain.PersistKeysToFileSystem(...). By default the chain initializes eagerly at startup (ValidateOnStartup), so a missing KEK, a wrong passphrase, or an unwritable keystore fails fast at boot rather than on the first request. Full option list:docs/configuration.md.
builder.Services
.AddDataProtection()
.ProtectKeysWithPostQuantum(builder.Configuration.GetSection("PostQuantumDataProtection"));builder.Services.AddPostQuantumDataProtectionAzureKeyVault(new Uri("https://my-vault.vault.azure.net/"));builder.Services.AddPostQuantumDataProtectionAws(o => o.Region = Amazon.RegionEndpoint.USEast1);builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddPostQuantumDataProtectionInstrumentation().AddPrometheusExporter())
.WithTracing(t => t.AddPostQuantumDataProtectionInstrumentation().AddOtlpExporter());[Fact]
public void My_service_protects_and_unprotects()
{
var services = new ServiceCollection();
services.AddPostQuantumDataProtectionTesting();
using ServiceProvider sp = services.BuildServiceProvider();
IDataProtector p = sp.GetRequiredService<IDataProtectionProvider>().CreateProtector("p");
Assert.Equal("hi", p.Unprotect(p.Protect("hi")));
}dotnet tool install --global PostQuantum.DataProtection.Cli --prerelease
pq-dp inspect keys/data-protection/key-c6b3b03f-….xml # one envelope's routing fields
pq-dp keys list keys/pq-keystore.txt # every keypair: id, algorithm, age, active
pq-dp doctor keys/pq-keystore.txt # keystore health check (exit 1 on problems)
pq-dp verify keys/data-protection # decode every persisted envelope (exit 1 on failures)All pq-dp commands are read-only, emit no secrets, and never need the host KEK — safe for incident
response. Example inspect output:
Format version: 1
Mode: XWingHybrid
KEM algorithm: ML-KEM-768
Public key id: pq-mlkem768-4411e03446f5
KEM ciphertext: 1088 bytes
Classical wrap: 236 chars
AES-GCM nonce: 12 bytes
AES-GCM tag: 16 bytes
AES-GCM ciphertext: 120 bytes
<encryptedSecret decryptorType="PostQuantum.DataProtection.PostQuantumXmlDecryptor, …">
<pqEnvelope xmlns="https://schemas.systemslibrarian.dev/pq-dataprotection/2026/01"
version="1" mode="Hybrid" publicKeyId="pq-mlkem768-…">
BASE64URL…
</pqEnvelope>
</encryptedSecret>The Base64Url blob is a versioned binary envelope: [FormatVersion | Mode | KemAlgorithm | PublicKeyId | KemCiphertext | ClassicalWrappedKey | Nonce | Tag | Ciphertext]. Every field is
length-prefixed and capped at 1 MiB. Full byte layout in docs/wire-format.md.
Real numbers from BenchmarkDotNet on a modern x86_64 host (full table in
docs/benchmarks.md):
| Operation | Mean |
|---|---|
| ML-KEM-768 encapsulate | ~93 µs |
| ML-KEM-768 decapsulate | ~101 µs |
| Full envelope encrypt (Hybrid) | ~89 µs |
| Full envelope decrypt (Hybrid) | ~137 µs |
Envelope work happens at DP key persist / load — startup-path, not request-path. Cookie
verification, antiforgery validation, and IDataProtector.Unprotect all read keys from the
in-memory key ring; they never go through the envelope.
The library publishes a Meter and an ActivitySource named PostQuantum.DataProtection.
Subscribe with OpenTelemetry (or any IMeterListener):
| Signal | What it tells you |
|---|---|
pq_dataprotection.encryptions |
Rate of fresh DP keys being wrapped. Tagged by mode. |
pq_dataprotection.decryptions |
Rate of envelope reads. Tagged by mode. |
pq_dataprotection.decrypt_failures |
Tagged by reason: wrong_xml_element, malformed_envelope, unsupported_algorithm, unknown_keypair, auth_failed. Page on any non-zero rate. |
pq_dataprotection.rotations |
Rate of PQ keypair rotations. Should be quiet outside scheduled windows. |
pq_dataprotection.encrypt.duration / decrypt.duration |
Histograms in ms. P95 should sit < 1 ms on modern hardware. |
AddPostQuantumDataProtection() health check |
Real roundtrip on every probe. Page if Unhealthy. |
docs/threat-model.md is the precise statement: attacker model (A1 → A6 vs.
B1 → B4) and 10 numbered security invariants the library is designed to hold. Each invariant
corresponds to one or more tests.
SECURITY.md covers reporting vulnerabilities (use GitHub Security Advisories,
not public issues), supported versions, and the recommended deployment posture.
docs/deployment.md is the production operations checklist: pre-deploy
verification, multi-replica model, KEK rotation playbook, disaster recovery matrix, monitoring
signals.
The library wraps Data Protection keys at rest with a verified-against-FIPS-203 ML-KEM-768 + AES-256-GCM
hybrid envelope. That is the entire claim. We do not claim to make ASP.NET Core's request
pipeline post-quantum, to negotiate PQ session keys, or to be FIPS 140-3 validated today. See
KNOWN-GAPS.md for the full breakdown of what's deliberate, what's roadmap, and
what's been closed across previews.
Whether you're on ProtectKeysWithDpapiNG, ProtectKeysWithAzureKeyVault (the key wrap, not the
secrets store), or ProtectKeysWithCertificate, the migration is non-disruptive — existing keys
keep decrypting under their old decryptor type while fresh keys roll forward under PQ. Step by
step in docs/migration.md.
- Deterministic builds with CI-enforced reproducibility (the CI repacks twice and asserts
byte-identical
.nupkgSHA-256). - SourceLink + symbol packages so debuggers fetch the exact GitHub source for every commit you ship.
EnablePackageValidationcatches API surface drift between framework targets.TreatWarningsAsErrorspluslatest-recommendedanalyzers — zero-warning policy across the repo.- Pinned transitive overrides for known-vulnerable packages (e.g.
System.Security.Cryptography.Xmlpinned to the patched 8.0.3 to avoid GHSA-37gx-xxp4-5rgx and GHSA-w3x6-4m5h-cxqf). - SBOM-friendly metadata. Every dependency is an explicit
<PackageReference>. Recipes for CycloneDX and the Microsoft SBOM tool indocs/supply-chain.md.
To verify a published package:
sourcelink test PostQuantum.DataProtection.<version>.nupkg
# Reproduce the build from the matching commit and compare SHA-256:
git checkout v<version>
dotnet pack -c Release -o /tmp/local
sha256sum /tmp/local/PostQuantum.DataProtection.<version>.nupkgdotnet build PostQuantum.DataProtection.slnx -c Release
dotnet test PostQuantum.DataProtection.slnx -c Release --no-build131 tests across five suites — core (including the FIPS deployment-marker tripwire tests),
AzureKeyVault, Aws, Redis, Testing — including combiner
known-answer (KAT) tests, a parameter-set agility matrix, a future-version-rejection test, and a
multi-replica rotation-lock concurrency proof. Coverage gate at ≥ 85% line / ≥ 75% branch.
Property-based fuzz-lite contract tests drive 30 000 random inputs through both decoders on every
run; a standalone SharpFuzz harness in fuzz/ is set up for AFL-driven exploration.
- .NET 8.0, 9.0, or 10.0 (multi-targeted).
- A registered
PostQuantum.KeyManagementIContentKeyProviderfor the classical KEK layer.
1.0.2 — stable release. The public API and the binary envelope/keypair formats are
frozen at version 1 (decoders reject unknown versions and modes); SemVer is in force. Depends on the
stable PostQuantum.KeyManagement 1.0.0.
Honest limitations carried into 1.0 (documented, not hidden — see
KNOWN-GAPS.md §D):
- Not yet independently audited. Written with care, known-answer tests, fuzzing, a published threat model, and an auditable crypto spec — but no third-party review yet. It's on the roadmap; weigh it against your own risk tolerance.
- Cloud stores not yet production-proven. The Azure Key Vault / AWS / Redis stores are built and tested, including a multi-replica rotation-lock concurrency proof, but haven't yet been run in a named production deployment.
Neither is a "post-quantum is incomplete" caveat — the cryptographic claim (ML-KEM + AES-256-GCM
hybrid wrap of Data Protection keys at rest) is fully delivered. The roadmap is in
future.md.
MIT © 2026 Paul Clark.
To God be the glory — 1 Corinthians 10:31.
{ "PostQuantumDataProtection": { "KeyStorePath": "keys/pq-keystore.txt", "Mode": "XWingHybrid", "RotationInterval": "90.00:00:00" } }