# @suaveplan/crypto-keys - Hierarchical keychain chain-signing for the crypto ecosystem: pluggable ChainHasher + ChainCodec interfaces with three shipped implementations each (RawHasher / DomainHasher / MerkleHasher and TlvChainCodec / RecursiveEd25519Codec / CborChainCodec); ChainedKeySigner class with per-slot algorithm dispatch across Ed25519/secp256k1/p256/p384/p521; hierarchical key factory (createRootKeysWithChain / createServerKeysWithChain / rollServerKeysWithChain). Universal. Hierarchical keychain chain-signing for the crypto ecosystem. Store-agnostic, free of `node:` / `bun:` imports. ## Documentation tree - [cbor](./src/codecs/cbor/cbor.md) — `CborChainCodec` is the CBOR-based wire-format `ChainCodec` for `@suaveplan/crypto-keys`. It serialises an ordered list… - [cbor](./src/codecs/cbor/cbor.md) — `CborChainCodec` is the CBOR-based wire-format `ChainCodec` for `@suaveplan/crypto-keys`. It serialises an ordered list… - [recursive-ed25519](./src/codecs/recursive-ed25519/recursive-ed25519.md) — `RecursiveEd25519Codec` is a fixed-Ed25519 alternate wire format for signed key chains in `@suaveplan/crypto-keys`. It… - [recursive-ed25519](./src/codecs/recursive-ed25519/recursive-ed25519.md) — `RecursiveEd25519Codec` is a fixed-Ed25519 alternate wire format for signed key chains in `@suaveplan/crypto-keys`. It… - [tlv](./src/codecs/tlv/tlv.md) — `TlvChainCodec` is the default wire-format codec for `@suaveplan/crypto-keys`. It encodes and decodes the `ChainLevel[]… - [tlv](./src/codecs/tlv/tlv.md) — `TlvChainCodec` is the default wire-format codec for `@suaveplan/crypto-keys`. It encodes and decodes the `ChainLevel[]… - [error-codes](./src/error-codes/error-codes.md) — The `@suaveplan/crypto-keys` package centralises every failure mode it can produce behind a single registry built with… - [error-codes](./src/error-codes/error-codes.md) — The `@suaveplan/crypto-keys` package centralises every failure mode it can produce behind a single registry built with… - [factory](./src/factory/factory.md) — Hierarchical-keychain factory helpers that compose `generateKeychain` from `@suaveplan/crypto` with `ChainedKeySigner.s… - [factory](./src/factory/factory.md) — Hierarchical-keychain factory helpers that compose `generateKeychain` from `@suaveplan/crypto` with `ChainedKeySigner.s… - [domain](./src/hashers/domain/domain.md) — `DomainHasher` is the `ChainHasher` strategy that domain-separates the signed pre-image with a caller-supplied tag and… - [domain](./src/hashers/domain/domain.md) — `DomainHasher` is the `ChainHasher` strategy that domain-separates the signed pre-image with a caller-supplied tag and… - [merkle](./src/hashers/merkle/merkle.md) — `MerkleHasher` is the `ChainHasher` strategy that binds each non-root level to its parent's content via a Merkle-style… - [merkle](./src/hashers/merkle/merkle.md) — `MerkleHasher` is the `ChainHasher` strategy that binds each non-root level to its parent's content via a Merkle-style… - [raw](./src/hashers/raw/raw.md) — `RawHasher` is the trivial `ChainHasher` strategy: it returns the payload bytes verbatim, performing no transformation.… - [raw](./src/hashers/raw/raw.md) — `RawHasher` is the trivial `ChainHasher` strategy: it returns the payload bytes verbatim, performing no transformation.… - [signer](./src/signer/signer.md) — `ChainedKeySigner` composes a `ChainHasher` (bytes-to-sign strategy) and a `ChainCodec` (wire-format strategy) to produ… - [signer](./src/signer/signer.md) — `ChainedKeySigner` composes a `ChainHasher` (bytes-to-sign strategy) and a `ChainCodec` (wire-format strategy) to produ… - [chain-level](./src/types/chain-level.md) — Shared type module declaring the value-shapes that every other module in `@suaveplan/crypto-keys` references. Defining… - [codec](./src/types/codec.md) — `ChainCodec` is the wire-format strategy that turns an array of `ChainLevel` values into bytes and back. Decoupling the… - [hasher](./src/types/hasher.md) — `ChainHasher` is the strategy interface that decides what bytes a `ChainedKeySigner` actually signs for one chain level… ## Purpose Ships strong primitives in `@suaveplan/crypto`, `@suaveplan/crypto-curve`, and `@suaveplan/crypto-hash`, plus a keychain layer in `@suaveplan/crypto` itself (`encodePublicKeychain`, `generateKeychain`, per-slot `AlgorithmId`). What it does not ship is the *pattern* of binding a chain of public keychains together with signatures — root signs server, server signs user, and so on — with end-to-end verification back to a trusted root. This package fills that gap. The design is pluggable. Two interfaces (`ChainHasher` for the bytes-to-sign and `ChainCodec` for the wire format) decouple the three shipped hash modes (raw / domain-tagged / merkle) from the three shipped codecs (TLV / recursive-Ed25519 / CBOR). The `ChainedKeySigner` class composes a hasher + codec and dispatches signing per-algorithm to the matching `@suaveplan/crypto-curve` primitive — so a single chain can mix Ed25519, secp256k1, and the P-curves across its levels. ## Features - Three shipped hashers — `RawHasher`, `DomainHasher({ domainTag })`, `MerkleHasher` - Three shipped codecs — `TlvChainCodec` (default), `RecursiveEd25519Codec`, `CborChainCodec` - `ChainedKeySigner` class — `sign` / `verify` / `parse` / `stripToDepth` - Per-slot algorithm dispatch across Ed25519 / secp256k1 / P-256 / P-384 / P-521 - Hierarchical factory — `createRootKeysWithChain`, `createServerKeysWithChain`, `rollServerKeysWithChain` - `ChainKeysError` extending `SuaveplanError` with `depth?` / `algorithm?` discriminants and 7 stable `CHAIN_KEYS.*` codes - Constant-time root-key compare via `@suaveplan/crypto-utils` - 13 tree-shakeable subpath exports — import the codec you want without dragging in the rest - 100% statement / branch / function / line coverage via Istanbul ## Usage: Basic ```ts import { ChainedKeySigner, createRootKeysWithChain, createServerKeysWithChain, } from "@suaveplan/crypto-keys"; // Build a 2-level chain: root → server const { keys: rootKeys, chain: rootChain } = await createRootKeysWithChain(); const { keys: serverKeys, chain: serverChain } = await createServerKeysWithChain({ rootKeys, rootChain, }); // Verify end-to-end against the trusted root sign-slot const signer = new ChainedKeySigner(); const result = await signer.verify(serverChain, rootKeys.sign.public); if (!result.valid) throw new Error(result.errors.join("; ")); console.log("Chain depth:", result.depth); // 2 ``` ## Usage: Advanced ```ts import { ChainedKeySigner, DomainHasher, CborChainCodec, createRootKeysWithChain, createServerKeysWithChain, rollServerKeysWithChain, } from "@suaveplan/crypto-keys"; // Domain-tagged signing inside CBOR-encoded chains const signer = new ChainedKeySigner({ hasher: new DomainHasher({ domainTag: "acme/key-chain-v1" }), codec: new CborChainCodec(), }); const { keys: rootKeys, chain: rootChain } = await createRootKeysWithChain({ signer }); const { keys: serverV1, chain: chainV1 } = await createServerKeysWithChain({ rootKeys, rootChain, signer, }); // Six months later, roll the server keychain forward const { keys: serverV2, chain: chainV2 } = await rollServerKeysWithChain({ rootKeys, rootChain, currentVersion: serverV1.master.private.layout.fields.version, signer, }); // Hand a verifier a partial chain (just the leaf + its signature) const leafOnly = await signer.stripToDepth(chainV2, 1); // Verifiers reproduce the same hasher + codec to verify const verifier = new ChainedKeySigner({ hasher: new DomainHasher({ domainTag: "acme/key-chain-v1" }), codec: new CborChainCodec(), }); const result = await verifier.verify(chainV2, rootKeys.sign.public); ``` ## API Reference ### Class `ChainedKeySigner` | Method | Signature | | ----------------------- | ----------------------------------------------------------------------------------------------- | | `constructor` | `(options?: { hasher?: ChainHasher; codec?: ChainCodec })` | | `sign` | `(publicKeys: PublicKeychain, signSlot: KeySlotPrivate, parent?: Uint8Array) => Promise` | | `verify` | `(chainBytes: Uint8Array, trustedRoot: KeySlotPublic) => Promise` | | `parse` | `(chainBytes: Uint8Array) => Promise<{ depth: number; levels: readonly ChainLevel[] }>` | | `stripToDepth` | `(chainBytes: Uint8Array, depth: number) => Promise` | ### Hashers - `class RawHasher implements ChainHasher` - `class DomainHasher implements ChainHasher` — constructor `({ domainTag: string })`, tag must be 1–64 ASCII bytes - `class MerkleHasher implements ChainHasher` ### Codecs - `class TlvChainCodec implements ChainCodec` (default) - `class RecursiveEd25519Codec implements ChainCodec` — Ed25519 only - `class CborChainCodec implements ChainCodec` ### Factory functions - `async function createRootKeysWithChain(options?: { signer?: ChainedKeySigner; generate?: GenerateKeychainOptions }): Promise<{ keys: Keychain; chain: Uint8Array }>` - `async function createServerKeysWithChain(options: { rootKeys: Keychain; rootChain: Uint8Array; signer?: ChainedKeySigner; generate?: GenerateKeychainOptions }): Promise<{ keys: Keychain; chain: Uint8Array }>` - `async function rollServerKeysWithChain(options: { rootKeys: Keychain; rootChain: Uint8Array; currentVersion: number; signer?: ChainedKeySigner }): Promise<{ keys: Keychain; chain: Uint8Array }>` ### Errors - `class ChainKeysError extends SuaveplanError` with optional `depth?: number` and `algorithm?: AlgorithmId` discriminants - `CHAIN_KEYS_CODES` — frozen registry of 7 codes: `UNSUPPORTED_ALGORITHM`, `INVALID_FORMAT`, `INVALID_SIGNATURE`, `TRUST_MISMATCH`, `INVALID_DEPTH`, `INVALID_KEYCHAIN`, `INVALID_DOMAIN_TAG` ## Implementation Notes The package is universal — no `node:` / `bun:` imports anywhere in `src/`. All cryptographic primitives come from `@suaveplan/crypto-curve` (per-algorithm `sign(digest, privateKey)` / `verify(...)`) and `@suaveplan/crypto-hash` (`sha256` for the `DomainHasher` / `MerkleHasher`). Constant-time byte compare comes from `@suaveplan/crypto-utils`. The wire format is **self-describing**. Every codec prepends `[version: u8][hasher: u8]` so a verifier reproduces the exact hash that was signed. The TLV codec also tags each level with its algorithm byte, letting a chain mix algorithms across levels — useful when the root uses Ed25519 for speed and intermediate authorities use P-256 for compliance. Signing dispatches at the slot level: the signer reads `signSlot.algorithm` and calls the matching `crypto-curve` primitive. This means upstream pre-suaveplan chain bytes are **not** byte-compatible with this package — they were emitted by an older bitpacker-based scheme. Migration is one-way: re-sign chains in the new format. Errors are never raw `Error` — every failure goes through `ChainKeysError` with a stable `CHAIN_KEYS.*` code. Verification failures don't throw; they return `{ valid: false, errors: [...] }` so callers can chain multiple verification attempts without try/catch. Only construction-time errors (e.g. an invalid `domainTag`) throw synchronously.