Module: BTC::OpenSSL

Extended by:
OpenSSL
Includes:
FFI::Library
Included in:
OpenSSL
Defined in:
lib/btcruby/openssl.rb

Defined Under Namespace

Classes: AutoreleasePool, ECDSA_SIG

Constant Summary collapse

NID_secp256k1 =
714
POINT_CONVERSION_COMPRESSED =
0x02
POINT_CONVERSION_UNCOMPRESSED =
0x04

Instance Method Summary collapse

Instance Method Details

#autorelease(&block) ⇒ Object

Creates autorelease pool from which various objects can be created. When block returns, pool deallocates all created objects. Available methods on pool instance:

  • ec_key - last EC_KEY (created lazily if needed)

  • group - group of the ec_key

  • bn_ctx - lazily created single instance of BN_CTX

  • new_ec_key - creates new instance of EC_KEY

  • new_bn - creates new instance of BIGNUM

  • new_ec_point - creates new instance of EC_POINT



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/btcruby/openssl.rb', line 132

def autorelease(&block) # {|pool|  }
  prepare_if_needed
  result = nil
  begin
    pool = AutoreleasePool.new
    result = yield(pool)
  ensure
    pool.drain
  end
  result
end

#BN_num_bytes(a) ⇒ Object

in openssl this is defined by a macro



87
88
89
# File 'lib/btcruby/openssl.rb', line 87

def BN_num_bytes(a) # in openssl this is defined by a macro
  (BN_num_bits(a)+7)/8
end

#data_from_bn(bn, min_length: nil, required_length: nil) ⇒ Object

Returns data from bignum

Raises:

  • (ArgumentError)


456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/btcruby/openssl.rb', line 456

def data_from_bn(bn, min_length: nil, required_length: nil)
  raise ArgumentError, "Missing big number" if !bn

  length = BN_num_bytes(bn)
  buf = FFI::MemoryPointer.from_string("\x00"*length)
  BN_bn2bin(bn, buf)
  s = buf.read_string(length)
  s = s.rjust(min_length, "\x00") if min_length
  if required_length && s.bytesize != required_length
    raise BTCError, "Non-matching length of the number: #{s.bytesize} bytes vs required #{required_length}"
  end
  s
end

#ecdsa_normalized_signature(signature) ⇒ Object

Normalizes S value of the signature and returns normalized signature. Returns nil if signature is completely invalid.



347
348
349
# File 'lib/btcruby/openssl.rb', line 347

def ecdsa_normalized_signature(signature)
  ecdsa_reserialize_signature(signature, normalize_s: true)
end

#ecdsa_reserialize_signature(signature, normalize_s: false) ⇒ Object

Raises:

  • (ArgumentError)


351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/btcruby/openssl.rb', line 351

def ecdsa_reserialize_signature(signature, normalize_s: false)
  raise ArgumentError, "Signature is missing" if !signature

  autorelease do |pool|

    # Order of our curve
    n = self.group_order
    halfn = self.group_half_order

    # ECDSA_SIG *psig = NULL;
    # d2i_ECDSA_SIG(&psig, &input, vchSig.size());
    buf = FFI::MemoryPointer.from_string(signature)
    psig = d2i_ECDSA_SIG(nil, pointer_to_pointer(buf), buf.size-1)
    if psig.null?
      Diagnostics.current.add_message("OpenSSL failed to read ECDSA signature with DER during reserialize: #{BTC.to_hex(signature).inspect}")
      return signature
      #raise BTCError, "OpenSSL failed to read ECDSA signature with DER: #{BTC.to_hex(signature).inspect}"
    end

    sig = ECDSA_SIG.new(psig) # read sig from its pointer
    
    if normalize_s
      # Enforce low S values, by negating the value (modulo the order) if above order/2.
      s = sig[:s]
      if BN_cmp(s, halfn) > 0
        BN_sub(s, n, s)
      end
    end

    # Note: we'll place new s value back to s bignum,
    # so we don't need another sig structure.

    # Encode signature in DER format.
    sig_size = 72 # typical size of a signature (when both numbers are 33 bytes).

    # allocate a bit more memory just in case (cargo cult)
    buffer = FFI::MemoryPointer.new(:uint8, sig_size + 16)
    sig_size = i2d_ECDSA_SIG(sig.pointer, pointer_to_pointer(buffer))

    # read actual number of bytes composed by OpenSSL
    signature = buffer.read_string(sig_size)

    # Free the signature created by d2i_ECDSA_SIG above.
    ECDSA_SIG_free(psig)

    signature
  end
