Module: KeyPairs::Secp256k1

Defined in:
lib/key-pairs/secp256k1.rb

Overview

Secp256k1 implementation for XRPL key pairs.

Constant Summary collapse

GROUP =
ECDSA::Group::Secp256k1

Class Method Summary collapse

Class Method Details

.derive_key_pair(seed) ⇒ Hash

Derives a key pair from a seed.

Parameters:

  • seed (Array<Integer>)

    16 bytes of seed entropy.

Returns:

  • (Hash)

    A hash containing :public_key and :private_key (hex strings).



15
16
17
18
19
20
21
22
23
24
25
# File 'lib/key-pairs/secp256k1.rb', line 15

def self.derive_key_pair(seed)
  # XRPL Secp256k1 uses a specific derivation algorithm (seed -> family seed -> sequence 0 key).
  # Seed is passed as a 16-byte array.
  private_key = derive_private_key(seed)
  public_key = derive_public_key(private_key)
  
  {
    public_key: public_key.unpack1('H*').upcase,
    private_key: private_key.upcase.rjust(66, '00')
  }
end

.derive_private_key(seed, account_index = 0) ⇒ Object

Derives the 32-byte private key from the 16-byte seed. This follows the Ripple seed derivation algorithm.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/key-pairs/secp256k1.rb', line 127

def self.derive_private_key(seed,  = 0)
  # 1. Derive root private key from seed
  root_private_key = derive_scalar(seed)
  
  # 2. Derive root public key from root private key
  root_public_key = derive_root_public_key(root_private_key)
  
  # 3. Derive child scalar from root public key and account_index
  child_scalar = derive_scalar(root_public_key, )
  
  # 4. child_private_key = (root_private_key + child_scalar) % order
  child_private_key = (root_private_key.to_i(16) + child_scalar.to_i(16)) % GROUP.order
  child_private_key.to_s(16).rjust(64, '0')
end

.derive_public_key(private_key_hex) ⇒ Object



165
166
167
# File 'lib/key-pairs/secp256k1.rb', line 165

def self.derive_public_key(private_key_hex)
  derive_root_public_key(private_key_hex)
end

.derive_root_public_key(private_key_hex) ⇒ Object



159
160
161
162
163
# File 'lib/key-pairs/secp256k1.rb', line 159

def self.derive_root_public_key(private_key_hex)
  key_bn = private_key_hex.to_i(16)
  pub_key_point = GROUP.generator.multiply_by_scalar(key_bn)
  ECDSA::Format::PointOctetString.encode(pub_key_point, compression: true)
end

.derive_scalar(seed, sequence = nil) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/key-pairs/secp256k1.rb', line 142

def self.derive_scalar(seed, sequence = nil)
  seed_bytes = seed.is_a?(Array) ? seed.pack('C*') : seed
  loop_count = 0
  loop do
    data = sequence ? seed_bytes + [sequence].pack('N') : seed_bytes
    data += [loop_count].pack('N')
    hash = Digest::SHA512.digest(data)[0...32]
    scalar = hash.unpack1('H*').to_i(16)
    
    if scalar > 0 && scalar < GROUP.order
      return scalar.to_s(16).rjust(64, '0')
    end
    loop_count += 1
    raise "Too many loops" if loop_count > 100
  end
end

.generate_k(private_key_bn, message_hash) ⇒ Object

Deterministic k generation (RFC 6979)



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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/key-pairs/secp256k1.rb', line 53

def self.generate_k(private_key_bn, message_hash)
  q = GROUP.order
  q_len = q.bit_length
  holeren = (q_len + 7) / 8
  
  # Step b
  v = "\x01" * holeren
  # Step c
  k = "\x00" * holeren
  
  # bits2octets(x)
  x_bytes = [private_key_bn.to_s(16).rjust(holeren * 2, '0')].pack('H*')
  # bits2octets(bits2int(h1))
  h1_val = message_hash.unpack1('H*').to_i(16)
  if h1_val >= q
    h1_val %= q
  end
  h1 = [h1_val.to_s(16).rjust(holeren * 2, '0')].pack('H*')

  # Step d
  k = OpenSSL::HMAC.digest('sha256', k, v + "\x00" + x_bytes + h1)
  # Step e
  v = OpenSSL::HMAC.digest('sha256', k, v)
  # Step f
  k = OpenSSL::HMAC.digest('sha256', k, v + "\x01" + x_bytes + h1)
  # Step g
  v = OpenSSL::HMAC.digest('sha256', k, v)
  
  # Step h
  loop do
    t = ""
    while t.length < holeren
      v = OpenSSL::HMAC.digest('sha256', k, v)
      t += v
    end
    
    k_val = t[0...holeren].unpack1('H*').to_i(16)
    # bits2int(T)
    if q_len < 8 * holeren
      k_val >>= (8 * holeren - q_len)
    end

    return k_val if k_val > 0 && k_val < q
    
    k = OpenSSL::HMAC.digest('sha256', k, v + "\x00")
    v = OpenSSL::HMAC.digest('sha256', k, v)
  end
end

.sign(message, private_key) ⇒ String

Signs a message with a private key.

Parameters:

  • message (Array<Integer>, String)

    The message to sign (hex string or byte array).

  • private_key (String)

    The private key (hex string).

Returns:

  • (String)

    The signature (hex string).



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/key-pairs/secp256k1.rb', line 31

def self.sign(message, private_key)
  msg_hash = message.is_a?(String) ? [message].pack('H*') : message.pack('C*')
  
  # If message is not already 32 bytes, we hash it.
  # This is a bit of a heuristic. XRPL signs the SHA512Half hash of the serialized transaction.
  if msg_hash.length != 32
    msg_hash = Digest::SHA512.digest(msg_hash)[0...32]
  end

  priv_key_bn = private_key.to_i(16)
  k = generate_k(priv_key_bn, msg_hash)
  signature = ECDSA.sign(GROUP, priv_key_bn, msg_hash, k)
  
  # XRPL requires "canonical" signatures: S <= order / 2
  if signature.s > (GROUP.order / 2)
    signature = ECDSA::Signature.new(signature.r, GROUP.order - signature.s)
  end
  
  ECDSA::Format::SignatureDerString.encode(signature).unpack1('H*').upcase
end

.verify(message, signature, public_key) ⇒ Boolean

Verifies a signature.

Parameters:

  • message (Array<Integer>, String)

    The message (hex string or byte array).

  • signature (String)

    The signature (hex string).

  • public_key (String)

    The public key (hex string).

Returns:

  • (Boolean)

    True if the signature is valid.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/key-pairs/secp256k1.rb', line 107

def self.verify(message, signature, public_key)
  msg_hash = message.is_a?(String) ? [message].pack('H*') : message.pack('C*')
  
  # If message is not already 32 bytes, we hash it.
  if msg_hash.length != 32
    msg_hash = Digest::SHA512.digest(msg_hash)[0...32]
  end

  sig_bytes = [signature].pack('H*')
  
  point = ECDSA::Format::PointOctetString.decode([public_key].pack('H*'), GROUP)
  sig = ECDSA::Format::SignatureDerString.decode(sig_bytes)
  
  ECDSA.valid_signature?(point, msg_hash, sig)
end