Skip to content

fix(security): resolve findings from the Fable adversarial bug hunt#231

Open
systemslibrarian wants to merge 3 commits into
mainfrom
security/fable-bug-hunt-fixes
Open

fix(security): resolve findings from the Fable adversarial bug hunt#231
systemslibrarian wants to merge 3 commits into
mainfrom
security/fable-bug-hunt-fixes

Conversation

@systemslibrarian

Copy link
Copy Markdown
Owner

Fixes the 28 findings in Fable-Bug-Hunt-Results.md (1 critical, 2 high, 8 medium, 17 low) surfaced by the multi-agent Fable bug hunt. On-disk/wire formats changed backward-compatibly; PoC regression tests added for the top findings.

Top fixes

  • C1 (critical): encode.py/crypto.py no longer emit a PQ-mode manifest with no PQ ciphertext. pack_manifest fails closed, so --high-security can't wipe the source after producing a permanently undecodable artifact.
  • H1 (high): FountainDecoder (Rust try_new + Python) and SchrodingerManifest.unpack bounds-check attacker-controlled k_blocks/block_count/block_size before allocating. WASM/PyO3 callers surface a clean error instead of an unbounded (~100 GB) allocation.
  • H2 (high): master-ratchet state KEK is now Argon2id-derived (format MRCV3; legacy MRCV2 still readable, migrated on next save).

Medium/low themes

X25519 low-order (all-zero) shared-secret rejection in pure_crypto + wasm; effective zeroization across crypto_core, rust_crypto handles, and Python ratchet/x25519; stream-chunk MAC binds byte offset (anti reorder/replay); Argon2id at-rest KDF for X25519 key storage; fail-closed stego/signing gates; OQS ML-KEM decapsulation fix; palette Lehmer-code overflow caps; Verus/doc overclaim corrections; ratchet handle-leak fixes.

Intentionally partial (SECURITY TODO markers + doc)

  • L8 — signing key is now zeroized and compute_public_key_commitment is invoked, but fully binding the pk-commitment into the manifest HMAC needs a coordinated decode_gif.py + manifest-version change (follow-up).
  • M4derive_frame_master_key_legacy warns loudly and supports allow_legacy=False, but defaults to True for compat; fail-closed default needs a decode_gif.py --allow-legacy-frame-mac flag.

Verification

  • crypto_core (native/pq/wasm) and rust_crypto build clean; all Rust tests pass.
  • Python suite green except pre-existing Windows-only environmental failures (missing pyzbar/libzbar QR DLL, cp1252 codec on emoji in templates/Cargo.toml, Unix-only resource module) — these are unrelated to the changes and should pass in Linux CI.
  • New tests/test_fable_bug_hunt_poc.py executes the C1/H1 fixes (3 passing).

🤖 Generated with Claude Code

systemslibrarian and others added 3 commits July 5, 2026 17:39
Fixes the findings documented in Fable-Bug-Hunt-Results.md (1 critical,
2 high, 8 medium, 17 low) across the Rust crates and the Python pipeline,
with backward-compatible on-disk/wire formats and executable PoC
regression tests for the top findings.

Highlights:
- C1: encode.py/crypto.py no longer emit a PQ-mode manifest without a PQ
  ciphertext; pack_manifest fails closed, so --high-security can no longer
  wipe the source after producing a permanently undecodable artifact.
- H1: FountainDecoder (Rust try_new + Python) and SchrodingerManifest.unpack
  now bounds-check attacker-controlled k_blocks/block_count/block_size
  before allocating (mirrors the encoder's guards). WASM/PyO3 callers
  surface a clean error instead of an unbounded allocation.
- H2: master-ratchet state KEK is now Argon2id-derived (format MRCV3;
  legacy MRCV2 still readable, migrated on next save).
- Crypto correctness/hardening: X25519 low-order (all-zero) shared-secret
  rejection in pure_crypto + wasm; effective zeroization of key material
  across crypto_core, rust_crypto handles, and the Python ratchet/x25519
  paths; stream-chunk MAC now binds byte offset (anti reorder/replay);
  Argon2id at-rest KDFs for X25519 key storage; fail-closed stego/signing
  gates; OQS ML-KEM decapsulation fix; palette Lehmer-code overflow caps;
  Verus/doc overclaim corrections; ratchet handle-leak fixes.

Two items are intentionally partial (see SECURITY TODO markers + doc):
L8 (pk-commitment HMAC binding needs a coordinated decode_gif.py change)
and M4 (legacy frame-MAC fail-closed default needs a decode_gif.py flag).

Verification: crypto_core (native/pq/wasm) and rust_crypto build clean;
all Rust tests pass; Python suite green except pre-existing Windows-only
environmental failures (missing pyzbar/libzbar DLL, cp1252 codec on emoji,
Unix-only resource module). New tests/test_fable_bug_hunt_poc.py asserts
the C1/H1 fixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l-closed legacy frame-MAC

Finishes the two items left partial in the Fable bug-hunt branch.

L8 (INV-041): bind the signer's in-band public key to the password-derived
key material. The encoder MACs compute_public_key_commitment(pk) (previously
dead code) with a key derived from the same handle that frame-MACs the
signature-chunk transport, and appends the 32-byte tag to the signature blob.
The decoder recomputes it over the transported pk and rejects a mismatch
(fail-closed), defeating in-band key substitution. The tag is detected purely
by blob length, so there is no signature/manifest format-version break — old
artifacts carry no tag and old decoders ignore it. (Literal binding into the
manifest HMAC is infeasible: the signing pubkey isn't available at
manifest-HMAC-check time on decode.)

M4: decode_gif now fails closed by default on the legacy fast-SHA-256
frame-MAC KDF (a captured legacy frame is an offline password oracle). If the
modern Argon2id-derived frame MAC fails, the legacy fallback is refused unless
the caller passes the existing --allow-legacy opt-in, which decodes pre-v2
files with a loud warning.

Tests: tests/test_fable_bug_hunt_poc.py adds test_l8_pk_commitment_binds_
signing_key_to_password (legit key accepted, substituted key rejected) and
test_m4_legacy_frame_mac_is_fail_closed_by_default. Existing encode/signing
(test_encode.py, test_phase5_modules.py: 143) and crypto/ratchet/security
suites (237) pass with no regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve the CI gate failures on the branch:
- lint (cargo fmt): reformat the use-block in rust_crypto/src/fountain.rs and
  the touched crypto_core files to satisfy `cargo fmt --check`.
- preflight (mypy): wrap struct.unpack(...)[0] in int() in master_ratchet.py
  so _read_generation_watermark returns int, not Any (no-any-return).
- security (pytest): test_decode_gif_frame_mac_legacy_valid now passes
  allow_legacy=True — the M4 change made the legacy fast-SHA-256 frame-MAC
  fallback fail-closed by default, so the test must opt in like the CLI's
  --allow-legacy.

Local: test_decode_gif.py + test_adversarial.py (73) pass; mypy clean on
meow_decoder/; cargo fmt --check clean for both crates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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