end

#ecdsa_signature(hash, privkey, normalized: true) ⇒ Object

Computes a deterministic ECDSA signature with canonical (lowest) S value. Nonce k is equal to HMAC-SHA256(data: hash, key: privkey)

Raises:

  • (ArgumentError)


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

def ecdsa_signature(hash, privkey, normalized: true)
  raise ArgumentError, "Hash is missing" if !hash
  raise ArgumentError, "Private key is missing" if !privkey

  # ECDSA signature is a pair of numbers: (Kx, s)
  # Where Kx = x coordinate of k*G mod n (n is the order of secp256k1).
  # And s = (k^-1)*(h + Kx*privkey).
  # By default, k is chosen randomly on interval [0, n - 1].
  # But this makes signatures harder to test and allows faulty or
  # backdoored RNGs to leak private keys from ECDSA signatures.
  # To avoid these issues, we'll generate k = Hash256(hash || privatekey)
  # and make all computations by hand.

  autorelease do |pool|

    # Order of our curve
    n = self.group_order
    halfn = self.group_half_order

    # Generate k deterministically from private key and message using HMAC-SHA256
    # This is an important point #1.
    kdata = rfc6979_ecdsa_nonce(hash, privkey)
    k = pool.new_bn(kdata)

    # Enforce k within group order: k = k % n
    BN_div(nil, k, k, n, pool.bn_ctx)

    # Compute K = k*G
    #(can't use K variable name because Ruby does not allow
    # constant assignment in methods)
    kG = pool.new_ec_point
    EC_POINT_mul(self.group, kG, k, nil, nil, pool.bn_ctx)

    # Compute r = K.x. This is first half of the signature.
    r = pool.new_bn
    EC_POINT_get_affine_coordinates_GFp(self.group, kG, r, nil, pool.bn_ctx)

    # Compute s = (k^-1)*(h + r*privkey).
    h = pool.new_bn(hash)
    p = pool.new_bn(privkey)
    tmp = pool.new_bn
    s = pool.new_bn
    BN_mod_mul(tmp, r, p, n, pool.bn_ctx) # tmp = r*privkey
    BN_mod_add_quick(s, tmp, h, n)        # s = h + tmp = h + r*privkey
    BN_mod_inverse(k, k, n, pool.bn_ctx)  # k' = k^-1
    BN_mod_mul(s, s, k, n, pool.bn_ctx)   # s = k'*(h + r*privkey)

    # Enforce low S values, by negating the value (modulo the order) if above order/2.
    # This is an important point #2. Not doing that would yield (sometimes)
    # non-canonical signatures that will be rejected by many relaying nodes.
    if normalized
      if BN_cmp(s, halfn) > 0
        BN_sub(s, n, s)
      end
    end

    # Fill in ECDSA_SIG structure so we can convert it into a proper DER format.
    sig = ECDSA_SIG.new
    sig[:r] = r
    sig[:s] = s

    # Encode signature in DER format.

    sig_size = 72 # typical size of a signature (when both numbers are 33 bytes).

    # allocate a bit more memory just in case (cargo cult)
    buffer = FFI::MemoryPointer.new(:uint8, sig_size + 16)
    sig_size = i2d_ECDSA_SIG(sig.pointer, pointer_to_pointer(buffer))

    # read actual number of bytes composed by OpenSSL
    signature = buffer.read_string(sig_size)
    signature
  end
end

#ecdsa_verify(signature, hash, public_key) ⇒ Object

Raises:

  • (ArgumentError)


400
401
402
403
404
405
406
407
408
409
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
# File 'lib/btcruby/openssl.rb', line 400

