Files
skills/docs/security-stack/crypto/crypto-keys/src/factory/factory.md
2026-05-29 08:20:56 +01:00

6.3 KiB

factory

(no submodules)

Purpose

Hierarchical-keychain factory helpers that compose generateKeychain from @suaveplan/crypto with ChainedKeySigner.signWithSlot to produce signed chains in one call. Three functions cover the common pattern: bootstrap a root keychain, derive a server keychain bound to the root, and roll a server keychain forward to the next version without changing the root.

These functions are Ed25519-only in v0.0.0 because the crypto keychain v1 grammar only generates Ed25519 sign-slots. Callers that need a different curve at any chain level should reach for ChainedKeySigner.signRaw directly instead.

Features

  • createRootKeysWithChain — fresh keychain + self-signed chain
  • createServerKeysWithChain — derived keychain + chain bound to root
  • rollServerKeysWithChain — re-derive at next slot version, re-sign
  • All three accept an injected signer? for custom hasher/codec choices
  • Strict INVALID_KEYCHAIN errors when root slots are missing or empty
  • Sensible LayoutV1Fields defaults (masterVersion: 1, userId: 0, depth: 0, slotVersion: 1)
  • Default Clock is Date.now — caller can override via generate.clock

Usage: Basic

import {
    ChainedKeySigner,
    createRootKeysWithChain,
    createServerKeysWithChain,
} from "@suaveplan/crypto-keys";

// Bootstrap a root keychain + self-signed chain
const { keys: rootKeys, chain: rootChain } = await createRootKeysWithChain();

// Derive a server keychain whose chain anchors to the root
const { keys: serverKeys, chain: serverChain } = await createServerKeysWithChain({
    rootKeys,
    rootChain,
});

// Verify end-to-end
const verifier = new ChainedKeySigner();
const result = await verifier.verify(serverChain, {
    algorithm: "ed25519",
    publicKey: (rootKeys as Record<string, { publicKey: Uint8Array }>).sign.publicKey,
});
console.log(result.valid, result.depth); // true 2

Usage: Advanced

import {
    ChainedKeySigner,
    DomainHasher,
    createRootKeysWithChain,
    createServerKeysWithChain,
    rollServerKeysWithChain,
} from "@suaveplan/crypto-keys";

const signer = new ChainedKeySigner({
    hasher: new DomainHasher({ domainTag: "tenant.acme.v1" }),
});

// Tagged root
const { keys: rootKeys, chain: rootChain } = await createRootKeysWithChain({
    signer,
    generate: { userId: 42 as never }, // custom layout fields per app
});

// First server keychain (slot version 1)
const v1 = await createServerKeysWithChain({ rootKeys, rootChain, signer });

// Six months later: roll to slot version 2, same root binding
const v2 = await rollServerKeysWithChain({
    rootKeys,
    rootChain,
    currentVersion: 1,
    signer,
});

// Issuance chain: root -> v1 -> v2 -> leaf user
const leaf = await createServerKeysWithChain({
    rootKeys,
    rootChain: v2.chain,
    signer,
});

const verifier = new ChainedKeySigner({
    hasher: new DomainHasher({ domainTag: "tenant.acme.v1" }),
});
const result = await verifier.verify(leaf.chain, {
    algorithm: "ed25519",
    publicKey: (rootKeys as Record<string, { publicKey: Uint8Array }>).sign.publicKey,
});

API Reference

async function createRootKeysWithChain(options?)

Field Type Default
signer? ChainedKeySigner new ChainedKeySigner()
generate? Partial<GenerateKeychainOptions> sensible defaults

Returns { keys: Keychain<LayoutV1Fields>; chain: Uint8Array }.

async function createServerKeysWithChain(options)

Field Type Required
rootKeys Keychain<LayoutV1Fields> yes
rootChain Uint8Array yes
signer? ChainedKeySigner no
generate? Partial<GenerateKeychainOptions> no

Raises CHAIN_KEYS.INVALID_KEYCHAIN if rootKeys.master or rootKeys.sign is missing, has an empty secretKey, or has a non-Ed25519 algorithm.

async function rollServerKeysWithChain(options)

Field Type Required
rootKeys Keychain<LayoutV1Fields> yes
rootChain Uint8Array yes
currentVersion number (≥ 0, integer) yes
signer? ChainedKeySigner no

Generates a new keychain with slotVersion: currentVersion + 1 and re-signs against the same root. Raises CHAIN_KEYS.INVALID_KEYCHAIN when currentVersion is negative or non-integer, or when the root slots are missing/invalid.

Implementation Notes

crypto's Keychain is Record<PurposeTag, KeySlotPrivate> where PurposeTag is a branded string type. The factory module uses a small getSlot(keys, tag) helper that casts through Record<string, KeySlotPrivate> to access individual slots; this keeps the TypeScript narrowing clean without pulling in the PurposeTag brand machinery.

LayoutV1Fields is re-declared locally because it's not currently re-exported from @suaveplan/crypto/keychain. The shape is identical to the upstream definition and generateKeychain accepts any object satisfying it structurally. If a future crypto release adds a ./derivation subpath export, this declaration can be replaced with the canonical import.

The factory functions trust generateKeychain's contract — that every generated keychain contains all 8 default-purpose slots with valid Ed25519 sign-slots — and do not re-validate the freshly-generated output. Validation is concentrated at the boundaries: signWithSlot rejects non-Ed25519 slots, and assertRootSlots checks every caller-supplied root before use. This keeps the happy path lean and pushes the cost of bad input onto the caller's stack frame.

signWithSlot takes an optional parent: Uint8Array; the factory functions pass rootChain when extending and omit it for the self-signed root. ChainedKeySigner distinguishes a root signature (no parent) from a non-root signature (parent supplied) by the presence of the argument — there's no separate "root" API.