6.0 KiB
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 --helpand 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=<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=abcis 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-utilsand pure-hash uses fromcrypto-hashremain 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) importsaeadfrom@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
aeadfrom@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/toBase64from@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"