diff --git a/.travis.yml b/.travis.yml index 092a3d20..afc5ae3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: ruby rvm: - - 2.2.9 - - 2.3.6 - - 2.4.3 - - 2.5.1 + - 2.4.9 + - 2.5.7 + - 2.6.5 script: - bundle exec rake build_libsecp256k1 - bundle exec rake rspec diff --git a/Gemfile.lock b/Gemfile.lock index 21232185..f2557c91 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bitcoin-ruby (0.0.18) + bitcoin-ruby (0.0.20) eventmachine ffi scrypt @@ -82,4 +82,4 @@ DEPENDENCIES simplecov (~> 0.16.1) BUNDLED WITH - 1.16.2 + 1.17.3 diff --git a/README.rdoc b/README.rdoc index 563456d6..26fd6b71 100644 --- a/README.rdoc +++ b/README.rdoc @@ -13,11 +13,9 @@ Some of the main features are: == Compatible with... -* ruby 1.9.3 -* ruby 2.0.0 -* ruby 2.1.2 -* ruby 2.2.0 -* ruby 2.2.2 +* ruby 2.4.x +* ruby 2.5.x +* ruby 2.6.x == Installation diff --git a/lib/bitcoin.rb b/lib/bitcoin.rb index a54353f7..8848a130 100644 --- a/lib/bitcoin.rb +++ b/lib/bitcoin.rb @@ -47,6 +47,7 @@ module Trezor module Util def address_version; Bitcoin.network[:address_version]; end + def version_bytes; address_version.size / 2; end def p2sh_version; Bitcoin.network[:p2sh_version]; end # hash160 is a 20 bytes (160bits) rmd610-sha256 hexdigest. @@ -65,7 +66,7 @@ def checksum(hex) def base58_checksum?(base58) hex = decode_base58(base58) rescue nil return false unless hex - checksum( hex[0...42] ) == hex[-8..-1] + checksum(hex[0...(version_bytes + 20) * 2]) == hex[-8..-1] end alias :address_checksum? :base58_checksum? @@ -95,7 +96,9 @@ def hash160_from_address(address) _, witness_program_hex = decode_segwit_address(address) witness_program_hex when :hash160, :p2sh - decode_base58(address)[2...42] + start_idx = version_bytes * 2 + stop_idx = start_idx + 40 # 20 bytes (2 chars per byte) + decode_base58(address)[start_idx...stop_idx] end end @@ -116,14 +119,16 @@ def address_type(address) end hex = decode_base58(address) rescue nil - if hex && hex.bytesize == 50 && address_checksum?(address) + + target_size = (version_bytes + 20 + 4) * 2 # version_bytes + 20 bytes hash + 4 bytes checksum + if hex && hex.bytesize == target_size && address_checksum?(address) # Litecoin updates the P2SH version byte, and this method should recognize both. p2sh_versions = [p2sh_version] if Bitcoin.network[:legacy_p2sh_versions] p2sh_versions += Bitcoin.network[:legacy_p2sh_versions] end - case hex[0...2] + case hex[0...(version_bytes * 2)] when address_version return :hash160 when *p2sh_versions diff --git a/lib/bitcoin/ffi/openssl.rb b/lib/bitcoin/ffi/openssl.rb index 1950d8af..d3abb316 100644 --- a/lib/bitcoin/ffi/openssl.rb +++ b/lib/bitcoin/ffi/openssl.rb @@ -10,16 +10,68 @@ module OpenSSL_EC # rubocop:disable Naming/ClassAndModuleCamelCase if FFI::Platform.windows? ffi_lib 'libeay32', 'ssleay32' else - ffi_lib ['libssl.so.1.0.0', 'ssl'] + ffi_lib [ + 'libssl.so.1.1.0', 'libssl.so.1.1', + 'libssl.so.1.0.0', 'libssl.so.10', + 'ssl' + ] end NID_secp256k1 = 714 # rubocop:disable Naming/ConstantName POINT_CONVERSION_COMPRESSED = 2 POINT_CONVERSION_UNCOMPRESSED = 4 - attach_function :SSL_library_init, [], :int - attach_function :ERR_load_crypto_strings, [], :void - attach_function :SSL_load_error_strings, [], :void + # OpenSSL 1.1.0 version as a numerical version value as defined in: + # https://www.openssl.org/docs/man1.1.0/man3/OpenSSL_version.html + VERSION_1_1_0_NUM = 0x10100000 + + # OpenSSL 1.1.0 engine constants, taken from: + # https://github.com/openssl/openssl/blob/2be8c56a39b0ec2ec5af6ceaf729df154d784a43/include/openssl/crypto.h + OPENSSL_INIT_ENGINE_RDRAND = 0x00000200 + OPENSSL_INIT_ENGINE_DYNAMIC = 0x00000400 + OPENSSL_INIT_ENGINE_CRYPTODEV = 0x00001000 + OPENSSL_INIT_ENGINE_CAPI = 0x00002000 + OPENSSL_INIT_ENGINE_PADLOCK = 0x00004000 + OPENSSL_INIT_ENGINE_ALL_BUILTIN = ( + OPENSSL_INIT_ENGINE_RDRAND | + OPENSSL_INIT_ENGINE_DYNAMIC | + OPENSSL_INIT_ENGINE_CRYPTODEV | + OPENSSL_INIT_ENGINE_CAPI | + OPENSSL_INIT_ENGINE_PADLOCK + ) + + # OpenSSL 1.1.0 load strings constant, taken from: + # https://github.com/openssl/openssl/blob/c162c126be342b8cd97996346598ecf7db56130f/include/openssl/ssl.h + OPENSSL_INIT_LOAD_SSL_STRINGS = 0x00200000 + + # This is the very first function we need to use to determine what version + # of OpenSSL we are interacting with. + begin + attach_function :OpenSSL_version_num, [], :ulong + rescue FFI::NotFoundError + attach_function :SSLeay, [], :long + end + + # Returns the version of SSL present. + # + # @return [Integer] version number as an integer. + def self.version + if self.respond_to?(:OpenSSL_version_num) + OpenSSL_version_num() + else + SSLeay() + end + end + + if version >= VERSION_1_1_0_NUM + # Initialization procedure for the library was changed in OpenSSL 1.1.0 + attach_function :OPENSSL_init_ssl, [:uint64, :pointer], :int + else + attach_function :SSL_library_init, [], :int + attach_function :ERR_load_crypto_strings, [], :void + attach_function :SSL_load_error_strings, [], :void + end + attach_function :RAND_poll, [], :int attach_function :BN_CTX_free, [:pointer], :int @@ -28,7 +80,6 @@ module OpenSSL_EC # rubocop:disable Naming/ClassAndModuleCamelCase attach_function :BN_bin2bn, %i[pointer int pointer], :pointer attach_function :BN_bn2bin, %i[pointer pointer], :int attach_function :BN_cmp, %i[pointer pointer], :int - attach_function :BN_copy, %i[pointer pointer], :pointer attach_function :BN_dup, [:pointer], :pointer attach_function :BN_free, [:pointer], :int attach_function :BN_mod_inverse, %i[pointer pointer pointer pointer], :pointer @@ -51,22 +102,17 @@ module OpenSSL_EC # rubocop:disable Naming/ClassAndModuleCamelCase attach_function :EC_KEY_set_private_key, %i[pointer pointer], :int attach_function :EC_KEY_set_public_key, %i[pointer pointer], :int attach_function :EC_POINT_free, [:pointer], :int - attach_function :EC_POINT_is_at_infinity, %i[pointer pointer], :int attach_function :EC_POINT_mul, %i[pointer pointer pointer pointer pointer pointer], :int attach_function :EC_POINT_new, [:pointer], :pointer attach_function :EC_POINT_set_compressed_coordinates_GFp, %i[pointer pointer pointer int pointer], :int - attach_function :d2i_ECPrivateKey, %i[pointer pointer long], :pointer - attach_function :i2d_ECPrivateKey, %i[pointer pointer], :int attach_function :i2o_ECPublicKey, %i[pointer pointer], :uint - attach_function :EC_KEY_check_key, [:pointer], :uint attach_function :ECDSA_do_sign, %i[pointer uint pointer], :pointer attach_function :BN_num_bits, [:pointer], :int attach_function :ECDSA_SIG_free, [:pointer], :void attach_function :EC_POINT_add, %i[pointer pointer pointer pointer pointer], :int attach_function :EC_POINT_point2hex, %i[pointer pointer int pointer], :string attach_function :EC_POINT_hex2point, %i[pointer string pointer pointer], :pointer - attach_function :ECDSA_SIG_new, [], :pointer attach_function :d2i_ECDSA_SIG, %i[pointer pointer long], :pointer attach_function :i2d_ECDSA_SIG, %i[pointer pointer], :int attach_function :OPENSSL_free, :CRYPTO_free, [:pointer], :void @@ -82,68 +128,17 @@ 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 = EC_KEY_get0_group(eckey) - order = BN_new() - ctx = 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 + group = OpenSSL::PKey::EC::Group.new('secp256k1') + key = OpenSSL::PKey::EC.new(group) + key.private_key = OpenSSL::BN.new(private_key_hex, 16) + key.public_key = group.generator.mul(key.private_key) + priv_hex = key.private_key.to_bn.to_s(16).downcase.rjust(64, '0') 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 = buf.read_string(length).unpack('H*')[0] if i2o_ECPublicKey(eckey, ptr) == length - - EC_KEY_free(eckey) - - [priv_hex, pub_hex] - end - - # extract private key from uncompressed DER format - 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] + [priv_hex, key.public_key.to_bn.to_s(16).downcase] end # Given the components of a signature and a selector value, recover and @@ -395,9 +390,18 @@ def self.repack_der_signature(signature) def self.init_ffi_ssl @ssl_loaded ||= false return if @ssl_loaded - SSL_library_init() - ERR_load_crypto_strings() - SSL_load_error_strings() + + if version >= VERSION_1_1_0_NUM + OPENSSL_init_ssl( + OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ENGINE_ALL_BUILTIN, + nil + ) + else + SSL_library_init() + ERR_load_crypto_strings() + SSL_load_error_strings() + end + RAND_poll() @ssl_loaded = true end diff --git a/lib/bitcoin/version.rb b/lib/bitcoin/version.rb index e280deb3..e0a7421c 100644 --- a/lib/bitcoin/version.rb +++ b/lib/bitcoin/version.rb @@ -1,3 +1,3 @@ module Bitcoin - VERSION = "0.0.18" + VERSION = "0.0.20" end diff --git a/spec/unit/bitcoin/bitcoin_spec.rb b/spec/unit/bitcoin/bitcoin_spec.rb index a162a6ab..34decd93 100644 --- a/spec/unit/bitcoin/bitcoin_spec.rb +++ b/spec/unit/bitcoin/bitcoin_spec.rb @@ -426,6 +426,28 @@ ).to be_nil end end + + context 'zcash' do + before { + Bitcoin::NETWORKS[:zcash] = Bitcoin::NETWORKS[:bitcoin].merge( + project: :zcash, + address_version: '1cb8', + p2sh_version: '1cbd', + ) + Bitcoin.network = :zcash + } + + it 'works for a hash160 address' do + expect(Bitcoin.address_type('t1KBT8oCGAfisNNWnSD3h7TSsZ7qKah935g')) + .to eq(:hash160) + end + + it 'is nil for invalid addresses' do + expect( + Bitcoin.address_type('2MyLngQnhzjzatKsB7XfHYoP9e2XUXSiBMM') + ).to be_nil + end + end end describe '.checksum' do @@ -750,82 +772,63 @@ end end - describe '.der_to_private_key' do - it 'extracts the private key from uncompressed DER format' do - der = - '308201130201010420a29fe0f28b2936dbc89f889f74cd1f0662d18a873ac15d6c' \ - 'd417b808db1ccd0aa081a53081a2020101302c06072a8648ce3d0101022100ffff' \ - 'fffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300604' \ - '010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959' \ - 'f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47' \ - 'd08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af48a03b' \ - 'bfd25e8cd0364141020101a14403420004768cfc6c44b927b0e69e9dd343e96132' \ - 'f7cd1d360d8cb8d65c83d89d7beaceadfd19918e076606a099344156acdb026b10' \ - '65a958e39f098cfd0a34dd976291d6' - - expect( - Bitcoin::OpenSSL_EC.der_to_private_key(der) - ).to eq('a29fe0f28b2936dbc89f889f74cd1f0662d18a873ac15d6cd417b808db1ccd0a') + describe 'signing and verifying messages' do + context 'testnet' do + before { Bitcoin.network = :testnet3 } + + it 'verifies the signature of a testnet address' do + expect( + Bitcoin.verify_message( + 'mwPVMbZQgkpwJJt2YP3sLSgbEBQw3FWZSc', + 'H5GER0Nz+L7TPZMQzXtv0hnLSsyfPok9lkdHIv01vksREpEpOhTPTonU1xvy' \ + 'PAOIIKhU3++Ol+LaWKWmsfyxDXk=', + 'A' * 500 + ) + ).to be true + end end - describe 'signing and verifying messages' do - context 'testnet' do - before { Bitcoin.network = :testnet3 } - - it 'verifies the signature of a testnet address' do - expect( - Bitcoin.verify_message( - 'mwPVMbZQgkpwJJt2YP3sLSgbEBQw3FWZSc', - 'H5GER0Nz+L7TPZMQzXtv0hnLSsyfPok9lkdHIv01vksREpEpOhTPTonU1xvy' \ - 'PAOIIKhU3++Ol+LaWKWmsfyxDXk=', - 'A' * 500 - ) - ).to be true - end + context 'mainnet' do + before { Bitcoin.network = :bitcoin } + let(:address_and_keys1) do + %w[ + 1QFqqMUD55ZV3PJEJZtaKCsQmjLT6JkjvJ + 12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747 + 040b4c866585dd868a9d62348a9cd008d6a312937048fff31670e7e920cfc7a7 \ + 447b5f0bba9e01e6fe4735c8383e6e7a3347a0fd72381b8f797a19f694054e5a69 + ] + end + let(:address_and_keys2) do + %w[ + 1NoJrossxPBKfCHuJXT4HadJrXRE9Fxiqs + 12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747 + 030b4c866585dd868a9d62348a9cd008d6a312937048fff31670e7e920cfc7a744 + ] end - context 'mainnet' do - before { Bitcoin.network = :bitcoin } - let(:address_and_keys1) do - %w[ - 1QFqqMUD55ZV3PJEJZtaKCsQmjLT6JkjvJ - 12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747 - 040b4c866585dd868a9d62348a9cd008d6a312937048fff31670e7e920cfc7a7 \ - 447b5f0bba9e01e6fe4735c8383e6e7a3347a0fd72381b8f797a19f694054e5a69 - ] - end - let(:address_and_keys2) do - %w[ - 1NoJrossxPBKfCHuJXT4HadJrXRE9Fxiqs - 12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747 - 030b4c866585dd868a9d62348a9cd008d6a312937048fff31670e7e920cfc7a744 - ] - end - - it 'successfully signs and verifies the message' do - [address_and_keys1, address_and_keys2].each do |_addr, privkey, _pubkey| - key = Bitcoin.open_key(privkey) - 16.times.each do |count| - signature = Bitcoin.sign_message( - key.private_key_hex, - key.public_key_hex, - format('Very secret message %d: 11', count: count) + it 'successfully signs and verifies the message' do + [address_and_keys1, address_and_keys2].each do |_addr, privkey, _pubkey| + key = Bitcoin.open_key(privkey) + 16.times.each do |count| + signature = Bitcoin.sign_message( + key.private_key_hex, + key.public_key_hex, + format('Very secret message %d: 11', count: count) + ) + expect( + Bitcoin.verify_message( + signature['address'], + 'invalid-signature', + signature['message'] + ) + ).to be false + expect( + Bitcoin.verify_message( + signature['address'], + signature['signature'], + signature['message'] ) - expect( - Bitcoin.verify_message( - signature['address'], - 'invalid-signature', - signature['message'] - ) - ).to be false - expect( - Bitcoin.verify_message( - signature['address'], - signature['signature'], - signature['message'] - ) - ).to be true - end + ).to be true end end end