Zack Hobson

Pictured: Ruby-core demonstrates effective secrecy.

SSL and Ruby, part 2 Monday February 10, 2014

Recently I wrote about an issue with Ruby and SSL that was publicized by Jeff Hodges. Mr. Hodges has uncovered what seems like a fairly serious issue: Known insecure cipher suites and other options are being used by the OpenSSL bindings that ship with Ruby. In my original article I asserted that updating Ruby to the most recent version of OpenSSL will fix this issue, but this is not actually the case! There is still no officially published fix at the time of this writing, but there are ways you can fix this in your own Ruby installation, if you’re so inclined.

Here is the leaked ruby-security thread in question, I still recommend reading it in its entirety. I’ll attempt to summarize below. First a little background:

Cipher suites are named sets of security algorithms used to negotiate a secure connection. The SSL client maintains a list of these suites, to be attempted (in order) until the client and server can agree on a set of ciphers and establish a connection. Of course, weaknesses and vulnerabilities are found in encryption algorithms all the time. Better techniques are discovered. Best practices change. When this happens, it can render some cipher suites less than optimal for security. Ideally, the cipher suites using these algorithms would be deprecated, but servers are under pressure to support as many encryption techniques as they can, for the widest compatibility possible. This means that if the client prefers a known-insecure suite, the server is unlikely to refuse it.

Now then, the problem: Ruby specifies its own set of default cipher suites for OpenSSL, including some that have known problems. It also allows some other behaviors (like TLS compression) that are known to be insecure. In addition to this, the OpenSSL defaults themselves are not always secure. It follows from this that Ruby itself is the last line of defense for users of Ruby’s OpenSSL bindings.

Some of Ruby’s maintainers pushed back on this, arguing that Ruby is not a security project, and that bypassing OpenSSL defaults carries further risks. But as far as I can tell, this argument doesn’t hold water, as Ruby already specifies its own default options and ciphers for OpenSSL. Someone proposed a patch that demonstrated which options and ciphers would be preferable, which gives us some insight into how we might solve this problem on our own.

If you’d like to see this problem for yourself, you can demonstrate the weakness on any Ruby installation using a simple Ruby program:

require "net/https"
require "uri"
require "json"

uri = URI.parse("https://www.howsmyssl.com/a/check")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
resp = JSON.parse(http.request(Net::HTTP::Get.new(uri.request_uri)).body)
puts JSON.pretty_generate(resp)

When I run the above code locally, I get a JSON-formatted report explaining the numerous problems with my SSL client, including a list of known-insecure cipher suites. As you can see, it’s not looking so good (I’ve elided the complete list of cipher suites for brevity):

{
  "given_cipher_suites": [
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    <...>
    "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
  ],
  "ephemeral_keys_supported": true,
  "session_ticket_supported": true,
  "tls_compression_supported": true,
  "unknown_cipher_suite_supported": false,
  "beast_vuln": false,
  "able_to_detect_n_minus_one_splitting": false,
  "insecure_cipher_suites": {
    "TLS_DHE_DSS_WITH_DES_CBC_SHA": [
      "uses keys smaller than 128 bits in its encryption"
    ],
    "TLS_DHE_RSA_WITH_DES_CBC_SHA": [
      "uses keys smaller than 128 bits in its encryption"
    ],
    "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ],
    "TLS_ECDH_anon_WITH_AES_128_CBC_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ],
    "TLS_ECDH_anon_WITH_AES_256_CBC_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ],
    "TLS_ECDH_anon_WITH_RC4_128_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ],
    "TLS_RSA_WITH_DES_CBC_SHA": [
      "uses keys smaller than 128 bits in its encryption"
    ],
    "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ],
    "TLS_SRP_SHA_WITH_AES_128_CBC_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ],
    "TLS_SRP_SHA_WITH_AES_256_CBC_SHA": [
      "is open to man-in-the-middle attacks because it does not authenticate the server"
    ]
  },
  "tls_version": "TLS 1.2",
  "rating": "Bad"
}

Look at that, ten known insecure cipher suites! TLS compression! Rating “Bad”! There is good news, however: it’s possible to provide your own list of ciphers to Net::HTTP when creating a connection:

require "net/https"
require "uri"
require "json"

uri = URI.parse("https://www.howsmyssl.com/a/check")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# corrected ciphers
http.ciphers = "DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2"
resp = JSON.parse(http.request(Net::HTTP::Get.new(uri.request_uri)).body)
puts JSON.pretty_generate(resp)

If I run this code, I get a different report with no insecure ciphers listed, but unfortunately, the overall score for my TLS is still “Bad”:

{
  "given_cipher_suites": [
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    <...>
    "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
  ],
  "ephemeral_keys_supported": true,
  "session_ticket_supported": true,
  "tls_compression_supported": true,
  "unknown_cipher_suite_supported": false,
  "beast_vuln": false,
  "able_to_detect_n_minus_one_splitting": false,
  "insecure_cipher_suites": {
  },
  "tls_version": "TLS 1.2",
  "rating": "Bad"
}

This is because there are other insecure options apart from the ciphers, and at the moment Ruby does not have hooks to set OpenSSL options from within Ruby code. There is a patch to allow setting options on the SSL context, applying it should allow us to provide our own, more secure options to SSL using Ruby. I’ve stolen and re-purposed a script that will install the patched Ruby using rbenv. If you’d like to try this out yourself, that script might be a good starting point.

Once you have a version of Ruby with the above patch applied, we can set SSL options in our TLS testing code, using the patch proposed in the original thread as a guide:

require "net/https"
require "uri"
require "json"

uri = URI.parse("https://www.howsmyssl.com/a/check")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# avoid unsafe cipher suites
http.ciphers = "DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2"
# provide options for the SSL context
http.ssl_options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_COMPRESSION & ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS
resp = JSON.parse(http.request(Net::HTTP::Get.new(uri.request_uri)).body)
puts JSON.pretty_generate(resp)

When I run the above test code with a patched Ruby, the report improves:

{
  "given_cipher_suites": [
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    <...>
    "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
  ],
  "ephemeral_keys_supported": true,
  "session_ticket_supported": true,
  "tls_compression_supported": false,
  "unknown_cipher_suite_supported": false,
  "beast_vuln": false,
  "able_to_detect_n_minus_one_splitting": false,
  "insecure_cipher_suites": {
  },
  "tls_version": "TLS 1.2",
  "rating": "Probably Okay"
}

Probably okay! A positively glowing appraisal from our TLS testing service. This demonstrates what is possible with some fairly small changes to Ruby’s OpenSSL bindings and the right SSL options.

While we’re able to get some positive results in the end, this is still mostly bad news. Ruby’s maintainers are pushing back on improving the known-insecure defaults already provided by Ruby’s OpenSSL bindings, and there is no official release of Ruby that will even allow users to provide more secure options. On the bright side, the patch to add ssl_options to Net::HTTP is pretty non-controversial, and will probably make it into Ruby at some point. This at least will allow HTTP clients to work around it in their own Ruby code.