163 lines
9.4 KiB
Markdown
163 lines
9.4 KiB
Markdown
# 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"`
|