module Ethereum::OpenSSL_EC
bindings for elliptic curve inside OpenSSL
@see # github.com/lian/bitcoin-ruby/blob/master/lib/bitcoin/ffi/openssl.rb
Constants
- NID_secp256k1
- POINT_CONVERSION_COMPRESSED
- POINT_CONVERSION_UNCOMPRESSED
Public Class Methods
# File lib/ethereum/ffi/openssl.rb, line 76 def self.BN_num_bytes(ptr); (BN_num_bits(ptr) + 7) / 8; end
extract private key from uncompressed DER format
# File lib/ethereum/ffi/openssl.rb, line 136 def self.der_to_private_key(der_hex) init_ffi_ssl #k = EC_KEY_new_by_curve_name(NID_secp256k1) #kp = FFI::MemoryPointer.new(:pointer).put_pointer(0, eckey) buf = FFI::MemoryPointer.from_string([der_hex].pack("H*")) ptr = FFI::MemoryPointer.new(:pointer).put_pointer(0, buf) #ec_key = d2i_ECPrivateKey(kp, ptr, buf.size-1) ec_key = d2i_ECPrivateKey(nil, ptr, buf.size-1) return nil if ec_key.null? bn = EC_KEY_get0_private_key(ec_key) BN_bn2bin(bn, buf) buf.read_string(32).unpack("H*")[0] end
lifted from github.com/GemHQ/money-tree
# File lib/ethereum/ffi/openssl.rb, line 338 def self.ec_add(point_0, point_1) init_ffi_ssl eckey = EC_KEY_new_by_curve_name(NID_secp256k1) group = EC_KEY_get0_group(eckey) point_0_hex = point_0.to_bn.to_s(16) point_0_pt = EC_POINT_hex2point(group, point_0_hex, nil, nil) point_1_hex = point_1.to_bn.to_s(16) point_1_pt = EC_POINT_hex2point(group, point_1_hex, nil, nil) sum_point = EC_POINT_new(group) success = EC_POINT_add(group, sum_point, point_0_pt, point_1_pt, nil) hex = EC_POINT_point2hex(group, sum_point, POINT_CONVERSION_UNCOMPRESSED, nil) EC_KEY_free(eckey) EC_POINT_free(sum_point) hex end
# File lib/ethereum/ffi/openssl.rb, line 380 def self.init_ffi_ssl return if @ssl_loaded SSL_library_init() ERR_load_crypto_strings() SSL_load_error_strings() RAND_poll() @ssl_loaded = true end
# File lib/ethereum/ffi/openssl.rb, line 325 def self.recover_compact(hash, signature) return false if signature.bytesize != 65 #i = signature.unpack("C")[0] - 27 #pubkey = recover_public_key_from_signature(hash, signature, (i & ~4), i >= 4) version = signature.unpack('C')[0] return false if version < 27 or version > 34 compressed = (version >= 31) ? (version -= 4; true) : false pubkey = recover_public_key_from_signature(hash, signature, version-27, compressed) end
Given the components of a signature and a selector value, recover and return the public key that generated the signature according to the algorithm in SEC1v2 section 4.1.6.
rec_id is an index from 0 to 3 that indicates which of the 4 possible keys is the correct one. Because the key recovery operation yields multiple potential keys, the correct key must either be stored alongside the signature, or you must be willing to try each rec_id in turn until you find one that outputs the key you are expecting.
If this method returns nil, it means recovery was not possible and rec_id should be iterated.
Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, and if the output is nil OR a key that is not the one you expect, you try again with the next rec_id.
message_hash = hash of the signed message. signature = the R and S components of the signature, wrapped. rec_id = which possible key to recover. is_compressed = whether or not the original pubkey was compressed.
# File lib/ethereum/ffi/openssl.rb, line 173 def self.recover_public_key_from_signature(message_hash, signature, rec_id, is_compressed) return nil if rec_id < 0 or signature.bytesize != 65 init_ffi_ssl signature = FFI::MemoryPointer.from_string(signature) #signature_bn = BN_bin2bn(signature, 65, BN_new()) r = BN_bin2bn(signature[1], 32, BN_new()) s = BN_bin2bn(signature[33], 32, BN_new()) n, i = 0, rec_id / 2 eckey = EC_KEY_new_by_curve_name(NID_secp256k1) EC_KEY_set_conv_form(eckey, POINT_CONVERSION_COMPRESSED) if is_compressed group = EC_KEY_get0_group(eckey) order = BN_new() EC_GROUP_get_order(group, order, nil) x = BN_dup(order) BN_mul_word(x, i) BN_add(x, x, r) field = BN_new() EC_GROUP_get_curve_GFp(group, field, nil, nil, nil) if BN_cmp(x, field) >= 0 [r, s, order, x, field].each{|i| BN_free(i) } EC_KEY_free(eckey) return nil end big_r = EC_POINT_new(group) EC_POINT_set_compressed_coordinates_GFp(group, big_r, x, rec_id % 2, nil) big_q = EC_POINT_new(group) n = EC_GROUP_get_degree(group) e = BN_bin2bn(message_hash, message_hash.bytesize, BN_new()) BN_rshift(e, e, 8 - (n & 7)) if 8 * message_hash.bytesize > n ctx = BN_CTX_new() zero, rr, sor, eor = BN_new(), BN_new(), BN_new(), BN_new() BN_set_word(zero, 0) BN_mod_sub(e, zero, e, order, ctx) BN_mod_inverse(rr, r, order, ctx) BN_mod_mul(sor, s, rr, order, ctx) BN_mod_mul(eor, e, rr, order, ctx) EC_POINT_mul(group, big_q, eor, big_r, sor, ctx) EC_KEY_set_public_key(eckey, big_q) BN_CTX_free(ctx) [r, s, order, x, field, e, zero, rr, sor, eor].each{|i| BN_free(i) } [big_r, big_q].each{|i| EC_POINT_free(i) } length = i2o_ECPublicKey(eckey, nil) buf = FFI::MemoryPointer.new(:uint8, length) ptr = FFI::MemoryPointer.new(:pointer).put_pointer(0, buf) pub_hex = if i2o_ECPublicKey(eckey, ptr) == length buf.read_string(length).unpack("H*")[0] end EC_KEY_free(eckey) pub_hex end
resolve public from private key, using ffi and libssl.so example:
keypair = Bitcoin.generate_key; Bitcoin::OpenSSL_EC.regenerate_key(keypair.first) == keypair
# File lib/ethereum/ffi/openssl.rb, line 82 def self.regenerate_key(private_key) private_key = [private_key].pack("H*") if private_key.bytesize >= (32*2) private_key_hex = private_key.unpack("H*")[0] #private_key = FFI::MemoryPointer.new(:uint8, private_key.bytesize) # .put_bytes(0, private_key, 0, private_key.bytesize) private_key = FFI::MemoryPointer.from_string(private_key) init_ffi_ssl eckey = EC_KEY_new_by_curve_name(NID_secp256k1) #priv_key = BN_bin2bn(private_key, private_key.size, BN_new()) priv_key = BN_bin2bn(private_key, private_key.size-1, BN_new()) group, order, ctx = EC_KEY_get0_group(eckey), BN_new(), BN_CTX_new() EC_GROUP_get_order(group, order, ctx) pub_key = EC_POINT_new(group) EC_POINT_mul(group, pub_key, priv_key, nil, nil, ctx) EC_KEY_set_private_key(eckey, priv_key) EC_KEY_set_public_key(eckey, pub_key) BN_free(order) BN_CTX_free(ctx) EC_POINT_free(pub_key) BN_free(priv_key) length = i2d_ECPrivateKey(eckey, nil) buf = FFI::MemoryPointer.new(:uint8, length) ptr = FFI::MemoryPointer.new(:pointer).put_pointer(0, buf) priv_hex = if i2d_ECPrivateKey(eckey, ptr) == length size = buf.get_array_of_uint8(8, 1)[0] buf.get_array_of_uint8(9, size).pack("C*").rjust(32, "\x00").unpack("H*")[0] #der_to_private_key( ptr.read_pointer.read_string(length).unpack("H*")[0] ) end if priv_hex != private_key_hex raise "regenerated wrong private_key, raise here before generating a faulty public_key too!" end length = i2o_ECPublicKey(eckey, nil) buf = FFI::MemoryPointer.new(:uint8, length) ptr = FFI::MemoryPointer.new(:pointer).put_pointer(0, buf) pub_hex = if i2o_ECPublicKey(eckey, ptr) == length buf.read_string(length).unpack("H*")[0] end EC_KEY_free(eckey) [ priv_hex, pub_hex ] end
repack signature for OpenSSL 1.0.1k handling of DER signatures github.com/bitcoin/bitcoin/pull/5634/files
# File lib/ethereum/ffi/openssl.rb, line 359 def self.repack_der_signature(signature) init_ffi_ssl return false if signature.empty? # New versions of OpenSSL will reject non-canonical DER signatures. de/re-serialize first. norm_der = FFI::MemoryPointer.new(:pointer) sig_ptr = FFI::MemoryPointer.new(:pointer).put_pointer(0, FFI::MemoryPointer.from_string(signature)) norm_sig = d2i_ECDSA_SIG(nil, sig_ptr, signature.bytesize) derlen = i2d_ECDSA_SIG(norm_sig, norm_der) ECDSA_SIG_free(norm_sig) return false if derlen <= 0 ret = norm_der.read_pointer.read_string(derlen) OPENSSL_free(norm_der.read_pointer) ret end
# File lib/ethereum/ffi/openssl.rb, line 281 def self.sign_compact(hash, private_key, public_key_hex = nil, pubkey_compressed = nil) private_key = [private_key].pack("H*") if private_key.bytesize >= 64 private_key_hex = private_key.unpack("H*")[0] public_key_hex = regenerate_key(private_key_hex).last unless public_key_hex pubkey_compressed = (public_key_hex[0..1] == "04" ? false : true) unless pubkey_compressed init_ffi_ssl eckey = EC_KEY_new_by_curve_name(NID_secp256k1) priv_key = BN_bin2bn(private_key, private_key.bytesize, BN_new()) group, order, ctx = EC_KEY_get0_group(eckey), BN_new(), BN_CTX_new() EC_GROUP_get_order(group, order, ctx) pub_key = EC_POINT_new(group) EC_POINT_mul(group, pub_key, priv_key, nil, nil, ctx) EC_KEY_set_private_key(eckey, priv_key) EC_KEY_set_public_key(eckey, pub_key) signature = ECDSA_do_sign(hash, hash.bytesize, eckey) BN_free(order) BN_CTX_free(ctx) EC_POINT_free(pub_key) BN_free(priv_key) EC_KEY_free(eckey) buf, rec_id, head = FFI::MemoryPointer.new(:uint8, 32), nil, nil r, s = signature.get_array_of_pointer(0, 2).map{|i| BN_bn2bin(i, buf); buf.read_string(BN_num_bytes(i)).rjust(32, "\x00") } if signature.get_array_of_pointer(0, 2).all?{|i| BN_num_bits(i) <= 256 } 4.times{|i| head = [ 27 + i + (pubkey_compressed ? 4 : 0) ].pack("C") if public_key_hex == recover_public_key_from_signature(hash, [head, r, s].join, i, pubkey_compressed) rec_id = i; break end } end ECDSA_SIG_free(signature) [ head, [r,s] ].join if rec_id end
Regenerate a DER-encoded signature such that the S-value complies with the BIP62 specification.
# File lib/ethereum/ffi/openssl.rb, line 240 def self.signature_to_low_s(signature) init_ffi_ssl buf = FFI::MemoryPointer.new(:uint8, 34) temp = signature.unpack("C*") length_r = temp[3] length_s = temp[5+length_r] sig = FFI::MemoryPointer.from_string(signature) # Calculate the lower s value s = BN_bin2bn(sig[6 + length_r], length_s, BN_new()) eckey = EC_KEY_new_by_curve_name(NID_secp256k1) group, order, halforder, ctx = EC_KEY_get0_group(eckey), BN_new(), BN_new(), BN_CTX_new() EC_GROUP_get_order(group, order, ctx) BN_rshift1(halforder, order) if BN_cmp(s, halforder) > 0 BN_sub(s, order, s) end BN_free(halforder) BN_free(order) BN_CTX_free(ctx) length_s = BN_bn2bin(s, buf) # p buf.read_string(length_s).unpack("H*") # Re-encode the signature in DER format sig = [0x30, 0, 0x02, length_r] sig.concat(temp.slice(4, length_r)) sig << 0x02 sig << length_s sig.concat(buf.read_string(length_s).unpack("C*")) sig[1] = sig.size - 2 BN_free(s) EC_KEY_free(eckey) sig.pack("C*") end