4.0 KiB
commands/keys/keys-chain
Purpose
Hierarchical ChainedKeySigner-backed key-lifecycle commands. Each
chain command wraps the matching crypto-vault orchestration in a
two-step pipeline: generate-or-derive a fresh keychain via
@suaveplan/crypto-keys (which produces a signed chain bytes blob), then
encrypt the keychain with packVault from @suaveplan/crypto-vault.
Persistence writes <name>.vault plus a <name>.chain file instead of
the non-chain <name>.pub so operators can tell at a glance which key
family a directory holds.
verify-key-chain rounds out the surface: it loads a chain file +
trusted root file, extracts the root's sign public key (whether the
root file is a signed-public-key file or another chain file), and
returns a structured { valid, depth, errors } result without
throwing on a signature failure.
Features
- Chain command parity with the non-chain commands:
create-root,create-server,roll-server, all producing matching vault + chain artefacts. verifyKeyChainCommandaccepts both signed-public-key files and chain files as the trusted-root input — automatic fallback.- Same
KeyCommandOptionsshape askeys.ts: output dir, logger, abort signal. parseChainhelper for non-verifying introspection (depth count).
Usage: Basic
import {
createRootKeysChainCommand,
createServerKeysChainCommand,
verifyKeyChainCommand,
} from "@suaveplan/server-keys/commands/keys/keys-chain";
await createRootKeysChainCommand("rootpw1234", { output: "/tmp/keys" });
await createServerKeysChainCommand(
"/tmp/keys/root.vault",
"rootpw1234",
"serverpw1234",
{ output: "/tmp/keys" },
);
const result = await verifyKeyChainCommand(
"/tmp/keys/server.chain",
"/tmp/keys/root.chain",
);
expect(result.valid).toBe(true);
expect(result.depth).toBe(2);
Usage: Advanced
Detect a tampered chain by flipping a byte in the chain file and
re-running the verifier; the result reports valid: false with a
populated errors array instead of throwing:
import { promises as fs } from "node:fs";
import {
rollServerKeysChainCommand,
verifyKeyChainCommand,
} from "@suaveplan/server-keys/commands/keys/keys-chain";
await rollServerKeysChainCommand(
"/tmp/keys/root.vault",
"rootpw1234",
"serverpw1234",
1,
{ output: "/tmp/keys" },
);
const chainPath = "/tmp/keys/server.v2.chain";
const original = await fs.readFile(chainPath);
const tampered = new Uint8Array(original);
tampered[tampered.length - 1] ^= 0xff;
await fs.writeFile(chainPath, tampered);
const r = await verifyKeyChainCommand(chainPath, "/tmp/keys/root.chain");
expect(r.valid).toBe(false);
expect(r.errors.length).toBeGreaterThan(0);
await fs.writeFile(chainPath, original); // restore
API Reference
createRootKeysChainCommand(password, options?) => Promise<void>createServerKeysChainCommand(rootVaultPath, rootPassword, serverPassword, options?) => Promise<void>rollServerKeysChainCommand(rootVaultPath, rootPassword, serverPassword, currentVersion, options?) => Promise<void>verifyKeyChainCommand(chainPath, rootKeyPath, options?) => Promise<{ valid, depth, errors }>parseChain(chainBytes) => Promise<{ depth }>viewPublicKeysre-export — for callers that need to decode a signed-public-key file in the same import.
Implementation Notes
The cast through
as unknown as Parameters<typeof createServerKeysWithChain>[0]["rootKeys"]
is the safe-narrowing shape crypto-keys expects — it requires a
Keychain typed against its own LayoutV1Fields import, which is
nominally distinct from crypto-vault's despite being structurally
identical. The double declaration of LayoutV1Fields across packages
mirrors the deliberate decoupling there; the cast keeps the boundary
explicit.
verifyKeyChainCommand does not re-throw on valid: false because the
spec scenario "tampered chain fails verification" requires a structured
result, not an exception — exceptions are reserved for input failures
(INVALID_INPUT, FILE_NOT_FOUND, missing sign slot).