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

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.
  • 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

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 }>
  • 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<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).