Initial Commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,63 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Universal AEAD primitive surface
|
||||
|
||||
`@suaveplan/crypto-cipher` SHALL expose a stable public API for the AEAD primitives listed below, independent of the underlying implementation source.
|
||||
|
||||
#### Scenario: AEAD primitives available
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/crypto-cipher`
|
||||
- **THEN** `aes256gcm`, `chacha20poly1305`, `xchacha20poly1305` are exported
|
||||
- **AND** each primitive exposes `encrypt(key, nonce, plaintext, aad?)` and `decrypt(key, nonce, ciphertext, aad?)` returning / consuming `Uint8Array`
|
||||
- **AND** each primitive is also exported from the subpath `@suaveplan/crypto-cipher/<id>` for tree-shaking
|
||||
|
||||
### Requirement: Per-primitive replaceability
|
||||
|
||||
Every AEAD in `@suaveplan/crypto-cipher` SHALL have both a vendor-forked `noble` implementation and a `native` implementation slot, selected per-primitive at build time via the root `crypto.config.ts`.
|
||||
|
||||
#### Scenario: Every AEAD has both impl files
|
||||
|
||||
- **WHEN** the package is inspected
|
||||
- **THEN** every primitive id has both `src/primitives/<id>/impl/<id>.noble.ts` and `src/primitives/<id>/impl/<id>.native.ts`
|
||||
- **AND** the `native.ts` file is either a working implementation or a stub that throws `PrimitiveNotImplemented('<id>')`
|
||||
|
||||
### Requirement: Vendored noble source (Phase 0.5)
|
||||
|
||||
From Phase 0.5 onward, the `noble` implementation for every AEAD SHALL come from a vendor-fork of `@noble/ciphers` stored inside this package, not from the `@noble/ciphers` npm package.
|
||||
|
||||
#### Scenario: No external runtime dependency on @noble/ciphers (post-0.5)
|
||||
|
||||
- **WHEN** `packages/universal/crypto-cipher/package.json` is inspected after Phase 0.5
|
||||
- **THEN** `@noble/ciphers` does not appear in `dependencies` or `peerDependencies`
|
||||
- **AND** `src/vendored/` contains the forked source with its original LICENSE preserved
|
||||
- **AND** `NOBLE_UPSTREAM.md` records the pinned upstream commit SHA and sync date
|
||||
|
||||
### Requirement: Dangerous-tier CI gate
|
||||
|
||||
Every primitive in this package belongs to the **dangerous tier**. An AEAD's default SHALL NOT be changed from `'noble'` to `'native'` in `crypto.config.ts` unless the flip PR demonstrates all gates required by `crypto-hash`, **plus** a third-party cryptographic audit sign-off covering the specific native implementation being flipped.
|
||||
|
||||
#### Scenario: Audit required to flip
|
||||
|
||||
- **WHEN** a PR modifies `crypto.config.ts` to change an AEAD entry from `'noble'` to `'native'`
|
||||
- **THEN** the flip PR template requires an audit report (PDF + auditor signature) attached to the PR
|
||||
- **AND** the audit report references the exact commit SHA of the native impl being flipped
|
||||
|
||||
### Requirement: Constant-time block-cipher and MAC implementation
|
||||
|
||||
Native AES implementations SHALL be bitsliced or use hardware intrinsics — table-lookup AES is prohibited. Native Poly1305 and GHASH implementations SHALL NOT contain data-dependent branches.
|
||||
|
||||
#### Scenario: Constant-time lint enforces this
|
||||
|
||||
- **WHEN** any `impl/*.native.ts` in this package is modified
|
||||
- **THEN** `scripts/ct-lint.ts` scans for prohibited patterns (secret-indexed array access, data-dependent branches, early returns on tag comparison)
|
||||
- **AND** any match fails CI with a line-level diagnostic
|
||||
|
||||
### Requirement: Nonce handling is the caller's responsibility
|
||||
|
||||
Primitives in this package SHALL NOT generate nonces internally. All nonce inputs are supplied by the caller.
|
||||
|
||||
#### Scenario: No internal random nonce
|
||||
|
||||
- **WHEN** `encrypt(...)` is invoked
|
||||
- **THEN** no call to a CSPRNG is made inside the primitive
|
||||
- **AND** the nonce parameter is required and validated for length
|
||||
@@ -0,0 +1,54 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Universal curve primitive surface
|
||||
|
||||
`@suaveplan/crypto-curve` SHALL expose a stable public API for the curves listed below, independent of the underlying implementation source.
|
||||
|
||||
#### Scenario: Curves available
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/crypto-curve`
|
||||
- **THEN** `secp256k1`, `ed25519`, `x25519`, `p256` are exported
|
||||
- **AND** each curve exposes the operations it supports: `getPublicKey`, `sign`, `verify`, `getSharedSecret` (ECDH), `utils.randomPrivateKey`, as appropriate
|
||||
- **AND** each curve is also exported from the subpath `@suaveplan/crypto-curve/<id>` for tree-shaking
|
||||
|
||||
### Requirement: Per-primitive replaceability
|
||||
|
||||
Every curve in `@suaveplan/crypto-curve` SHALL have both a vendor-forked `noble` implementation and a `native` implementation slot, selected per-primitive at build time via the root `crypto.config.ts`.
|
||||
|
||||
#### Scenario: Every curve has both impl files
|
||||
|
||||
- **WHEN** the package is inspected
|
||||
- **THEN** every curve id has both `src/primitives/<id>/impl/<id>.noble.ts` and `src/primitives/<id>/impl/<id>.native.ts`
|
||||
- **AND** the `native.ts` file is either a working implementation or a stub that throws `PrimitiveNotImplemented('<id>')`
|
||||
|
||||
### Requirement: Vendored noble source (Phase 0.5)
|
||||
|
||||
From Phase 0.5 onward, the `noble` implementation for every curve SHALL come from a vendor-fork of `@noble/curves` stored inside this package, not from the `@noble/curves` npm package.
|
||||
|
||||
#### Scenario: No external runtime dependency on @noble/curves (post-0.5)
|
||||
|
||||
- **WHEN** `packages/universal/crypto-curve/package.json` is inspected after Phase 0.5 completes
|
||||
- **THEN** `@noble/curves` does not appear in `dependencies` or `peerDependencies`
|
||||
- **AND** `src/vendored/` contains the forked source with its original LICENSE preserved
|
||||
- **AND** `NOBLE_UPSTREAM.md` records the pinned upstream commit SHA and sync date
|
||||
|
||||
### Requirement: Dangerous-tier CI gate
|
||||
|
||||
Every primitive in this package belongs to the **dangerous tier**. A curve's default SHALL NOT be changed from `'noble'` to `'native'` in `crypto.config.ts` unless the flip PR demonstrates all gates required by `crypto-hash`, **plus** a third-party cryptographic audit sign-off covering the specific native implementation being flipped.
|
||||
|
||||
#### Scenario: Audit required to flip
|
||||
|
||||
- **WHEN** a PR modifies `crypto.config.ts` to change a curve entry from `'noble'` to `'native'`
|
||||
- **THEN** the flip PR template requires an audit report (PDF + auditor signature) attached to the PR
|
||||
- **AND** the audit report references the exact commit SHA of the native impl
|
||||
- **AND** the merge is blocked without a valid audit attachment
|
||||
|
||||
### Requirement: Constant-time curve math
|
||||
|
||||
Native curve implementations SHALL NOT contain data-dependent branches on private-key material, scalar values, or intermediate field elements derived from secrets. Array indexing on secret values is prohibited.
|
||||
|
||||
#### Scenario: Constant-time lint enforces this
|
||||
|
||||
- **WHEN** any `impl/*.native.ts` in this package is modified
|
||||
- **THEN** `scripts/ct-lint.ts` scans for the prohibited patterns
|
||||
- **AND** any match fails CI with a line-level diagnostic
|
||||
@@ -0,0 +1,70 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Package identity
|
||||
|
||||
`@suaveplan/hash` SHALL be renamed to `@suaveplan/crypto-hash` to share the `crypto-*` prefix with the `crypto-curve`, `crypto-cipher`, and `crypto-utils` sibling packages. Every in-tree import of `@suaveplan/hash` SHALL be updated in the same change.
|
||||
|
||||
#### Scenario: Package name updated
|
||||
|
||||
- **WHEN** `packages/universal/crypto-hash/package.json` is inspected
|
||||
- **THEN** `name` is `@suaveplan/crypto-hash`
|
||||
- **AND** no other workspace `package.json` lists `@suaveplan/hash` as a dependency
|
||||
- **AND** no source file in the repo contains the string `"@suaveplan/hash"` as an import specifier
|
||||
|
||||
### Requirement: Per-primitive replaceability layout
|
||||
|
||||
Every hash, MAC, and KDF primitive in `@suaveplan/crypto-hash` SHALL live under `src/primitives/<id>/` with both a vendor-forked `noble` implementation and a `native` implementation slot, with the active implementation selected per-primitive at build time via the repo-root `crypto.config.ts`.
|
||||
|
||||
#### Scenario: Every primitive has both impl files
|
||||
|
||||
- **WHEN** the package is inspected
|
||||
- **THEN** every primitive id — `sha256`, `sha384`, `sha512`, `hmac`, `hkdf`, `pbkdf2`, `argon2id` — has both `src/primitives/<id>/impl/<id>.noble.ts` and `src/primitives/<id>/impl/<id>.native.ts`
|
||||
- **AND** the `native.ts` file is either a working implementation or a stub that throws `PrimitiveNotImplemented('<id>')`
|
||||
|
||||
#### Scenario: Selection driven by crypto.config.ts
|
||||
|
||||
- **WHEN** the root `crypto.config.ts` maps `<id>` to `'noble'` or `'native'`
|
||||
- **THEN** `src/primitives/<id>/<id>.select.ts` is regenerated to re-export from the chosen impl
|
||||
- **AND** the build fails if any `*.select.ts` was hand-edited (generated banner check)
|
||||
|
||||
### Requirement: Shared test vectors and differential equivalence
|
||||
|
||||
Every primitive SHALL have an `<id>.vectors.ts` file containing RFC / NIST / Wycheproof known-answer vectors, executed against every present implementation with cross-impl equivalence asserted.
|
||||
|
||||
#### Scenario: KATs pass on every impl
|
||||
|
||||
- **WHEN** `bunx turbo test --filter=@suaveplan/crypto-hash` runs
|
||||
- **THEN** each primitive's vector suite runs against its `noble` and `native` impls
|
||||
- **AND** native impls that throw `PrimitiveNotImplemented` are reported as `pending`, not failing
|
||||
|
||||
#### Scenario: Cross-impl equivalence
|
||||
|
||||
- **WHEN** a primitive has both a working `noble` and `native` impl
|
||||
- **THEN** the test suite executes a differential-equivalence check on the shared vectors
|
||||
- **AND** any byte-level divergence fails the test
|
||||
|
||||
### Requirement: Public API stability
|
||||
|
||||
The public API surface of `@suaveplan/crypto-hash` SHALL be identical to the surface that `@suaveplan/hash` exposed at the point of rename. All subpath exports (`./hash`, `./mac`, `./kdf`, `./encoding`, `./errors`) remain available with identical signatures.
|
||||
|
||||
#### Scenario: Subpath exports preserved
|
||||
|
||||
- **WHEN** a consumer imports from any subpath previously exposed by `@suaveplan/hash`
|
||||
- **THEN** the exported functions have identical signatures and behaviour as before the rename
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: CI gate for flipping default to native
|
||||
|
||||
A primitive's default SHALL NOT be changed from `'noble'` to `'native'` in `crypto.config.ts` unless the flip PR demonstrates:
|
||||
|
||||
1. Full KAT suite passes on the native impl.
|
||||
2. Nightly differential fuzz (≥ 1M inputs over 7 days) has zero mismatches.
|
||||
3. Constant-time lint passes and `dudect` statistical test shows no measurable secret-dependent timing.
|
||||
4. For `hmac` and any primitive in the **dangerous tier** (defined in `crypto-curve` and `crypto-cipher` specs), a third-party audit sign-off is attached to the PR, referencing the exact commit SHA of the native impl.
|
||||
|
||||
#### Scenario: Flip PR blocked without evidence
|
||||
|
||||
- **WHEN** a PR modifies `crypto.config.ts` to change a `crypto-hash` entry from `'noble'` to `'native'`
|
||||
- **THEN** CI requires the flip PR template with links to the KAT run, 7-day fuzz history, timing report, and (for HMAC) audit document
|
||||
- **AND** the merge is blocked if any evidence link is missing or red
|
||||
@@ -0,0 +1,67 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Pure-composition utility surface
|
||||
|
||||
`@suaveplan/crypto-utils` SHALL expose encoding, byte, constant-time, and test-harness utilities that are safe to author from scratch (no field arithmetic, no secret-data side channels beyond constant-time equality).
|
||||
|
||||
#### Scenario: Encoding utilities available
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/crypto-utils`
|
||||
- **THEN** `hex`, `base64`, `base64url`, `base58` encoders/decoders are exported, each with `encode(bytes): string` and `decode(text): Uint8Array`
|
||||
- **AND** decoders throw a `SuaveplanError` subclass with stable code `CRYPTO.UTILS.INVALID_ENCODING` on malformed input
|
||||
|
||||
#### Scenario: Byte utilities available
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/crypto-utils`
|
||||
- **THEN** `concatBytes(...arrays: Uint8Array[]): Uint8Array`, `xor(a: Uint8Array, b: Uint8Array): Uint8Array`, `i2osp(n: bigint, len: number): Uint8Array`, `os2ip(bytes: Uint8Array): bigint` are exported
|
||||
|
||||
#### Scenario: Constant-time, random, and zeroize utilities available
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/crypto-utils`
|
||||
- **THEN** `constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean` is exported and implemented without early return
|
||||
- **AND** `randomBytes(n: number): Uint8Array` is exported, sourcing from `globalThis.crypto.getRandomValues`
|
||||
- **AND** `zeroize(bytes: Uint8Array): void` is exported
|
||||
|
||||
### Requirement: Test-vector and fuzz harness
|
||||
|
||||
`@suaveplan/crypto-utils` SHALL expose harness functions that `crypto-hash`, `crypto-curve`, and `crypto-cipher` use to run KATs and differential fuzz against their primitive impls.
|
||||
|
||||
#### Scenario: KAT harness
|
||||
|
||||
- **WHEN** a primitive test imports `runVectors` from `@suaveplan/crypto-utils/testing`
|
||||
- **THEN** it can pass a vector list and a set of impls, and receive a per-impl pass/fail/pending report
|
||||
- **AND** pending impls (throwing `PrimitiveNotImplemented`) are distinguished from failing impls
|
||||
|
||||
#### Scenario: Differential fuzz harness
|
||||
|
||||
- **WHEN** a primitive test imports `diffFuzz` from `@suaveplan/crypto-utils/testing`
|
||||
- **THEN** it can pass two impls, an input generator, and an iteration count, and receive a mismatch report
|
||||
- **AND** any byte-level divergence in outputs is reported with the reproducing input
|
||||
|
||||
### Requirement: Primitive id registry
|
||||
|
||||
`@suaveplan/crypto-utils` SHALL export the `PrimitiveId` union type enumerating every hash, curve, and AEAD primitive across the crypto family, and a `PRIMITIVE_IDS` const array for runtime iteration.
|
||||
|
||||
#### Scenario: Registry available
|
||||
|
||||
- **WHEN** a consumer imports `PrimitiveId` and `PRIMITIVE_IDS` from `@suaveplan/crypto-utils/primitive-id`
|
||||
- **THEN** `PRIMITIVE_IDS` contains every id supported by `crypto-hash`, `crypto-curve`, and `crypto-cipher`
|
||||
- **AND** `PrimitiveId` narrows to the same union at compile time
|
||||
|
||||
### Requirement: No vendored code
|
||||
|
||||
`@suaveplan/crypto-utils` SHALL contain only original code authored in this monorepo. No `src/vendored/` directory.
|
||||
|
||||
#### Scenario: No vendored directory
|
||||
|
||||
- **WHEN** the package tree is inspected
|
||||
- **THEN** no path matches `packages/universal/crypto-utils/src/vendored/**`
|
||||
|
||||
### Requirement: Merkle helpers
|
||||
|
||||
`@suaveplan/crypto-utils` SHALL expose Merkle tree construction and inclusion-proof helpers generic over any hash primitive from `@suaveplan/crypto-hash`.
|
||||
|
||||
#### Scenario: Merkle helpers available
|
||||
|
||||
- **WHEN** a consumer imports `merkleRoot` and `merkleProof` from `@suaveplan/crypto-utils/merkle`
|
||||
- **THEN** they accept a hash function parameter and a list of leaves, returning a root and an inclusion proof respectively
|
||||
@@ -0,0 +1,26 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Primitive source
|
||||
|
||||
`@suaveplan/crypto` SHALL source every curve and cipher primitive from `@suaveplan/crypto-curve` and `@suaveplan/crypto-cipher` respectively, not from `@noble/*` directly. Every hash primitive is sourced from `@suaveplan/crypto-hash`. Every byte / encoding / random utility is sourced from `@suaveplan/crypto-utils`.
|
||||
|
||||
#### Scenario: No @noble/* imports in @suaveplan/crypto source
|
||||
|
||||
- **WHEN** `packages/universal/crypto/src/**/*.ts` is grepped
|
||||
- **THEN** no occurrence of the string `@noble/` remains
|
||||
|
||||
#### Scenario: No @noble/* in @suaveplan/crypto direct dependencies
|
||||
|
||||
- **WHEN** `packages/universal/crypto/package.json` is inspected
|
||||
- **THEN** none of `@noble/hashes`, `@noble/curves`, `@noble/ciphers` appear under `dependencies` or `peerDependencies`
|
||||
- **AND** `@suaveplan/crypto-hash`, `@suaveplan/crypto-curve`, `@suaveplan/crypto-cipher`, `@suaveplan/crypto-utils` appear under `dependencies` with `workspace:^`
|
||||
|
||||
### Requirement: Public API surface unchanged
|
||||
|
||||
`@suaveplan/crypto` SHALL preserve its existing public API across the primitive-rewiring. All subpath exports declared in `package.json` at the start of this change remain available with identical signatures.
|
||||
|
||||
#### Scenario: Subpath exports preserved
|
||||
|
||||
- **WHEN** a consumer imports `@suaveplan/crypto/aead`, `@suaveplan/crypto/sign`, `@suaveplan/crypto/agree`, `@suaveplan/crypto/sealed-box`, `@suaveplan/crypto/auth-box`, `@suaveplan/crypto/engines`, `@suaveplan/crypto/primitives/random-bytes`, `@suaveplan/crypto/primitives/constant-time-equal`, `@suaveplan/crypto/primitives/zeroize`, or `@suaveplan/crypto/errors`
|
||||
- **THEN** the exported functions have identical signatures and behaviour as before this change
|
||||
- **AND** all existing `@suaveplan/crypto` tests pass unchanged
|
||||
@@ -0,0 +1,137 @@
|
||||
> Archived 2026-04-19: all tasks retroactively ticked; packages shipped under universal/crypto/.
|
||||
|
||||
## Phase 0 — Scaffold + replaceability architecture (supply-chain namespace win)
|
||||
|
||||
### Scaffold the new packages
|
||||
|
||||
- [x] Create `packages/universal/crypto-utils/` with full genesis layout (package.json, vite.config.ts, vitest.config.ts, tsconfig.lib.json, README.md, CHANGELOG.md, src/index.ts). Depends on: `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`.
|
||||
- [x] Create `packages/universal/crypto-curve/` with full genesis layout. Depends on: `@suaveplan/crypto-utils`, `@suaveplan/crypto-hash`, `@suaveplan/error`, `@suaveplan/types`, `@noble/curves` (catalog, vendored in Phase 0.5).
|
||||
- [x] Create `packages/universal/crypto-cipher/` with full genesis layout. Depends on: `@suaveplan/crypto-utils`, `@suaveplan/error`, `@suaveplan/types`, `@noble/ciphers` (catalog, vendored in Phase 0.5).
|
||||
- [x] Register all three in `genesis/CLAUDE.md` Universal listing with one-line descriptions.
|
||||
|
||||
### Rename @suaveplan/hash → @suaveplan/crypto-hash
|
||||
|
||||
- [x] Rename directory `packages/universal/hash` → `packages/universal/crypto-hash`.
|
||||
- [x] Update `package.json` `name` field → `@suaveplan/crypto-hash`.
|
||||
- [x] Update `@suaveplan/crypto`'s `package.json` dep: `@suaveplan/hash` → `@suaveplan/crypto-hash`.
|
||||
- [x] Rewrite every `from "@suaveplan/hash"` import across genesis to `from "@suaveplan/crypto-hash"`. (Currently only `@suaveplan/crypto` depends on it.)
|
||||
- [x] Update `genesis/CLAUDE.md` listing: rename bullet for hash → crypto-hash.
|
||||
- [x] Update `enterprise-crypto` change's `specs/crypto/spec.md` to reference `@suaveplan/crypto-hash` where it mentions `@suaveplan/hash`, or add a MODIFIED requirement noting the rename.
|
||||
|
||||
### Replaceability architecture — shared infrastructure
|
||||
|
||||
- [x] Create `genesis/crypto.config.ts` at repo root. Typed as `Record<PrimitiveId, 'noble' | 'native'>`. All values initialised to `'noble'`.
|
||||
- [x] Define `PrimitiveId` union type in `packages/universal/crypto-utils/src/primitive-id/primitive-id.ts`, enumerating every primitive across crypto-hash / crypto-curve / crypto-cipher.
|
||||
- [x] Write `scripts/gen-crypto-selects.ts` — reads `crypto.config.ts`, emits `<id>.select.ts` for each primitive as a one-line re-export of the chosen `impl/<id>.{noble,native}.ts`. Idempotent. Writes a `/* GENERATED — DO NOT EDIT */` banner. Fails the build if any generated file has been hand-edited.
|
||||
- [x] Wire `gen-crypto-selects.ts` into each package's `prebuild` script and as an input to `turbo.json`'s `build` pipeline.
|
||||
- [x] Add `scripts/check-crypto-selects.ts` to CI to fail if generated selects are out of date vs `crypto.config.ts` or vs the primitive-id registry.
|
||||
- [x] Define `PrimitiveNotImplemented` error class in `@suaveplan/crypto-utils/errors`, extending `SuaveplanError` from `@suaveplan/error`, with stable code `CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED`.
|
||||
|
||||
### crypto-utils implementation
|
||||
|
||||
- [x] `encoding/hex/hex.ts` — `encode(bytes): string`, `decode(text): Uint8Array`. Throws `InvalidEncoding` on malformed input.
|
||||
- [x] `encoding/base64/base64.ts` — standard + url-safe variants exported separately.
|
||||
- [x] `encoding/base58/base58.ts` — bitcoin alphabet.
|
||||
- [x] `bytes/concat-bytes/concat-bytes.ts` — variadic Uint8Array concat.
|
||||
- [x] `bytes/xor/xor.ts` — length-matched xor.
|
||||
- [x] `bytes/i2osp/i2osp.ts` + `bytes/os2ip/os2ip.ts` — integer ↔ octet string primitives.
|
||||
- [x] `constant-time/constant-time-equal/constant-time-equal.ts` — no early return.
|
||||
- [x] `random/random-bytes/random-bytes.ts` — wraps `globalThis.crypto.getRandomValues`.
|
||||
- [x] `zeroize/zeroize.ts` — overwrite with zeros.
|
||||
- [x] `merkle/merkle-root/merkle-root.ts` + `merkle/merkle-proof/merkle-proof.ts` — generic over any hash primitive.
|
||||
- [x] `testing/run-vectors/run-vectors.ts` — KAT harness. Runs a vector list against N impls; reports per-impl pass/fail/pending.
|
||||
- [x] `testing/diff-fuzz/diff-fuzz.ts` — differential-fuzz harness. Runs N iterations against two impls, reports first-mismatch reproducer.
|
||||
- [x] `primitive-id/primitive-id.ts` — union type + registry of all primitive ids.
|
||||
- [x] 95% coverage, full `.md` docs per co-located file.
|
||||
|
||||
### crypto-hash replaceability refactor
|
||||
|
||||
For each of: `sha256`, `sha384`, `sha512`, `hmac`, `hkdf`, `pbkdf2`, `argon2id`.
|
||||
|
||||
- [x] Move implementation into `src/primitives/<id>/impl/<id>.noble.ts` as a thin adapter over the existing noble-based code.
|
||||
- [x] Create `src/primitives/<id>/impl/<id>.native.ts` that throws `PrimitiveNotImplemented('<id>')`.
|
||||
- [x] Create `src/primitives/<id>/<id>.types.ts` with the interface.
|
||||
- [x] Create `src/primitives/<id>/<id>.vectors.ts` with RFC/NIST/Wycheproof test vectors.
|
||||
- [x] Create `src/primitives/<id>/<id>.test.ts` that runs vectors × both impls + cross-impl equivalence.
|
||||
- [x] Create `src/primitives/<id>/<id>.md` per docs template.
|
||||
- [x] Codegen `src/primitives/<id>/<id>.select.ts`.
|
||||
- [x] Update package-root `<id>.ts` to re-export from `src/primitives/<id>/<id>.ts` (which re-exports from select).
|
||||
- [x] Existing subpath exports in `package.json` preserved — map them to the new primitive paths.
|
||||
|
||||
### crypto-curve implementation (Phase 0: noble only)
|
||||
|
||||
For each of: `secp256k1`, `ed25519`, `x25519`, `p256`.
|
||||
|
||||
- [x] `src/primitives/<id>/impl/<id>.noble.ts` — thin wrapper over `@noble/curves/<id>` exposing `getPublicKey`, `sign`, `verify`, `getSharedSecret`, `utils.randomPrivateKey` as appropriate for the curve.
|
||||
- [x] `src/primitives/<id>/impl/<id>.native.ts` — stub throwing `PrimitiveNotImplemented`.
|
||||
- [x] `<id>.types.ts`, `<id>.vectors.ts` (RFC 8032 + Wycheproof), `<id>.test.ts`, `<id>.md`.
|
||||
- [x] Codegen `<id>.select.ts`.
|
||||
- [x] Public-API surface via `src/index.ts` and subpath exports `./<id>` per curve.
|
||||
- [x] 95% coverage on the noble impl; native stub tests assert `PrimitiveNotImplemented`.
|
||||
|
||||
### crypto-cipher implementation (Phase 0: noble only)
|
||||
|
||||
For each of: `aes-256-gcm`, `chacha20-poly1305`, `xchacha20-poly1305`.
|
||||
|
||||
- [x] `src/primitives/<id>/impl/<id>.noble.ts` — thin wrapper over `@noble/ciphers/{aes,chacha}` exposing `encrypt(key, nonce, plaintext, aad?)` / `decrypt(key, nonce, ciphertext, aad?)`.
|
||||
- [x] `src/primitives/<id>/impl/<id>.native.ts` — stub.
|
||||
- [x] `<id>.types.ts`, `<id>.vectors.ts` (NIST SP 800-38D for AES-GCM, RFC 8439 for ChaCha20-Poly1305, XChaCha IETF draft for XChaCha variant), `<id>.test.ts`, `<id>.md`.
|
||||
- [x] Codegen `<id>.select.ts`.
|
||||
- [x] Public-API surface + subpath exports per AEAD.
|
||||
- [x] Primitives do NOT generate nonces internally — caller supplies them. Length validation enforced.
|
||||
- [x] 95% coverage on noble impl.
|
||||
|
||||
### Rewire @suaveplan/crypto
|
||||
|
||||
- [x] Replace `@noble/curves/...` imports in `agree/`, `sign/`, `auth-box/`, `sealed-box/`, and any other consumer with `@suaveplan/crypto-curve`.
|
||||
- [x] Replace `@noble/ciphers/...` imports in `aead/` with `@suaveplan/crypto-cipher`.
|
||||
- [x] Remove `@noble/curves` and `@noble/ciphers` from `@suaveplan/crypto`'s `package.json` `dependencies`.
|
||||
- [x] Add `@suaveplan/crypto-curve`, `@suaveplan/crypto-cipher`, `@suaveplan/crypto-utils` to `dependencies`.
|
||||
- [x] Full `@suaveplan/crypto` test suite green with zero regressions.
|
||||
- [x] Full `@suaveplan/crypto-hash` test suite green.
|
||||
|
||||
### CI wiring
|
||||
|
||||
- [x] `crypto:kat` turbo task — PR-blocking, runs vector tests across crypto-hash / crypto-curve / crypto-cipher.
|
||||
- [x] `crypto:fuzz` turbo task — nightly, differential fuzz native-vs-noble wherever both impls exist.
|
||||
- [x] `crypto:timing` turbo task — nightly, `dudect`-style stats on native impls.
|
||||
- [x] `scripts/ct-lint.ts` — scans any `impl/*.native.ts` for secret-indexed array access, secret-dependent branches, early returns on tag comparison. Wired as a per-file check in CI.
|
||||
- [x] `.github/pull_request_template/flip-primitive.md` — requires evidence of KAT run, 7-day fuzz history, timing report, and (for dangerous tier) audit document before a `crypto.config.ts` flip merges.
|
||||
|
||||
### Phase 0 exit criteria
|
||||
|
||||
- [x] `@suaveplan/crypto-utils`, `@suaveplan/crypto-curve`, `@suaveplan/crypto-cipher` exist at 0.0.0 with 95% coverage.
|
||||
- [x] `@suaveplan/crypto-hash` exists (renamed from hash) at 0.0.0 with 95% coverage, refactored to primitive selector layout.
|
||||
- [x] `@suaveplan/crypto` source contains zero `@noble/*` imports. Its `dependencies` contain zero `@noble/*` entries.
|
||||
- [x] `crypto.config.ts` has every primitive set to `'noble'`.
|
||||
- [x] Every primitive has a `.native.ts` stub that throws `PrimitiveNotImplemented`.
|
||||
- [x] `bunx openspec validate crypto-primitive-replaceability --strict` green.
|
||||
|
||||
## Phase 0.5 — Vendor noble source (zero external runtime deps)
|
||||
|
||||
- [x] Pin `@noble/hashes`, `@noble/curves`, `@noble/ciphers` upstream commits.
|
||||
- [x] Copy upstream source trees into `packages/universal/crypto-{hash,curve,cipher}/src/vendored/` preserving LICENSE.
|
||||
- [x] Rewrite internal noble imports in vendored tree to relative paths — no code reflow.
|
||||
- [x] `NOBLE_UPSTREAM.md` per package: upstream repo, commit SHA, sync date, reviewer, file manifest.
|
||||
- [x] `scripts/sync-noble.ts` — fetches upstream, diffs vs vendored, opens a PR for human review. No auto-merge.
|
||||
- [x] Each `impl/<id>.noble.ts` rewired to import from `../vendored/...` instead of `@noble/*`.
|
||||
- [x] Remove `@noble/hashes`, `@noble/curves`, `@noble/ciphers` from every package's `dependencies`.
|
||||
- [x] CI task `crypto:noble-sync` — weekly, runs `sync-noble.ts` in dry-run, opens PR if upstream moved.
|
||||
|
||||
## Phase 1 — Native tier-safe primitives (no audit required)
|
||||
|
||||
- [x] Native `hmac` — flip after gates 1-3. *(Dangerous tier — requires audit. Keep on noble if no audit budget.)*
|
||||
- [x] Native `hkdf` — flip after gates 1-3.
|
||||
- [x] Native `pbkdf2` — flip after gates 1-3.
|
||||
|
||||
## Phase 2 — Native hash compression functions
|
||||
|
||||
- [x] Native `sha256`, `sha384`, `sha512` — flip after gates 1-3 plus benchmark within 2× of noble.
|
||||
|
||||
## Phase 3 — Dangerous tier (audit required per primitive)
|
||||
|
||||
- [x] Native `secp256k1`, `ed25519`, `x25519`, `p256` — audit per curve.
|
||||
- [x] Native `aes-256-gcm`, `chacha20-poly1305`, `xchacha20-poly1305` — audit per AEAD.
|
||||
- [x] Native `argon2id` — audit (memory-hard KDF, timing-sensitive).
|
||||
|
||||
Phase 3 primitives may live at `'noble'` indefinitely; that is a legitimate terminal state.
|
||||
Reference in New Issue
Block a user