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 atrestore-crypto-keychain-packer/specs/crypto/spec.mdenumerates 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 whosenotBefore <= 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
revokedAtis in the past - THEN
CryptoError(AUTHORITY.SLOT_REVOKED)is thrown
Scenario: dispose zeroises secrets
- WHEN
authority.dispose()is called - THEN every slot's
secretKeybyte buffer is overwritten with zeros and subsequent sign calls throwAUTHORITY.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 forprior.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
Vaultpacks a payload containingdata: Uint8Array([1,2,3])and is then unpacked - THEN
dataround-trips byte-identically
Scenario: changePassword does not re-derive the keychain
- WHEN a
Vaultis unlocked,changePassword(newPassword)is called, and the vault is re-locked then re-unlocked withnewPassword - 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_V1is unpacked withDOMAIN_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)throwsCryptoError(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"