# `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 `.vault` plus a `.chain` file instead of the non-chain `.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. - `verifyKeyChainCommand` accepts both signed-public-key files and chain files as the trusted-root input — automatic fallback. - Same `KeyCommandOptions` shape as `keys.ts`: output dir, logger, abort signal. - `parseChain` helper for non-verifying introspection (depth count). ## Usage: Basic ```ts 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: ```ts 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` - `createServerKeysChainCommand(rootVaultPath, rootPassword, serverPassword, options?) => Promise` - `rollServerKeysChainCommand(rootVaultPath, rootPassword, serverPassword, currentVersion, options?) => Promise` - `verifyKeyChainCommand(chainPath, rootKeyPath, options?) => Promise<{ valid, depth, errors }>` - `parseChain(chainBytes) => Promise<{ depth }>` - `viewPublicKeys` re-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[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).