From f0b5bb9857b9a22cc4d414487e6c0711e4f97a04 Mon Sep 17 00:00:00 2001 From: Stephane Bounmy <159814+sbounmy@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:04:26 +0100 Subject: [PATCH 1/6] ruby 3.2 and devcontainer --- .devcontainer/Dockerfile | 2 ++ .devcontainer/devcontainer.json | 26 ++++++++++++++++++++++++++ .gitignore | 9 ++++++++- Gemfile.lock | 17 +++++++++-------- bitcoin-ruby.gemspec | 4 ++++ spec/unit/bitcoin/bitcoin_spec.rb | 2 +- 6 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..d4c460d6 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,2 @@ +ARG RUBY_VERSION=3.2.2 +FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..1b8f421f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +{ + "name": "Ruby Bitcoin Development", + "dockerFile": "Dockerfile", + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "Shopify.ruby-lsp" + ], + "settings": { + "editor.formatOnSave": true, + "ruby.useBundler": true, + "ruby.useLanguageServer": true, + "ruby.lint": { + "rubocop": true + } + } + } + }, + "forwardPorts": [], + "postCreateCommand": "bundle install", + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 71814124..198291a3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,11 @@ spec-rspec/examples.txt # emacs *~ \#*\# -.\#* \ No newline at end of file +.\#* + +# VS Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index f2557c91..bdb30ee5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,17 +12,17 @@ GEM ast (2.4.0) bacon (1.2.0) byebug (10.0.2) - coderay (1.1.2) + coderay (1.1.3) diff-lcs (1.3) docile (1.3.1) eventmachine (1.2.7) - ffi (1.9.25) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) + ffi (1.17.0) + ffi-compiler (1.3.2) + ffi (>= 1.15.5) rake jaro_winkler (1.5.1) json (2.1.0) - method_source (0.9.0) + method_source (0.9.2) minitest (5.11.3) parallel (1.12.1) parser (2.5.1.2) @@ -35,7 +35,7 @@ GEM byebug (~> 10.0) pry (~> 0.10) rainbow (3.0.0) - rake (12.3.1) + rake (12.3.3) rspec (3.7.0) rspec-core (~> 3.7.0) rspec-expectations (~> 3.7.0) @@ -58,8 +58,9 @@ GEM ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.9.0) - scrypt (3.0.5) + scrypt (3.0.8) ffi-compiler (>= 1.0, < 2.0) + rake (>= 9, < 14) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -82,4 +83,4 @@ DEPENDENCIES simplecov (~> 0.16.1) BUNDLED WITH - 1.17.3 + 2.5.23 diff --git a/bitcoin-ruby.gemspec b/bitcoin-ruby.gemspec index 1301b1d5..34c177fd 100644 --- a/bitcoin-ruby.gemspec +++ b/bitcoin-ruby.gemspec @@ -24,4 +24,8 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'ffi' s.add_runtime_dependency 'scrypt' # required by Litecoin s.add_runtime_dependency 'eventmachine' # required for connection code + + # Add development dependencies + s.add_development_dependency 'rspec', '~> 3.12' + s.add_development_dependency 'pry', '~> 0.13.1' # Downgrade Pry to a more compatible version end diff --git a/spec/unit/bitcoin/bitcoin_spec.rb b/spec/unit/bitcoin/bitcoin_spec.rb index 34decd93..cd7bd8aa 100644 --- a/spec/unit/bitcoin/bitcoin_spec.rb +++ b/spec/unit/bitcoin/bitcoin_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'pry' +# require 'pry' require 'spec_helper' describe Bitcoin do From 2c0bd5aeb00719604df2a533b3de15052d116bdd Mon Sep 17 00:00:00 2001 From: Stephane Bounmy <159814+sbounmy@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:10:45 +0100 Subject: [PATCH 2/6] first bitcoin spec passing with ruby 3.2.2 and openssl 3.0 --- Gemfile.lock | 17 ++++++++++++++++- bitcoin-ruby.gemspec | 2 +- lib/bitcoin.rb | 22 ++++++++++++++++------ lib/bitcoin/ffi/openssl.rb | 22 +++++++++++++++++----- lib/bitcoin/key.rb | 25 ++++++++++++++++++++++--- spec/unit/bitcoin/bitcoin_spec.rb | 15 ++++++++------- 6 files changed, 80 insertions(+), 23 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bdb30ee5..067124cf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,9 @@ GEM bacon (1.2.0) byebug (10.0.2) coderay (1.1.3) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) diff-lcs (1.3) docile (1.3.1) eventmachine (1.2.7) @@ -20,8 +23,12 @@ GEM ffi-compiler (1.3.2) ffi (>= 1.15.5) rake + io-console (0.7.2) + irb (1.14.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) jaro_winkler (1.5.1) - json (2.1.0) + json (2.8.2) method_source (0.9.2) minitest (5.11.3) parallel (1.12.1) @@ -34,8 +41,14 @@ GEM pry-byebug (3.6.0) byebug (~> 10.0) pry (~> 0.10) + psych (5.2.0) + stringio rainbow (3.0.0) rake (12.3.3) + rdoc (6.8.1) + psych (>= 4.0.0) + reline (0.5.11) + io-console (~> 0.5) rspec (3.7.0) rspec-core (~> 3.7.0) rspec-expectations (~> 3.7.0) @@ -66,6 +79,7 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) + stringio (3.1.2) unicode-display_width (1.4.0) PLATFORMS @@ -74,6 +88,7 @@ PLATFORMS DEPENDENCIES bacon (~> 1.2.0) bitcoin-ruby! + debug minitest (~> 5.11.3) pry (~> 0.11.3) pry-byebug (~> 3.6.0) diff --git a/bitcoin-ruby.gemspec b/bitcoin-ruby.gemspec index 34c177fd..18903226 100644 --- a/bitcoin-ruby.gemspec +++ b/bitcoin-ruby.gemspec @@ -27,5 +27,5 @@ Gem::Specification.new do |s| # Add development dependencies s.add_development_dependency 'rspec', '~> 3.12' - s.add_development_dependency 'pry', '~> 0.13.1' # Downgrade Pry to a more compatible version + s.add_development_dependency 'debug' end diff --git a/lib/bitcoin.rb b/lib/bitcoin.rb index 8848a130..a83bc75f 100644 --- a/lib/bitcoin.rb +++ b/lib/bitcoin.rb @@ -286,11 +286,11 @@ def decode_target(target_bits) end def bitcoin_elliptic_curve - ::OpenSSL::PKey::EC.new("secp256k1") + ::OpenSSL::PKey::EC.generate("secp256k1") end def generate_key - key = bitcoin_elliptic_curve.generate_key + key = bitcoin_elliptic_curve inspect_key( key ) end @@ -407,11 +407,21 @@ def verify_signature(hash, signature, public_key) end def open_key(private_key, public_key=nil) - key = bitcoin_elliptic_curve - key.private_key = ::OpenSSL::BN.from_hex(private_key) + group = OpenSSL::PKey::EC::Group.new('secp256k1') + private_key_bn = OpenSSL::BN.new(private_key, 16) public_key = regenerate_public_key(private_key) unless public_key - key.public_key = ::OpenSSL::PKey::EC::Point.from_hex(key.group, public_key) - key + public_key_bn = OpenSSL::BN.new(public_key, 16) + public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn) + + # Create ASN1 structure for EC private key + asn1 = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer.new(1), + OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)), + OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT), + OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT) + ]) + + OpenSSL::PKey::EC.new(asn1.to_der) end def regenerate_public_key(private_key) diff --git a/lib/bitcoin/ffi/openssl.rb b/lib/bitcoin/ffi/openssl.rb index d3abb316..7f4ff947 100644 --- a/lib/bitcoin/ffi/openssl.rb +++ b/lib/bitcoin/ffi/openssl.rb @@ -129,11 +129,23 @@ def self.regenerate_key(private_key) private_key_hex = private_key.unpack('H*')[0] 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') + private_key_bn = OpenSSL::BN.new(private_key_hex, 16) + + # Generate public key point by multiplying generator with private key + public_key_point = group.generator.mul(private_key_bn) + + # Create ASN1 structure for EC key + asn1 = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer.new(1), + OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)), + OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT), + OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT) + ]) + + key = OpenSSL::PKey::EC.new(asn1.to_der) + + # Verify the private key was generated correctly + priv_hex = key.private_key.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 diff --git a/lib/bitcoin/key.rb b/lib/bitcoin/key.rb index f8c6a855..647eb18d 100644 --- a/lib/bitcoin/key.rb +++ b/lib/bitcoin/key.rb @@ -48,7 +48,7 @@ def initialize(privkey = nil, pubkey = nil, opts={compressed: true}) # Generate new priv/pub key. def generate - @key.generate_key + @key end # Get the private key (in hex). @@ -261,13 +261,32 @@ def regenerate_pubkey def set_priv(priv) value = priv.to_i(16) raise 'private key is not on curve' unless MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER - @key.private_key = OpenSSL::BN.from_hex(priv) + + # OpenSSL 3 compatibility + bn = OpenSSL::BN.from_hex(priv) + if @key.respond_to?(:set_private_key) + @key.set_private_key(bn) + else + @key.private_key = bn + end end # Set +pub+ as the new public key (converting from hex). def set_pub(pub, compressed = nil) @pubkey_compressed = compressed == nil ? self.class.is_compressed_pubkey?(pub) : compressed - @key.public_key = OpenSSL::PKey::EC::Point.from_hex(@key.group, pub) + + # OpenSSL 3 compatibility + group = @key.group + asn1 = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::ObjectId("id-ecPublicKey"), + OpenSSL::ASN1::ObjectId(group.curve_name) + ]), + OpenSSL::ASN1::BitString(OpenSSL::PKey::EC::Point.from_hex(group, pub).to_octet_string(:uncompressed)) + ]) + + new_key = OpenSSL::PKey::EC.new(asn1.to_der) + @key.public_key = new_key.public_key end def self.is_compressed_pubkey?(pub) diff --git a/spec/unit/bitcoin/bitcoin_spec.rb b/spec/unit/bitcoin/bitcoin_spec.rb index cd7bd8aa..45c8315c 100644 --- a/spec/unit/bitcoin/bitcoin_spec.rb +++ b/spec/unit/bitcoin/bitcoin_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# require 'pry' +require 'debug' require 'spec_helper' describe Bitcoin do @@ -973,14 +973,16 @@ describe 'bitcoin base58 test vectors' do # Port of Bitcoin Core test vectors. - # https://github.com/bitcoin/bitcoin/blob/595a7bab23bc21049526229054ea1fff1a29c0bf/src/test/base58_tests.cpp#L139 + # https://github.com/bitcoin/bitcoin/blob/master/src/test/base58_tests.cpp let(:valid_base58_keys) do - JSON.parse(fixtures_file('base58_keys_valid.json')) + path = File.join(File.dirname(__FILE__), '../../fixtures/base58_keys_valid.json') + JSON.parse(File.read(path)) end # Port of Bitcoin Core test vectors. - # https://github.com/bitcoin/bitcoin/blob/595a7bab23bc21049526229054ea1fff1a29c0bf/src/test/base58_tests.cpp#L179 + # https://github.com/bitcoin/bitcoin/blob/master/src/test/base58_tests.cpp let(:invalid_base58_keys) do - JSON.parse(fixtures_file('base58_keys_invalid.json')) + path = File.join(File.dirname(__FILE__), '../../fixtures/base58_keys_invalid.json') + JSON.parse(File.read(path)) end it 'passes the valid keys cases' do @@ -1014,8 +1016,7 @@ end it 'fails the invalid keys cases' do - test_cases = JSON.parse(fixtures_file('base58_keys_invalid.json')) - test_cases.each do |test_case| + invalid_base58_keys.each do |test_case| address = test_case[0] %i[bitcoin testnet3 regtest].each do |network_name| From 6141741026559f03041222b84b18035356e07408 Mon Sep 17 00:00:00 2001 From: Stephane Bounmy <159814+sbounmy@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:13:47 +0100 Subject: [PATCH 3/6] removed trailling space --- lib/bitcoin/key.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/bitcoin/key.rb b/lib/bitcoin/key.rb index 647eb18d..6f1a4d18 100644 --- a/lib/bitcoin/key.rb +++ b/lib/bitcoin/key.rb @@ -155,7 +155,7 @@ def self.recover_compact_signature_to_key(data, signature_base64) version = signature.unpack('C')[0] return nil if version < 27 or version > 34 - + compressed = (version >= 31) ? (version -= 4; true) : false hash = Bitcoin.bitcoin_signed_message_hash(data) @@ -261,7 +261,7 @@ def regenerate_pubkey def set_priv(priv) value = priv.to_i(16) raise 'private key is not on curve' unless MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER - + # OpenSSL 3 compatibility bn = OpenSSL::BN.from_hex(priv) if @key.respond_to?(:set_private_key) @@ -274,7 +274,7 @@ def set_priv(priv) # Set +pub+ as the new public key (converting from hex). def set_pub(pub, compressed = nil) @pubkey_compressed = compressed == nil ? self.class.is_compressed_pubkey?(pub) : compressed - + # OpenSSL 3 compatibility group = @key.group asn1 = OpenSSL::ASN1::Sequence([ @@ -284,7 +284,7 @@ def set_pub(pub, compressed = nil) ]), OpenSSL::ASN1::BitString(OpenSSL::PKey::EC::Point.from_hex(group, pub).to_octet_string(:uncompressed)) ]) - + new_key = OpenSSL::PKey::EC.new(asn1.to_der) @key.public_key = new_key.public_key end From a0c9a4b217f1c37b73187a3b136bd13d3b422332 Mon Sep 17 00:00:00 2001 From: Stephane Bounmy <159814+sbounmy@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:47:25 +0100 Subject: [PATCH 4/6] move changes to a pkey class rather tahn dirty patching the existing code --- lib/bitcoin.rb | 30 ++++++-------- lib/bitcoin/key.rb | 23 +---------- lib/bitcoin/pkey_ec.rb | 69 +++++++++++++++++++++++++++++++ spec/unit/bitcoin/pkey_ec_spec.rb | 46 +++++++++++++++++++++ 4 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 lib/bitcoin/pkey_ec.rb create mode 100644 spec/unit/bitcoin/pkey_ec_spec.rb diff --git a/lib/bitcoin.rb b/lib/bitcoin.rb index a83bc75f..072eea61 100644 --- a/lib/bitcoin.rb +++ b/lib/bitcoin.rb @@ -12,7 +12,7 @@ module Bitcoin # deprecated in favor of a unification of Fixnum and BigInteger named Integer. # Since this project strivers for backwards-compatability, we determine the # appropriate class to use at initialization. - # + # # This avoids annoying deprecation warnings on newer versions for ourselves # and library consumers. Integer = @@ -29,6 +29,7 @@ module Bitcoin autoload :Script, 'bitcoin/script' autoload :VERSION, 'bitcoin/version' autoload :Key, 'bitcoin/key' + autoload :PKeyEC, 'bitcoin/pkey_ec' autoload :ExtKey, 'bitcoin/ext_key' autoload :ExtPubkey, 'bitcoin/ext_key' autoload :Builder, 'bitcoin/builder' @@ -39,6 +40,7 @@ module Bitcoin autoload :ContractHash, 'bitcoin/contracthash' + module Trezor autoload :Mnemonic, 'bitcoin/trezor/mnemonic' end @@ -286,11 +288,11 @@ def decode_target(target_bits) end def bitcoin_elliptic_curve - ::OpenSSL::PKey::EC.generate("secp256k1") + Bitcoin::PKeyEC.new end def generate_key - key = bitcoin_elliptic_curve + key = Bitcoin::PKeyEC.generate_key inspect_key( key ) end @@ -407,21 +409,13 @@ def verify_signature(hash, signature, public_key) end def open_key(private_key, public_key=nil) - group = OpenSSL::PKey::EC::Group.new('secp256k1') - private_key_bn = OpenSSL::BN.new(private_key, 16) - public_key = regenerate_public_key(private_key) unless public_key - public_key_bn = OpenSSL::BN.new(public_key, 16) - public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn) - - # Create ASN1 structure for EC private key - asn1 = OpenSSL::ASN1::Sequence([ - OpenSSL::ASN1::Integer.new(1), - OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)), - OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT), - OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT) - ]) - - OpenSSL::PKey::EC.new(asn1.to_der) + key = bitcoin_elliptic_curve + key.private_key = ::OpenSSL::BN.from_hex(private_key) + key + end + + def group + OpenSSL::PKey::EC::Group.new('secp256k1') end def regenerate_public_key(private_key) diff --git a/lib/bitcoin/key.rb b/lib/bitcoin/key.rb index 6f1a4d18..adf2bf4b 100644 --- a/lib/bitcoin/key.rb +++ b/lib/bitcoin/key.rb @@ -48,7 +48,7 @@ def initialize(privkey = nil, pubkey = nil, opts={compressed: true}) # Generate new priv/pub key. def generate - @key + @key = Bitcoin::PKeyEC.generate_key end # Get the private key (in hex). @@ -261,32 +261,13 @@ def regenerate_pubkey def set_priv(priv) value = priv.to_i(16) raise 'private key is not on curve' unless MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER - - # OpenSSL 3 compatibility - bn = OpenSSL::BN.from_hex(priv) - if @key.respond_to?(:set_private_key) - @key.set_private_key(bn) - else @key.private_key = bn - end end # Set +pub+ as the new public key (converting from hex). def set_pub(pub, compressed = nil) @pubkey_compressed = compressed == nil ? self.class.is_compressed_pubkey?(pub) : compressed - - # OpenSSL 3 compatibility - group = @key.group - asn1 = OpenSSL::ASN1::Sequence([ - OpenSSL::ASN1::Sequence([ - OpenSSL::ASN1::ObjectId("id-ecPublicKey"), - OpenSSL::ASN1::ObjectId(group.curve_name) - ]), - OpenSSL::ASN1::BitString(OpenSSL::PKey::EC::Point.from_hex(group, pub).to_octet_string(:uncompressed)) - ]) - - new_key = OpenSSL::PKey::EC.new(asn1.to_der) - @key.public_key = new_key.public_key + @key.public_key = OpenSSL::PKey::EC::Point.from_hex(@key.group, pub) end def self.is_compressed_pubkey?(pub) diff --git a/lib/bitcoin/pkey_ec.rb b/lib/bitcoin/pkey_ec.rb new file mode 100644 index 00000000..fe9fe2b3 --- /dev/null +++ b/lib/bitcoin/pkey_ec.rb @@ -0,0 +1,69 @@ +module Bitcoin + class PKeyEC + + CURVE = 'secp256k1' + + attr_reader :private_key, :group + + attr_accessor :public_key, :private_key + + def private_key_hex; private_key.to_hex.rjust(64, '0'); end + def public_key_hex; public_key.to_hex.rjust(130, '0'); end + + def initialize + @group = OpenSSL::PKey::EC::Group.new(CURVE) + end + + def self.generate_key + OpenSSL::PKey::EC.generate(CURVE) + end + + def private_key + @public_key = restore_public_key(@private_key) + + private_key_bn = OpenSSL::BN.new(@private_key, 16) + public_key_bn = OpenSSL::BN.new(@public_key, 16) + public_key_point = OpenSSL::PKey::EC::Point.new(@group, public_key_bn) + + asn1 = OpenSSL::ASN1::Sequence( + [ + OpenSSL::ASN1::Integer.new(1), + OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)), + OpenSSL::ASN1::ObjectId(CURVE, 0, :EXPLICIT), + OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT) + ] + ) + + OpenSSL::PKey::EC.new(asn1.to_der).private_key + end + + def public_key + return nil unless @private_key + @public_key = restore_public_key(@private_key) + public_key_bn = OpenSSL::BN.new(@public_key, 16) + public_key_point = OpenSSL::PKey::EC::Point.new(@group, public_key_bn) + + asn1 = OpenSSL::ASN1::Sequence.new( + [ + OpenSSL::ASN1::Sequence.new([ + OpenSSL::ASN1::ObjectId.new('id-ecPublicKey'), + OpenSSL::ASN1::ObjectId.new(@group.curve_name) + ]), + OpenSSL::ASN1::BitString.new(public_key_point.to_octet_string(:uncompressed)) + ] + ) + OpenSSL::PKey::EC.new(asn1.to_der).public_key + end + + private + + def restore_public_key(private_key) + private_bn = OpenSSL::BN.new private_key, 16 + group = OpenSSL::PKey::EC::Group.new CURVE + public_bn = group.generator.mul(private_bn).to_bn + public_bn = OpenSSL::PKey::EC::Point.new(group, public_bn).to_bn + + public_bn.to_s(16).downcase + end + end +end \ No newline at end of file diff --git a/spec/unit/bitcoin/pkey_ec_spec.rb b/spec/unit/bitcoin/pkey_ec_spec.rb new file mode 100644 index 00000000..e6cd5ed8 --- /dev/null +++ b/spec/unit/bitcoin/pkey_ec_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +module Bitcoin + describe PKeyEC do + let(:pkey) { PKeyEC } + describe '.generate_key' do + let(:generated_key) { Bitcoin::PKeyEC.generate_key } + + it 'generates a valid EC key' do + expect(generated_key).to be_a(OpenSSL::PKey::EC) + end + + it 'generates a key with private key component' do + expect(generated_key.private_key?).to be true + end + + it 'generates a key with public key component' do + expect(generated_key.public_key?).to be true + end + + it 'generates a key on the secp256k1 curve' do + expect(generated_key.group.curve_name).to eq('secp256k1') + end + + it 'generates unique keys on subsequent calls' do + key1 = pkey.generate_key + key2 = pkey.generate_key + expect(key1.private_key.to_s(16)).not_to eq(key2.private_key.to_s(16)) + end + end + + describe '#private_key' do + let(:key) { Bitcoin::PKeyEC.new } + + it 'sets the private key' do + key.private_key = '0123456789abcdef' + expect(key.private_key.to_s(16)).to eq('0123456789ABCDEF') + end + + it 'returns the public key' do + key.private_key = '0123456789abcdef' + expect(key.public_key_hex).to eq('041a1fd15fce078234aa292fc024178056bf006433c9b4bd208f59eb4c9efec95ba18af1fe46980989d3ff75bf9601121151ef46e2cfab8999408319ce8f3be725') + end + end + end +end \ No newline at end of file From 70d064874aaf117947f15839fc433786dc9bd5bc Mon Sep 17 00:00:00 2001 From: Stephane Bounmy <159814+sbounmy@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:50:19 +0100 Subject: [PATCH 5/6] missing fix set_priv --- lib/bitcoin/key.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitcoin/key.rb b/lib/bitcoin/key.rb index adf2bf4b..1d76ceba 100644 --- a/lib/bitcoin/key.rb +++ b/lib/bitcoin/key.rb @@ -261,7 +261,7 @@ def regenerate_pubkey def set_priv(priv) value = priv.to_i(16) raise 'private key is not on curve' unless MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER - @key.private_key = bn + @key.private_key = OpenSSL::BN.from_hex(priv) end # Set +pub+ as the new public key (converting from hex). From 63b690a7c647b5ae3a0d430a8bf606edd2ef5737 Mon Sep 17 00:00:00 2001 From: Stephane Bounmy <159814+sbounmy@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:03:38 +0100 Subject: [PATCH 6/6] Passing key_spec.rb; private_key= and public_key= should be OpenSSL::BN rather than hex --- lib/bitcoin.rb | 1 + lib/bitcoin/key.rb | 4 +-- lib/bitcoin/pkey_ec.rb | 46 +++++++++++++++---------------- spec/spec_helper.rb | 3 +- spec/unit/bitcoin/pkey_ec_spec.rb | 4 +-- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/bitcoin.rb b/lib/bitcoin.rb index 072eea61..3e579db4 100644 --- a/lib/bitcoin.rb +++ b/lib/bitcoin.rb @@ -443,6 +443,7 @@ def verify_message(address, signature, message) return false unless valid_address?(address) return false unless signature return false unless signature.bytesize == 65 + hash = bitcoin_signed_message_hash(message) pubkey = OpenSSL_EC.recover_compact(hash, signature) pubkey_to_address(pubkey) == address if pubkey diff --git a/lib/bitcoin/key.rb b/lib/bitcoin/key.rb index 1d76ceba..79c34d92 100644 --- a/lib/bitcoin/key.rb +++ b/lib/bitcoin/key.rb @@ -183,7 +183,7 @@ def to_bip38(passphrase) addresshash = Digest::SHA256.digest( Digest::SHA256.digest( self.addr ) )[0...4] require 'scrypt' unless defined?(::SCrypt::Engine) - buf = SCrypt::Engine.__sc_crypt(passphrase, addresshash, 16384, 8, 8, 64) + buf = SCrypt::Engine.scrypt(passphrase, addresshash, 16384, 8, 8, 64) derivedhalf1, derivedhalf2 = buf[0...32], buf[32..-1] aes = proc{|k,a,b| @@ -212,7 +212,7 @@ def self.from_bip38(encrypted_privkey, passphrase) raise "Invalid checksum" unless Digest::SHA256.digest(Digest::SHA256.digest(version + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2))[0...4] == checksum require 'scrypt' unless defined?(::SCrypt::Engine) - buf = SCrypt::Engine.__sc_crypt(passphrase, addresshash, 16384, 8, 8, 64) + buf = SCrypt::Engine.scrypt(passphrase, addresshash, 16384, 8, 8, 64) derivedhalf1, derivedhalf2 = buf[0...32], buf[32..-1] aes = proc{|k,a| diff --git a/lib/bitcoin/pkey_ec.rb b/lib/bitcoin/pkey_ec.rb index fe9fe2b3..0af85aac 100644 --- a/lib/bitcoin/pkey_ec.rb +++ b/lib/bitcoin/pkey_ec.rb @@ -1,11 +1,10 @@ module Bitcoin - class PKeyEC + class PKeyEC #< OpenSSL::PKey::EC CURVE = 'secp256k1' - attr_reader :private_key, :group + attr_reader :group, :private_key, :public_key - attr_accessor :public_key, :private_key def private_key_hex; private_key.to_hex.rjust(64, '0'); end def public_key_hex; public_key.to_hex.rjust(130, '0'); end @@ -18,52 +17,51 @@ def self.generate_key OpenSSL::PKey::EC.generate(CURVE) end - def private_key - @public_key = restore_public_key(@private_key) - - private_key_bn = OpenSSL::BN.new(@private_key, 16) - public_key_bn = OpenSSL::BN.new(@public_key, 16) - public_key_point = OpenSSL::PKey::EC::Point.new(@group, public_key_bn) + def public_key=(public_key_bn) + @public_key = public_key_bn + end + def private_key=(private_key_bn) + @public_key = restore_public_key(private_key_bn) asn1 = OpenSSL::ASN1::Sequence( [ OpenSSL::ASN1::Integer.new(1), OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)), OpenSSL::ASN1::ObjectId(CURVE, 0, :EXPLICIT), - OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT) + OpenSSL::ASN1::BitString(@public_key.to_octet_string(:uncompressed), 1, :EXPLICIT) ] ) + @pk = OpenSSL::PKey::EC.new(asn1.to_der) + @private_key = @pk.private_key + end - OpenSSL::PKey::EC.new(asn1.to_der).private_key + def dsa_sign_asn1(data) + @pk.dsa_sign_asn1(data) end - def public_key - return nil unless @private_key - @public_key = restore_public_key(@private_key) - public_key_bn = OpenSSL::BN.new(@public_key, 16) - public_key_point = OpenSSL::PKey::EC::Point.new(@group, public_key_bn) + def dsa_verify_asn1(data, signature) + initialize_from_public_key unless @pk + @pk.dsa_verify_asn1(data, signature) + end + def initialize_from_public_key asn1 = OpenSSL::ASN1::Sequence.new( [ OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::ObjectId.new('id-ecPublicKey'), OpenSSL::ASN1::ObjectId.new(@group.curve_name) ]), - OpenSSL::ASN1::BitString.new(public_key_point.to_octet_string(:uncompressed)) + OpenSSL::ASN1::BitString.new(@public_key.to_octet_string(:uncompressed)) ] ) - OpenSSL::PKey::EC.new(asn1.to_der).public_key + @pk = OpenSSL::PKey::EC.new(asn1.to_der) end private - def restore_public_key(private_key) - private_bn = OpenSSL::BN.new private_key, 16 - group = OpenSSL::PKey::EC::Group.new CURVE + def restore_public_key(private_bn) public_bn = group.generator.mul(private_bn).to_bn - public_bn = OpenSSL::PKey::EC::Point.new(group, public_bn).to_bn - - public_bn.to_s(16).downcase + public_bn = OpenSSL::PKey::EC::Point.new(@group, public_bn) end end end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4181bbae..9a07266e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,6 +7,7 @@ # Code coverage generation require 'simplecov' +require 'debug' SimpleCov.start do add_group('Bitcoin') do |file| ['bitcoin.rb', 'opcodes.rb', 'script.rb', 'key.rb'].include?( @@ -106,4 +107,4 @@ config.before(:each) do Bitcoin.network = :bitcoin end -end +end \ No newline at end of file diff --git a/spec/unit/bitcoin/pkey_ec_spec.rb b/spec/unit/bitcoin/pkey_ec_spec.rb index e6cd5ed8..0a9be1d3 100644 --- a/spec/unit/bitcoin/pkey_ec_spec.rb +++ b/spec/unit/bitcoin/pkey_ec_spec.rb @@ -33,12 +33,12 @@ module Bitcoin let(:key) { Bitcoin::PKeyEC.new } it 'sets the private key' do - key.private_key = '0123456789abcdef' + key.private_key = OpenSSL::BN.new('0123456789abcdef', 16) expect(key.private_key.to_s(16)).to eq('0123456789ABCDEF') end it 'returns the public key' do - key.private_key = '0123456789abcdef' + key.private_key = OpenSSL::BN.new('0123456789abcdef', 16) expect(key.public_key_hex).to eq('041a1fd15fce078234aa292fc024178056bf006433c9b4bd208f59eb4c9efec95ba18af1fe46980989d3ff75bf9601121151ef46e2cfab8999408319ce8f3be725') end end