Skip to content

fix: 16 hunt-confirmed bugs, stage-uniform errors, the KS2 vector, safer tooling - #79

Merged
systemslibrarian merged 4 commits into
mainfrom
fixes/bug-hunt
Sep 5, 2026
Merged

systemslibrarian merged 4 commits into
mainfrom
fixes/bug-hunt

Conversation

@systemslibrarian

Copy link
Copy Markdown
Owner

Stacked on #78 (which stacks on #77) — merge in order; this then retargets cleanly.

Every change traces to an adversarially verified finding from the 27-agent bug hunt (each bug independently re-traced through real control flow before fixing — several refuted candidates were discarded) or to an item both external reviews demanded. No on-disk byte meaning or reader acceptance changes anywhere.

The 16 confirmed bugs, fixed

Class Fix
Key hygiene (medium, found twice independently) Engine chunk staging buffers zeroed with the content key — for PQKF traffic they held a full private key on the heap after export/import; LocalKek ctor zeroes its cloned KEK when AesGcm throws
Exception contract ×4 Empty byte-passphrase + Argon2id header no longer escapes Konscious's raw ArgumentException (family was steered by an unauthenticated header byte); PNSE no longer mislabeled "wrong kind of key file"; bytes-API capacity int overflow near Array.MaxLength (3 sites); oversized wrapInfo message
Data loss ×4 (one high) pqfe sign --signature <input> silently replaced the signed file (or private key!) with the signature and exited 0 — now refused at library and CLI level; WebUpload Keygen overwrote existing key pairs (orphaning every prior upload) — now CreateNew, private-first, 0600, half-pair cleanup; CLI overwrite refusal now holds across the passphrase prompt (was check-then-act); second Ctrl+C always terminates
Azure provider ×2 Null-KeyId client: rejected all legitimate containers and accepted hostile /-prefixed ids (null + "/" wildcard); recorded key id now control-character-sanitized in messages
Rust ×2 encrypt_bytes_with validated nothing (chunk 0 = infinite loop; bad prefix = panic; long salt = silently undecryptable output); KS3/4 block parsers had zero fuzz coverage → new decrypt_hybrid target with the pinned conformance key, wired into nightly CI
Samples/tooling ×3 Backup run no longer dies on a locked file; pqfe-web no longer hangs forever on worker crash; docs link checker handles titles + %XX

Stage-uniform errors (deliberate behavior change)

