Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to load EllipticalCurve EC class from JRuby's OpenSSL library #1261

Closed
michaelgpearce opened this issue Nov 24, 2013 · 13 comments
Closed

Comments

@michaelgpearce
Copy link

I hope this is the right place to report this bug (i think it is!).

The class OpenSSL::PKey::EC does not load on JRuby version 1.7.8. The library loads with the same commands in MRI version 1.9.3p125 and 2.0.0p195.

~/myprojects/test $ jruby --version
jruby 1.7.8 (1.9.3p392) 2013-11-14 0ce429e on Java HotSpot(TM) 64-Bit Server VM 1.6.0_65-b14-462-11M4609 [darwin-x86_64]
~/myprojects/test $ bundle show jruby-openssl
/Users/test/.rvm/gems/jruby-1.7.8@global/gems/jruby-openssl-0.9.4
~/myprojects/test $ bundle exec irb
irb(main):001:0> require 'openssl'
=> true
irb(main):002:0> ::OpenSSL::PKey::EC.new("secp256k1")
NameError: uninitialized constant OpenSSL::PKey::EC
    from org/jruby/RubyModule.java:2686:in `const_missing'
    from (irb):2:in `evaluate'
    from org/jruby/RubyKernel.java:1123:in `eval'
    from org/jruby/RubyKernel.java:1519:in `loop'
    from org/jruby/RubyKernel.java:1284:in `catch'
    from org/jruby/RubyKernel.java:1284:in `catch'
    from /Users/test/.rvm/rubies/jruby-1.7.8/bin/irb:13:in `(root)'

In MRI:

1.9.3p125 :001 > require 'openssl'
 => true 
1.9.3p125 :002 > ::OpenSSL::PKey::EC.new("secp256k1")
 => #<OpenSSL::PKey::EC:0x007f91e31d9670> 
@michaelgpearce
Copy link
Author

Below is a partial implementation (mostly taken from Bitcoinj). It may be useful to someone or may be a starting point for an actual JRuby implementation. The script below signs and verifies a message. It works on both MRI 1.9.2 and JRuby 1.7.8:

# JRUBY_OPTS='-J-classpath ./bcprov-jdk15on-150.jar' jruby ec_bouncy_castle.rb

require 'openssl'

if RUBY_PLATFORM == 'java'
  import 'java.math.BigInteger'
  import 'java.security.SecureRandom'
  import 'java.io.ByteArrayOutputStream'
  import 'org.bouncycastle.asn1.sec.SECNamedCurves'
  import 'org.bouncycastle.asn1.x9.X9ECParameters'
  import 'org.bouncycastle.asn1.ASN1InputStream'
  import 'org.bouncycastle.asn1.DERInteger'
  import 'org.bouncycastle.asn1.DERSequenceGenerator'
  import 'org.bouncycastle.asn1.DLSequence'
  import 'org.bouncycastle.crypto.signers.ECDSASigner'
  import 'org.bouncycastle.crypto.params.ECDomainParameters'
  import 'org.bouncycastle.crypto.params.ECPrivateKeyParameters'
  import 'org.bouncycastle.crypto.params.ECPublicKeyParameters'
  import 'org.bouncycastle.math.ec.ECPoint'

  module ::OpenSSL::PKey
    class EC
      attr_accessor :private_key, :public_key
      attr_reader :group

      def initialize(curve_name)
        params = SECNamedCurves.getByName(curve_name)
        @curve = ECDomainParameters.new(params.getCurve(), params.getG(), params.getN(), params.getH())
        @group = ::OpenSSL::PKey::EC::Group.new
      rescue
        raise OpenSSL::PKey::ECError.new($!.message)
      end

      def generate_key
        @private_key = random_bn

        @group = ::OpenSSL::PKey::EC::Group.new

        point = @curve.getG().multiply(BigInteger.new(get_java_bytes(@private_key)))
        point = compressPoint(point) if @group.point_conversion_form == :compressed
        public_key_java_bytes = point.getEncoded()
        public_key_bn = get_bn_from_java_bytes(public_key_java_bytes)
        @public_key = ::OpenSSL::PKey::EC::Point.new(self.group, public_key_bn)

        self
      rescue
        raise OpenSSL::PKey::ECError.new($!.message)
      end

      def dsa_sign_asn1(data)
        private_key_big_integer = BigInteger.new(get_java_bytes(@private_key))

        signer = ECDSASigner.new
        signer.init(true, ECPrivateKeyParameters.new(private_key_big_integer, @curve))
        components = signer.generateSignature(data.bytes.to_a)
        signature = get_der_java_bytes(components[0], components[1])
        signatureBytes = signature.collect(&:to_i).pack('c*')
      rescue
        raise OpenSSL::PKey::ECError.new($!.message)
      end

      def dsa_verify_asn1(data, signature)
        d = data.unpack('c*').to_java(:byte)
        r, s = get_r_and_s_from_bytes(signature.unpack('c*').to_java(:byte))
        pub = get_java_bytes(public_key.to_bn)

        signer = ECDSASigner.new
        params = ECPublicKeyParameters.new(@curve.getCurve().decodePoint(pub), @curve)
        signer.init(false, params)
        signer.verifySignature(d, r, s)
      rescue
        raise OpenSSL::PKey::ECError.new($!.message)
      end

      private

      def get_r_and_s_from_bytes(der_encoded_java_bytes)
        decoder = ASN1InputStream.new(der_encoded_java_bytes)
        seq = decoder.readObject()
        [seq.getObjectAt(0).getPositiveValue(), seq.getObjectAt(1).getPositiveValue()]
      ensure
        decoder.close
      end

      def get_der_java_bytes(r, s)
        bos = ByteArrayOutputStream.new(72)
        seq = DERSequenceGenerator.new(bos)
        seq.addObject(DERInteger.new(r))
        seq.addObject(DERInteger.new(s))
        seq.close()
        bos.toByteArray()
      end

      def get_java_bytes(number)
        hex_string = number.to_s(16)
        bytes = []
        if hex_string.length % 2 == 1
          bytes << ['0' + hex_string[0]].pack('H*').unpack('c*')[0]
          hex_string = hex_string[1..-1]
        end
        bytes += [hex_string].pack('H*').unpack('c*')

        bytes.to_java(:byte)
      end

      def get_bn_from_java_bytes(java_bytes)
        ruby_hex_s = java_bytes.collect(&:to_i).pack('c*').unpack('H*').first
        ::OpenSSL::BN.new(ruby_hex_s, 16)
      end

      def random_bn
        OpenSSL::BN.new(OpenSSL::Random.random_bytes(32).unpack('H*').first, 16)
      end
    end

    class ECError < OpenSSL::PKey::PKeyError
    end

    class EC::Point
      attr_accessor :group

      def initialize(group, bn)
        self.group = group
        @bn = bn
      end

      def to_bn
        @bn
      end
    end

    class EC::Group
      attr_accessor :point_conversion_form

      def initialize
        self.point_conversion_form = :uncompressed
      end

      def point_conversion_form=(form)
        raise ArgumentError.new("only :compressed and :uncompressed form supported: #{form}") unless [:uncompressed, :compressed].include?(form)

        @point_conversion_form = form
      end
    end
  end
end

key = ::OpenSSL::PKey::EC.new("secp256k1")
key.generate_key
private_key_int = key.private_key.to_s.to_i
private_key_hex_string = key.private_key.to_s(16)

puts "Private key: #{key.private_key.to_s(16)}"
puts "Public key: #{key.public_key.to_bn.to_s(16)}"
message = "this is a random message: #{rand}"
signature = key.dsa_sign_asn1(message)
puts "The signature for '#{message}' is: #{signature.unpack('H*')[0]}"
verified = key.dsa_verify_asn1(message, signature)
puts "Verified? #{verified}"

@headius
Copy link
Member

headius commented Dec 19, 2013

Very nice! I think we can incorporate your port of this code, since the bitcoinj code is licensed under Apache-2.0, a license compatible with EPL. We should add some note attributing the original logic to bitcoinj.

Does this do everything you would need to resolve this issue?

Can you turn this into a pull request?

@michaelgpearce
Copy link
Author

It's a near full implementation as far as i can tell. Is there a document on testing practices?

@michaelgpearce
Copy link
Author

Just saw this: https://github.com/michaelgpearce/jruby/blob/master/docs/README.test.md. I assume adding specs to RubySpec.

@bdusauso
Copy link

bdusauso commented Feb 9, 2014

What's the status of this issue ? I've just stumbled on this problem with JRuby 1.7.10.
If I can help I'd be glad to, or at least I can try to :)

@michaelgpearce
Copy link
Author

I've not done anything about adding this to JRuby as i switched away from the Ruby library that had this as a dependency and am instead using a Java library.

@headius headius modified the milestones: JRuby 1.7.12, JRuby 1.7.10 Feb 21, 2014
@mhanne
Copy link

mhanne commented Mar 22, 2014

I tried using this in bitcoin-ruby by just dropping in your code. It already goes a long way, but there still seem to be a few issues left. For example generating a key:
50.times { p OpenSSL::PKey::EC.new("secp256k1").generate_key }
often fails with:
OpenSSL::PKey::ECError: The multiplicator cannot be negative

@rtyler
Copy link

rtyler commented Aug 7, 2015

FWIW this issue is still present in JRuby 9k, I'm going to add the beginner label to this issue and maybe somebody can use @michaelgpearce's code as a launching point to implementing this behavior

@p0indexter
Copy link

Still seeing this issue in jRuby 9.0.5.0

@headius
Copy link
Member

headius commented Mar 14, 2016

@kares @mkristian: We've had a request to finally address this in the JRuby 9.1 timeframe. What do you think?

https://twitter.com/bdusauso/status/709448399809069056

@headius headius added this to the JRuby 9.1.0.0 milestone Mar 14, 2016
@kares
Copy link
Member

kares commented Mar 15, 2016

tried the above script before and I did not put this in since it was quite uncomplete. not sure how much works but it failed my attempts.

@headius
Copy link
Member

headius commented Apr 20, 2016

Moving this to jruby/jruby-openssl#90

@headius headius closed this as completed Apr 20, 2016
@mohamedhafez
Copy link
Contributor

just posted a $100 bounty with bountysource.com on the new issue at jruby/jruby-openssl#90, if anyone is interested in working on it, or contributing to the bounty:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants