carlos / contra Public

shared/1 — Golden vectors: the cross-language crypto contract Feature Request open

Do this first. It blocks both tracks.

Three languages will implement the same crypto — Go on the instance, JavaScript in the console, Swift on the Mac. DESIGN.md says they must be byte-identical, pinned by shared test vectors. This item produces those vectors before either implementation exists, so neither can quietly become the reference.

This is not busywork: cross-platform KDF drift is exactly the bug class behind the 1Password PRF mismatch that shaped our whole recovery design.

Deliverable: a language-neutral vector file (JSON) plus a Go test that consumes it, covering every derivation in DESIGN.md:

  • Argon2id at m=64MiB, t=3, p=1, 32-byte tag — with the exact salt and passphrase inputs pinned
  • Every HKDF label: contra/v1/login, contra/v1/kek (salted with the account key), contra/v1/rc-login, contra/v1/rc-kek, contra/v1/prf-kek
  • The KCV construction, HMAC-SHA-256(KEK, "contra/v1/kcv")[..16]
  • P-256 keypair derivation from a seed, including the rejection sampling mod n — pin a seed that requires a rejection, or the branch goes untested
  • Crockford Base32 encode/decode, including the O→0 and I/L→1 mappings
  • The 20-bit checksum, and at least one single-character-substitution repair case
  • AES-256-GCM with the AAD binding (account_id, object_type, object_id, version)

Done when: the vector file is committed, a Go test passes against it, and the file is documented as the thing the JS and Swift suites must also pass. No implementation work beyond what proves the vectors.

Trap: do not generate the vectors from an implementation you wrote this session and then test that implementation against them — that proves nothing. Derive them from the spec, and hand-verify at least the Argon2id and HKDF outputs against an independent tool.

Branch: shared/golden-vectors.

🤖 claiming — claude~paul@keymail.dev (remote agent, session ad0bdb88) — branch shared/golden-vectors — 2026-09-01T21:35:00Z

Overnight unattended run, web track only. Working 6 → 7 → … in the order from #18, one item at a time. Will reply here when I stop, finished or not.

🤖 Done. Branch shared/golden-vectors, pushed — commits a13df93..71fe556.

go test ./... && go vet ./... && gofmt -l . all clean. 18 tests, and the suite has been watched failing (below). No canary URL on this one: there is no deployable surface until web/1.

What landed

  • vectors/contra-v1.json — the contract. Argon2id at m=64MiB, t=3, p=1; all five contra/v1/* HKDF labels; the KCV; P-256 seed derivation including the rejection branch; Crockford Base32 with the O→0 / I,L→1 mappings and a strict pad rule; the 20-bit checksum with a repair case; AES-256-GCM under the AAD binding; and account_chain, the whole of "The shape" end to end so a diverging implementation can bisect rather than stare at one wrong key.
  • vectors/gen/ — the generator, and vectors/README.md carrying the rule.
  • internal/crypto/ — the Go side, and vectors_test.go consuming the file.

On the trap

The generator is C against OpenSSL 3.5, not Go, so no code written this session contributed a byte to the file it is tested against. Before generating anything, gen.c recomputes the published PHC Argon2id reference outputs, RFC 5869 cases 1 and 3, RFC 4231 case 1 and a NIST CAVP AES-256-GCM vector, and aborts on any mismatch — otherwise "independent implementation" would only mean the two are not the same, not that either is right. The hand-verification #6 asked for is that check, run on every generation rather than once by me.

Every fixture input is SHA-256("contra/v1/vectors/" || label), so the file regenerates from the source alone and nothing in it is or resembles a real secret.

The rejecting seed

A P-256 rejection is a ~2^-32 event, so the branch is unreachable by any seed anyone picks. vectors/gen/seedsearch.c found one at k = 10962785172 after about 1.1×10^10 candidates. Independently confirmed two ways — the openssl kdf CLI and RFC 5869 written out longhand against Python hashlib, which agree — that candidate_0 opens ffffffff6646… and so exceeds n. The witness is pinned in the Makefile; make search finds a new one if the contra/v1/ec-p256 label ever moves.

Watched failing

A canary nobody has seen fail is a canary nobody should trust, so I ran nine mutations. All nine turn the suite red: lowering the Argon2 time cost, typoing an HKDF label, cutting the KCV to 8 bytes, dropping the AAD length prefixes, dropping its domain tag, skipping the rejected EC candidate, accepting non-zero Base32 pad bits, widening the checksum to 24 bits, and dropping the O/I/L mappings.

Two guards are worth naming: the Argon2 parameters and the label set are read out of the file, so the constants cannot drift from the protocol and a label that loses its vector fails instead of passing quietly.

One thing that needs your eye — discussion 19

DESIGN.md names primitives without pinning wire formats, and vectors cannot exist without them. I chose and recorded six: HKDF mode and salt handling, the P-256 rejection loop, Crockford packing, the printed form and checksum, the AAD layout, and the Go module path. All are cheap to reverse now and expensive after web/3.

One is a real reconciliation rather than a gap-fill, so flag it if you disagree: DESIGN.md writes the AAD binding twice, as (account_id, object_type, object_id, version) and as (vault_id, item_id, version). I read the first field as a scope id — the account for account-scoped objects, the vault for vault-scoped ones — which makes both spellings the same rule. Item ciphertexts are then bound to their vault, and the object-type strings are namespaced so the two scopes cannot collide.

Moving on to web/1 (#7) next, claiming there.