first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
# `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
```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<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).