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

193 lines
11 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.
# @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.
Universal, store-agnostic, free of `node:` / `bun:` imports.
<!-- begin:auto-docs -->
## 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…
<!-- end:auto-docs -->
## 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<Uint8Array>` |
| `verify` | `(chainBytes: Uint8Array, trustedRoot: KeySlotPublic) => Promise<VerificationResult>` |
| `parse` | `(chainBytes: Uint8Array) => Promise<{ depth: number; levels: readonly ChainLevel[] }>` |
| `stripToDepth` | `(chainBytes: Uint8Array, depth: number) => Promise<Uint8Array>` |
### 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<ChainKeysErrorCode>` 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.