Files
2026-05-27 23:29:53 +01:00

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 --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=<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"