193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# @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.
|
||
|
||
<!-- 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.
|
||
|