PostQuantum.DataProtection
0.1.0-preview.7
dotnet add package PostQuantum.DataProtection --version 0.1.0-preview.7
NuGet\Install-Package PostQuantum.DataProtection -Version 0.1.0-preview.7
<PackageReference Include="PostQuantum.DataProtection" Version="0.1.0-preview.7" />
<PackageVersion Include="PostQuantum.DataProtection" Version="0.1.0-preview.7" />
<PackageReference Include="PostQuantum.DataProtection" />
paket add PostQuantum.DataProtection --version 0.1.0-preview.7
#r "nuget: PostQuantum.DataProtection, 0.1.0-preview.7"
#:package PostQuantum.DataProtection@0.1.0-preview.7
#addin nuget:?package=PostQuantum.DataProtection&version=0.1.0-preview.7&prerelease
#tool nuget:?package=PostQuantum.DataProtection&version=0.1.0-preview.7&prerelease
PostQuantum.DataProtection
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.
What you get
Six 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.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. |
Why hybrid (and why ML-KEM-768)
- 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.
When to use this
| 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 |
Quick start
dotnet add package PostQuantum.DataProtection --prerelease
dotnet add package PostQuantum.KeyManagement --prerelease
using 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";
options.Mode = HybridKemMode.Hybrid;
options.RotationInterval = TimeSpan.FromDays(90); // optional auto-rotation
});
builder.Services.AddHealthChecks().AddPostQuantumDataProtection();
Configure from appsettings.json instead of code
{
"PostQuantumDataProtection": {
"KeyStorePath": "keys/pq-keystore.txt",
"Mode": "Hybrid",
"RotationInterval": "90.00:00:00"
}
}
builder.Services
.AddDataProtection()
.ProtectKeysWithPostQuantum(builder.Configuration.GetSection("PostQuantumDataProtection"));
Replace the file store with Azure Key Vault
builder.Services.AddPostQuantumDataProtectionAzureKeyVault(new Uri("https://my-vault.vault.azure.net/"));
Replace the file store with AWS Secrets Manager
builder.Services.AddPostQuantumDataProtectionAws(o => o.Region = Amazon.RegionEndpoint.USEast1);
Wire OpenTelemetry in one line
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddPostQuantumDataProtectionInstrumentation().AddPrometheusExporter())
.WithTracing(t => t.AddPostQuantumDataProtectionInstrumentation().AddOtlpExporter());
Unit-test consumer code without standing up a real chain
[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")));
}
Inspect a persisted key file from the CLI
dotnet tool install --global PostQuantum.DataProtection.Cli --prerelease
pq-dp inspect keys/data-protection/key-c6b3b03f-b73a-477b-92e5-d19ae0e0b5fd.xml
Format version: 1
Mode: Hybrid
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
What lands on disk
<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.
Performance
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.
Observability
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. |
Threat model and security posture
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.
Honest scope of "post-quantum"
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.
Migrating from another IXmlEncryptor
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.
Supply chain
- 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>.nupkg
Testing
dotnet build PostQuantum.DataProtection.slnx -c Release
dotnet test PostQuantum.DataProtection.slnx -c Release --no-build
87 tests across four suites — core, AzureKeyVault, Aws, Testing. Coverage gate at ≥ 85% line
/ ≥ 75% branch (current 89.5% line / 78.6% 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.
Requirements
- .NET 8.0, 9.0, or 10.0 (multi-targeted).
- A registered
PostQuantum.KeyManagementIContentKeyProviderfor the classical KEK layer.
Project status
0.1.0-preview.4. The API surface and the binary envelope wire format are versioned and
backward-compatible across the preview series (every envelope written by preview.1 decodes
under preview.4). Pre-1.0 the wire format may still change with a deliberate version bump
and a CHANGELOG.md note. The path to 1.0 is mapped in future.md.
The two remaining gates on 1.0 are calendar-time, not code-time: third-party cryptographic
review, and at least one cloud-backed key store in real production use. Both are tracked in
KNOWN-GAPS.md §D.
License
MIT © 2026 Paul Clark.
To God be the glory — 1 Corinthians 10:31.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.DataProtection.Abstractions (>= 8.0.27)
- Microsoft.AspNetCore.DataProtection.Extensions (>= 8.0.27)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
- PostQuantum.KeyManagement (>= 0.4.0-preview.2)
- System.Security.Cryptography.Xml (>= 8.0.3)
-
net8.0
- BouncyCastle.Cryptography (>= 2.6.2)
- Microsoft.AspNetCore.DataProtection.Abstractions (>= 8.0.27)
- Microsoft.AspNetCore.DataProtection.Extensions (>= 8.0.27)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
- PostQuantum.KeyManagement (>= 0.4.0-preview.2)
- System.Security.Cryptography.Xml (>= 8.0.3)
-
net9.0
- BouncyCastle.Cryptography (>= 2.6.2)
- Microsoft.AspNetCore.DataProtection.Abstractions (>= 8.0.27)
- Microsoft.AspNetCore.DataProtection.Extensions (>= 8.0.27)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
- PostQuantum.KeyManagement (>= 0.4.0-preview.2)
- System.Security.Cryptography.Xml (>= 8.0.3)
NuGet packages (6)
Showing the top 5 NuGet packages that depend on PostQuantum.DataProtection:
| Package | Downloads |
|---|---|
|
PostQuantum.DataProtection.Testing
Test fakes for PostQuantum.DataProtection. Provides an in-memory IPostQuantumKeyStore (FakePostQuantumKeyStore) so consumer projects can write unit tests against their own code without standing up a real ML-KEM keypair, a real classical KEK, or a real file on disk. One-line wiring: services.AddPostQuantumDataProtectionTesting(). |
|
|
PostQuantum.DataProtection.Aws
AWS Secrets Manager-backed IPostQuantumKeyStore for PostQuantum.DataProtection. Stores ML-KEM-768 keypairs as Secrets Manager secrets (one secret per keypair plus a small "active" pointer secret). The PQ secret key is already envelope-encrypted by the host IContentKeyProvider before it touches AWS, so AWS sees only opaque bytes — the service is a durable persistence target, not a new trust boundary. One-line wiring: services.AddPostQuantumDataProtectionAws(...). Multi-targets net8.0 / net9.0 / net10.0 with deterministic builds, SourceLink, and symbol packages. |
|
|
PostQuantum.DataProtection.Redis
StackExchange.Redis-backed IPostQuantumKeyStore for PostQuantum.DataProtection. Stores ML-KEM keypairs in a Redis hash and the active key id in a single pointer key. Suitable for hosts that already use Redis for ASP.NET Core Data Protection key persistence (PersistKeysToStackExchangeRedis). One-line wiring: services.AddPostQuantumDataProtectionRedis(connectionString). |
|
|
PostQuantum.DataProtection.AzureKeyVault
Azure Key Vault-backed IPostQuantumKeyStore for PostQuantum.DataProtection. Stores the ML-KEM-768 keypairs that wrap ASP.NET Core Data Protection keys in an Azure Key Vault Secrets vault — durable, audited, and shareable across replicas — without ever placing plaintext on disk. The PQ secret key is wrapped by the host IContentKeyProvider before it reaches Key Vault, so Key Vault sees only an opaque blob; the vault is a durable persistence target, not a new trust boundary. One-line wiring: services.AddPostQuantumDataProtectionAzureKeyVault(vaultUri). Multi-targets net8.0 / net9.0 / net10.0 with deterministic builds, SourceLink, and symbol packages. |
|
|
PostQuantum.DataProtection.OpenTelemetry
OpenTelemetry instrumentation helper for PostQuantum.DataProtection. One-line wiring: builder.AddPostQuantumDataProtectionInstrumentation() on a MeterProviderBuilder and a TracerProviderBuilder, and the PQ data-protection counters / histograms / activities flow into whatever exporter the host configures (Prometheus, OTLP, Console, etc.). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-preview.7 | 82 | 6/4/2026 |
| 0.1.0-preview.6 | 81 | 6/4/2026 |
| 0.1.0-preview.5 | 81 | 6/4/2026 |
| 0.1.0-preview.4 | 72 | 6/4/2026 |
0.1.0-preview.7: dependency hygiene. No source changes; no behaviour change. Reflects Dependabot updates already verified clean against the 112-test suite: AWSSDK.SecretsManager 3.7.500 -> 4.0.5 (major; AwsSecretsManagerPostQuantumKeyStore compiles + 4/4 tests pass against the new SDK shape), coverlet.collector 6.0.2 -> 10.0.1 (major; coverage collection still produces the cobertura XML the CI gate expects), Azure.Identity 1.13.1 -> 1.21.0, Azure.Security.KeyVault.Secrets 4.7.0 -> 4.11.0, BenchmarkDotNet 0.14.0 -> 0.15.8, plus GitHub Actions checkout v4 -> v6, setup-dotnet v4 -> v5, deploy-pages v4 -> v5 in the workflow files. — earlier 0.1.0-preview.6: polish + new companion package. Backward-compatible. (1) Extended NIST ACVP / FIPS 203 KATs (AcvpKatExtendedTests): pinned vectors for ML-KEM-512 AND ML-KEM-1024 in addition to ML-KEM-768. Standard-conformance verification across all three parameter sets on both the BC (net8/9) and BCL (net10) paths. (2) Cloud-store concurrency stress tests (CloudStoreConcurrencyTests): file store under 32-thread contention; parallel rotations that must serialise correctly and grow the keyring monotonically; PruneAsync during concurrent rotations refuses to delete the active keypair. (3) AOT smoke test sample (samples/AotSmokeTest): consumer project that publishes PublishAot=true to prove the net10 target is genuinely AOT-clean end to end. (4) Repo hygiene files: .editorconfig (file-scoped namespaces, naming conventions, line-ending rules), .gitattributes (LF normalization), global.json (SDK floor pin). (5) docs/logging.md: complete reference for every EventId the library emits (1, 2, 3, 4, 10, 11, 12, 20, 21, 22, 23) with category, severity, and alert/dashboard guidance. (6) docs/keystores.md: side-by-side comparison + decision tree for choosing among File / AzureKeyVault / Aws / Redis stores, including a disaster recovery matrix. (7) CHANGELOG.md caught up with full entries for preview.3 / .4 / .5. Companion package: PostQuantum.DataProtection.Fips 0.1.0-preview.6 — declarative FipsDeploymentMarker tripwire for FIPS 140-3 deployments. Hosts call FipsDeploymentMarker.Require(FipsPosture.FipsOnly) at startup, then EnforceFor(providerName, providerIsFipsValidated) in their health check; the marker is process-wide, one-way, and fails fast if the active ML-KEM provider doesn't satisfy the declared posture. The package does NOT embed a FIPS-validated ML-KEM implementation; that comes from the consumer (BouncyCastle FIPS, HSM SDK, or — when available — the .NET FIPS-validated provider). 112 tests passing (up from 100 in preview.5). — earlier 0.1.0-preview.5: BCL ML-KEM on net10 + AOT compatibility. Backward-compatible. (1) On the net10.0 target, ML-KEM operations now route through System.Security.Cryptography.MLKem (BCL) instead of BouncyCastle. The MlKem static class is partial across three files (MlKem.cs shared, MlKem.Bcl.cs net10 only, MlKem.BouncyCastle.cs net8/net9) selected by the NET10_0_OR_GREATER preprocessor symbol. Envelope byte format unchanged — envelopes written by a net8/9 host decode correctly on a net10 host and vice versa, verified by the NIST ACVP / FIPS 203 KAT running against the same vector on both targets. (2) The BouncyCastle.Cryptography package reference is conditional on Condition="'' != 'net10.0'" so net10 consumers don't pay for a dependency they don't use. (3) IsAotCompatible=true and IsTrimmable=true on the net10.0 target. The library emits zero IL2026/IL3050 warnings under the AOT analyser. The one reflection-using public method (ProtectKeysWithPostQuantum(IConfigurationSection)) carries [RequiresUnreferencedCode] + [RequiresDynamicCode] so the warning propagates cleanly to the consumer. (4) docs/aot.md rewritten as the per-target audit. (5) 100/100 tests still pass; the KAT tests now use MlKem.GenerateKeyPairFromSeed so they're path-agnostic. (6) Roadmap items §C3 and §C4 in KNOWN-GAPS.md closed. — earlier 0.1.0-preview.4: Adoption-focused pass on top of preview.3. Backward-compatible. (1) ILogger<T> integration with structured EventIds across encryptor, decryptor, key manager, and the new rotation hosted service. (2) Actionable error messages naming the offending option, the file path, and the fix. (3) IConfigurationSection overload for ProtectKeysWithPostQuantum(...) — appsettings.json-only wiring. (4) PostQuantumRotationHostedService driven by PostQuantumDataProtectionOptions.RotationInterval; TimeSpan.Zero disables, any positive value enables. (5) New companion packages: PostQuantum.DataProtection.Aws (AWS Secrets Manager keystore), PostQuantum.DataProtection.Testing (FakePostQuantumKeyStore + AddPostQuantumDataProtectionTesting() for consumer unit tests), PostQuantum.DataProtection.OpenTelemetry (one-line AddPostQuantumDataProtectionInstrumentation()). (6) Companion CLI: PostQuantum.DataProtection.Cli ("pq-dp") — pq-dp inspect <key.xml> prints envelope routing fields, no secret material. (7) Migration guide for DPAPI / Azure Key Vault key / Certificate -> PQ wrap without disruption. (8) Community files: CONTRIBUTING.md, CODE_OF_CONDUCT.md, GitHub issue templates, PR template, dependabot.yml. (9) DocFX site + GitHub Pages workflow. (10) WorkerService sample demonstrating non-ASP.NET-Core hosting. Carries forward from preview.3: NIST ACVP / FIPS 203 KAT, Meter + ActivitySource telemetry, concurrency stress tests, coverage CI gate, reproducible-build verification, BenchmarkDotNet harness, AOT honesty doc, fuzz harness. — earlier 0.1.0-preview.3: Production-readiness pass on top of preview.2. Backward-compatible — every envelope and keystore file written by preview.1/preview.2 still decodes byte-for-byte. No crypto-logic changes (ML-KEM-768 encapsulation, HKDF-SHA-256 derivation, AES-256-GCM are byte-for-byte identical). (1) NIST ACVP / FIPS 203 KAT (AcvpKatTests): pins BouncyCastle's ML-KEM-768 implementation against a real NIST-aligned vector from the post-quantum-cryptography/KAT repository — d || z seed → expected pk (1184 bytes), expected sk (2400 bytes), decapsulate the NIST ct → expected ss (32 bytes). This is FIPS 203 standard-conformance verification, not "our integration is self-consistent" verification. (2) System.Diagnostics.Metrics integration: Meter("PostQuantum.DataProtection") with counters (encryptions/decryptions/decrypt_failures/rotations), duration histograms, and an ActivitySource for distributed tracing. (3) Concurrency stress tests: 16-thread Parallel.ForEachAsync exercise of the encryptor / decryptor / RotateAsync — the documented "thread-safe" claim is now a tested claim. Plus a "first-run race" test that proves N concurrent first-callers see exactly one inaugural keypair. (4) 89.5% line / 78.6% branch code coverage measured with coverlet; CI gates at 85% line / 75% branch. (5) Reproducible-build verification: CI repacks and asserts byte-identical .nupkg SHA-256 across two independent packs. (6) AOT/trimming audit (docs/aot.md): not AOT-compatible today because BouncyCastle 2.6.x uses runtime reflection — documented honestly, IsTrimmable=false set explicitly. (7) BenchmarkDotNet harness (benchmarks/) with real numbers (docs/benchmarks.md): ML-KEM-768 keygen ~75µs, encap ~93µs, decap ~101µs; full envelope encrypt ~89µs, decrypt ~137µs on the reference host. (8) Production deployment guide (docs/deployment.md): pre-deploy checklist, multi-replica model, KEK rotation playbook, disaster recovery matrix, monitoring signal list. (9) Property-based "fuzz-lite" tests: 30 000 randomly-generated byte arrays driven through HybridKemEnvelope.Decode and PostQuantumKeyPair.Decode on every CI run, asserting the documented exception-type contract. Companion SharpFuzz harness in fuzz/ for AFL-driven exploration. Companion package: PostQuantum.DataProtection.AzureKeyVault 0.1.0-preview.3 — Azure Key Vault-backed IPostQuantumKeyStore with DefaultAzureCredential, narrow seam-tested implementation, in-memory fake for unit tests. 87 tests passing in the core suite (+ 6 in the AKV suite + 3 NIST ACVP KATs).