Files
suaveplan/openspec/archive/2026-05-03-restore-crypto-opinionated-layer/specs/crypto/spec.md
2026-05-27 23:29:53 +01:00

9.4 KiB

Capability Spec Delta: crypto (restore-crypto-opinionated-layer)

Package: @suaveplan/crypto Status: PROPOSED

Each ADDED requirement carries a **Verified by:** line. The full umbrella spec at restore-crypto-keychain-packer/specs/crypto/spec.md enumerates per-scenario detail; this slice's spec covers the seven new modules.

ADDED Requirements

Requirement: InMemoryAuthority concrete backend

The package SHALL ship InMemoryAuthority, a concrete implementation of the Authority interface that signs under a DerivationCoordinate slot, rejects revoked slots, enforces the slot's validity window against the injected Clock, and zeroises every secret on dispose().

Scenario: signing with an in-window slot succeeds

  • WHEN a caller asks authority.sign(domainTag, payload) against a slot whose notBefore <= clock() < notAfter
  • THEN a verifiable signature is returned

Scenario: signing with a revoked slot fails

  • WHEN the same call is issued against a slot whose revokedAt is in the past
  • THEN CryptoError(AUTHORITY.SLOT_REVOKED) is thrown

Scenario: dispose zeroises secrets

  • WHEN authority.dispose() is called
  • THEN every slot's secretKey byte buffer is overwritten with zeros and subsequent sign calls throw AUTHORITY.DISPOSED

Verified by: src/authority/in-memory-authority/in-memory-authority.test.ts::"sign-with-in-window-slot succeeds" AND ::"sign-with-revoked-slot fails" AND ::"dispose-zeroises-secrets"

Requirement: Hierarchical identity operations

The package SHALL ship RootIdentity, ServerIdentity, createRoot, createServer, rollServer, and revokeServer. createServer produces a server identity signed under the supplied root authority; rollServer increments the version, emits a CRL entry for the prior version, and appends an audit-log entry; revokeServer appends CRL + audit entries without re-issuing.

Scenario: createRoot self-verifies

  • WHEN createRoot(rng, clock, options) runs
  • THEN the produced root's public bundle verifies under its own sign slot

Scenario: rollServer increments version and emits CRL

  • WHEN rollServer(rootAuthority, serverIdentity, options) runs
  • THEN the new server's version is prior.version + 1, a CRL entry exists for prior.version, and an audit-log entry exists for the rotation

Verified by: src/identity/operations/operations.test.ts::"createRoot self-verifies" AND ::"createServer is signed by root" AND ::"rollServer increments version + emits CRL" AND ::"revokeServer appends without re-issue"

Requirement: Three-regime Vault replaces the legacy implementation

The package SHALL ship a three-regime Vault: (a) public bundle (parent-signed plaintext), (b) private-key blob (password-AEAD, rotatable via changePassword without re-derivation), (c) vault payload (auth-box-sealed under keypair, parent-signed under DOMAIN_VAULT_V1, carries master/seedCount/keychain/data: Uint8Array). The transitional legacy-vault/ from slice 2 MUST be deleted in this slice.

Scenario: round-trip preserves payload data

  • WHEN a Vault packs a payload containing data: Uint8Array([1,2,3]) and is then unpacked
  • THEN data round-trips byte-identically

Scenario: changePassword does not re-derive the keychain

  • WHEN a Vault is unlocked, changePassword(newPassword) is called, and the vault is re-locked then re-unlocked with newPassword
  • THEN the recovered keychain's slots are byte-identical to the original (no re-derivation occurred)

Scenario: lock zeroises secrets

  • WHEN vault.lock() is called
  • THEN every secret byte buffer reachable from the vault is overwritten with zeros

Verified by: src/vault/vault.test.ts::"round-trip preserves payload data" AND ::"changePassword without re-derive" AND ::"lock zeroises secrets" AND ::"BIP39 restore re-derives identical keychain" AND ::"Argon2id parameter floor enforced"

Requirement: Packer / Unpacker envelope with replay protection

The package SHALL ship Packer.pack(value, schema, recipientAuthority, senderAuthority, domainTag) producing an encrypt-then-sign envelope including issuedAt and nonce. Unpacker.unpack(envelope, recipientAuthority, senderPublicBundle, domainTag, options) MUST reject envelopes whose issuedAt is older than options.replayWindowMs or whose nonce was already seen.

Scenario: round-trip preserves the value

  • WHEN Packer.pack then Unpacker.unpack with matching parties and domain tag
  • THEN the unpacked value is structurally equal to the original

Scenario: replay outside window rejected

  • WHEN an envelope produced 1 hour ago is unpacked with replayWindowMs = 60_000
  • THEN CryptoError(PACKER.REPLAY_REJECTED) is thrown

