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

4.6 KiB

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"