# Capability Spec Delta: crypto-cli + slsa + crypto-core (restore-crypto-cli-and-allowlist) **Packages:** `@suaveplan/crypto-cli` (new), `@suaveplan/slsa` (modified), `@suaveplan/crypto-core` (allowlist gate added) **Status:** PROPOSED ## ADDED Requirements ### Requirement: `keychain` CLI binary surface `@suaveplan/crypto-cli` SHALL expose a `keychain` binary with eight subcommands: `root init`, `server init`, `server roll`, `server revoke`, `inspect`, `verify`, `export-public`, `restore`. Every subcommand MUST be invokable with the documented flags, MUST stream via stdin/stdout when given `-` for input/output, and MUST emit OTel spans plus structured logger output for every invocation. #### Scenario: every documented subcommand exists - **WHEN** the test invokes `keychain --help` and parses the subcommand list - **THEN** all eight subcommands are present #### Scenario: stdin/stdout streaming via `-` - **WHEN** `keychain inspect -` is invoked with a piped public bundle on stdin - **THEN** the parsed report is written to stdout **Verified by:** `tests/cli-surface.test.ts::"every subcommand exists and is invokable"` AND `tests/streaming.test.ts::"stdin/stdout streaming via -"` ### Requirement: TTY-only password input The CLI MUST reject the `--password` flag with a security-pointer error and MUST accept passwords via TTY prompt or env var (`KEYCHAIN_PASSWORD`). Passwords MUST NOT appear in shell history, process listings, or log output. #### Scenario: `--password` flag is rejected - **WHEN** any subcommand is invoked with `--password=...` - **THEN** the CLI exits with code 2 and a message pointing at TTY/env-var alternatives #### Scenario: env-var input works - **WHEN** `KEYCHAIN_PASSWORD=...` is set and a subcommand needs a password - **THEN** the password is consumed without prompting **Verified by:** `tests/no-password-flag.test.ts::"--password is rejected with security pointer"` AND `tests/env-var-password.test.ts::"env-var password is consumed"` ### Requirement: Distinct exit codes per failure class Every CLI subcommand MUST exit with a documented, distinct exit code per failure class so operators can route incidents correctly: `0` success, `2` usage error, `10` bad password, `11` tampered vault, `12` expired key, `13` revoked key, `14` missing trust anchor, `15` audit-log mismatch. #### Scenario: each documented exit code is reachable - **WHEN** the test triggers each failure class against a fixture - **THEN** the exit code matches the documented value for that class **Verified by:** `tests/exit-codes.test.ts::"each documented exit code is reachable"` ### Requirement: `--idempotency-key` on state-changing commands `server roll` and `server revoke` MUST accept `--idempotency-key=` and MUST be no-ops on repeated invocation with the same key. The dedup is implemented via a local-file ledger keyed by the operation + idempotency key. #### Scenario: repeated roll with same idempotency key is a no-op - **WHEN** `keychain server roll --idempotency-key=abc` is invoked twice - **THEN** the second invocation does not produce a new server identity, does not append to the CRL, and does not append to the audit log **Verified by:** `tests/idempotency.test.ts::"repeated roll with same idempotency key is a no-op"` ### Requirement: `slsa` builder identity signs through `Authority` `@suaveplan/slsa` SHALL construct its builder identity as a `ServerIdentity` issued under a SLSA-builder root, and SHALL sign provenance through `Authority.sign(DOMAIN_SLSA_BUILDER_V1, …)` rather than calling primitive `ed25519.sign` directly. The DSSE envelope wire format MUST be unchanged — the proof is byte-for-byte equality against pre-migration fixtures. #### Scenario: builder constructed via Authority - **WHEN** the slsa builder issues provenance - **THEN** every signature originates from `Authority.sign(DOMAIN_SLSA_BUILDER_V1, payload)` #### Scenario: DSSE envelope round-trips against pre-migration fixtures - **WHEN** the post-migration builder regenerates a known DSSE envelope - **THEN** the bytes are identical to the pre-migration fixture committed at `tests/fixtures/dsse/pre-migration.bin` #### Scenario: no direct primitive imports - **WHEN** the test scans `slsa/src/` for imports of `@suaveplan/crypto-core`, `@suaveplan/crypto-curve`, or `@suaveplan/crypto-cipher` - **THEN** zero matches are found (encoding helpers from `crypto-utils` and pure-hash uses from `crypto-hash` remain allowed) **Verified by:** `tests/builder-via-authority.test.ts::"every signature originates from Authority.sign"` AND `tests/dsse-byte-equal.test.ts::"DSSE envelope bytes match pre-migration fixture"` AND `tests/no-direct-crypto-core.test.ts::"no direct primitive imports"` ### Requirement: Import-allowlist conformance gate The OpenSpec conformance script SHALL reject any package outside the committed allowlist (`scripts/conformance/crypto-core-allowlist.yml`) that imports cryptographic primitives from `@suaveplan/crypto-core`, `@suaveplan/crypto-cipher`, `@suaveplan/crypto-curve`, or `@suaveplan/crypto-hash`-as-MAC. Encoding helpers from `@suaveplan/crypto-utils` and pure-hash uses from `@suaveplan/crypto-hash` MUST remain open. #### Scenario: allowed package imports primitive — green - **WHEN** `@suaveplan/crypto` (allowlisted) imports `aead` from `@suaveplan/crypto-core` - **THEN** the conformance script exits 0 with no findings #### Scenario: non-allowed package imports primitive — red - **WHEN** a fixture package outside the allowlist imports `aead` from `@suaveplan/crypto-core` - **THEN** the conformance script exits non-zero with an `error`-severity finding citing the package and the import line #### Scenario: encoding helpers stay open - **WHEN** any package imports `toHex` / `fromHex` / `toBase64` from `@suaveplan/crypto-utils` - **THEN** the conformance script does not flag the import **Verified by:** `scripts/conformance/crypto-core-allowlist.test.ts::"allowed package imports primitive — green"` AND `::"non-allowed package imports primitive — red"` AND `::"encoding helpers stay open"`