Initial Commit
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Capability Spec Delta: crypto-cli (restore-crypto-keychain-packer)
|
||||
|
||||
**Package**: `@suaveplan/crypto-cli`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `keychain` binary surface
|
||||
|
||||
The package SHALL ship a `keychain` binary (declared in `package.json` `bin`) implementing eight subcommands: `root init`, `server init`, `server roll`, `server revoke`, `inspect`, `verify`, `export-public`, `restore`. The CLI is built on `@suaveplan/cli` (argument parser, command router, prompts, output formatters) and `@suaveplan/readline` (TTY input). All cryptographic work goes through `@suaveplan/crypto`; the CLI never imports `@suaveplan/crypto-core` directly.
|
||||
|
||||
Every subcommand supports `--output json|binary|text` (default `text`), reads/writes from stdin/stdout when paths are `-`, and emits OTel spans for the operation.
|
||||
|
||||
#### Scenario: every documented subcommand exists and is invokable
|
||||
|
||||
- **WHEN** `keychain --help` is run
|
||||
- **THEN** the output lists exactly the eight subcommands above with one-line descriptions
|
||||
|
||||
#### Scenario: stdin/stdout streaming via `-`
|
||||
|
||||
- **WHEN** `keychain inspect -` is run with a packed public bundle on stdin
|
||||
- **THEN** the inspector reads the bundle from stdin and writes the parsed report to stdout, exit code 0
|
||||
|
||||
**Verified by:** `tests/cli-surface.test.ts::"help lists all eight subcommands"` AND `tests/cli-surface.test.ts::"inspect reads from stdin via -"` AND `src/commands/<command>.test.ts::"<command> emits an OTel span"` (one per subcommand)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: TTY-only password input
|
||||
|
||||
The package SHALL accept passwords only through (a) the `cli`-package `password` prompt (TTY echo `*`), or (b) the `KEYCHAIN_PASSWORD` environment variable. A `--password` flag MUST be rejected at argument-parse time with `CryptoError` code `CLI.PASSWORD_FLAG_FORBIDDEN` and an exit code of 2 (usage error). The error message points the user at the two supported input paths.
|
||||
|
||||
This is a credential-handling rule: shell history is a documented credential leak path.
|
||||
|
||||
#### Scenario: `--password` flag is rejected
|
||||
|
||||
- **WHEN** `keychain root init --password=hunter2` is invoked
|
||||
- **THEN** the binary exits with code 2 and stderr names the forbidden flag and lists the two allowed alternatives
|
||||
|
||||
#### Scenario: env-var input works
|
||||
|
||||
- **WHEN** `KEYCHAIN_PASSWORD=hunter2 keychain root init --output json` is invoked in a non-interactive shell
|
||||
- **THEN** the binary uses the env-var value, exit code 0
|
||||
|
||||
#### Scenario: TTY prompt works
|
||||
|
||||
- **WHEN** the CLI is driven via a fake `InputReader` (per `cli` package) and a password is supplied at the prompt
|
||||
- **THEN** the binary uses the supplied password, exit code 0; the password is never echoed (the test asserts no plaintext appears in the captured TTY transcript)
|
||||
|
||||
**Verified by:** `tests/no-password-flag.test.ts::"--password flag exits with code 2 and security pointer"` AND `tests/tty-mock.test.ts::"env-var KEYCHAIN_PASSWORD is consumed"` AND `tests/tty-mock.test.ts::"TTY password prompt does not echo plaintext"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Distinct exit codes per failure class
|
||||
|
||||
The package SHALL exit with distinct, documented exit codes per failure class so operators can branch on them in scripts:
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| 0 | success |
|
||||
| 2 | usage error (bad flag, missing argument, `--password` rejected) |
|
||||
| 10 | bad password (`VAULT.BAD_PASSWORD`) |
|
||||
| 11 | tampered vault (`VAULT.TAMPERED`) |
|
||||
| 12 | expired key (`KEYCHAIN.EXPIRED`) |
|
||||
| 13 | revoked key (`AUTHORITY.REVOKED` / CRL hit) |
|
||||
| 14 | missing trust anchor (`TRUST_ANCHOR.MISSING`) |
|
||||
| 15 | audit-log mismatch (`AUDIT.TAMPERED`) |
|
||||
| 16 | replay rejected (`PACKER.REPLAY_TOO_OLD` / `PACKER.REPLAY_NONCE_SEEN`) |
|
||||
|
||||
Every other failure exits with code 1.
|
||||
|
||||
#### Scenario: each documented exit code is reachable
|
||||
|
||||
- **WHEN** the CLI is driven into each documented failure class
|
||||
- **THEN** the exit code matches the table above
|
||||
|
||||
**Verified by:** `tests/exit-codes.test.ts::"each documented exit code is produced by its failure class"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Idempotency on state-changing commands
|
||||
|
||||
The package SHALL accept `--idempotency-key <key>` on `server roll` and `server revoke`. The CLI maintains a local idempotency ledger at `$XDG_STATE_HOME/suaveplan/keychain/idempotency.json` (or platform equivalent). A repeated call with the same key returns the cached result without re-issuing or re-revoking; the cached result includes the original exit code.
|
||||
|
||||
The ledger entries expire after 24 hours; expired keys are treated as fresh.
|
||||
|
||||
#### Scenario: repeated roll with same idempotency key is a no-op
|
||||
|
||||
- **WHEN** `keychain server roll --idempotency-key abc` is invoked twice in succession
|
||||
- **THEN** the first call succeeds and produces a new server identity; the second call exits 0 with the same output and does not append a second `roll` entry to the audit log
|
||||
|
||||
**Verified by:** `src/commands/server-roll.test.ts::"idempotency key replays the cached result"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Per-command behaviour
|
||||
|
||||
Each subcommand SHALL have documented inputs, outputs, and side effects. The full table MUST be captured in `src/commands/COMMANDS.md` and asserted by per-command tests. Every subcommand MUST be invokable with the same flags documented here without runtime drift.
|
||||
|
||||
| Subcommand | Inputs | Output | Side effects |
|
||||
|---|---|---|---|
|
||||
| `root init` | `--user-id`, password (TTY/env), `--mnemonic` (optional) | encrypted vault + public bundle | writes vault to `--vault-path`, public bundle to `--public-path`; appends audit log if `--audit-log` is supplied |
|
||||
| `server init` | root vault path, root password, `--user-id`, server password | server vault + signed public bundle | writes server vault, server public bundle, appends audit log, no CRL change |
|
||||
| `server roll` | root vault, root password, server vault, server password, `--idempotency-key` | new server vault + new public bundle (version+1) | writes new files, appends CRL entry for prior version, appends audit log |
|
||||
| `server revoke` | root vault, root password, server identity ref, `--reason`, `--idempotency-key` | CRL entry + audit-log entry | appends CRL, appends audit log; no new server identity |
|
||||
| `inspect` | packed public bundle (path or `-`) | parsed report (text/json) | none (read-only) |
|
||||
| `verify` | envelope (path or `-`), `--trust-anchor`, `--crl` | OK / failure with code | none (read-only) |
|
||||
| `export-public` | vault path + password | public-only bundle | writes public bundle |
|
||||
| `restore` | mnemonic (TTY/env), `--user-id`, `--version`, password | restored vault | writes restored vault |
|
||||
|
||||
#### Scenario: each subcommand round-trips with itself
|
||||
|
||||
- **WHEN** `root init` produces a vault, then `inspect` reads its public bundle, then `verify` checks an envelope produced under that root, then `export-public` re-emits the public bundle
|
||||
- **THEN** all four steps succeed end-to-end and the public bundles produced by `root init` and `export-public` are byte-equal
|
||||
|
||||
**Verified by:** `tests/end-to-end.test.ts::"root-init → inspect → verify → export-public round-trips"` AND `src/commands/<command>.test.ts::"<command> happy path"` AND `src/commands/<command>.test.ts::"<command> failure modes"` (one per command)
|
||||
Reference in New Issue
Block a user