Files
alkblobs/tasks/fuzz-target-key-encoding.md
T
glm-5.3-flash 0fb451f945 docs(unicode-hygiene): gitea-flagged confusable characters out of repo prose
- en-dash (U+2013) -> ASCII hyphen across src/tests/docs/tasks
- multiply sign (U+00D7) -> x in the same prose
- logical or (U+2228) -> slash-list "(pins / markers / sources)" in
  src/gc.rs and its prose mirrors (gc-protocol.md, review-wave-2.md)
- task file's self-referencing mentions rewritten to codepoint/ASCII
  phrasing; fuzz/target and fuzz/corpus never touched
- rule recorded as AGENTS.md convention 11

Verification:
- cargo test (149+54+4), cargo test --all-features (328+125+4) green
- cargo clippy --all-targets / --all-features -D warnings clean
- cargo fmt --check clean; fuzz/shared corpus-replay gate green
- git grep -nP '[\x{2013}\x{00D7}\x{2228}]' over tracked files: empty
2026-10-16 08:31:59 +00:00

10 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
fuzz-target-key-encoding Fuzz target — `key_encoding` (the one-way door's parser cluster) completed
fuzz-scaffold
moderate low component implementation
wave-11
fuzzing
keys

Description

The plan's first named target cluster: the key-encoding parser — the one-way door's reject-never-guesses discipline (AGENTS convention 5; ADR-002; hashing-and-keys.md §Key encoding). Entirely public API — no exposure needed:

  • Key::from_bytes / Key::as_bytes (src/keys.rs) — the backend-boundary decode/encode.

  • Digest::from_git_oid_hex (src/hashing.rs) — the interop hex parse (lowercase-only, length selects the algorithm).

  • Algorithm::try_from_byte / byte / digest_len (src/hashing.rs) — the registry-byte mapping.

  • Digest::derive / Algorithm::derive / Key::derive — the canonical derivation (determinism + the known-answer git vectors).

  • fuzz_targets/key_encoding.rs — the thin fuzz_target! wrapper delegating to alkblobs_fuzz_shared::key_encoding::fuzz_key_encoding(data); the [[bin]] entry lands in fuzz/Cargo.toml (replacing the scaffold placeholder per its lifecycle note).

  • shared/src/key_encoding.rs — the invariant logic, with the #[cfg(test)] corpus-replay module (the gate). Input: raw &[u8] (the byte path) plus lossy-decoded strings and arbitrary-derived strings (the hex path). Invariants:

    • no-panic on any byte sequence (Key::from_bytes) and any string (Digest::from_git_oid_hex) — the baseline;
    • typed-reject partition: every from_bytes failure is exactly Error::KeyInvalid — no other variant, no panic; every from_git_oid_hex failure is Error::KeyInvalid (the rejects-never-guesses posture, demonstrated across the input space — truncated, over-long, unknown/reserved tags, wrong digest lengths, uppercase/wrong-length/non-hex hex);
    • round-trip closure: every accepted from_bytes key re-encodes through as_bytes to byte-identical input; every derived key's as_bytes decodes back to the same key;
    • determinism: double invocation, same verdict (both parsers);
    • total-order == byte order: over arbitrary accepted-key collections, the derived Ord sort equals the as_bytes byte sort (the sweep's live-set-diff requirement, ADR-002's sort stability clause);
    • hex posture: lowercase-only acceptance (uppercase rejected), length selects the algorithm (40 → sha-1, 64 → sha-256, every other length rejected), decoded length == digest_len;
    • registry agreement: byte(try_from_byte(b)) == b for every accepted byte; try_from_byte rejects 0x00 (reserved), 0x03, 0xFF, …;
    • derive determinism: derive(content, len) stable across invocations; the git-computed known-answer vectors pinned in the replay module (the unit tests' vectors re-pinned on the corpus).
  • Seeds (via gen_fuzz_seeds.py gaining key_encoding_seeds()): valid keys both algorithms from fixed contents; every truncation length of a valid key; unknown/reserved tags (0x00, 0x03, 0xFF); tag/digest-length mismatches (sha-256 tag + 20 bytes, sha-1 tag + 32); over-long; hex forms — the git-vector oids lowercase, uppercase, mixed case, 39/41/63/65 lengths, non-hex chars, empty, whitespace, unicode; derive known-answer pairs. Corpus committed under fuzz/corpus/key_encoding/; the fuzz/.gitignore gains the negation lines (the alkstore final form: dir re-included, contents ignored, !corpus/key_encoding/seed-*).

  • fuzz/README.md targets-table row.

Campaign flags are the campaign task's concern (-max_len=4096 -use_value_profile=1 starting point); this task lands target + seeds

  • replay.

Acceptance Criteria

  • fuzz_targets/key_encoding.rs + shared/src/key_encoding.rs exist; cargo fuzz build builds the target (nightly, from fuzz/)
  • All invariants above implemented in shared and exercised by both the fuzz path and the #[cfg(test)] replay module
  • Seed corpus committed (fuzz/corpus/key_encoding/), generated by python3 fuzz/gen_fuzz_seeds.py (deterministic — re-running is a no-op diff); gitignore negation in place
  • cargo test --manifest-path fuzz/shared/Cargo.toml green (replays the committed corpus through the invariants)
  • shared clippy/fmt gates green (cargo clippy --manifest-path fuzz/shared/Cargo.toml --all-targets -- -D warnings, fmt --check)
  • Root workspace gates still green; the placeholder target replaced (or removed if this is the first target to land)

References

  • docs/plans/implementation.md §Wave 11 (the key-encoding parser target, named)
  • docs/architecture/hashing-and-keys.md §Key encoding; §The Key/Digest surface (the rejects-never-guesses standard, the sort stability requirement)
  • docs/architecture/decisions/002-canonical-hash-and-key-encoding.md (the one-way door ADR)
  • src/keys.rs, src/hashing.rs (the surfaces; their unit tests are the invariant seeds' source)
  • /workspace/@alkdev/alkstore/tasks/fuzz-target-payload-codec.md (the family target-task pattern: wrapper + invariant module + replay + seeds + README row)
  • /workspace/@alkdev/alkstore/fuzz/shared/src/name_validation.rs (the invariant-module + replay-test pattern)

Notes

  • First target landed → the placeholder is removed, not replaced (the scaffold's lifecycle note; fuzz/Cargo.toml's placeholder bin entry swapped for key_encoding's; fuzz_targets/placeholder.rs deleted).
  • The fuzz package's manifest gained no alkblobs dep — unlike alkstore's post-target manifest, the wrapper drives the shared crate only, so libfuzzer-sys + the shared path dep stay sufficient.
  • No shared manifest changes — the known-answer vectors are compared as strings (to_hex helper, no hex dep) and the registry-byte sweep uses Algorithm::byte(); arbitrary was already present.
  • The typed-reject partition runs over verdict enums, not the raw Result (the name_validation pattern): KeyVerdict::Accept(key) / HexVerdict::Accept(digest) narrow the typed paths, and any non-KeyInvalid error panics at construction — the fuzz path demonstrates the partition across the raw bytes, lossy-decoded strings, the fixed batteries (every truncation length of both key widths; over-long; tags 0x00/0x03/0xFF bare and at both widths; uppercase/mixed-case/boundary-length/non-hex hex), and the arbitrary-structured path (strings + case/length/whitespace derivatives, byte prefixes).
  • The local recomputed rules (against which observed verdicts are asserted): from_bytes accepts exactly <tag 0x01/0x02> + <digest_len tag-matching bytes>; from_git_oid_hex accepts exactly lowercase hex of byte length 40 (→ sha-1) / 64 (→ sha-256). The total-order invariant is asserted both directions (derived sort == byte sort; every reparse of the byte sort stays accepted and re-encodes identically) over mixed-algorithm collections built from the input's chunks plus fixed contents.
  • Registry agreement is a full byte-space sweep (0u8..=255), not a battery — byte(try_from_byte(b)) == b for every accepted byte, digest_len pinned per tag, unaccepted bytes must not be assigned tags, reserved 0x00 rejects explicitly.
  • Seeds came out at 119 (valid keys x3 contents x2 algorithms, derivation-input preambles, raw contents, every truncation of a valid key both widths, over-long, reserved/unknown tags bare and full-width, tag/digest-length mismatches both directions, git-vector hex lowercase/uppercase/mixed/truncated/extended, 39/41/63/65 'a' lengths, empty/whitespace/unicode/non-hex shapes). Replay floor ≥ 32; the corpus replay reads ../corpus/key_encoding.
  • A 90 s detached smoke campaign (run-detached.sh key_encoding -max_len=4096, fork mode): ~112k execs, cov 769 / ft 1335, zero oom/timeout/crash; the grown (SHA-named) units it dropped into the corpus dir were deleted — seeds remain exactly the committed 119 (the campaign/gate task owns grown corpora).

Summary

Landed the wave-11 key_encoding fuzz target: fuzz_targets/key_encoding.rs (the thin fuzz_target! wrapper — the first real target, replacing the scaffold placeholder's bin entry) and fuzz/shared/src/key_encoding.rs implementing the invariants: no-panic baseline via the typed-reject partition (from_bytes failures exactly Error::KeyInvalid, proven against the recomputed accept rule tag ∈ {0x01,0x02} + matching digest_len; from_git_oid_hex failures exactly Error::KeyInvalid, proven against lowercase-only/length-selects-algorithm/decoded-length == digest_len), round-trip closure both ways (accepted keys byte-identical re-encodes; derived keys decode back), double-invocation determinism on both parsers, the Ord-sort == byte-sort agreement over mixed accepted-key collections with reparses staying accepted, the registry agreement (full 0u8..=255 sweep, reserved 0x00 rejects), and derive determinism with the four git-computed known-answer vectors re-pinned (Key::derive agreeing with the canonical derivation, including at the u64::MAX boundary). 119 deterministic seeds committed under fuzz/corpus/key_encoding/ via gen_fuzz_seeds.py's new key_encoding_seeds() (Python-computed git oids — re-run is a no-op diff), with the .gitignore negation lines (the alkstore final form) and the README targets-table row. No manifest changes beyond the bin swap — the fuzz package needs no alkblobs dep at this altitude.

Verified: cargo fuzz build green from fuzz/ (nightly/ASAN binary at target/x86_64-unknown-linux-gnu/release/key_encoding); cargo test --manifest-path fuzz/shared/Cargo.toml green (corpus replay 119/119 + the without-corpus and battery probes); shared clippy -D warnings + fmt --check green; a 90 s detached smoke campaign ran ~112k execs with zero oom/timeout/crash; root workspace cargo fmt --check / cargo clippy --all-targets -- -D warnings / cargo clippy --all-features --all-targets -- -D warnings / cargo test / cargo test --all-features all green (pg suites skip cleanly without the harness). Campaign flags remain the campaigns-and-gate task's concern.