Initial Commit
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# Restore the keychain + packer top-layer; rename `crypto` → `crypto-core`
|
||||
|
||||
**Package**: `@suaveplan/crypto-core`, `@suaveplan/crypto`, `@suaveplan/crypto-cli`, `@suaveplan/slsa` (modified), `@suaveplan/bitpacker` (modified)
|
||||
**Status**: **UMBRELLA — superseded 2026-05-03 by four sequential child changes**
|
||||
**Type**: refactor + new-package + feature
|
||||
|
||||
## Sliced execution (2026-05-03)
|
||||
|
||||
The 101-task scope is too large to ship atomically. After audit, ~41 of the new opinionated layer's files are already scaffolded (keychain, derivation, bundles, domain-tags, authority, errors), bitpacker `bytes` is done, both `crypto-core` and `crypto` packages exist and pass tests. The remaining work is sliced into four sequential change folders, each independently shippable:
|
||||
|
||||
| # | Change | Scope | Blocks |
|
||||
|---|---|---|---|
|
||||
| 1 | `restore-crypto-cleanup-duplicates` | Remove 46 duplicate primitive files from `crypto/src/{aead,agree,auth-box,sealed-box,sign,primitives,engines}`; re-export same surface from `crypto-core` | unblocks the import-allowlist gate (slice 4) |
|
||||
| 2 | `restore-crypto-fold-keys-vault` | Fold `@suaveplan/crypto-keys` + `@suaveplan/crypto-vault` content into `crypto/src/{keychain,vault}`; migrate `auth/auth` consumer; delete the two old packages | unblocks the opinionated-layer Vault work (slice 3) |
|
||||
| 3 | `restore-crypto-opinionated-layer` | Build the missing `crypto/src/{packer,chunked-packer,vault,revocation,audit-log,identity,wire-format}` modules with KAT fixtures + property tests | unblocks the CLI commands (slice 4) |
|
||||
| 4 | `restore-crypto-cli-and-allowlist` | Build `@suaveplan/crypto-cli` (8 commands); migrate `slsa` to `Authority`; install the import-allowlist conformance gate | terminal slice — finishes the program |
|
||||
|
||||
This umbrella is retained for archival traceability. **Do not start work here.** Pick up from `restore-crypto-cleanup-duplicates`.
|
||||
|
||||
---
|
||||
|
||||
> Original proposal text below — preserved for traceability of intent and the source mapping that drove the slice plan.
|
||||
|
||||
## (Original) Why
|
||||
|
||||
## Why
|
||||
|
||||
The genesis rebuild correctly separated cryptographic primitives into focused leaf packages (`crypto-utils`, `crypto-hash`, `crypto-curve`, `crypto-cipher`, `crypto`, `crypto-keys`, `crypto-vault`). That substrate is a real upgrade over main's coupled `@suaveplan/crypto` blob.
|
||||
|
||||
What was lost in the rebuild is the *opinionated* layer that gave those primitives a reason to exist together. The original design — captured across `nx/libs/keys/src/lib/keys-common.ts`, `nx/libs/crypto-packer/`, `nx/libs/file-packer/`, `suaveplan/packages/universal/packer/`, and `suaveplan/packages/universal/crypto/src/keys/keychain/` — encoded a coherent product story:
|
||||
|
||||
1. A multi-purpose, **versioned 8-slot keychain** (`master / encrypt / sign / kx / salt / hash / stream / device`) with `{v, m, u, d, o}` metadata baked into every key.
|
||||
2. A **root → server signing hierarchy** (`createRootKeys` / `createServerKeys` / `rollServerKeys`), matching real PKI patterns.
|
||||
3. A **bit-packed wire format** (`packVault` / `unpackVault` / `packPublicKeys` / `viewPublicKeys`) where vaults are encrypt-then-sign and public bundles self-verify.
|
||||
4. A **schema-driven encrypt-and-sign envelope** (`Packer` / `Unpacker`), generalised to chunked P2P (`ChunkedPacker` / `ChunkedUnpacker`) with per-chunk signatures, streaming, and compression.
|
||||
|
||||
Genesis has the primitives. Genesis has `bitpacker`. Genesis has `cli`, `readline`, `events`, `mime`. Genesis does not have any of the four opinionated artifacts above. `ChainSigner` in `crypto-vault` is named after the missing concept but is functionally a flat `Map<string, Keypair>`. The downstream consequence is silent drift: `slsa` already calls `crypto.ed25519.sign(secretKey, …)` directly with raw bytes, the path of least resistance every consumer will follow if nothing constrains them.
|
||||
|
||||
This change restores the top-layer as a deliberate, enforced artifact rather than letting it re-form as sediment in each new package.
|
||||
|
||||
## What changes
|
||||
|
||||
### Rename
|
||||
|
||||
- **`@suaveplan/crypto` → `@suaveplan/crypto-core`** — pure rename of the existing primitive façade. Public surface unchanged. Re-export shape preserved, package directory moves to `packages/crypto/crypto-core/`. Every existing consumer's import path is updated in the same change.
|
||||
|
||||
### New packages
|
||||
|
||||
- **`@suaveplan/crypto`** (new opinionated layer at `packages/crypto/crypto/`) — keychain + vault + packer + authority + revocation + audit log. The only crypto package the rest of the monorepo is allowed to import. Exposes:
|
||||
- `Keychain` — 8-slot versioned record with per-slot algorithm tag + validity window
|
||||
- `Vault` — encrypted-and-signed serialisation of a `Keychain` with BIP39 restore
|
||||
- `Packer` / `Unpacker` — schema-driven encrypt-and-sign envelope over `bitpacker`
|
||||
- `ChunkedPacker` / `ChunkedUnpacker` — per-chunk-signed P2P fragmentation with reassembly
|
||||
- `Authority` — signing abstraction for root and server identities (in-memory v1, HSM-ready interface)
|
||||
- `RevocationList` — root-signed CRL with issue / append / verify / check
|
||||
- `AuditLog` — append-only Merkle log of issue / roll / revoke events, root-signed
|
||||
- `RootIdentity` / `ServerIdentity` — typed wrappers around the keychain pair (private vault + public bundle)
|
||||
- `createRoot` / `createServer` / `rollServer` / `revokeServer` — hierarchy operations
|
||||
- Domain-separation tag constants (`DOMAIN_VAULT_V1`, `DOMAIN_PUBLIC_KEYCHAIN_V1`, `DOMAIN_PACKER_V1`, `DOMAIN_CHUNK_V1`, `DOMAIN_CRL_V1`, `DOMAIN_AUDIT_V1`)
|
||||
- **`@suaveplan/crypto-cli`** (new at `packages/crypto/crypto-cli/`) — `keychain` binary built on `@suaveplan/cli` + `@suaveplan/readline` + `@suaveplan/crypto`. Eight commands: `root init`, `server init`, `server roll`, `server revoke`, `inspect`, `verify`, `export-public`, `restore`. TTY-only password input, stdin/stdout streamable, distinct exit codes.
|
||||
|
||||
### Removed (folded into new `crypto`)
|
||||
|
||||
- **`@suaveplan/crypto-keys`** — REMOVED. `Keypair`, `Keychain` adapter, `validate`, `generate` move into `@suaveplan/crypto/keychain`. Folding eliminates the artificial split between "key shape" and "keychain" — they were always one concept.
|
||||
- **`@suaveplan/crypto-vault`** — REMOVED. `Vault`, `BIP39 mnemonic`, and the misleadingly-named `ChainSigner` move into `@suaveplan/crypto`. `ChainSigner` is replaced by `Authority` (which earns the name by actually signing under a hierarchy).
|
||||
|
||||
### Modified
|
||||
|
||||
- **`@suaveplan/slsa`** — builder identity is no longer constructed from raw `crypto.ed25519` calls. Provenance signing goes through `Authority.sign(DOMAIN_SLSA_BUILDER_V1, payload)`; builder keys are issued as server identities under a SLSA-builder root. The DSSE envelope format is unchanged; only the construction path changes.
|
||||
- **`@suaveplan/bitpacker`** — adds a `"bytes"` field type (fixed-length byte arrays, 1–65535 bytes per field, byte-aligned) and exposes `BitWriter.byteAlign()` / `BitReader.byteAlign()`. Required by `@suaveplan/crypto`'s wire-format schemas which carry 4-byte ASCII tags, 32-byte keypairs, 16-byte nonces, and 64-byte signatures. The original main BitPacker had this concept (`"32u8"` field syntax); the genesis rewrite dropped it and the rewrite was the constraint that drove the AI-generated `keys-common.ts` to cram `userId` into 4 bits. Restoring it lets the keychain use full-width derivation coordinates as designed.
|
||||
|
||||
### Wire-format schemas (introduced as part of this change)
|
||||
|
||||
The new `@suaveplan/crypto` package commits these schemas as canonical wire formats; every one has a golden-bytes KAT fixture:
|
||||
|
||||
- `DERIVATION_LAYOUT_V1` — pluggable u64 layout for libsodium's `subkey_id` (default: `masterVersion u16 ‖ userId u32 ‖ depth u8 ‖ slotVersion u8`); custom layouts registered via `defineDerivationLayout(id, schema)`
|
||||
- `DERIVATION_COORDINATE_SCHEMA` — `purpose ASCII[4] ‖ application ASCII[4] ‖ layoutId u8 ‖ <layout fields>` — fully replaces the AI-rewritten 28-bit `{v,m,u,d,o}` ceiling with the libsodium-native 128-bit derivation namespace
|
||||
- `KEY_SLOT_PRIVATE_SCHEMA` / `KEY_SLOT_PUBLIC_SCHEMA` — per-slot record with coordinate + algorithm enum + validity window + key material
|
||||
- `VAULT_PAYLOAD_SCHEMA` — full keychain + `seedCount` + arbitrary application `data: Uint8Array`, parent-signed under `DOMAIN_VAULT_V1`, then password-AEAD-encrypted (private-key blob is a separate password-encrypted regime so `changePassword` doesn't re-derive)
|
||||
- `PUBLIC_BUNDLE_SCHEMA` — `magic("SVPB") ‖ formatVersion ‖ issuerRef ‖ issuedAt ‖ nonce ‖ subjectRef ‖ slots[] ‖ parentSignature[64]`
|
||||
- `CHAIN_BUNDLE_SCHEMA` — `magic("SVCB") ‖ formatVersion ‖ linkCount ‖ links[]` for X.509-style chain traversal (root-of-trust at the tail; `verifyChain` walks leaf → root checking each link's parent signature, validity window, and CRL status)
|
||||
- `CRL_ENTRY_SCHEMA` and `AUDIT_ENTRY_SCHEMA` — revocation list and audit log entries (each root-signed)
|
||||
|
||||
### Enforcement (the anti-sediment gate)
|
||||
|
||||
- **Import allowlist in `scripts/openspec-conformance.ts`**: any package other than `@suaveplan/crypto` and `@suaveplan/crypto-cli` that imports `@suaveplan/crypto-core` (or `crypto-utils` / `crypto-hash` / `crypto-curve` / `crypto-cipher` for cryptographic operations — encoding helpers stay open) fails conformance with `error` severity. The allowlist is committed to `scripts/conformance/crypto-core-allowlist.yml` so additions are reviewable.
|
||||
- **Wire-format KAT vectors** at `packages/crypto/crypto/test-fixtures/wire-format/*.bin` with a golden-bytes test asserting byte-for-byte stability. Any future refactor that changes the format fails loudly.
|
||||
- **Domain-separation tag** prefixed before every signed payload. Tags are typed as branded strings, not raw `Uint8Array`s, so a caller cannot omit one or pass the wrong one.
|
||||
- **Algorithm tag** required on every keychain slot. Verifier checks the tag against an allowlist before dispatching to the engine. Keys without a tag are rejected at parse time.
|
||||
- **Validity windows** (`notBefore`, `notAfter`) baked into every keychain slot, checked against an injectable `Clock` at every verify call. No `Date.now()` anywhere in `src/`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Threshold / Shamir-split root.** The `Authority` interface is designed so a threshold backend can be dropped in later, but no threshold implementation ships in this change. Single-key root only, v1.
|
||||
- **HSM / YubiKey / TPM driver implementations.** `Authority` is the abstraction; concrete hardware-backed drivers are deferred to dedicated changes per backend (`crypto-authority-yubikey`, `crypto-authority-tpm`).
|
||||
- **Post-quantum algorithms.** The algorithm tag exists so a PQ algorithm can be added without a wire-format break, but no PQ engine is wired in v1.
|
||||
- **Network-fetched CRL distribution.** `RevocationList` is read from a local file or in-memory blob in v1; `--fetch-crl` and any HTTP/OCSP-style retrieval is a follow-up.
|
||||
- **Key escrow / recovery service.** BIP39-mnemonic-based local recovery only.
|
||||
- **Trust-anchor distribution mechanism.** v1 documents two patterns (compiled-in pinned constant; env-var path to a trusted public bundle); no opinionated registry / TUF-style updater ships.
|
||||
- **Migration of `slsa` Sigstore Rekor integration to share the `AuditLog`.** Flagged as a reuse opportunity but kept separate for v1 to keep this change reviewable.
|
||||
- **Re-pointing every existing consumer of `crypto-core` primitives that uses encoding helpers** (`toHex`, `fromHex`, `toBase64`) — those stay open. Only the *cryptographic* primitives (sign / verify / aead-encrypt / aead-decrypt / agree / sealed-box / auth-box / hash-when-used-as-MAC) trigger the conformance gate.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from five legacy locations across `../nx/` and `../suaveplan/`. Every requirement in the spec deltas cites at least one source range below.
|
||||
|
||||
### `../suaveplan/enterprise/keys.md` (118 lines)
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Keychain — 8 named slots with `{v, m, u, d, o}` metadata | L42-L49, L95-L117 (cross-ref `keys-common.ts`) | Reworded; algorithm tag and validity window added (above-baseline, see "no source line" table) |
|
||||
| Keychain — JWK / PEM / raw lossless serialisation | L40-L41 | Bit-packed binary kept as primary; JWK/PEM added as alternative encoders for ecosystem compatibility |
|
||||
| Keychain — rotation lifecycle (active / next / retired) | L41 | Implemented as version-bump (`rollServer`) + CRL (`revokeServer`); KID stable across rotations |
|
||||
| Keychain — Zod schemas, schema-registry integration | L41 | All public types validated; `crypto.wireFormatVersion` registered with `@suaveplan/schema-registry` |
|
||||
| Keychain — property-based round-trips | L42 | fast-check round-trip suite over BitPacker schemas |
|
||||
| Keychain — injectable Clock and RNG | L43 | `Clock` and `Rng` consumed via constructor; module scope deterministic |
|
||||
| Keychain — OTel instrumentation, no key bytes in attributes | L44 | Verified via OTel attribute-redaction test |
|
||||
| Keychain — structured errors, never leak key bytes | L45 | `CryptoError` codes; redaction test asserts no `Uint8Array` attribute over 16 bytes |
|
||||
| Keychain — secure memory hygiene (zeroise on release) | L46 | `Vault.lock()` / `Authority.dispose()` zeroise; tested via post-zeroise inspection |
|
||||
| Keychain — `AbortSignal` everywhere async | L47 | All async surfaces accept `AbortSignal` |
|
||||
| Keychain — i18n-ready, no hard-coded user-visible strings | L49 | All errors keyed via `@suaveplan/i18n` resolver |
|
||||
|
||||
### `../suaveplan/enterprise/server-keys.md` (118 lines)
|
||||
|
||||
| Requirement (in `specs/crypto-cli/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| CLI — server key management commands | L11-L12 | Reworded into eight concrete commands |
|
||||
| CLI — JWT identity & RBAC integration | L40 | Out-of-scope for v1 (CLI ships standalone); flagged as integration target |
|
||||
| CLI — Idempotency-Key handling | L42 | Adapted: CLI mutations (`server roll`, `revoke`) accept `--idempotency-key` and dedupe via local file ledger |
|
||||
| CLI — audit logging on writes | L43 | `AuditLog` append on every state-changing command |
|
||||
| CLI — observability (spans, logs, metrics) | L44 | OTel spans + structured logger output |
|
||||
| CLI — health / readiness | L46 | Out-of-scope (CLI is not a long-lived service) |
|
||||
|
||||
### `../suaveplan/enterprise/crypto.md` (117 lines)
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Engine-pluggable cryptographic backends | (entire file context) | Already satisfied by genesis `crypto-core` (formerly `crypto`); preserved by rename |
|
||||
| Domain-separation tags on every signed payload | (above-baseline) | Added in genesis; not in source — see "no source line" table |
|
||||
|
||||
### `../suaveplan/enterprise/packer.md` (118 lines) and `packer-browser.md` (114 lines)
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Packer — encrypt + sign envelope | packer.md L11-L40 | Implemented via `Packer.pack()` / `Unpacker.unpack()` |
|
||||
| Packer — chunked P2P fragmentation | packer.md L41-L60 | `ChunkedPacker.packFileToChunks()` / `ChunkedUnpacker.reassembleFile()` |
|
||||
| Packer — per-chunk signature | packer.md L41-L60 | Each chunk signed independently; tampering with any chunk fails on reassembly |
|
||||
| Packer — streaming for large files | packer.md L61-L80 | Web Streams; injectable highWaterMark |
|
||||
| Packer — compression (configurable level) | packer.md L41-L60 | Zstd preferred; level 0–9 |
|
||||
| Packer — event emission for progress | packer.md L61-L80 | `events` integration; deterministic FIFO |
|
||||
| Packer — schema-driven structured payloads | packer-browser.md L20-L60 | BitPacker schemas drive both packer and packer-public-keychain wire formats |
|
||||
|
||||
### `nx/libs/keys/src/lib/keys-common.ts` (296 lines, full file)
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Keychain — 8-slot wire schema (`fullKeychainSchema`) | L60-L69 | Verbatim slot names; field types preserved |
|
||||
| Keychain — public-only schema (`publicKeychainSchema`) | L33-L38 | Verbatim; used by `packPublicKeys` |
|
||||
| Vault — `packVault` (pack → sign → encrypt-with-password) | L111-L127 | Algorithm preserved; AES-GCM replaced with XChaCha20-Poly1305 (above-baseline rationale below) |
|
||||
| Vault — `unpackVault` (decrypt → verify → unpack) | L129-L173 | Domain-separation tag added (above-baseline) |
|
||||
| Public bundle — `packPublicKeys` with self-embedded signature | L91-L109 | Verbatim semantics; tag-prefixed |
|
||||
| Public bundle — `viewPublicKeys` self-verifies via embedded sign key | L175-L213 | Verbatim semantics |
|
||||
| Hierarchy — `createRootKeys` | L215-L226 | Renamed `createRoot` |
|
||||
| Hierarchy — `createServerKeys(root, password)` | L228-L247 | Renamed `createServer`; takes `Authority` instead of raw root keychain |
|
||||
| Hierarchy — `rollServerKeys(root, password, currentVersion)` | L249-L272 | Renamed `rollServer`; emits CRL entry for prior version (above-baseline) |
|
||||
| Vault file I/O (`openVault`) | L274-L280 | Folded into CLI; not in library surface |
|
||||
|
||||
### `nx/libs/crypto-packer/README.md` and `src/`
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| `BitPackerCrypto` (schema + encrypt + sign in one) | README L52-L168 | Reborn as `Packer` with the same triple, generalised |
|
||||
| `BitPackerCryptoConfig` shape | README L249-L260 | Mapped to `PackerOptions` |
|
||||
| Recipient-verification example (sender encrypts to recipient pubkey, signs with own privkey) | README L282-L366 | Preserved as `Packer` use case; tested as `it("seals to recipient and verifies sender", ...)` |
|
||||
| Event-driven error handling | README L262-L279 | Mapped to `events` integration on `Packer`/`ChunkedPacker` |
|
||||
|
||||
### `nx/libs/file-packer/README.md` and `src/`
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| File-level packing with metadata (filename, type, owner, expiry) | README L22-L91 | Metadata wrapper preserved; `verifyOwner` / `checkExpiry` mapped to `Authority`-checked claims |
|
||||
| Pluggable validator (size limit, type allowlist) | README L57-L62 | Out of scope here — kept as a future `@suaveplan/file-validator` package consuming `crypto`'s `Packer` |
|
||||
| Pluggable storage backend | README L52-L56 | Out of scope here — same rationale |
|
||||
|
||||
### `suaveplan/packages/universal/packer/src/index.md` (703 lines)
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| `Packer` / `Unpacker` / `ChunkedPacker` / `ChunkedUnpacker` surface | L20-L109 | Verbatim API shape preserved |
|
||||
| AEAD options (AES-256-GCM / XChaCha20-Poly1305 / ChaCha20-Poly1305) | L11-L18 | Verbatim; default switched to XChaCha20-Poly1305 |
|
||||
| Streaming via Web Streams + async iterables | L296-L348 | Verbatim |
|
||||
| P2P chunk verification (per-chunk signed; tampered chunks reject) | L356-L448 | Verbatim semantics; reassembly tested with tampered-chunk scenario |
|
||||
| BaseKey integration (every signing/encrypt op consumes a Keychain entry, not raw bytes) | L8-L18, L181-L246 | This is the central rule; enforced via `Authority` requiring `Keychain` slot reference |
|
||||
| Event system (pack/unpack/chunk/reassemble events) | L550-L604 | Mapped to `@suaveplan/events` |
|
||||
|
||||
### `suaveplan/packages/universal/crypto/src/keys/keychain/keychain.md`
|
||||
|
||||
| Requirement (in `specs/crypto/spec.md`) | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| `generateKeychain` / `filterKeychain` / `extractPublicKeys` / `extractPrivateKeys` | L16-L67 | Verbatim API; method names preserved |
|
||||
| Keychain structure (8 slots: master / encrypt / sign / kx / salt / hash / stream / device) | L95-L117 | Verbatim slot list and semantics |
|
||||
| Per-key fields (`s`, `p`, `v`, `m`, `d`, `u`, `o`) | L108-L117 | Verbatim — these are the wire format |
|
||||
| Hierarchical derivation for multiple purposes | L9-L13 | Each slot derived independently from master + slot offset |
|
||||
| Security: master never shared, `extractPublicKeys` before transmit, namespace separation by user ID | L118-L124 | Tested as separate scenarios |
|
||||
|
||||
### Requirements with no source line (above-baseline additions)
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Algorithm tag required on every keychain slot | Original is hard-wired ed25519/x25519. Without a tag, post-quantum migration or even ECDSA-P256 fallback requires a wire-format break. The tag is reserved space at zero cost in v1. |
|
||||
| Validity windows (`notBefore`, `notAfter`) | Original treats all keys as valid forever. SSL has these for a reason. Without them, a stolen key keeps verifying. |
|
||||
| Domain-separation tags before every signature | A signature over message X in protocol A must not be valid in protocol B. The original signs raw bytes; this is a real cross-protocol replay risk. Cheap to add now, expensive after deployment. |
|
||||
| `RevocationList` (signed CRL) | Without revocation, key rotation is a lie — old server keys verify forever. The original `rollServerKeys` increments version but never marks the prior version as revoked. |
|
||||
| `AuditLog` (signed Merkle log of issue/roll/revoke) | Required to prove "this server key was valid at time T" after the fact. Reuses `slsa`'s Rekor pattern conceptually. |
|
||||
| `Authority` interface (not raw secret-key references) | The root is the crown jewel. Even if v1 ships an in-memory backend, the *interface* must allow HSM/threshold backends without changing every caller. Passing `Uint8Array` secret keys around precludes this. |
|
||||
| Replay protection: `issuedAt` + `nonce` in signed envelopes | Original `packPublicKeys` signs only the bit-packed bytes; an old signed envelope replays indefinitely. Verifier rejects envelopes whose `issuedAt` is older than a configurable window or whose `nonce` was already seen. |
|
||||
| Default AEAD switched from AES-GCM to XChaCha20-Poly1305 | XChaCha20-Poly1305 has a 192-bit nonce, eliminating the nonce-reuse class of bugs that plague AES-GCM in long-lived vault scenarios. AES-GCM remains available as an explicit option. |
|
||||
| `crypto-core` import allowlist (anti-sediment gate) | The single most important guarantee in this proposal. Documentation alone has demonstrably failed to prevent direct primitive use (see `slsa` calling `crypto.ed25519.sign` today). Mechanically enforced. |
|
||||
| Wire-format KAT vectors with golden-bytes test | Pins the wire format against silent regression in any future refactor. Without this, v2 of the keychain could change a field width and old vaults break with no test failure. |
|
||||
| TTY-only password input in CLI | `--password` flag leaks via shell history. Non-negotiable for a credential-handling CLI. |
|
||||
| Distinct CLI exit codes per failure class | Operators need to distinguish "wrong password" from "tampered vault" from "expired key" from "revoked key" — each requires a different operational response. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
Every requirement is pinned by named `it(...)` titles in tests committed in this change. Coverage is asserted by the existing pre-publish gate (`bunx turbo test:coverage --filter=@suaveplan/<pkg>` at 100/100/100/100).
|
||||
|
||||
### `@suaveplan/crypto-core` (rename only)
|
||||
|
||||
- `tests/rename.test.ts` — pins that every previously-public symbol is still exported under the new package name with the same signature
|
||||
- `tests/import-allowlist.test.ts` — pins that the only packages allowed to import `@suaveplan/crypto-core` are `@suaveplan/crypto` and `@suaveplan/crypto-cli`
|
||||
|
||||
### `@suaveplan/crypto`
|
||||
|
||||
- `src/keychain/keychain.test.ts` — slot generation, algorithm tag enforcement, validity-window enforcement, public/private extraction, hierarchical derivation
|
||||
- `src/keychain/keychain.spec-conformance.test.ts` — type-level slot map; OTel attribute-redaction; strict-mode behaviour; `expectTypeOf` on every literal claim
|
||||
- `src/vault/vault.test.ts` — pack → sign → encrypt round-trip; tamper detection (swap byte, expect verify failure); BIP39 restore; `Vault.lock()` zeroes secrets
|
||||
- `src/vault/vault.spec-conformance.test.ts` — KAT vectors against committed `test-fixtures/wire-format/vault-v1.bin`; Argon2id parameter floor enforced
|
||||
- `src/packer/packer.test.ts` — encrypt-and-sign envelope; recipient-verification scenario from `nx/libs/crypto-packer/README.md`; replay rejection via `issuedAt`+`nonce`
|
||||
- `src/packer/packer.spec-conformance.test.ts` — KAT vectors; domain-separation tag enforcement (mismatched tag must fail verify)
|
||||
- `src/chunked-packer/chunked-packer.test.ts` — fragment / per-chunk-sign / reassemble; tampered-chunk rejection at reassembly
|
||||
- `src/chunked-packer/chunked-packer.spec-conformance.test.ts` — streaming via Web Streams; event emission order; `AbortSignal` cancellation
|
||||
- `src/authority/authority.test.ts` — sign-under-domain-tag; revoked-key signing rejection; `dispose()` zeroes; Clock-driven validity-window enforcement
|
||||
- `src/authority/authority.spec-conformance.test.ts` — interface conformance harness reusable by future HSM backends
|
||||
- `src/revocation/revocation.test.ts` — append signed entry; verify entry against trust anchor; reject mutation of past entries; check-by-version
|
||||
- `src/audit-log/audit-log.test.ts` — append → Merkle root → root-signature; tamper detection (modify any past entry, expect root mismatch)
|
||||
- `src/identity/root-identity.test.ts` and `src/identity/server-identity.test.ts` — `createRoot` / `createServer` / `rollServer` / `revokeServer` happy paths and edge cases (re-roll same version, revoke non-existent server)
|
||||
- `tests/dep-graph.test.ts` — pins that `@suaveplan/crypto`'s own dependencies are exactly: `crypto-core`, `crypto-utils`, `crypto-hash`, `crypto-curve`, `crypto-cipher`, `bitpacker`, `error`, `events`, `types`, `i18n`, `opentelemetry`, plus external `@scure/bip39`
|
||||
- `tests/wire-format-kat.test.ts` — golden-bytes assertion against every fixture under `test-fixtures/wire-format/`
|
||||
- `tests/property-roundtrips.test.ts` — fast-check property tests for all pack/unpack pairs
|
||||
|
||||
### `@suaveplan/crypto-cli`
|
||||
|
||||
- `src/commands/root-init.test.ts` through `src/commands/restore.test.ts` — one test file per command; happy path + every failure mode (bad password, tampered vault, expired key, revoked key, missing trust anchor)
|
||||
- `tests/exit-codes.test.ts` — pins distinct exit codes per failure class
|
||||
- `tests/no-password-flag.test.ts` — pins that `--password` is rejected with a security error and a pointer to TTY/env-var input
|
||||
- `tests/tty-mock.test.ts` — driven via the `cli` package's `InputReader` interface; verifies password-prompt UX without a real TTY
|
||||
|
||||
### `@suaveplan/slsa` (modified)
|
||||
|
||||
- `src/builder/builder-identity.test.ts` — modified: builder constructed via `Authority`, signing via `Authority.sign(DOMAIN_SLSA_BUILDER_V1, …)`. The DSSE envelope output is unchanged — covered by the existing round-trip test.
|
||||
- `tests/no-direct-crypto-core.test.ts` — pins that `slsa` no longer imports `@suaveplan/crypto` (now `crypto-core`) directly for signing operations
|
||||
|
||||
### Conformance gate
|
||||
|
||||
- `scripts/conformance/crypto-core-allowlist.test.ts` — runs `scripts/openspec-conformance.ts` across the monorepo and asserts the import allowlist is honoured
|
||||
|
||||
## Migration impact
|
||||
|
||||
### `@suaveplan/crypto` (old name, now `crypto-core`) consumers in the monorepo
|
||||
|
||||
`grep -rl "@suaveplan/crypto\"" packages/` lists every consumer that needs an import-path update. Most consumers will be moved to `@suaveplan/crypto` (the new opinionated layer); only signed-construction-internal cases stay on `@suaveplan/crypto-core`.
|
||||
|
||||
| Consumer | Today's import | Migrates to | Reason |
|
||||
|---|---|---|---|
|
||||
| `@suaveplan/crypto-keys` | `@suaveplan/crypto/sign`, `@suaveplan/crypto-utils` | (deleted; merged into `@suaveplan/crypto`) | Folded |
|
||||
| `@suaveplan/crypto-vault` | `@suaveplan/crypto/sign`, `@suaveplan/crypto-keys` | (deleted; merged into `@suaveplan/crypto`) | Folded |
|
||||
| `@suaveplan/crypto-sodium` | `@suaveplan/crypto` | `@suaveplan/crypto-core` | Engine adapter — primitive-layer consumer |
|
||||
| `@suaveplan/crypto-browser` | `@suaveplan/crypto` | `@suaveplan/crypto-core` | Engine adapter — primitive-layer consumer |
|
||||
| `@suaveplan/slsa` | `@suaveplan/crypto`, `@suaveplan/crypto-hash`, `@suaveplan/crypto-utils` | `@suaveplan/crypto` (for signing); `@suaveplan/crypto-hash` and `@suaveplan/crypto-utils` stay (hashing + encoding helpers) | Builder identity moved to `Authority` |
|
||||
| Any package that today writes `import { ed25519 } from "@suaveplan/crypto"` | (drop) | `@suaveplan/crypto` `Authority.sign(...)` | Anti-sediment rule |
|
||||
| Any package that uses `toHex`/`fromHex`/`toBase64` from `@suaveplan/crypto-utils` | unchanged | unchanged | Encoding helpers stay open |
|
||||
|
||||
### Removed package consumers
|
||||
|
||||
- `@suaveplan/crypto-keys` is currently imported only by `@suaveplan/crypto-vault` (folding both eliminates the edge entirely).
|
||||
- `@suaveplan/crypto-vault` is currently not imported by anything else in genesis (cross-checked against `MANIFEST.md`).
|
||||
|
||||
### Manifest / CLAUDE.md
|
||||
|
||||
- `CLAUDE.md` package list: remove `crypto-keys`, `crypto-vault`; rename `crypto` → `crypto-core`; add `crypto`, `crypto-cli`. The single `## Packages` listing in CLAUDE.md is the canonical edit; `MANIFEST.md` is regenerated via `bun scripts/folder-reorg/generate-manifest.ts`.
|
||||
- `scripts/folder-reorg/assignment.yml` — update plugin assignments for the renamed/new packages (all stay under the `crypto` plugin).
|
||||
|
||||
### Backwards compatibility
|
||||
|
||||
None. Genesis is at `0.1.0` for every package; no downstream contracts exist yet. The rename and the fold both happen in a single commit set; no shim is shipped.
|
||||
Reference in New Issue
Block a user