def ecdsa_verify(signature, hash, public_key)
  raise ArgumentError, "Signature is missing" if !signature
  raise ArgumentError, "Hash is missing" if !hash
  raise ArgumentError, "Public key is missing" if !public_key
  
  # New versions of OpenSSL will reject non-canonical DER signatures. de/re-serialize first.
  signature = ecdsa_reserialize_signature(signature, normalize_s: false)

  autorelease do |pool|
    eckey = pool.new_ec_key

    buf = FFI::MemoryPointer.from_string(public_key)
    eckey = o2i_ECPublicKey(pointer_to_pointer(eckey), pointer_to_pointer(buf), buf.size - 1)
    if eckey.null?
      Diagnostics.current.add_message("OpenSSL failed to create EC_KEY with public key: #{BTC.to_hex(public_key).inspect}")
      raise BTCError, "OpenSSL failed to create EC_KEY with public key: #{BTC.to_hex(public_key).inspect}"
    end

    # -1 = error, 0 = bad sig, 1 = good
    hash_buf = FFI::MemoryPointer.from_string(hash)
    sig_buf = FFI::MemoryPointer.from_string(signature)
    result = ECDSA_verify(0, hash_buf, hash.bytesize, sig_buf, signature.bytesize, eckey)

    if result == 1
      return true
    end

    if result == 0
      Diagnostics.current.add_message("OpenSSL detected invalid ECDSA signature. Signature: #{BTC.to_hex(signature).inspect}; Hash: #{BTC.to_hex(hash).inspect}; Pubkey: #{BTC.to_hex(public_key).inspect}")
    else
      Diagnostics.current.add_message("OpenSSL failed with error while verifying ECDSA signature. Signature: #{BTC.to_hex(signature).inspect}; Hash: #{BTC.to_hex(hash).inspect}; Pubkey: #{BTC.to_hex(public_key).inspect}; Result: #{result}")
      return false
      # raise BTCError, "OpenSSL failed with error while verifying ECDSA signature. Signature: #{BTC.to_hex(signature).inspect}; Hash: #{BTC.to_hex(hash).inspect}; Pubkey: #{BTC.to_hex(public_key).inspect}; Result: #{result}"
    end
    return false
  end
  false
end

#groupObject



101
102
103
# File 'lib/btcruby/openssl.rb', line 101

def group
  @group ||= EC_GROUP_new_by_curve_name(NID_secp256k1)
end

#group_half_orderObject



115
116
117
118
119
120
121
# File 'lib/btcruby/openssl.rb', line 115

def group_half_order
  @group_half_order ||= begin
    halforder = BN_new()
    BN_rshift1(halforder, self.group_order)
    halforder
  end
end

#group_orderObject



105
106
107
108
109
110
111
112
113
# File 'lib/btcruby/openssl.rb', line 105

def group_order
  @group_order ||= begin
    n = BN_new()
    bn_ctx = BN_CTX_new()
    EC_GROUP_get_order(self.group, n, bn_ctx)
    BN_CTX_free(bn_ctx)
    n
  end
end

#prepare_if_neededObject



91
92
93
94
95
96
97
98
99
# File 'lib/btcruby/openssl.rb', line 91

def prepare_if_needed
  if !@prepared_openssl
    SSL_library_init()
    ERR_load_crypto_strings()
    SSL_load_error_strings()
    RAND_poll()
    @prepared_openssl = true
  end
end

#private_key_from_der_format(der_key) ⇒ Object

extract private key from uncompressed DER format

Raises:

  • (ArgumentError)


440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/btcruby/openssl.rb', line 440

def private_key_from_der_format(der_key)
  raise ArgumentError, "Missing DER private key" if !der_key

  prepare_if_needed

  buf = FFI::MemoryPointer.from_string(der_key)
  ec_key = d2i_ECPrivateKey(nil, pointer_to_pointer(buf), buf.size-1)
  if ec_key.null?
    raise BTCError, "OpenSSL failed to create EC_KEY with DER private key"
  end
  bn = EC_KEY_get0_private_key(ec_key)
  BN_bn2bin(bn, buf)
  buf.read_string(32)
end

#public_key_with_compression(pubkey, compressed) ⇒ Object

Raises:

  • (ArgumentError)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/btcruby/openssl.rb', line 144

def public_key_with_compression(pubkey, compressed)
  raise ArgumentError, "Public key is missing" if !pubkey

  autorelease do |pool|

    eckey = pool.new_ec_key

    # 1. Load EC_KEY with pubkey binary data.
    buf = FFI::MemoryPointer.from_string(pubkey)
    eckey = o2i_ECPublicKey(pointer_to_pointer(eckey), pointer_to_pointer(buf), buf.size-1)
    if eckey.null?
      raise BTCError, "OpenSSL failed to create EC_KEY with public key: #{BTC.to_hex(pubkey).inspect}"
    end

    # 2. Extract re-compressed pubkey from EC_KEY
    EC_KEY_set_conv_form(eckey, compressed ? POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED);

    length = i2o_ECPublicKey(eckey, nil)
    buf = FFI::MemoryPointer.new(:uint8, length)
    if i2o_ECPublicKey(eckey, pointer_to_pointer(buf)) == length
      public_key = buf.read_string(length)
    else
      raise BTCError, "OpenSSL failed to regenerate a public key."
    end

    public_key
  end
