Files
skills/plugins/document-navigator/docs/security-stack/crypto/crypto-keys/README.md
2026-06-09 21:06:27 +01:00

11 KiB
Raw Blame History

@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 — CborChainCodec is the CBOR-based wire-format ChainCodec for @suaveplan/crypto-keys. It serialises an ordered list…
    • cbor — CborChainCodec is the CBOR-based wire-format ChainCodec for @suaveplan/crypto-keys. It serialises an ordered list…
  • recursive-ed25519 — RecursiveEd25519Codec is a fixed-Ed25519 alternate wire format for signed key chains in @suaveplan/crypto-keys. It…
    • recursive-ed25519 — RecursiveEd25519Codec is a fixed-Ed25519 alternate wire format for signed key chains in @suaveplan/crypto-keys. It…
  • tlv — TlvChainCodec is the default wire-format codec for @suaveplan/crypto-keys. It encodes and decodes the `ChainLevel[]…
    • tlv — TlvChainCodec is the default wire-format codec for @suaveplan/crypto-keys. It encodes and decodes the `ChainLevel[]…
  • error-codes — The @suaveplan/crypto-keys package centralises every failure mode it can produce behind a single registry built with…
    • error-codes — The @suaveplan/crypto-keys package centralises every failure mode it can produce behind a single registry built with…
  • factory — Hierarchical-keychain factory helpers that compose generateKeychain from @suaveplan/crypto with `ChainedKeySigner.s…
    • factory — Hierarchical-keychain factory helpers that compose generateKeychain from @suaveplan/crypto with `ChainedKeySigner.s…
  • domain — DomainHasher is the ChainHasher strategy that domain-separates the signed pre-image with a caller-supplied tag and…
    • domain — DomainHasher is the ChainHasher strategy that domain-separates the signed pre-image with a caller-supplied tag and…
  • merkle — MerkleHasher is the ChainHasher strategy that binds each non-root level to its parent's content via a Merkle-style…
    • merkle — MerkleHasher is the ChainHasher strategy that binds each non-root level to its parent's content via a Merkle-style…
  • raw — RawHasher is the trivial ChainHasher strategy: it returns the payload bytes verbatim, performing no transformation.…
    • raw — RawHasher is the trivial ChainHasher strategy: it returns the payload bytes verbatim, performing no transformation.…
  • signer — ChainedKeySigner composes a ChainHasher (bytes-to-sign strategy) and a ChainCodec (wire-format strategy) to produ…
    • signer — ChainedKeySigner composes a ChainHasher (bytes-to-sign strategy) and a ChainCodec (wire-format strategy) to produ…
  • chain-level — Shared type module declaring the value-shapes that every other module in @suaveplan/crypto-keys references. Defining…
  • codec — ChainCodec is the wire-format strategy that turns an array of ChainLevel values into bytes and back. Decoupling the…
  • hasher — 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

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

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.