Known Gaps
This document is the place where we are deliberately honest about what PostQuantum.Hybrid does not do. Every entry here is either (a) on the roadmap, (b) intentionally out of scope, or (c) something we'd accept a PR for. The README, SECURITY.md, and docs/ are not allowed to make claims that contradict this file.
Documentation coherence is a release gate. Stale claims here are treated as bugs. If you find a gap not listed here, please open an issue.
Cryptographic gaps
No native X25519 / Ed25519
State: BouncyCastle is used for X25519 and Ed25519 on both target
frameworks because System.Security.Cryptography does not yet expose them
publicly.
Impact: One unavoidable dependency on BouncyCastle.Cryptography.
Plan: Switch to native implementations on whatever .NET version first exposes them. The public API does not need to change.
Published-vector validation runs unconditionally; sigGen vectors not covered
State: Three layers of FIPS-203/204 validation now ship:
NistKatTestsships three in-repo seed-based regression vectors per algorithm. Each vector pins the SHA-256 of both the derived public key and the derived private key. Onnet10.0the same seeds are fed through the nativeSystem.Security.Cryptography.MLKem/MLDsapaths (when the OS exposes them) and the resulting public keys are asserted bit-equal between backends.NistAcvpKatTestsvalidates both backends against vendored copies of NIST's published ACVP gen-val vectors for the final standards (tests/.../fixtures/nist-acvp/, fetched and filtered to ML-KEM-768 / ML-DSA-65 bytools/fetch-nist-acvp.ps1; provenance and license in the fixtures'NOTICE.md). Covered: ML-KEM keyGen (ek bit-equality + functional dk check), ML-KEM decapsulation (including NIST's implicit-rejection vectors), ML-DSA keyGen, and ML-DSA sigVer (including NIST's deliberate negative vectors). These run in the normal test suite — a missing fixture fails, it does not skip.NistKatRunnerparses the legacy.rspKAT format, gated onPQH_NIST_KAT_DIR/vars.NIST_KAT_MIRROR— retained for maintainers who want to point it at additional vector files; NIST published the final FIPS-203/204 vectors only in ACVP JSON form, so layer 2 is the authoritative check.
In addition, WycheproofTests runs vendored Wycheproof (C2SP) negative
vectors (fixtures/wycheproof/, fetched by tools/fetch-wycheproof.ps1)
covering X25519 (low-order / zero-shared-secret rejection, asserted
through HybridKem.Encapsulate as well as at the primitive), Ed25519
signature malleability and encoding negatives (driven through
HybridSignature.Verify), and ML-DSA-65 verify negatives on both
backends. Wycheproof does not ship ML-KEM vectors; candidate extra
negative material for ML-KEM (e.g. non-canonical encapsulation keys) is
C2SP/CCTV — not yet vendored, license review pending.
Impact (residual): ACVP sigGen vectors are not exercised: the
library signs with randomized ML-DSA ("hedged"), so deterministic
sigGen vectors would validate a code path we do not ship; sign
correctness is covered indirectly by sigVer + round-trip tests.
ML-KEM encapsulation AFT vectors (fixed m) are also skipped —
neither backend exposes deterministic encapsulation publicly.
Plan: Re-fetch fixtures periodically (the script pins the upstream commit SHA) and extend coverage if either backend exposes deterministic encapsulation / deterministic signing seams.
No formal proof of the v1 default combiner (but X-Wing preview ships)
State: The v1 default KEM combiner is HKDF-SHA256 with transcript
binding (see ADR 0003). We argue
informally that it inherits IND-CCA security from each component; we
do not have a written-up formal proof. The X-Wing combiner — which
does have a published security analysis — ships at algorithm-id
0x02 as a preview opt-in via
HybridKemAlgorithm.X25519MlKem768XWing (see
ADR 0013), and the full
IETF X-Wing KEM — wire-interoperable with other implementations and
validated against the draft's official vectors — ships at 0x03 via
HybridKemAlgorithm.XWing (see
ADR 0015).
Impact: Reviewers of the v1 default surface still must accept the
informal argument or reference the broader hybrid-KEM literature.
Callers who want the formal property today can opt in to
algorithm-id 0x02 or 0x03.
Plan: Either commission a write-up for the v1 default, or promote the X-Wing combiner to the default in a future major release. The v1 default remains HKDF for backward compatibility with shipped artifacts.
No side-channel hardening beyond what the primitives provide
State: The library does no constant-time comparison of our own (we delegate to the primitives), and we do not implement countermeasures for cache/timing/EM/power-analysis attacks beyond what BC and the BCL provide.
Impact: Co-located adversaries on the same physical host (shared hosting, VM-on-VM with bad isolation) may extract key material via side channels.
Plan: Document in SECURITY.md; defer hardening to deployment guidance. We do not plan to implement library-level side-channel mitigations.
Feature gaps
PKCS#8 / SPKI framing ships with placeholder OIDs (preview)
State: HybridKemPublicKey.ExportSubjectPublicKeyInfo() /
ImportSubjectPublicKeyInfo, HybridKemPrivateKey.ExportPkcs8PrivateKey()
/ ImportPkcs8PrivateKey, and the analogous four methods on the
signature key types all ship in v1.x. The DER encoding follows X.509
SPKI and PKCS#8 v1 verbatim; the inner key bytes are the existing
PostQuantum.Hybrid wire format including the algorithm-id byte. See
ADR 0014.
Impact (residual): The algorithm OIDs are placeholders under the
IANA Example PEN 1.3.6.1.4.1.32473 (RFC 5612). The IETF LAMPS WG's
composite-KEM / composite-signature drafts have not finalized their
OIDs yet; when they do, the codec will accept both the placeholder
OIDs (for backward compatibility with early adopters) and the
IETF-allocated values. Cross-implementation interop today is
limited to other PostQuantum.Hybrid consumers — third-party tools
that key OID lookups against the IETF registries will not recognise
our preview OIDs.
Plan: Watch
IETF LAMPS WG. When the
drafts allocate final OIDs, add them as additional accepted values
in Internal.Pkcs8SpkiCodec and document the migration in CHANGELOG.
No streaming sign/verify or streaming encapsulation
State: All APIs take complete byte spans. Signing a 4 GB file requires that file fit in memory.
Impact: Awkward for large-payload signing.
Plan: Add HybridSignature.SignPreHash(pubKey, hashAlg, hash, ...)
once .NET native ML-DSA exposes a generic prehash mode (it currently has
SignPreHash(string oid, ...) but with constraints). Track .NET 11.
No algorithm negotiation helpers
State: Negotiating "which hybrid combination does the other side support" is a protocol concern, not a primitives concern. The library exposes algorithm identifiers but does not implement any negotiation.
Impact: Callers must implement negotiation themselves.
Plan: Out of scope for the primitives package. See
PostQuantum.Hybrid.AspNetCore for DI-friendly key wiring patterns.
PostQuantum.Hybrid.AspNetCore — feature-complete for v1.0
State: v1.0 ships IRotatingHybridKemKeyProvider /
IRotatingHybridSignatureKeyProvider with FileSystemWatcher-based
atomic key rotation, plus a HybridEnvelopeDataProtector that adapts
the hybrid envelope construction to ASP.NET Core's IDataProtector
pipeline with per-purpose AAD binding.
Plan: No further follow-up planned for v1.0. v1.x may add a versioned multi-key reader for read-old-write-new rotation patterns.
PostQuantum.Hybrid.Analyzers — five rules shipped with code-fixes for all five
State: v1.0 ships PQH001 (undisposed sensitive types), PQH002
(SharedSecret without HKDF), PQH003 (Decapsulate-before-Verify
ordering), PQH004 (ignored Verify result), and PQH005 (AEAD without
KEM-ciphertext-as-associatedData binding). Code-fix providers ship
for all five rules: AddUsingDeclarationCodeFix (PQH001),
HkdfWrapSharedSecretCodeFix (PQH002), MoveVerifyBeforeDecapsulateCodeFix
(PQH003), WrapVerifyCodeFix (PQH004), and AddAssociatedDataCodeFix
(PQH005).
Plan: Additional rules will be added if real-world misuse patterns emerge.
Test / CI gaps
Coverage-guided fuzzing runs weekly in CI, not continuously
State: fuzz/PostQuantum.Hybrid.Fuzz runs ~7,200 random/mutated
inputs per execution through every parser entry point and asserts only
expected exception types fire. fuzz/PostQuantum.Hybrid.Fuzz.Sharp
provides a SharpFuzz harness with 7 targets, and
.github/workflows/fuzz.yml drives all of them under AFL++ weekly —
one time-boxed (~25 min) matrix job per target, seeded from
harness-generated minimal-valid blobs, failing on any crash and
uploading findings as artifacts. The AFL queue is now persisted between
runs (actions/cache, per-target, afl-cmin-minimized before save)
so coverage accumulates rather than restarting from minimal seeds each
Tuesday. What we still do not have is a dedicated machine fuzzing
continuously between the weekly runs.
Plan: Optional — stand up a long-running fuzz worker separate from CI if the weekly cadence ever surfaces a class of bug that several weeks of continuous coverage would have caught sooner. The persisted-queue path closes the in-CI half of this gap.
Cross-implementation interop covers ML-KEM-768 and ML-DSA-65; classical pending
State: .github/workflows/interop.yml runs two weekly jobs:
mlkem— ML-KEM-768 vs Go's standard-librarycrypto/mlkem(FIPS 203): keygen equality from a shared seed + encapsulate/decapsulate in both directions, on BouncyCastle (net8.0 + net10.0) and the nativeSystem.Security.Cryptography.MLKembackend when the runner's OS exposes it.mldsa— ML-DSA-65 vscloudflare/circl'ssign/mldsa/mldsa65(FIPS 204): keygen equality from a shared 32-byte ξ seed + Go signs / .NET verifies + .NET signs / Go verifies, with empty context, on the same BouncyCastle + native matrix.
X25519 and Ed25519 still have no cross-implementation leg in this
suite. They are covered by the in-repo Wycheproof negative vectors
(tests/.../fixtures/wycheproof/) driven through HybridKem.Encapsulate
and HybridSignature.Verify, so the gap is narrower than for the PQ
primitives but not zero.
Plan: Optional — add an X25519/Ed25519 leg against Go's stdlib
crypto/ecdh + crypto/ed25519 if a future bug suggests our
BouncyCastle delegation has drifted. Otherwise the Wycheproof coverage
is treated as sufficient for the classical side.
Mutation testing CI shipped with fixed threshold gate
State: .github/workflows/mutation.yml runs Stryker weekly and
uploads HTML/JSON reports as artifacts. stryker-config.json sets
thresholds.break = 70, so the workflow fails when the mutation score
drops below 70% — the regression gate is wired with a fixed bound.
Per-PR baseline-comparison ("worse than last main run") is not in
place; we rely on the absolute threshold instead.
Plan: Tighten the threshold as the score climbs (today's score is the floor we keep raising). Optionally add a per-PR baseline diff if the fixed-threshold approach proves too coarse.
Benchmark baseline + comparison shipped, gate active on Linux
State: .github/workflows/benchmark.yml runs BenchmarkDotNet
weekly on both TFMs. tools/compare-benchmarks.ps1 compares the
results against pinned benchmarks/baseline-{tfm}-{os}.json files
(baseline-net10.0-linux.json for the CI gate, *-windows.json
retained for local maintainer use). The Linux gate runs without
continue-on-error — regressions past the per-baseline threshold
fail the workflow.
The Linux threshold is set to 35% (vs 25% on the Windows baselines)
because the GH-hosted runner tier varies more day-to-day and the CI
bench uses --warmupCount 2 --iterationCount 3 for speed.
Plan: Tighten the Linux threshold as the runner stabilises (or swap to a self-hosted runner if noise becomes the limiting factor).
Distribution gaps
No author-signed package (provenance attestations ship instead)
State: Released .nupkg files carry GitHub build provenance
attestations (Sigstore): release.yml runs
actions/attest-build-provenance, so any consumer can verify a
package was built by this repo's release workflow from a given tag
with gh attestation verify <file>.nupkg --owner systemslibrarian.
Builds are deterministic and source-linked via SourceLink. What the
packages do not have is NuGet author signing
(Authenticode) — that requires a paid code-signing certificate.
Plan: SignPath (or another sponsored-cert program) integration for author signing once the project qualifies.
SBOM CI workflow shipped
State: .github/workflows/sbom.yml generates a CycloneDX SBOM per
package on release and attaches them as release assets.
No follow-up required for v1.0.
(v1.x.)
API baseline checking shipped
State: Microsoft.CodeAnalysis.PublicApiAnalyzers is wired into
src/PostQuantum.Hybrid/PostQuantum.Hybrid.csproj and the public
surface is locked in src/PostQuantum.Hybrid/PublicAPI.Shipped.txt /
PublicAPI.Unshipped.txt. Renaming, removing, or adding a public type
is now a build-time error until the appropriate API file is updated.
No follow-up required for v1.0.