Files
suaveplan/openspec/archive/2026-04-19-crypto-primitive-replaceability/proposal.md
2026-05-27 23:29:53 +01:00

88 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Change: crypto-primitive-replaceability
## Why
`enterprise-crypto` establishes `@suaveplan/hash` + `@suaveplan/crypto` at enterprise quality but keeps a direct runtime dependency on `@noble/{hashes,curves,ciphers}`. That delivers audited primitives today but leaves two properties on the table: **supply-chain ownership** (every byte of crypto code lives under `@suaveplan/*` with no external runtime dep) and **per-primitive replaceability** (ability to swap individual primitives from the vendored `noble` impl to an owned `native` impl incrementally, under a rigorous CI gate).
Those properties matter because primitive-level crypto is the one place where a single compromised upstream release — or a single npm-registry takeover — silently owns every downstream service we ship. Vendoring noble source into our tree and adding a per-primitive selector lets us: (a) freeze the exact bytes we trust; (b) review upstream diffs on our cadence instead of theirs; (c) replace individual primitives with owned implementations one at a time, without ever going through a big-bang rewrite; (d) leave cryptographically dangerous primitives (curves, AES, MACs) on the audited `noble` impl indefinitely if we choose — that is a legitimate terminal state.
This change builds *on top of* `enterprise-crypto`'s primitives, it does not replace them. The public surface of `@suaveplan/crypto` does not change. What changes is where the underlying bytes come from and how we swap them.
## What Changes
### Package family (new)
- **NEW** `@suaveplan/crypto-curve` — ECC primitives (secp256k1, Ed25519, X25519, P-256) as vendor-fork of `@noble/curves`, exposed behind per-primitive selector modules. Dangerous-tier CI gate.
- **NEW** `@suaveplan/crypto-cipher` — AEAD primitives (AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305) as vendor-fork of `@noble/ciphers`, same selector architecture. Dangerous-tier CI gate.
- **NEW** `@suaveplan/crypto-utils` — pure-composition utilities (hex, base64, base64url, base58, concatBytes, xor, I2OSP / OS2IP, Merkle helpers, constant-time-equal, zeroize, KAT harness, differential-fuzz harness, constant-time lint). No vendored code. Depends only on `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`.
### Package rename (no external consumers — genesis only)
- **RENAME** `@suaveplan/hash` → `@suaveplan/crypto-hash` so the family shares the `crypto-*` prefix. The one in-tree consumer (`@suaveplan/crypto`) is updated in the same change. No other workspace imports `@suaveplan/hash`.
### Replaceability architecture (new, applied to crypto-hash / crypto-curve / crypto-cipher)
Every hash, curve, and AEAD primitive is refactored to the following layout:
```text
src/primitives/<id>/
<id>.types.ts # Interface
<id>.ts # Public API — re-exports from <id>.select.ts
<id>.vectors.ts # RFC/NIST/Wycheproof KATs, used by tests
<id>.test.ts # Runs vectors × every impl + cross-impl equivalence
<id>.md
<id>.select.ts # GENERATED — do not edit
impl/
<id>.noble.ts # Vendored-noble impl (Phase 0: via @noble/* dep; Phase 0.5: vendored source)
<id>.native.ts # Owned impl (starts: throw PrimitiveNotImplemented)
```
A root `crypto.config.ts` maps `primitiveId → 'noble' | 'native'`. A pre-build codegen script (`scripts/gen-crypto-selects.ts`) emits each `<id>.select.ts` as a one-line re-export of the chosen impl, so Vite tree-shakes the unused impl out. Flipping a primitive is a one-line config change.
### Modifications
- **MODIFIED** `@suaveplan/crypto` — `@noble/curves` and `@noble/ciphers` imports are rewired to `@suaveplan/crypto-curve` and `@suaveplan/crypto-cipher`. `@noble/*` removed from `dependencies`. Public API surface unchanged.
- **MODIFIED** `@suaveplan/crypto-hash` (formerly `@suaveplan/hash`) — each primitive (sha256 / sha384 / sha512 / hmac / hkdf / pbkdf2 / argon2id / encoding) refactored into the replaceability layout. Public API surface unchanged.
### CI gate for flipping a primitive to `native`
A primitive default SHALL NOT change from `'noble'` to `'native'` in `crypto.config.ts` unless the flip PR demonstrates:
1. Full KAT suite (RFC / NIST / Wycheproof) passes on the native impl.
2. Nightly differential fuzz (≥ 1M inputs over 7 days) shows zero mismatches vs noble.
3. Constant-time lint: automated scan for secret-dependent branches / secret-indexed lookups + reviewer checklist + `dudect`-style statistical timing test on Bun.
4. **Dangerous tier** (every primitive in `crypto-curve` and `crypto-cipher`, plus HMAC and any MAC-like primitive in `crypto-hash`): third-party cryptographic audit sign-off attached to the flip PR, referencing the exact commit SHA of the native impl. Hashes, HKDF, and PBKDF2 can flip at gate 3 without audit.
### Vendoring hygiene (Phase 0.5 — after initial rollout)
- `@noble/{hashes,curves,ciphers}` source copied into `packages/universal/crypto-{hash,curve,cipher}/src/vendored/` at a pinned upstream commit, with LICENSE preserved.
- `NOBLE_UPSTREAM.md` per package records upstream repo, commit SHA, sync date, reviewer, and files copied.
- `scripts/sync-noble.ts` fetches upstream, diffs, and opens a PR for human review — never auto-merges.
- After vendoring: `@noble/*` removed from every package's `dependencies`; `impl/<id>.noble.ts` imports from `../vendored/...` relative paths.
## Capabilities
### New Capabilities
- `crypto-curve`: ECC primitive family (secp256k1, Ed25519, X25519, P-256) with per-primitive noble/native selector and dangerous-tier audit gate.
- `crypto-cipher`: AEAD primitive family (AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305) with per-primitive selector and dangerous-tier audit gate.
- `crypto-utils`: encoding + byte + test-harness utilities. Zero vendored code, pure-composition only.
### Modified Capabilities
- `crypto-hash` (renamed from `hash`): primitives refactored into the per-primitive selector layout. Public API unchanged.
- `crypto`: curve and cipher primitive sources migrate from `@noble/*` to `@suaveplan/crypto-{curve,cipher}`. `@noble/*` removed from `dependencies`. Public API unchanged.
## Impact
- **Consumers of `@suaveplan/crypto`**: no source changes required. Public API stable.
- **Consumers of `@suaveplan/hash`**: one-line import rewrite `@suaveplan/hash` → `@suaveplan/crypto-hash`. Codemod provided.
- **Bundle size**: neutral in Phase 0. Tree-shakes per-primitive as natives land.
- **CI**: four new tasks — `crypto:kat` (PR-blocking), `crypto:fuzz` (nightly), `crypto:timing` (nightly), `crypto:noble-sync` (weekly).
- **Phase 0 exit criteria**: four packages publish at 0.0.0 with 95% coverage; every primitive selector defaults to `'noble'`; every `.native.ts` stub is present; `@suaveplan/crypto` has zero `@noble/*` imports in source and zero `@noble/*` entries in its direct `dependencies`; `crypto-hash` / `crypto-curve` / `crypto-cipher` still declare `@noble/*` as catalog deps (vendoring deferred to Phase 0.5).
- **Supersession**: updates the `@noble/*` dep decision documented in `enterprise-crypto` without reverting any of its other work. `enterprise-crypto` remains the enterprise-quality baseline; this change layers replaceability on top.
## Relationship to enterprise-crypto
`enterprise-crypto` is the enterprise-quality baseline for `@suaveplan/crypto` + `@suaveplan/hash`: OTel, structured errors, Zod schemas, 100% coverage, benchmark suite. This change preserves every one of those properties and adds the replaceability architecture. The two changes are composable — whichever ships first, the other rebases cleanly by updating import paths (for rename) and adding the `impl/` and `select.ts` files per primitive (for architecture).