first commit
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user