end

#regenerate_keypair(private_key, public_key_compressed: false) ⇒ Object

Returns a pair of private key, public key



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
209
210
211
212
213
214
# File 'lib/btcruby/openssl.rb', line 174

def regenerate_keypair(private_key, public_key_compressed: false)

  autorelease do |pool|

    eckey = pool.new_ec_key

    priv_bn = pool.new_bn(private_key)

    pub_key = pool.new_ec_point
    EC_POINT_mul(self.group, pub_key, priv_bn, nil, nil, pool.bn_ctx)
    EC_KEY_set_private_key(eckey, priv_bn)
    EC_KEY_set_public_key(eckey, pub_key)

    length = i2d_ECPrivateKey(eckey, nil)
    buf = FFI::MemoryPointer.new(:uint8, length)
    if i2d_ECPrivateKey(eckey, pointer_to_pointer(buf)) == length
      # We have a full DER representation of private key, it contains a length
      # of a private key at offset 8 and private key at offset 9.
      size = buf.get_array_of_uint8(8, 1)[0]
      private_key2 = buf.get_array_of_uint8(9, size).pack("C*").rjust(32, "\x00")
    else
      raise BTCError, "OpenSSL failed to convert private key to DER format"
    end

    if private_key2 != private_key
      raise BTCError, "OpenSSL somehow regenerated a wrong private key."
    end

    EC_KEY_set_conv_form(eckey, public_key_compressed ? POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED);

    length = i2o_ECPublicKey(eckey, nil)
    buf = FFI::MemoryPointer.new(:uint8, length)
    if i2o_ECPublicKey(eckey, pointer_to_pointer(buf)) == length
      public_key = buf.read_string(length)
    else
      raise BTCError, "OpenSSL failed to regenerate a public key."
    end

    [ private_key2, public_key ]
  end
end

#rfc6979_ecdsa_nonce(hash, privkey) ⇒ Object

Returns k value computed deterministically from message hash and privkey. See tools.ietf.org/html/rfc6979

Raises:

  • (ArgumentError)


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/btcruby/openssl.rb', line 218

def rfc6979_ecdsa_nonce(hash, privkey)
  raise ArgumentError, "Hash must be 32 bytes long" if hash.bytesize != 32
  raise ArgumentError, "Private key must be 32 bytes long" if privkey.bytesize != 32

  autorelease do |pool|
    order = self.group_order

    # Step 3.2.a. hash = H(message). Already performed by the caller.

    # Step 3.2.b. V = 0x01 0x01 0x01 ... 0x01 (32 bytes equal 0x01)
    v = "\x01".b*32

    # Step 3.2.c. K = 0x00 0x00 0x00 ... 0x00 (32 bytes equal 0x00)
    k = "\x00".b*32

    # Step 3.2.d. K = HMAC-SHA256(key: K, data: V || 0x00 || int2octets(privkey) || bits2octets(hash))
    h1 = pool.new_bn(hash)
    BN_div(nil, h1, h1, order, pool.bn_ctx) # h1 = h1 % order
    h1data = data_from_bn(h1, min_length: 32)
    k = BTC.hmac_sha256(key: k, data: v + "\x00".b + privkey + h1data)

    # Step 3.2.e. V = HMAC-SHA256(key: K, data: V)
    v = BTC.hmac_sha256(key: k, data: v)

    # Step 3.2.f. K = HMAC-SHA256(key: K, data: V || 0x01 || int2octets(privkey) || bits2octets(hash))
    k = BTC.hmac_sha256(key: k, data: v + "\x01".b + privkey + h1data)

    # Step 3.2.g. V = HMAC-SHA256(key: K, data: V)
    v = BTC.hmac_sha256(key: k, data: v)

    # Step 3.2.h.
    zero32 = "\x00".b*32
    10000.times do
      t = BTC.hmac_sha256(key: k, data: v)
      tn = pool.new_bn(t)
      if BN_cmp(tn, order) < 0
        nonce = data_from_bn(tn, min_length: 32)
        if nonce != zero32
          return nonce
        end
      end
      # Note: the probability of not succeeding at the first try is about 2^-127.
      k = BTC.hmac_sha256(key: k, data: v + zero32)
      v = BTC.hmac_sha256(key: k, data: v)
    end
    # we generated 10000 numbers, none of them is good -> fail.
    raise "Cannot find any good ECDSA nonce after 10000 iterations of RFC6979."
  end
end