Scenario: cross-protocol domain tag rejected

  • WHEN an envelope packed under DOMAIN_PACKER_V1 is unpacked with DOMAIN_VAULT_V1
  • THEN CryptoError(PACKER.DOMAIN_TAG_MISMATCH) is thrown

Verified by: src/packer/packer.test.ts::"round-trip preserves the value" AND ::"replay outside window rejected" AND ::"cross-protocol domain tag rejected"

Requirement: ChunkedPacker per-chunk-signed fragmentation

The package SHALL ship ChunkedPacker.packFileToChunks and ChunkedUnpacker.reassembleFile. Each chunk MUST carry an independent signature; tampering with any single chunk's bytes MUST cause reassembly to fail.

Scenario: round-trip reassembles byte-identical

  • WHEN a 10 KiB stream is packed into 4 chunks then reassembled
  • THEN the reassembled stream is byte-identical to the input

Scenario: tampered chunk rejected at reassembly

  • WHEN any byte of any single chunk is flipped before reassembly
  • THEN CryptoError(CHUNK.TAMPERED) is thrown with the offending chunk index

Verified by: src/chunked-packer/chunked-packer.test.ts::"round-trip reassembles byte-identical" AND ::"tampered chunk rejected at reassembly"

Requirement: RevocationList root-signed CRL

The package SHALL ship a RevocationList that is append-only, root-signed, and verify-able against a root public bundle. Mutating any past entry MUST cause the list-level signature to fail verification.

Scenario: append then verify succeeds

  • WHEN a CRL is appended to and then verified against the issuing root's public bundle
  • THEN verification succeeds

Scenario: mutating a past entry invalidates the list

  • WHEN any byte of an existing CRL entry is altered post-append
  • THEN CryptoError(CRL.MUTATED) is thrown on next verify

Verified by: src/revocation/revocation-list.test.ts::"append then verify succeeds" AND ::"mutating a past entry invalidates the list"

Requirement: AuditLog append-only Merkle log

The package SHALL ship an AuditLog whose root is signed by a root authority. Tampering with any past entry MUST invalidate the root signature; an inclusion proof MUST be derivable for any appended entry.

Scenario: append then root signature verifies

  • WHEN events are appended and the log root is signed
  • THEN auditLog.verify(rootPublicBundle) returns true

Scenario: tampering with a past entry invalidates the root

  • WHEN any byte of an appended event is flipped
  • THEN auditLog.verify(rootPublicBundle) throws CryptoError(AUDIT.ROOT_MISMATCH)

Verified by: src/audit-log/audit-log.test.ts::"append then root signature verifies" AND ::"tampering with a past entry invalidates the root" AND ::"inclusion proof verifies for appended entries"

Requirement: Eight wire-format schemas with golden-bytes KAT vectors

The package SHALL define and module-export eight wire-format schemas (DERIVATION_COORDINATE_SCHEMA, KEY_SLOT_PRIVATE_SCHEMA, KEY_SLOT_PUBLIC_SCHEMA, VAULT_PAYLOAD_SCHEMA, PUBLIC_BUNDLE_SCHEMA, CHAIN_BUNDLE_SCHEMA, CRL_ENTRY_SCHEMA, AUDIT_ENTRY_SCHEMA) and ship a golden-bytes KAT fixture per schema under test-fixtures/wire-format/. CI MUST assert byte-for-byte equality between the schema's encoding of canonical inputs and the committed fixtures.

Scenario: every schema is module-exported

  • WHEN the test imports every named schema from @suaveplan/crypto/wire-format
  • THEN every import resolves

Scenario: KAT bytes match committed fixtures

  • WHEN each schema encodes its canonical input
  • THEN the bytes are equal to the matching test-fixtures/wire-format/*.bin

Verified by: tests/wire-format-kat.test.ts::"every wire-format schema is module-exported" AND tests/wire-format-kat.test.ts::"KAT bytes match committed fixtures for each schema"

Requirement: Property-based round-trip coverage

Every pack/unpack pair in the package SHALL be covered by a fast-check property test asserting unpack(pack(v)) ≡ v over at least 100 generated inputs per property.

Scenario: round-trip is identity for every pack/unpack pair

  • WHEN a fast-check property runs over 100 generated inputs for any pack/unpack pair
  • THEN every iteration round-trips structurally

Verified by: tests/property-roundtrips.test.ts::"round-trip identity for keychain" AND ::"round-trip identity for vault" AND ::"round-trip identity for public-bundle" AND ::"round-trip identity for chain-bundle" AND ::"round-trip identity for packer-envelope" AND ::"round-trip identity for chunk" AND ::"round-trip identity for crl-entry" AND ::"round-trip identity for audit-entry"