Customizing JRuby Cipher Suites

A flurry of security vulnerabilities in the last couple of years have accelerated the deprecation of many cryptographic protocols and cipher suites. As a result, you might have run into this error if you use JRuby:

Java::JavaLang::RuntimeException: Could not generate DH keypair
    from sun.security.ssl.Handshaker.checkThrown(Handshaker.java:1362)
    from sun.security.ssl.SSLEngineImpl.checkTaskThrown(SSLEngineImpl.java:529)
    ...

The error occurs when you are trying to invoke a secure service, and the SSL/TLS handshake cannot be completed.

There are many potential causes for this error, including:

  • An outdated JDK or JRuby
  • A missing cipher suite
  • A key size larger than what your JVM supports

If you’re hitting this error, the first step is to try updating your JDK. Java 7 only supports key sizes up to 1024 bits by default. But Java 8 raised this limit to 2048 bits.

Next, try upgrading your JRuby version. Then try updating jruby-openssl independently.

$ gem install jruby-openssl

This will ensure you have the latest version of Bouncy Castle, the cryptographic library JRuby uses.

If you’re still having problems – don’t worry. Not all is lost.

Before trying anything else, you’ll need to capture some data about the service. Try invoking it outside of the JVM by running cURL like this (I’m using httpbin.org but you’ll replace it with your service’s URL):

$ curl -Iv https://httpbin.org/
*   Trying 54.175.219.8...
* Connected to httpbin.org (54.175.219.8) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
...

Early in the output, you’ll see the security protcol (TLSv1.2) and cipher suite (TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384).

Now let’s see if your JVM supports these. Create a file called ciphers.rb and put the following code in it:

require 'openssl'
con = OpenSSL::SSL::SSLContext.new
con.ciphers.each { |c| puts c[0] }

Save the file and run it like this:

$ jruby ciphers.rb
EXP-DES-CBC-SHA
EXP-EDH-RSA-DES-CBC-SHA
EXP-EDH-DSS-DES-CBC-SHA
DES-CBC-SHA
EDH-RSA-DES-CBC-SHA
...

This list is not exactly the same as what the JVM supports natively. You can get the list of natively support JVM ciphers by running this script:

require 'java'
java_import 'javax.net.ssl.SSLServerSocketFactory'
ssf = SSLServerSocketFactory.get_default
ciphers = ssf.get_default_cipher_suites
ciphers.each { |c| puts c }

Hopefully you’ll see the cipher cURL used in one of these lists (for the OpenSSL list you have to convert the snake-case format to kebab-case and remove the TLS).

Now test you service in JRuby, independently of your application, by creating a test.rb script with these contents:

require 'net/http'
uri = URI.parse("https://httpbin.org/")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.get(uri.request_uri)

And run the script with this command:

$ jruby -J-Djavax.net.debug=all test.rb

The javax.net.debug=all property will cause the JVM to print out a lot of information about the SSL/TLS handshake. Somewhere in the loads of output you’ll see some lines like this:

...
Ignoring unavailable cipher suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
Ignoring unavailable cipher suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
Ignoring unavailable cipher suite: TLS_DH_anon_WITH_AES_256_GCM_SHA384
Cipher Suites: [TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA]
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA
%% Initialized:  [Session-1, TLS_DHE_RSA_WITH_AES_128_CBC_SHA]
...

Notice that the list of cipher suites is not exactly complete. And there are probably some important cipher suites in the list of those ignored.

Even though the runtime says they are unavailable, you can still enable them. Modify your test.rb script to include to look like this:

require 'openssl'
require 'net/http'

uri = URI.parse("https://httpbin.org/")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.ssl_version = :"TLSv1_2"
http.ciphers = OpenSSL::SSL::SSLContext.new.ciphers.map do |c|
  c[0].gsub("-", "+")
end

http.get(uri.request_uri)

This sets the list of default ciphers available to the Ruby runtime as those provided by openssl, and ultimately the underlying Bouncy Castle implementation. It also set the ssl_version to TLSv1_2, which is a good idea because earlier versions have vulnerabilities.

Run the script again, and you should be able to make a successful connection.

I hope this helps anyone running into the same problem.