fix: 16 hunt-confirmed bugs, stage-uniform errors, the KS2 vector, safer tooling - #79
Merged
Merged
Conversation
…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
changed the base branch from
assurance/tests-vectors-hardening
to
main
September 5, 2026 00:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
AesGcmthrowsArgumentException(family was steered by an unauthenticated header byte); PNSE no longer mislabeled "wrong kind of key file"; bytes-API capacityintoverflow nearArray.MaxLength(3 sites); oversizedwrapInfomessagepqfe 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) — nowCreateNew, private-first,0600, half-pair cleanup; CLI overwrite refusal now holds across the passphrase prompt (was check-then-act); second Ctrl+C always terminatesKeyIdclient: rejected all legitimate containers and accepted hostile/-prefixed ids (null + "/"wildcard); recorded key id now control-character-sanitized in messagesencrypt_bytes_withvalidated nothing (chunk 0 = infinite loop; bad prefix = panic; long salt = silently undecryptable output); KS3/4 block parsers had zero fuzz coverage → newdecrypt_hybridtarget with the pinned conformance key, wired into nightly CI%XXStage-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 underUntrustedlimits (its own documented anti-pattern, fixed); TEST-VECTORS/KNOWN-GAPS/SECURITY/FUZZING updated to match.Verification
🤖 Generated with Claude Code
https://claude.ai/code/session_01Sym7RJ7ehNhbMXytE5rMmS