SIGN IN SIGN UP

feat(rustd): afd_crypto — envelope encryption with parity proven, not asserted

§1 of M176. Seven modules, eight test files, 55 tests, and no new Zig.

The envelope is the Zig daemon's, ported rather than reinvented: a per-row Data
Encryption Key (DEK) wrapping the payload, that key wrapped under the process
Key Encryption Key (KEK), both binding the same associated data so a row cannot
be replayed under another workspace or name. Six components, `kek_version` 2,
AES-256-GCM throughout, matching `crypto_store.zig::openEnvelopeAt` column for
column.

## Parity without a fixture emitter

The spec asked for a Zig-side emitter writing committed fixtures, mirroring how
M175 did the wire. It does not transfer. M175's emitter compiles under bare
`zig run` because every `src/lib/contract` import is a sibling path;
`crypto_primitives.zig` imports `common` and `log`, so an envelope emitter needs
an entry in the Zig BUILD GRAPH — modifying a frozen daemon to generate test
vectors for its own port. Indy cut it. Three oracles replace it, none synthetic:

- **The primitive** — published AES-256-GCM vectors, transcribed as constants
  and driven through this crate's own seal path with a pinned DEK and nonce, so
  what is proven is the bytes THIS crate emits.
- **The format** — the associated data asserted byte-for-byte, including the
  asymmetry a tidy-up would silently break: `std.ascii.allocLowerString` is
  applied to the workspace id ALONE, never the key name. Lowercasing both would
  orphan every credential stored under a capitalised name.
- **The suite itself** — `tests/zig_parity.rs` re-runs all six
  `crypto_primitives.zig` tests with identical inputs: the same `TEST_KEK_HEX`,
  the same `"super-secret-api-key-12345"`, the same `workspace-a`/`workspace-b`
  contrast, the same `bad_tag[0] ^= 0x01`. A seventh Zig test fails
  `zig_pure_crypto_suite_is_fully_mirrored`, so the claim cannot go stale
  quietly. Nothing is compiled or executed outside this crate.

## Visibility, answering the M175 review note

Private fields throughout. `afd_wire`'s public fields are transparent serde
payloads with no invariant to guard; these types carry invariants that matter,
and a public field on a secret newtype lets a caller move the buffer out and
bypass zeroize — Invariant 5 defeated by syntax, not by a logic error. Every
constructor is fallible where an invariant exists (M-STRONG-TYPES-GUARD), the
`Debug`/`Display` impls are hand-written with a redaction test behind them
(M-PUBLIC-DEBUG), and the one system call is a mockable core behind `test-util`
(M-MOCKABLE-SYSCALLS, M-TEST-UTIL).

## Three structural fixes, not three more tests

- `Mac256::compute` returned `Result` for an error arm no test could enter —
  HMAC accepts a key of any length and this one is a fixed-width array. Dead
  code wearing a safety jacket, and an impossible error pushed onto every
  caller. Now infallible, with M-LINT-OVERRIDE-EXPECT covering the scoped
  override that replaced it.
- The mock's poisoned-mutex arm became `PoisonError::into_inner`: a plain queue
  has no invariant a panic could break, so that branch was unreachable too.
- gitleaks flagged the case-16 vector key. No allowlist entry was added — that
  is the user's call, and the default is to restructure. The specification's
  AES-256 key for that case IS one 128-bit block stated twice, so it is composed
  from its half: more faithful than transcribing the doubled form, and it leaves
  no key-shaped literal behind. The vectors still reproduce, which is itself the
  proof the composition is right.

## Lanes widened

`lint-rustd` and `test-unit-rustd` ran without `--all-features`, so every
`test-util`-gated mock — the code whose entire job is to be exercised by tests —
was compiled by neither. Both now carry it.

`afd_core` gains the two registry codes these errors answer, `UZ-VAULT-001` and
`UZ-INTERNAL-003`, both already declared in the Zig registry of record.

Coverage 92.26% → 99.62% lines, error paths 100% (all seven `ErrorKind`
variants carry a negative test). The residue is `entropy.rs:47`, the closure
mapping a `getrandom` failure — reaching it needs the kernel's entropy pool to
fail, which cannot be provoked without mocking the mock. The bar is NOT moved
for it here; it is named in the spec so §7 closes it or moves it once, with
every unreachable line listed.

Gates: harness-verify ALL GATES GREEN · lint-rustd ✓ · test-unit-rustd 235
passed / 0 failed (baseline 180, +55) · check-version 0.26.2 · gitleaks no
leaks · no file over 300 lines (largest 285).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
K
Kishore Kumar committed
cb219a6a4296fffda880eaafde17ad367ba754f4
Parent: d47a9d3