Class: BSV::Primitives::Signature

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/primitives/signature.rb

Overview

An ECDSA signature consisting of (r, s) components.

Supports DER encoding/decoding with strict BIP-66 validation, low-S normalisation (BIP-62 rule 5), and hex convenience methods.

Examples:

Parse a DER-encoded signature

sig = BSV::Primitives::Signature.from_der(der_bytes)
sig.low_s? #=> true

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(r, s) ⇒ Signature

Returns a new instance of Signature.

Parameters:

  • r (OpenSSL::BN, Integer)

    the r component

  • s (OpenSSL::BN, Integer)

    the s component



24
25
26
27
# File 'lib/bsv/primitives/signature.rb', line 24

def initialize(r, s)
  @r = r.is_a?(OpenSSL::BN) ? r : OpenSSL::BN.new(r.to_s, 10)
  @s = s.is_a?(OpenSSL::BN) ? s : OpenSSL::BN.new(s.to_s, 10)
end

Instance Attribute Details

#rOpenSSL::BN (readonly)

Returns the r component.

Returns:

  • (OpenSSL::BN)

    the r component



17
18
19
# File 'lib/bsv/primitives/signature.rb', line 17

def r
  @r
end

#sOpenSSL::BN (readonly)

Returns the s component.

Returns:

  • (OpenSSL::BN)

    the s component



20
21
22
# File 'lib/bsv/primitives/signature.rb', line 20

def s
  @s
end

Class Method Details

.from_der(der_bytes) ⇒ Signature

Parse a signature from DER-encoded bytes with strict BIP-66 validation.

Parameters:

  • der_bytes (String)

    DER-encoded signature bytes

Returns:

Raises:

  • (ArgumentError)

    if the DER encoding is invalid



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/bsv/primitives/signature.rb', line 34

def self.from_der(der_bytes)
  der_bytes = der_bytes.b if der_bytes.encoding != Encoding::ASCII_8BIT
  bytes = der_bytes.bytes

  raise ArgumentError, 'signature too short' if bytes.length < 8
  raise ArgumentError, 'invalid sequence tag' unless bytes[0] == 0x30

  # BIP-66 strict DER: length must be a single byte (0x00–0x7F).
  # Multi-byte length encoding (where the high bit of bytes[1] is set,
  # e.g. 0x81, 0x82 …) is not permitted in ECDSA signatures.
  raise ArgumentError, 'non-canonical DER length (multi-byte encoding not allowed)' if bytes[1] & 0x80 != 0

  total_len = bytes[1]
  raise ArgumentError, 'length mismatch' unless total_len == bytes.length - 2

  # Parse R
  raise ArgumentError, 'invalid integer tag for R' unless bytes[2] == 0x02

  r_len = bytes[3]
  raise ArgumentError, 'R length overflows' if 4 + r_len > bytes.length
  raise ArgumentError, 'R is zero length' if r_len.zero?

  r_bytes = bytes[4, r_len]
  raise ArgumentError, 'R has negative flag' if r_bytes[0] & 0x80 != 0
  raise ArgumentError, 'R has excessive padding' if r_len > 1 && r_bytes[0].zero? && (r_bytes[1] & 0x80).zero? # rubocop:disable Style/BitwisePredicate

  # Parse S
  s_offset = 4 + r_len
  raise ArgumentError, 'invalid integer tag for S' unless bytes[s_offset] == 0x02

  s_len = bytes[s_offset + 1]
  raise ArgumentError, 'S length overflows' if s_offset + 2 + s_len > bytes.length
  raise ArgumentError, 'S is zero length' if s_len.zero?

  s_bytes = bytes[s_offset + 2, s_len]
  raise ArgumentError, 'S has negative flag' if s_bytes[0] & 0x80 != 0
  raise ArgumentError, 'S has excessive padding' if s_len > 1 && s_bytes[0].zero? && (s_bytes[1] & 0x80).zero? # rubocop:disable Style/BitwisePredicate

  raise ArgumentError, 'trailing bytes' unless s_offset + 2 + s_len == bytes.length

  r = OpenSSL::BN.new(r_bytes.pack('C*'), 2)
  s = OpenSSL::BN.new(s_bytes.pack('C*'), 2)
  new(r, s)
end

.from_hex(hex) ⇒ Signature

Parse a signature from a hex-encoded DER string.

Parameters:

  • hex (String)

    hex-encoded DER signature

Returns:



96
97
98
# File 'lib/bsv/primitives/signature.rb', line 96

def self.from_hex(hex)
  from_der(Hex.decode(hex, name: 'signature hex'))
end

Instance Method Details

#==(other) ⇒ Boolean

Returns true if both signatures have equal r and s values.

Parameters:

  • other (Object)

    the object to compare

Returns:

  • (Boolean)

    true if both signatures have equal r and s values



127
128
129
# File 'lib/bsv/primitives/signature.rb', line 127

def ==(other)
  other.is_a?(Signature) && @r == other.r && @s == other.s
end

#low_s?Boolean

Check whether the S value is in the lower half of the curve order.

BIP-62 rule 5 requires S <= N/2 for transaction malleability protection.

Returns:

  • (Boolean)

    true if S is in the lower half



112
113
114
# File 'lib/bsv/primitives/signature.rb', line 112

def low_s?
  @s <= Curve::HALF_N
end

#to_derString

Serialise the signature in DER format.

Returns:

  • (String)

    DER-encoded binary string



82
83
84
85
86
87
88
89
90
# File 'lib/bsv/primitives/signature.rb', line 82

def to_der
  rb = canonicalise_int(@r)
  sb = canonicalise_int(@s)

  der = [0x30, rb.length + sb.length + 4,
         0x02, rb.length, *rb,
         0x02, sb.length, *sb]
  der.pack('C*')
end

#to_hexString

Serialise the signature as a hex-encoded DER string.

Returns:

  • (String)

    hex-encoded DER signature



103
104
105
# File 'lib/bsv/primitives/signature.rb', line 103

def to_hex
  to_der.unpack1('H*')
end

#to_low_sSignature

Return a new signature with S normalised to the lower half of the curve order.

Returns:

  • (Signature)

    a new signature with low-S, or self if already low-S



119
120
121
122
123
# File 'lib/bsv/primitives/signature.rb', line 119

def to_low_s
  return self if low_s?

  self.class.new(@r, Curve::N - @s)
end