88 lines
7.6 KiB
Markdown
88 lines
7.6 KiB
Markdown
# 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).
|