Class: BTC::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/btcruby/key.rb

Constant Summary collapse

COMPRESSED_PUBKEY_LENGTH =
33
UNCOMPRESSED_PUBKEY_LENGTH =
65
KEY_ZERO =

Zero-filled 32-byte buffer

"\x00"*32
KEY_MAX_MOD_ORDER =

Order of secp256k1’s generator minus 1.

("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" +
"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE" +
"\xBA\xAE\xDC\xE6\xAF\x48\xA0\x3B" +
"\xBF\xD2\x5E\x8C\xD0\x36\x41\x40").b.freeze
KEY_MAX_MOD_HALF_ORDER =

Half of the order of secp256k1’s generator minus 1.

("\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF" +
"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" +
"\x5D\x57\x6E\x73\x57\xA4\x50\x1D" +
"\xDF\xE9\x2F\x46\x68\x1B\x20\xA0").b.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(private_key: nil, public_key: nil, public_key_compressed: true, wif: nil, network: nil) ⇒ Key

Initializes a key with one of the given keys (public or private). Usage:

  • Key.new(private_key: …[, public_key_compressed: …][, network: …])

  • Key.new(public_key: …[, network: …])

  • Key.new(wif: …)



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
# File 'lib/btcruby/key.rb', line 43

def initialize(private_key: nil,
               public_key: nil,
               public_key_compressed: true,
               wif: nil,
               network: nil)

  @public_key_compressed = public_key_compressed
  @network = network || BTC::Network.default

  if private_key
    if !Key.validate_private_key_range(private_key)
      raise FormatError, "Private key is outside the valid range"
    end
    @private_key = private_key
  elsif public_key
    if !Key.valid_pubkey?(public_key)
      raise FormatError, "Invalid public key: #{public_key.inspect}"
    end
    @public_key_compressed = (self.class.length_for_pubkey(public_key) == COMPRESSED_PUBKEY_LENGTH)
    @public_key = public_key
  elsif wif
    wif = wif.is_a?(WIF) ? wif : Address.parse(wif)
    if !wif.is_a?(WIF)
      raise FormatError, "Invalid WIF string"
    end
    key = wif.key
    @private_key = key.private_key
    @public_key = key.public_key
    @public_key_compressed = key.public_key_compressed
    @network = wif.network
  else
    raise ArgumentError, "Must specify either private_key or public_key"
  end
end

Instance Attribute Details

#compressed_keyObject (readonly)

Returns a copy of BTC::Key instance with public_key_compressed == true



13
14
15
# File 'lib/btcruby/key.rb', line 13

def compressed_key
  @compressed_key
end

#compressed_public_keyObject (readonly)

A binary string containing a compressed public key.



25
26
27
# File 'lib/btcruby/key.rb', line 25

def compressed_public_key
  @compressed_public_key
end

#networkObject

Accessors




33
34
35
# File 'lib/btcruby/key.rb', line 33

def network
  @network
end

#private_keyObject (readonly)

A binary string containing a private key. Returns nil if there is only public key.



19
20
21
# File 'lib/btcruby/key.rb', line 19

def private_key
  @private_key
end

#public_keyObject (readonly)

A binary string containing compressed or uncompressed public key (depends on public_key_compressed flag).



22
23
24
# File 'lib/btcruby/key.rb', line 22

def public_key
  @public_key
end

#public_key_compressedObject (readonly)

Flag specifying if the public key should be compressed. Default is true.



10
11
12
# File 'lib/btcruby/key.rb', line 10

def public_key_compressed
  @public_key_compressed
end

#uncompressed_keyObject (readonly)

Returns a copy of BTC::Key instance with public_key_compressed == false



16
17
18
# File 'lib/btcruby/key.rb', line 16

def uncompressed_key
  @uncompressed_key
end

#uncompressed_public_keyObject (readonly)

A binary string containing an uncompressed public key.



28
29
30
# File 'lib/btcruby/key.rb', line 28

def uncompressed_public_key
  @uncompressed_public_key
end

Class Method Details

.compare_big_endian(s1, s2) ⇒ Object

Private helper to compare two big numbers in big-endian notation. Higher byte has higher value, but strings can be of different length.



381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/btcruby/key.rb', line 381