All in-library key-dependent failures now throw one identical message, no inner exception — wrong passphrase / wrong hybrid or inline recipient key / wrong KEK / tampered wrap / tampered body are indistinguishable across stages. The old distinct unwrap messages were a runtime-probed key-possession oracle (external review #2's sharpest finding). Pinned by a new cross-stage test asserting message equality and null inners. Cloud-KMS providers' remote-unwrap messages documented as the residual in KNOWN-GAPS.

Vector 9 — the last unpinned path, closed

The inline ML-KEM-768 recipient path (KeySource 2) had no KAT anywhere — its tests self-skip without platform ML-KEM. Generated once (and round-trip-verified) inside a Debian 13 container with OpenSSL 3.5, committed + hash-pinned on every host, decrypt-exercised wherever PqKeyPair.IsSupported. Verified live on Linux: 40/40 including the first known execution of RecipientTests.

Also

pqfe decrypt --untrusted; the Blazor demo decrypts under Untrusted limits (its own documented anti-pattern, fixed); TEST-VECTORS/KNOWN-GAPS/SECURITY/FUZZING updated to match.

Verification

  • macOS: 317×2 + 9 + 3 tests green; Rust 26 tests + both fuzz targets build.
  • Linux container (ML-KEM live): 40/40 targeted (KS2 KAT, Recipient, Hybrid incl. new cross-stage/KS4, Zeroization).
  • Docs guard green; solution builds 0 errors (4 pre-existing benchmark warnings only).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sym7RJ7ehNhbMXytE5rMmS

systemslibrarian and others added 3 commits September 4, 2026 06:35
…ps it that way

Merged follow-through from two independent reviews (the multi-agent security
audit and an external product review), scoped strictly inside the 1.x format
freeze: docs, metadata, and one XML-docs addition. No byte meaning, reader
acceptance, or API change.

Resurrect the docs-consistency guard:
- .github/workflows/docs-consistency.yml now exists — the workflow the 1.7.0
  changelog advertised but never shipped. It runs check-docs-consistency.sh on
  every push and PR.
- The script is portable (no GNU-only grep -P, so it runs on macOS) and now
  also checks the SECURITY.md supported-versions cell, the ROADMAP-2.0 "Today"
  cell, the AUDIT-SCOPE pinned tag, artifact-name examples, release-download /
  reproducibility / clone-at-tag commands, and install snippets in all Markdown
  (CHANGELOG excluded as history). Verified green on this tree and on macOS.

Version sweep to 1.7.1 (was scattered across 1.0.1–1.7.0): SECURITY.md table,
ROADMAP "Now" heading, KNOWN-GAPS + GOLD-STANDARD review markers, ROADMAP-2.0
Today cell, AUDIT-SCOPE pinned tag (v1.7.1 / e0d332a), README + SUPPLY-CHAIN +
REPRODUCIBLE-BUILDS worked examples, ANNOUNCE snippets, CHANGELOG compare-link
footer (adds 1.7.0/1.7.1). PackageValidationBaselineVersion 1.5.0 → 1.7.1
(pack verified against the published baseline). PackageReleaseNotes reworded
version-agnostic so they cannot silently go stale on nuget.org.

Recalibrate claims to what the code does and tests pin:
- SECURITY.md: no-oracle bullet now states the true property (one exception
  type; byte-identical messages for key-dependent failures; structural
  diagnostics are key-independent); integrity scoped to the authenticated
  envelope (trailing bytes after the final frame are ignored — KNOWN-GAPS);
  bounded-work notes tight ceilings are opt-in via PqDecryptionLimits.Untrusted;
  Dependencies now names Signing's BouncyCastle use and the Rust demo core's
  pre-1.0 RustCrypto crates; SBOM list covers all eight per-package SBOMs.
- SUPPLY-CHAIN.md: artifact table lists all nine packages; snupkg files are
  documented as NOT attested (verifying one fails by construction); the
  reproducibility script's 4-of-9 package coverage is stated; "every push"
  heading acknowledges scheduled jobs.
- GOLD-STANDARD.md: binary-compat row downgraded to ⚠️ — validation is
  overridden off in Aws/AzureKeyVault/Gcp/Analyzers (stale pre-first-publish
  gate); PublicAPI baseline count corrected to seven packages.

Correct the rotation story (KEY-MANAGEMENT.md): header-only CEK rewrap cannot
ship on format v2 — the header is AAD for every frame, so a rewrap re-tags
every chunk, and doing so under the same CEK and nonces with new AAD emits
second GCM tags for existing (key, nonce) pairs while old copies coexist,
making the GCM authentication key recoverable. Rotation is now documented as a
streaming transcode (fresh CEK + nonce prefix, full-file I/O, detached
signatures invalidated); true header-only rewrap is banked as a format-v3
candidate. Status list also gains the shipped Gcp provider and multi-recipient
moves to past tense.

Format-doc errata (doc-only, matching the frozen implementation):
- KEY-FILE-FORMAT.md: a PQKF body is a KeySource-1 passphrase container whose
  KeyParams carry KdfId 1 (PBKDF2) or 2 (Argon2id) — previous text conflated
  KeySource with KdfId and would misdirect an independent implementer.
- FILE-FORMAT.md: KeySource-2 HKDF formulas now state salt = absent.

KNOWN-GAPS.md gains five verified entries: header-only rewrap (v3 candidate),
IContentKeyProvider key-freshness contract and its GCM consequences, passphrase
Unicode normalization, non-interruptible KDF derivation, and default
permissions on decrypted output. IContentKeyProvider.WrapNewKeyAsync's XML docs
now state why the fresh-key contract is load-bearing (build clean, 0 warnings).

CLAUDE.md's version-bump checklist now names every reference class that
drifted, so the next release sweep is complete by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sym7RJ7ehNhbMXytE5rMmS
…heal the fuzzers

The assurance batch from the merged review follow-through (multi-agent audit +
two external reviews), all inside the 1.x format freeze — no byte meaning or
reader acceptance changes; the corpus grows, nothing in it changes.

Conformance corpus (+9 vectors, both implementations green):
- Deterministic two-chunk positive pos-passphrase-pbkdf2-multichunk.
- Argon2id bounds negatives (memory/iterations/parallelism/salt): the Rust
  reader's cost checks previously had zero test coverage anywhere.
- Framing negatives: clean-boundary truncation (first exercise of the Rust
  saw_final path), frame swap (first Rust ordinal-binding coverage), dropped
  final frame, and a cross-container frame transplant backing SECURITY.md's
  "splicing between containers" claim with committed bytes.
- The generator now reuses the pinned hybrid lenient vector instead of
  re-randomizing it on regen, and regeneration reproduces the corpus
  bit-for-bit (verified: regen produced only additions).

Limits: PqDecryptionLimits.MaxArgon2Parallelism (default 255 = format maximum,
so acceptance is unchanged; Untrusted caps at 8), enforced pre-derivation
beside the memory/iteration ceilings. A hostile header could previously demand
255 Argon2 lanes and no limit could say no.

Key-file hardening: PqKeyFileFormat.Decrypt output is a fixed-capacity buffer
sized to the expected key rather than the whole file — kills the
attacker-proportional allocation and the wrong-type reallocation that escaped
the zeroing finally. Same exceptions, same messages.

Fuzz health: both .NET targets now run under Untrusted limits (format-legal
2 GiB-KDF headers stalled single iterations ~20 min and failed 13/30 recent
scheduled runs), plus -timeout=20 and timeout-*/oom-* artifact upload in CI.

Tests: ZeroizationTests (engine success/failure/cancellation + Dispose paths —
a documented defense with previously zero coverage that has regressed before),
cross-container transplant, KeySource-4 block swap/strip/duplicate,
SHA256SUMS integrity (was verified by nothing), Argon2 parallelism limits.
Suite: 636 passing across net8.0+net10.0; cargo conformance suite green.

Hygiene: analyzer rules PQFE101-104 moved to the shipped ledger (1.5.0);
System.Formats.Asn1 transitive floored at 8.0.1 in the analyzer test project
(CVE-2024-38095, test-only).

Docs made true: SECURITY.md discloses the stage-distinguishable error caveat
(key-unwrap vs body-auth — recorded in KNOWN-GAPS with the unification decision
explicitly open); CONFORMANCE.md §3 names the corpus vector for each MUST;
REPRODUCIBLE-BUILDS.md retracts the cross-OS claim — a macOS arm64 rebuild of
v1.7.1 differs from the published package (reproduced locally with the repo's
own verifier; Linux CI remains the demonstrated envelope); README says
"provenance-attested releases" (there is no author/tag signing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sym7RJ7ehNhbMXytE5rMmS
…d safer tooling

Everything here traces to an adversarially verified finding (27-agent bug hunt,
each bug independently re-traced before fixing) or to a doubly-recommended item
from the external reviews. No on-disk byte meaning or reader acceptance changes.

Key hygiene: engine chunk staging buffers zeroed alongside the content key (for
PQKF traffic they held a full private key on the heap after export/import);
LocalKek ctor zeroes its cloned KEK if AesGcm construction throws.

Exception contract: empty byte-passphrase vs an Argon2id header no longer
escapes as Konscious's raw ArgumentException; PqKeyFileFormat no longer
mislabels PlatformNotSupportedException as wrong-key-type; bytes-API capacity
hints no longer overflow int near Array.MaxLength (core + Hybrid); oversized
provider wrapInfo gets its intended message.

Data-loss guards: PqSigner and pqfe sign refuse signature==input/key (silently
destroyed the signed file or private key, exit 0); WebUpload Keygen opens
CreateNew (an overwrite orphaned every prior upload), writes private-first with
0600, cleans up half-pairs; CLI overwrite refusal claims the path atomically
across the passphrase prompt; second Ctrl+C always terminates.

Azure: null-KeyId clients no longer reject everything with a nonsense pin (nor
accept '/'-prefixed hostile ids via null+"/" wildcard); recorded key id is
control-character-sanitized before reaching exception text.

Rust: encrypt_bytes_with validates parameters (chunk 0 looped forever, bad
nonce-prefix length panicked, long salt silently made undecryptable output);
new decrypt_hybrid fuzz target reaches the KS3/4 block parsers with the pinned
conformance key, wired into CI.

Stage-uniform errors (behavior change): every in-library key-dependent failure
now throws one identical message with no inner exception — wrong passphrase,
wrong hybrid/inline recipient key, wrong KEK, tampered wrap, tampered body are
indistinguishable across stages. The old distinct unwrap messages let a service
exposing raw errors act as a key-possession oracle (externally probed). Pinned
by a new cross-stage test; cloud-KMS providers documented as residual.

Vector 9: the inline ML-KEM-768 recipient path gets its first pinned vector
(decrypt-only, generated + round-trip-verified on Linux/OpenSSL 3.5), closing
the last unpinned key-establishment path; hash-pinned on every host, executed
wherever platform ML-KEM exists (verified 40/40 on Linux, first known run).

Tooling: pqfe decrypt --untrusted; Blazor demo decrypts under Untrusted limits;
Backup sample survives locked files; pqfe-web no longer hangs if the worker
crashes; docs link checker handles titles and %XX targets.

Verified: dotnet 317x2+9+3 green (macOS), 40/40 targeted on Linux (ML-KEM
live), cargo 26 tests + both fuzz targets build, docs guard green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sym7RJ7ehNhbMXytE5rMmS
@systemslibrarian
systemslibrarian changed the base branch from assurance/tests-vectors-hardening to main September 5, 2026 00:13
@systemslibrarian
systemslibrarian merged commit c91ed17 into main Sep 5, 2026
10 checks passed
@systemslibrarian
systemslibrarian deleted the fixes/bug-hunt branch September 5, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant