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

75 lines
4.6 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: crypto-curve (crypto-completion)
**Package**: `@suaveplan/crypto-curve`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: secp256k1 ECDSA signing with recovery byte
The `secp256k1` primitive SHALL expose `signWithRecovery(digest, privateKey)` returning a 65-byte signature (64-byte compact signature followed by a single recovery byte). The recovery byte SHALL be the byte returned by `secp256k1.sign(digest, privateKey).recovery` from the `@noble/curves` library; valid values are `0` or `1`.
#### Scenario: produces 65-byte signature
- **WHEN** `signWithRecovery(digest, privateKey)` is called for a valid digest and private key
- **THEN** the result is a `Uint8Array` of length 65 AND `result[64]` is `0` or `1`
#### Scenario: signWithRecovery output verifies via standard verify
- **WHEN** the first 64 bytes of `signWithRecovery(digest, privateKey)` are passed to the existing `verify(digest, sig64, publicKey)`
- **THEN** the signature verifies successfully
**Verified by:** `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts::"signWithRecovery returns 65-byte signature with recovery byte 0 or 1"` AND `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts::"signWithRecovery output verifies via standard verify on first 64 bytes"`
---
### Requirement: secp256k1 public-key recovery
The `secp256k1` primitive SHALL expose `recoverPublicKey(signatureWithRecovery, digest)` returning the 33-byte compressed public key recovered from a 65-byte signature-with-recovery and the original digest. Inputs may be supplied as `Uint8Array` or hex strings.
#### Scenario: recovers the signing public key
- **WHEN** `signWithRecovery(digest, privateKey)` produces `sig` and `recoverPublicKey(sig, digest)` is called
- **THEN** the recovered public key equals `getPublicKey(privateKey)` byte-for-byte
#### Scenario: missing recovery byte rejected
- **WHEN** a 64-byte signature (without recovery byte) is passed to `recoverPublicKey`
- **THEN** it throws `SuaveplanError` with code `CRYPTO_CURVE.RECOVERY_BYTE_MISSING` and `context.signatureLength === 64`
#### Scenario: invalid recovery byte rejected
- **WHEN** a 65-byte signature is passed but `signatureWithRecovery[64]` is `2`, `3`, or any value > 3
- **THEN** it throws `SuaveplanError` with code `CRYPTO_CURVE.RECOVERY_BYTE_INVALID` and `context.recoveryByte` reflects the offending value
**Verified by:** `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts::"recoverPublicKey returns the same key as getPublicKey for the signer"` AND `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.spec-conformance.test.ts::"throws RECOVERY_BYTE_MISSING for 64-byte signature"` AND `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.spec-conformance.test.ts::"throws RECOVERY_BYTE_INVALID when recovery byte is out of range"`
---
### Requirement: secp256k1 verify-with-recovery
The `secp256k1` primitive SHALL expose `verifyWithRecovery(signatureWithRecovery, digest, expectedPublicKey)` returning `true` only when the public key recovered from the signature equals `expectedPublicKey` (compared in constant time on the byte buffer). When the recovered key differs, the function SHALL return `false` (it does NOT throw on mismatch — mismatch is a routine path).
#### Scenario: returns true on matching key
- **WHEN** `verifyWithRecovery(sig, digest, expectedPublicKey)` is called with the public key that produced `sig`
- **THEN** the result is `true`
#### Scenario: returns false on non-matching key
- **WHEN** `verifyWithRecovery(sig, digest, otherPublicKey)` is called with a public key that did not produce `sig`
- **THEN** the result is `false` (no exception is thrown)
#### Scenario: comparison is constant-time
- **WHEN** the recovered public key and `expectedPublicKey` differ in their first byte vs differ only in their last byte
- **THEN** the timing distribution observed across many invocations does not reveal the position of the difference (verified via a statistical timing test)
**Verified by:** `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts::"verifyWithRecovery returns true on matching public key"` AND `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts::"verifyWithRecovery returns false on non-matching public key"` AND `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.spec-conformance.test.ts::"verifyWithRecovery comparison is constant-time"`
<!-- markdownlint-enable MD024 -->