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 chaincreateServerKeysWithChain— derived keychain + chain bound to rootrollServerKeysWithChain— re-derive at next slot version, re-sign- All three accept an injected
signer?for custom hasher/codec choices - Strict
INVALID_KEYCHAINerrors when root slots are missing or empty - Sensible
LayoutV1Fieldsdefaults (masterVersion: 1, userId: 0, depth: 0, slotVersion: 1) - Default
ClockisDate.now— caller can override viagenerate.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.