Files
2026-05-27 23:29:53 +01:00

99 lines
14 KiB
Markdown
Raw Permalink 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.
# Add @suaveplan/crypto-keys — hierarchical keychain chain-signing
**Package**: `@suaveplan/crypto-keys`
**Status**: PROPOSED
**Type**: genesis-original (inspired-by-upstream)
- **Design**: ./design.md
- **Spec delta**: ./specs/crypto-keys/spec.md
## Why
Upstream `@suaveplan/keys` at `../suaveplan/packages/universal/keys/` shipped a hierarchical key-signing primitive (`ChainedKeySigner`) that genesis has no equivalent for. The pattern is well-defined: a chain of public keychains, each one signed by its parent's sign-key, with the root self-signed and verification walking back up the chain to a trusted root. Use cases include: root → server → user key issuance with end-to-end provenance; attestation of derived keychains in multi-tenant deployments; recoverable root-of-trust trees in vault systems. None of these are achievable today on genesis with the existing `@suaveplan/crypto`, `@suaveplan/crypto-curve`, `@suaveplan/crypto-vault`, or `@suaveplan/vault` packages — they cover primitives, vault storage, and password-based recovery, but not the chain-of-signatures pattern itself.
Direct port is not viable. Upstream `ChainedKeySigner` is tightly coupled to obsolete machinery: it uses `BitPacker(publicKeychainSchema)` for the wire format and a high-level `sign(signKey, packed)` API that don't exist on genesis. Genesis's `@suaveplan/crypto` re-implemented the keychain layer with a v1 derivation grammar (`encodePublicKeychain`, per-slot `AlgorithmId`, `KEY_SLOT_PUBLIC_BYTE_SIZE`, `slotAlgorithmMatchesDefault`, `ALGORITHMS` registry) and `@suaveplan/crypto-curve` exposes only per-algorithm `sign(digest, privateKey)` primitives. The chain-signing pattern needs to be **re-designed** on top of these primitives, not transliterated.
This proposal defines that redesign as a fresh genesis-original package built around two pluggable interfaces (`ChainHasher` for what-gets-signed and `ChainCodec` for the wire format), three shipped implementations of each (raw / domain-tagged / merkle hashers; TLV / recursive-Ed25519 / CBOR codecs), the `ChainedKeySigner` class that composes them, and a hierarchical-key-factory layer (`createRootKeysWithChain` / `createServerKeysWithChain` / `rollServerKeysWithChain`) on top.
## What changes
- ADDED: `ChainHasher` interface — `hash(encodedKeys, parent?): Uint8Array`. Produces the bytes a signer signs for one chain level. Three shipped implementations:
- `RawHasher` — returns `encodedKeys` unchanged. Default.
- `DomainHasher({ domainTag: string })` — returns `sha256(domainTag || encodedKeys)`. Domain tag is mandatory at construction; empty/oversize strings raise `CHAIN_KEYS.INVALID_DOMAIN_TAG`.
- `MerkleHasher` — for non-root levels returns `sha256(encodedKeys || sha256(parent.encodedKeys || parent.signature))`; for root returns `sha256(encodedKeys)`. Each level commits to its parent's content.
- ADDED: `ChainCodec` interface — `encode(levels): Uint8Array`, `decode(bytes): ChainLevel[]`. Three shipped implementations:
- `TlvChainCodec` (default) — `[version=1: u8][hasher: u8]` header, then per level `[algorithm: u8][keys_len: u16 BE][keys][sig_len: u16 BE][sig]`. Self-describing; per-algorithm signatures supported.
- `RecursiveEd25519Codec` — `[version=1: u8][hasher: u8][keys][sig: 64][parent?]` recursive. Restricted to Ed25519; any other algorithm raises `CHAIN_KEYS.UNSUPPORTED_ALGORITHM`. Keys-section length is the fixed `encodePublicKeychain` output size (read from the genesis crypto constant), so the recursion boundary is unambiguous without a length prefix.
- `CborChainCodec` — `[version=1: u8][cbor]` where the CBOR payload is `{ v: 1, hasher: <name>, levels: [{ a: <algorithm>, k: <keys>, s: <signature> }, ...] }`. Depends on `@suaveplan/cbor`.
- ADDED: `ChainedKeySigner` class — constructor accepts `{ hasher?, codec? }` with defaults `RawHasher` and `TlvChainCodec`. Two signing surfaces (slot-based ergonomic helper + raw-key fully-general) plus shared verify / parse / strip:
- `async signWithSlot(payload: Uint8Array, signSlot: KeySlotPrivate, parent?: Uint8Array): Promise<Uint8Array>` — convenience for sign-slots from genesis `@suaveplan/crypto` keychain. In v0.0.0 only Ed25519 sign-slots are supported (the only signing-capable algorithm in the v1 keychain grammar); other slot algorithms raise `CHAIN_KEYS.UNSUPPORTED_ALGORITHM`. Extracts `algorithm` and `publicKey` from the slot.
- `async signRaw(options: { payload: Uint8Array; algorithm: SignAlgorithmId; privateKey: Uint8Array; publicKey: Uint8Array; parent?: Uint8Array }): Promise<Uint8Array>` — fully general; supports all 5 signing algorithms exposed by `@suaveplan/crypto-curve` (`ed25519`, `secp256k1`, `p256`, `p384`, `p521`). Caller supplies the algorithm tag, private+public key material, and an arbitrary payload. Dispatches to the matching crypto-curve primitive.
- `async verify(chainBytes: Uint8Array, trustedRoot: { algorithm: SignAlgorithmId; publicKey: Uint8Array }): Promise<VerificationResult>` — decodes the chain via the configured codec, walks every level recomputing bytes-to-sign via the hasher, dispatches verification to crypto-curve by each level's stored algorithm, and checks the root level's `(algorithm, signerPublicKey)` matches `trustedRoot` via constant-time compare. Returns `{ valid, depth, levels, errors }`.
- `async parse(chainBytes: Uint8Array): Promise<{ depth: number; levels: readonly ChainLevel[] }>` — decode-only (no verification).
- `async stripToDepth(chainBytes: Uint8Array, depth: number): Promise<Uint8Array>` — re-encodes the leaf-most `depth` levels only. `depth < 1` or `depth > chain.length` raises `CHAIN_KEYS.INVALID_DEPTH`.
- ADDED: `SignAlgorithmId` type — `"ed25519" | "secp256k1" | "p256" | "p384" | "p521"`. Distinct from `@suaveplan/crypto`'s `AlgorithmId` numeric enum (which covers all crypto algorithms, not just signing); `SignAlgorithmId` is the string-tagged subset this package signs/verifies. The TLV codec maps `SignAlgorithmId` ↔ `u8` enum bytes.
- ADDED: `ChainLevel` type — `{ algorithm: SignAlgorithmId; signerPublicKey: Uint8Array; payload: Uint8Array; signature: Uint8Array }`. The `payload` is whatever the signer was asked to sign (typically `encodePublicKeychain(...)` output for keychain chains, but the package treats it as opaque bytes). The `signerPublicKey` is recorded per-level so verifiers can run end-to-end without parsing each level's payload. `VerificationResult` — `{ valid, depth, levels, errors }`.
- ADDED: Hierarchical factory functions — Ed25519-only convenience wrappers over `generateKeychain` + `signWithSlot`:
- `createRootKeysWithChain({ signer?, ...generateOptions }): Promise<{ keys: Keychain; chain: Uint8Array }>`
- `createServerKeysWithChain({ rootKeys, rootChain, signer?, ...generateOptions }): Promise<{ keys; chain }>`
- `rollServerKeysWithChain({ rootKeys, rootChain, currentVersion, signer? }): Promise<{ keys; chain }>` — bumps the keychain version using `generateKeychain({ version: currentVersion + 1, masterKey: rootKeys.master.private })`.
- ADDED: `CHAIN_KEYS_CODES` registry via `defineCodes("CHAIN_KEYS", [...])` — 7 codes: `UNSUPPORTED_ALGORITHM`, `INVALID_FORMAT`, `INVALID_SIGNATURE`, `TRUST_MISMATCH`, `INVALID_DEPTH`, `INVALID_KEYCHAIN`, `INVALID_DOMAIN_TAG`.
- ADDED: `ChainKeysError extends SuaveplanError<ChainKeysErrorCode>` with optional `depth?: number` and `algorithm?: AlgorithmId` diagnostic discriminants.
- ADDED: Subpath exports for every feature module — `./error-codes/error-codes`, `./types/chain-level`, `./types/hasher`, `./types/codec`, `./hashers/raw/raw`, `./hashers/domain/domain`, `./hashers/merkle/merkle`, `./codecs/tlv/tlv`, `./codecs/recursive-ed25519/recursive-ed25519`, `./codecs/cbor/cbor`, `./signer/signer`, `./factory/factory`.
## Out of scope
- **VaultManager / VaultFactory / VaultStreams / `packVault` / `unpackVault` / `openVault`.** These are vault-orchestration concerns (password+mnemonic recovery, vault rollover, ChunkedPacker integration). They belong in an extension of the existing `@suaveplan/crypto-vault` package, not here. A follow-up `extend-crypto-vault-vault-manager` proposal will own them. Reason: the chain-signing primitive and the vault-as-key-manager are independent layers; keeping them in separate packages prevents `crypto-keys` consumers from pulling in `@suaveplan/drive` and `@suaveplan/streams` they don't need.
- **The upstream bitpacker keychain Pack schemas** (`fullKeychainSchema`, `publicKeychainSchema`, `baseKeySchema*`) and the helpers built on them (`packPublicKeys`, `viewPublicKeys`, `prepareKeysForPacking`). Genesis `@suaveplan/crypto` already provides `encodePublicKeychain` / `decodePublicKeychain` / `extractPublicKeys` over the v1 derivation grammar. Re-shipping the upstream bitpacker layer would create two incompatible wire formats for the same data. Reason: genesis crypto's v1 grammar supersedes the upstream pack schemas with stronger algorithm enforcement and validity windows.
- **OpenTelemetry instrumentation.** No spans, counters, or meters on this initial version. The package is a pure data transform (encode/decode/sign/verify); the call sites that wrap it will own their observability. Reason: emitting OTel from a leaf primitive doubles the surface for marginal value; revisit once a real consumer asks.
- **Schema-registry integration.** `@suaveplan/schema-registry` versioning of the wire format is overkill for a single chain-format version that's already self-describing via its version byte. Reason: the version byte covers forward-compatibility; registry overhead isn't justified for an internal binary contract.
- **Streaming verification.** The chain wire format is a small fixed-shape blob (bounded by `8 * KEY_SLOT_PUBLIC_BYTE_SIZE * max_depth` + signatures + framing); streaming adds complexity for no realistic payload size. Reason: chains are always small enough to verify in memory.
## Source mapping
Upstream reference: `../suaveplan/packages/universal/keys/src/lib/chain-signer.ts` (419 lines) plus the hierarchical factory chunks of `keys-common.ts` (`createRootKeysWithChain` / `createServerKeysWithChain` / `rollServerKeysWithChain`). The upstream supplies the **contract sketch** (sign/verify/parse/stripToDepth shape, root → server → user pattern, recursive wire format) but every implementation detail is re-derived from genesis primitives. The package is therefore **genesis-original**, not a 1:1 migration.
### Parity / divergence checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| Hierarchical sign-verify-parse-strip surface | ✅ `signKeysWithChain` / `verifyChain` / `extractKeys`/ `getChainDepth` / `stripToDepth` | ⬆️ Modernized | Methods renamed to `sign`/`verify`/`parse`/`stripToDepth` (idiomatic async API); `extractKeys` and `getChainDepth` fold into `parse(...)`'s return |
| Bitpacker-based keychain wire format | ✅ via `BitPacker(publicKeychainSchema)` | 🚫 Replaced | Uses `encodePublicKeychain` from genesis `@suaveplan/crypto` — the v1 derivation grammar |
| Fixed secp256k1 signing with recovery | ✅ via `sign(signKey, packed, true)` | ⬆️ Enhanced | Dual surface: slot-based (`signWithSlot`, Ed25519-only in v0.0.0 matching genesis keychain v1) plus raw-key (`signRaw`, all 5 curves via `@suaveplan/crypto-curve` primitives) |
| `[keys][sig][parent_chain?]` recursive wire format | ✅ | ⬆️ Generalized | TLV codec ships as default; recursive-Ed25519 codec preserves the upstream shape under its constraint; CBOR codec adds an extensibility option |
| `compareKeys` byte-wise equality | ✅ | ⬆️ Constant-time | Verifier uses constant-time compare from `@suaveplan/crypto-utils` |
| Raw `throw new Error("...")` | ✅ | ⬆️ Replaced | All raised errors extend `SuaveplanError` with stable `CHAIN_KEYS.*` codes |
| `SignedKeyChain` interface | ✅ recursive | ⬆️ Generalized | `ChainLevel[]` array shape; codecs free to encode flat or recursive |
| `ChainVerificationResult` | ✅ | ✅ | Preserved as `VerificationResult` |
| Domain-tagged signing | 🚫 upstream | ⬆️ Added | `DomainHasher({ domainTag })` ships as an opt-in |
| Merkle-style parent commitment | 🚫 upstream | ⬆️ Added | `MerkleHasher` ships as an opt-in |
| CBOR codec | 🚫 upstream | ⬆️ Added | `CborChainCodec` ships for extensibility |
| Hierarchical factory layer (`createRoot/Server/RollServerKeysWithChain`) | ✅ | ✅ | Preserved but adapted to genesis `generateKeychain` + injected `ChainedKeySigner` |
| Vault-side helpers (`packVault`/`unpackVault`/`VaultManager`/`VaultStreams`) | ✅ in upstream `keys` | 🚫 deferred | Belong in a follow-up `crypto-vault` extension; out of scope here |
### Reference docs consulted
- ✅ `../suaveplan/packages/universal/keys/src/lib/chain-signer.ts` — full read
- ✅ `../suaveplan/packages/universal/keys/src/lib/keys-common.ts` — full read (extracted factory section)
- ✅ `../suaveplan/packages/universal/keys/src/keys.types.ts` — full read (informs the divergence rationale)
- ✅ `../suaveplan/enterprise/keys.md` — read; notes that the upstream package's purpose is broader than what's scoped here, with clear sectioning of what stays out
- ✅ `/home/paul/web/genesis/packages/crypto/crypto/src/keychain/keychain.ts` — full read (the genesis Keychain / PublicKeychain / encode/decode surface)
- ✅ `/home/paul/web/genesis/packages/crypto/crypto/src/keychain/index.ts` — full read (public exports of `@suaveplan/crypto` keychain layer)
- ✅ `/home/paul/web/genesis/packages/crypto/crypto-curve/src/primitives/*/impl/` — sampled all 5 algorithm primitives (Ed25519 / secp256k1 / p256 / p384 / p521) to confirm per-algorithm `sign(digest, privateKey)` / `verify(...)` signatures
## Impact
### Affected dependents
No dependents today. `@suaveplan/crypto-keys` is being added net-new. The deferred `crypto-vault` extension will be the first consumer, importing `ChainedKeySigner` to wire vault-signing semantics.
### Risk
- **Low to medium.** Pure data transform with well-defined primitives. The novel pieces are the wire format (three codecs, each with self-describing version bytes) and the hierarchical factory layer; both are deterministic, in-memory, and exercised by property-based tests on round-trips.
- **No breaking changes** to any existing package. Genesis `@suaveplan/crypto` / `crypto-curve` / `crypto-hash` / `cbor` are consumed read-only.
- **Test surface is non-trivial:** 3 hashers × 3 codecs × 5 sign algorithms = 45 combinations the round-trip tests cover. Property-based fast-check generators reduce the manual surface considerably.
### Migration notes
None. Net-new package on genesis.