- 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
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 |
|
moderate | low | component | implementation |
|
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 thinfuzz_target!wrapper delegating toalkblobs_fuzz_shared::key_encoding::fuzz_key_encoding(data); the[[bin]]entry lands infuzz/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 andarbitrary-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_bytesfailure is exactlyError::KeyInvalid— no other variant, no panic; everyfrom_git_oid_hexfailure isError::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_byteskey re-encodes throughas_bytesto byte-identical input; every derived key'sas_bytesdecodes back to the same key; - determinism: double invocation, same verdict (both parsers);
- total-order == byte order: over arbitrary accepted-key
collections, the derived
Ordsort equals theas_bytesbyte 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)) == bfor every accepted byte;try_from_byterejects0x00(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).
- no-panic on any byte sequence (
-
Seeds (via
gen_fuzz_seeds.pygainingkey_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 underfuzz/corpus/key_encoding/; thefuzz/.gitignoregains the negation lines (the alkstore final form: dir re-included, contents ignored,!corpus/key_encoding/seed-*). -
fuzz/README.mdtargets-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.rsexist;cargo fuzz buildbuilds the target (nightly, fromfuzz/)- All invariants above implemented in
sharedand exercised by both the fuzz path and the#[cfg(test)]replay module - Seed corpus committed (
fuzz/corpus/key_encoding/), generated bypython3 fuzz/gen_fuzz_seeds.py(deterministic — re-running is a no-op diff); gitignore negation in place cargo test --manifest-path fuzz/shared/Cargo.tomlgreen (replays the committed corpus through the invariants)sharedclippy/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 forkey_encoding's;fuzz_targets/placeholder.rsdeleted). - The
fuzzpackage's manifest gained noalkblobsdep — unlike alkstore's post-target manifest, the wrapper drives the shared crate only, solibfuzzer-sys+ the shared path dep stay sufficient. - No shared manifest changes — the known-answer vectors are
compared as strings (
to_hexhelper, nohexdep) and the registry-byte sweep usesAlgorithm::byte();arbitrarywas 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-KeyInvaliderror 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; tags0x00/0x03/0xFFbare and at both widths; uppercase/mixed-case/boundary-length/non-hex hex), and thearbitrary-structured path (strings + case/length/whitespace derivatives, byte prefixes). - The local recomputed rules (against which observed verdicts are
asserted):
from_bytesaccepts exactly<tag 0x01/0x02> + <digest_len tag-matching bytes>;from_git_oid_hexaccepts 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)) == bfor every accepted byte, digest_len pinned per tag, unaccepted bytes must not be assigned tags, reserved0x00rejects 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.