def self.compare_big_endian(s1, s2)
  s1 = BTC::Data.ensure_binary_encoding(s1)
  s2 = BTC::Data.ensure_binary_encoding(s2)

  if s1.bytesize < s2.bytesize
    s1 = "\x00"*(s2.bytesize - s1.bytesize) + s1
  end

  if s2.bytesize < s1.bytesize
    s2 = "\x00"*(s1.bytesize - s2.bytesize) + s2
  end

  s1 <=> s2
end

.normalized_signature(signature) ⇒ Object



228
229
230
# File 'lib/btcruby/key.rb', line 228

def self.normalized_signature(signature)
  BTC::OpenSSL.ecdsa_normalized_signature(signature)
end

.random(public_key_compressed: true, network: nil) ⇒ Object

Creates a randomly-generated key pair.



79
80
81
82
83
84
85
86
87
88
# File 'lib/btcruby/key.rb', line 79

def self.random(public_key_compressed: true, network: nil)
  # Chances that we'll enter the loop are below 2^-127.
  privkey = BTC::Data.random_data(32)
  while !self.validate_private_key_range(privkey)
    privkey = BTC::Data.random_data(32)
  end
  return self.new(private_key: privkey,
                  public_key_compressed: public_key_compressed,
                  network: network)
end

.self_validate_signature_element(data, check_half: false) ⇒ Object

Private helper to validate portion of a signate. Follows style of bitcoind.



397
398
399
400
# File 'lib/btcruby/key.rb', line 397

def self.self_validate_signature_element(data, check_half: false)
  return self.compare_big_endian(data, KEY_ZERO) > 0 &&
         self.compare_big_endian(data, check_half ? KEY_MAX_MOD_HALF_ORDER : KEY_MAX_MOD_ORDER) <= 0
end

.validate_and_normalize_script_signature(data) ⇒ Object

Validates and normalizes script signature to make it canonical. Note: signature must have hashtype byte appended. Returns nil if signature is invalid and cannot be normalized. Returns original signature if it is canonical. Returns normalized signature script if signature can be normalized.

Raises:

  • (ArgumentError)


237
238
239
240
241
242
243
244
245
246
# File 'lib/btcruby/key.rb', line 237

def self.validate_and_normalize_script_signature(data)
  raise ArgumentError, "Missing script signature data" if !data || data.size == 0
  if validate_script_signature(data)
    return data
  end
  data = BTC::Data.ensure_binary_encoding(data)
  normalized_sig = normalized_signature(data[0, data.size-1])
  return nil if !normalized_sig
  return normalized_sig + data[data.size-1, 1]
end

.validate_private_key_range(private_key) ⇒ Object

Returns true if data representing a private key is within a valid range.



403
404
405
406
# File 'lib/btcruby/key.rb', line 403

def self.validate_private_key_range(private_key)
    return self.compare_big_endian(private_key, KEY_ZERO) > 0 &&
           self.compare_big_endian(private_key, KEY_MAX_MOD_ORDER) <= 0
end

.validate_public_key(data) ⇒ Object

Checks if this public key is valid and well-formed. Logs detailed info using Diagnostics and returns true or false.

Raises:

  • (ArgumentError)


410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/btcruby/key.rb', line 410

def self.validate_public_key(data)
  raise ArgumentError, "Missing public key" if !data

  length = data.bytesize

  # Non-canonical public key: too short
  if length < 33
    Diagnostics.current.add_message("Non-canonical public key: too short.")
    return false
  end

  bytes = data.bytes

  if bytes[0] == 0x04
    # Length of an uncompressed key must be 65 bytes.
    return true if length == 65
    Diagnostics.current.add_message("Non-canonical public key: length of uncompressed key must be 65 bytes.")
    return false
  elsif bytes[0] == 0x02 || bytes[0] == 0x03
    # Length of compressed key must be 33 bytes.
    return true if length == 33
    Diagnostics.current.add_message("Non-canonical public key: length of compressed key must be 33 bytes.")
    return false
  end

  # Unknown public key format.
  Diagnostics.current.add_message("Unknown non-canonical public key.")
  return false
end

.validate_script_signature(data, verify_lower_s: true, verify_hashtype: true) ⇒ Object

Checks if this signature with appended script hash type is well-formed. Logs detailed info using Diagnostics and returns true or false. Set verify_lower_s:false when processing incoming blocks.

Raises:

  • (ArgumentError)


251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/btcruby/key.rb', line 251

