Module: BSV::Primitives::ECDSA

Defined in:
lib/bsv/primitives/ecdsa.rb

Overview

Deterministic ECDSA signing and verification on secp256k1.

Implements RFC 6979 deterministic nonce generation to produce signatures that are fully reproducible from the same (key, hash) pair. All signatures are normalised to low-S form (BIP-62 rule 5).

Typically used indirectly via PrivateKey#sign and PublicKey#verify rather than calling this module directly.

Constant Summary collapse

BYTE_LEN =

Byte length of a secp256k1 scalar (256 bits).

32

Class Method Summary collapse

Class Method Details

.recover_public_key(hash, signature, recovery_id) ⇒ PublicKey

Recover a public key from a signature and recovery ID.

Given a message hash, signature, and the recovery ID produced during signing, reconstructs the public key that created the signature.

Parameters:

  • hash (String)

    32-byte message digest that was signed

  • signature (Signature)

    the ECDSA signature

  • recovery_id (Integer)

    recovery ID (0-3)

Returns:

Raises:

  • (ArgumentError)

    if the recovered point is at infinity



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/bsv/primitives/ecdsa.rb', line 65

def recover_public_key(hash, signature, recovery_id)
  r = signature.r
  s = signature.s
  n = Curve::N

  # Reconstruct R.x (may include overflow when recovery_id >= 2)
  x = recovery_id >= 2 ? r + n : r

  # Decompress R from x-coordinate and y-parity
  prefix = (recovery_id & 1).odd? ? "\x03".b : "\x02".b
  x_bytes = x.to_s(2)
  x_bytes = ("\x00".b * (32 - x_bytes.length)) + x_bytes if x_bytes.length < 32
  r_point = Curve.point_from_bytes(prefix + x_bytes)

  # Q = r^(-1) * (s*R - e*G)
  r_inv = r.mod_inverse(n)
  e = OpenSSL::BN.new(hash, 2)
  u1 = ((n - e) * r_inv) % n
  u2 = (s * r_inv) % n

  p1 = Curve.multiply_generator(u1)
  p2 = Curve.multiply_point(r_point, u2)
  q = Curve.add_points(p1, p2)

  raise ArgumentError, 'recovered point is at infinity' if q.infinity?

  PublicKey.new(q)
end

.sign(hash, private_key_bn, force_low_s: false) ⇒ Signature

Sign a 32-byte message hash with a private key.

The signature is always low-S normalised per BIP-62 rule 5, as required by BSV consensus (sign_raw normalises internally). The force_low_s: keyword makes this explicit at the call site for readability — it is currently a no-op because sign_raw already guarantees low-S. It exists so callers can document intent without relying on implementation details.

Parameters:

  • hash (String)

    32-byte message digest

  • private_key_bn (OpenSSL::BN)

    the private key scalar

  • force_low_s (Boolean) (defaults to: false)

    explicit low-S normalisation (currently a no-op since sign_raw already guarantees low-S; provided for documentation intent at call sites)

Returns:

  • (Signature)

    a low-S normalised deterministic signature



36
37
38
39
# File 'lib/bsv/primitives/ecdsa.rb', line 36

def sign(hash, private_key_bn, force_low_s: false) # rubocop:disable Lint/UnusedMethodArgument
  sig, _recovery_id = sign_raw(hash, private_key_bn)
  sig
end

.sign_recoverable(hash, private_key_bn) ⇒ Array(Signature, Integer)

Sign a hash and return both the signature and recovery ID.

The recovery ID (0-3) allows the public key to be recovered from the signature without knowing it in advance, as used by Bitcoin Signed Messages (BSM) and compact signature formats.

Parameters:

  • hash (String)

    32-byte message digest

  • private_key_bn (OpenSSL::BN)

    the private key scalar

Returns:

  • (Array(Signature, Integer))

    the signature and recovery ID



50
51
52
# File 'lib/bsv/primitives/ecdsa.rb', line 50

def sign_recoverable(hash, private_key_bn)
  sign_raw(hash, private_key_bn)
end

.verify(hash, signature, public_key_point) ⇒ Boolean

Verify an ECDSA signature against a message hash and public key.

Parameters:

  • hash (String)

    32-byte message digest

  • signature (Signature)

    the signature to verify

  • public_key_point (OpenSSL::PKey::EC::Point)

    the signer’s public key point

Returns:

  • (Boolean)

    true if the signature is valid



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/bsv/primitives/ecdsa.rb', line 100

def verify(hash, signature, public_key_point)
  r = signature.r
  s = signature.s
  n = Curve::N

  return false if r <= OpenSSL::BN.new('0') || r >= n
  return false if s <= OpenSSL::BN.new('0') || s >= n

  e = OpenSSL::BN.new(hash, 2)
  s_inv = s.mod_inverse(n)

  u1 = (e * s_inv) % n
  u2 = (r * s_inv) % n

  # R' = u1*G + u2*Q
  point1 = Curve.multiply_generator(u1)
  point2 = Curve.multiply_point(public_key_point, u2)
  result_point = Curve.add_points(point1, point2)

  return false if result_point.infinity?

  x = Curve.point_x(result_point) % n
  x == r
end