--- id: core-hashing-keys name: Hashing & keys — the canonical derivation and the one-way-door encoding status: completed depends_on: [core-error-model] scope: narrow risk: medium impact: project level: implementation tags: [wave-1, keys] --- ## Description Implement the content-address layer (`src/keys.rs` or `keys/` + `hashing`, one module per file per convention) per ADR-002 and hashing-and-keys.md. This is the crate's declared one-way door — key bytes persist in backend rows and filenames with no migration story — so the encoding is implemented exactly to the ADR text and reviewed line-by-line at the wave gate. - **`Digest`** — algorithm + digest pair. Algorithm enum with exactly two members: `GitBlobSha256` (canonical), `GitBlobSha1` (tolerated legacy). BLAKE3 is absent from the crate entirely (ADR-002 §Decision class 3). - **Canonical derivation**: `H("blob \0" + content)` — SHA-256 canonical, SHA-1 same derivation. The preamble is the algorithm's domain separation, not a special case (ADR-002 §The preamble as abstraction). Known-length derivation is the primary constructor; the unknown-length path's derivation-over-staged-bytes restart is wave 2 facade logic, not here. - **Interop constructor**: `from_git_oid_hex` shape (POC #1 finding 4) — a pre-computed pair from an external git oid, both algorithms. - **`Key`** — the typed wrapper; `as_bytes()` / `from_bytes()` are the only conversions across the backend boundary. Physical encoding per ADR-002: `1 algorithm byte + digest bytes` — `0x01` = git-blob-sha-256 (32-byte digest; total 33), `0x02` = git-blob-sha-1 (20-byte digest; total 21). `0x00` is reserved and never assigned. - **Validation**: truncated keys and unknown algorithm bytes are rejects (`Error::KeyInvalid`), never guesses (the POC's length-checking test is the standard). - **Sort stability**: fixed-length enum-tagged keys sort in total order (required for the sweep's live-set diffs) — asserted by test. - **Registry constants**: the algorithm bytes are the ADR-002 registry; new algorithms require a new ADR (doc comment states this). - **Interop test (the load-bearing test)**: byte-exact derivation against the `git hash-object` CLI for both algorithms (POC #1 findings 4/5's standard — validated against git, not just hardcoded vectors), plus the external-oid round-trip: `from_git_oid_hex` → `as_bytes` → `from_bytes` → equal, and tagged-key coexistence of both algorithms in one flat collection. - **Named design point for the review gate**: oid hex parsing policy — git oids are lowercase hex; pin lowercase-only acceptance (reject uppercase as malformed) or case-insensitive acceptance, with the choice recorded. Lean lowercase-only (git's own output is always lowercase; strictness at the boundary matches rejects-never-guesses) unless implementation finds a concrete interop reason otherwise. - Deps: `sha2`, `sha1` (RustCrypto), `hex` — added to the manifest by this task. ## Acceptance Criteria - [x] `Digest`/`Key`/algorithm enum per ADR-002; encoding byte-exact (33/21 total lengths; 0x01/0x02 tags; 0x00 rejected) - [x] Canonical derivation matches `git hash-object` byte-exact for both algorithms (CLI-driven test, both a small and a multi-KiB content case) - [x] `from_git_oid_hex` round-trips external oids into pool keys - [x] Rejects: truncated keys, unknown algorithm bytes (incl. 0x00, 0xFF), wrong digest lengths → `Error::KeyInvalid`, never guesses - [x] Total-order sort stability asserted (mixed-algorithm collection sorts deterministically) - [x] Doc comments carry the one-way-door warning and the registry note (new algorithm = new ADR) - [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` green ## References - docs/architecture/hashing-and-keys.md - docs/architecture/decisions/002-canonical-hash-and-key-encoding.md - docs/research/poc-trait-dispatch-findings.md findings 4/5 - AGENTS.md convention 5 (the one-way door) ## Notes - Module layout: two files, one module per file — `src/hashing.rs` (the hash side: `Algorithm` + `Digest` + the derivation + the interop constructor) and `src/keys.rs` (the key side: the typed `Key` wrapper + the physical encoding). The scaffold's stub doc on `hashing.rs` was replaced by the full module doc. Both re-exported from `lib.rs` (`Algorithm`, `Digest`, `Key`, mirroring the `Error` re-export). - **Named design point resolved — oid hex parsing policy: lowercase-only acceptance.** `git hash-object`'s own output is always lowercase; strictness at the boundary is the rejects-never-guesses posture. Uppercase oids are rejected as malformed (`Error::KeyInvalid`) — the check is a byte scan for `[0-9a-f]` before decode (the `hex` crate's `decode` alone would accept uppercase). Policy stated verbatim on `Digest::from_git_oid_hex` and covered by test. If an external interop consumer ever needs uppercase acceptance, that is a change at this boundary with its own test. - `Algorithm` is declared `GitBlobSha256` before `GitBlobSha1` so derived `Ord` matches registry-byte order; the sort-stability test asserts derived order == `as_bytes()` byte order. `Digest` keeps its fields private behind `from_parts` (pub(crate)), so every digest's length stays pinned by its algorithm. - Registry constants live in `hashing::registry` (`GIT_BLOB_SHA256` 0x01, `GIT_BLOB_SHA1` 0x02, `RESERVED` 0x00) with the new-ADR doc note. `Algorithm::try_from_byte` is public; `Key::from_bytes` is the boundary consumer. - Deps pinned as the task scoped: `sha2 0.10`, `sha1 0.10`, `hex 0.4` (RustCrypto + hex). The generic derivation helper is bound over the RustCrypto digest trait so the derivation body has zero per-algorithm special-casing; the match arms pick only the hasher. - No `unwrap`/`expect`/panic outside tests; library paths are `Result`/`Option` only (error.rs's `Error::KeyInvalid` carries the boundary rejections). ## Summary `src/hashing.rs` + `src/keys.rs` implement the content-address layer per ADR-002, the crate's declared one-way door. `Algorithm` (exactly `GitBlobSha256` canonical / `GitBlobSha1` tolerated; BLAKE3 absent), `Digest` (known-length derivation `H("blob \0" + content)` as primary constructor; `from_git_oid_hex` interop constructor with lowercase-only acceptance, both oid formats selected by length), and `Key` (typed wrapper; `as_bytes`/`from_bytes` the only backend-boundary conversions; `1 algorithm byte + digest bytes` — 0x01+32, 0x02+20, 33/21 total) with rejects-never-guesses validation (`Error::KeyInvalid` for truncated/unknown-tag/wrong-length/reserved-0x00; never a guess). Registry constants in `hashing::registry` with the new-ADR doc note; one-way-door warnings in both module docs. Tests: byte-exact derivation against the live `git hash-object` CLI (the load-bearing test — both algorithms × empty/short/binary-NUL/75-KiB, sha256 repos via `git init --object-format=sha256` per POC finding 4's harness gotcha), external-oid round-trip into pool keys, both-oid-formats coexistence in one flat pool, hardcoded git-computed vectors, encoding byte-exactness, full rejection matrix, and derived-order == byte-order sort stability on mixed-algorithm collections. Verified: `cargo test` (15 passed), `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, and `cargo test --all-features` all green. Deps added: `sha1`, `sha2`, `hex`.