def self.validate_script_signature(data, verify_lower_s: true, verify_hashtype: true)
  # See https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623
  # A canonical signature exists of: <30> <total len> <02> <len R> <R> <02> <len S> <S> <hashtype>
  # Where R and S are not negative (their first byte has its highest bit not set), and not
  # excessively padded (do not start with a 0 byte, unless an otherwise negative number follows,
  # in which case a single 0 byte is necessary and even required).

  raise ArgumentError, "Missing script signature data" if !data

  data = BTC::Data.ensure_binary_encoding(data) # so we can use #[] on byte level.

  length = data.bytesize

  # Non-canonical signature: too short
  if length < 9
    Diagnostics.current.add_message("Non-canonical signature: too short.")
    return false
  end

  # Non-canonical signature: too long
  if length > 73
    Diagnostics.current.add_message("Non-canonical signature: too long.")
    return false
  end

  bytes = data.bytes

  if verify_hashtype
    hashtype = bytes[length - 1] & (~(SIGHASH_ANYONECANPAY))

    if hashtype < SIGHASH_ALL || hashtype > SIGHASH_SINGLE
      Diagnostics.current.add_message("Non-canonical signature: unknown hashtype byte.")
      return false
    end
  end

  if bytes[0] != 0x30
    Diagnostics.current.add_message("Non-canonical signature: wrong type.")
    return false
  end

  if bytes[1] != length-3
    Diagnostics.current.add_message("Non-canonical signature: wrong length marker.")
    return false
  end

  lenR = bytes[3]

  if (5 + lenR) >= length
    Diagnostics.current.add_message("Non-canonical signature: S length misplaced.")
    return false
  end

  lenS = bytes[5 + lenR]

  if (lenR + lenS + 7) != length
    Diagnostics.current.add_message("Non-canonical signature: R+S length mismatch")
    return false
  end

  bufR = bytes[4, lenR]
  if bytes[4 - 2] != 0x02
    Diagnostics.current.add_message("Non-canonical signature: R value type mismatch")
    return false
  end

  if lenR == 0
    Diagnostics.current.add_message("Non-canonical signature: R length is zero")
    return false
  end

  if bufR[0] & 0x80 != 0
    Diagnostics.current.add_message("Non-canonical signature: R value negative")
    return false
  end

  if lenR > 1 && (bufR[0] == 0x00) && ((bufR[1] & 0x80) == 0)
    Diagnostics.current.add_message("Non-canonical signature: R value excessively padded")
    return false
  end

  bufS = bytes[6 + lenR, lenS]
  s = data[6 + lenR, lenS]
  if bytes[6 + lenR - 2] != 0x02
    Diagnostics.current.add_message("Non-canonical signature: S value type mismatch")
    return false
  end

  if lenS == 0
    Diagnostics.current.add_message("Non-canonical signature: S length is zero")
    return false
  end

  if bufS[0] & 0x80 != 0
    return false
    Diagnostics.current.add_message("Non-canonical signature: S value is negative")
  end

  if lenS > 1 && (bufS[0] == 0x00) && ((bufS[1] & 0x80) == 0)
    Diagnostics.current.add_message("Non-canonical signature: S value excessively padded")
    return false
  end

  if verify_lower_s
    if !self_validate_signature_element(s, check_half: true)
      Diagnostics.current.add_message("Non-canonical signature: S value is unnecessarily high")
      return false
    end
  end

  return true
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?

Two keypairs are equal only when they are equally complete (both with or without a private key), have matching keys and compression.



154
155
156
157
# File 'lib/btcruby/key.rb', line 154

def ==(other)
  self.public_key == other.public_key &&
  self.private_key == other.private_key
end

#address(network: nil) ⇒ Object

Returns a PublicKeyAddress instance that encodes a public key hash.



128
129
130
# File 'lib/btcruby/key.rb', line 128

def address(network: nil)
  PublicKeyAddress.new(public_key: self.public_key, network: network)
end

#compact_signature(hash) ⇒ Object

Non-standard “compact” signature used for Bitcoin signed messages. It features fixed length and allows efficient extraction of a public key from it.

Raises:



442
443
444
# File 'lib/btcruby/key.rb', line 442

def compact_signature(hash)
  raise BTCError, "Not implemented"
end

#diffie_hellman(key2) ⇒ Object

Multiplies a public key of the receiver with a given private key and returns resulting curve point as BTC::Key object (pubkey only). Pubkey compression flag is the same as on receiver.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/btcruby/key.rb', line 163

def diffie_hellman(key2)

  lib = BTC::OpenSSL
  lib.autorelease do |pool|

    pk = pool.new_bn(key2.private_key)
    n = lib.group_order

    # Convert pubkey to a EC point
    pubkey_x = pool.new_bn(self.compressed_public_key)
    pubkey_point = pool.new_ec_point
    lib.EC_POINT_bn2point(lib.group, pubkey_x, pubkey_point, pool.bn_ctx)

    # Compute point = pubkey*pk + 0*G
    point = pool.new_ec_point
    # /** Computes r = generator * n + q * m
    #  *  \param  group  underlying EC_GROUP object
    #  *  \param  r      EC_POINT object for the result
    #  *  \param  n      BIGNUM with the multiplier for the group generator (optional)
    #  *  \param  q      EC_POINT object with the first factor of the second summand
    #  *  \param  m      BIGNUM with the second factor of the second summand
    #  *  \param  ctx    BN_CTX object (optional)
    #  *  \return 1 on success and 0 if an error occured
    #  */
    # int EC_POINT_mul(const EC_GROUP *group, EC_POINT *r, const BIGNUM *n, const EC_POINT *q, const BIGNUM *m, BN_CTX *ctx);
    lib.EC_POINT_mul(lib.group, point, nil, pubkey_point, pk, pool.bn_ctx)

    # Check for invalid derivation.
    if 1 == lib.EC_POINT_is_at_infinity(lib.group, point)
      raise MathError, "Resulting point is at infinity."
    end

    lib.EC_POINT_point2bn(
      lib.group,
      point,
      self.public_key_compressed ?
      BTC::OpenSSL::POINT_CONVERSION_COMPRESSED :
      BTC::OpenSSL::POINT_CONVERSION_UNCOMPRESSED,
      pubkey_x,
      pool.bn_ctx
    )

    result_pubkey = lib.data_from_bn(pubkey_x, required_length: 33)
    return Key.new(public_key: result_pubkey)
  end
end

#dupObject



144
145
146
147
148
149
150
# File 'lib/btcruby/key.rb', line 144

def dup
  self.class.new(
    private_key: @private_key,
    public_key: @public_key,
    public_key_compressed: @public_key_compressed,
    network: @network)
end

#ecdsa_signature(hash, normalized: true) ⇒ Object

Standard ECDSA signature for a given hash. Used by OP_CHECKSIG and friends. Ensures canonical lower S value and makes a deterministic signature (k = HMAC-SHA256(key: privkey, data: hash))



219
220
221
# File 'lib/btcruby/key.rb', line 219

def ecdsa_signature(hash, normalized: true)
  BTC::OpenSSL.ecdsa_signature(hash, @private_key, normalized: normalized)
end

#message_signature(message) ⇒ Object

Compact signature for a given message. Prepends it with standard prefix “x18Bitcoin Signed Message:n” and encodes message in wire format.

Raises:



453
454
455
# File 'lib/btcruby/key.rb', line 453

def message_signature(message)
  raise BTCError, "Not implemented"
end

#to_wif(network: nil) ⇒ Object

Returns private key encoded in WIF format (aka Sipa format).



139
140
141
142
# File 'lib/btcruby/key.rb', line 139

def to_wif(network: nil)
  return nil if !self.private_key
  self.to_wif_object(network: network).to_s
end

#to_wif_object(network: nil) ⇒ Object

Returns a WIF instance that encodes private key.



133
134
135
136
# File 'lib/btcruby/key.rb', line 133

def to_wif_object(network: nil)
  return nil if !self.private_key
  WIF.new(key: self, network: network)
end

#verify_compact_signature(signature, hash) ⇒ Object

Returns true if compact signature is valid for a given hash

Raises:



447
448
449
# File 'lib/btcruby/key.rb', line 447

def verify_compact_signature(signature, hash)
  raise BTCError, "Not implemented"
end

#verify_ecdsa_signature(signature, hash) ⇒ Object

Returns true if ECDSA signature is valid for a given hash



224
225
226
# File 'lib/btcruby/key.rb', line 224

def verify_ecdsa_signature(signature, hash)
  BTC::OpenSSL.ecdsa_verify(signature, hash, self.public_key)
end

#verify_message_signature(signature, message) ⇒ Object

Raises:



457
458
459
# File 'lib/btcruby/key.rb', line 457

def verify_message_signature(signature, message)
  raise BTCError, "Not implemented"
end