Ruby Tuesday – SSL Version In Ruby

Today’s Ruby Tuesday takes a look at the OpenSSL::SSL::SSLContext#ssl_version.

At work today, I was pulled into a bit of a “fire”, where I was told that one of the sets of services our app at work depends on is going to be removing support for all but TLS 1.2.

Just doing a basic Net::HTTP.get did not get us a result back, and we first had to figure out how to get TLS 1.2 as something that Ruby as the client would say it supports.

Before we can start testing any of this, we need to require openssl and net/http.

require 'openssl'
# => true
require 'net/http'
# => true

It turns out when you don’t specify a version the default version is SSLv23, which can be found by looking at DEFAULT_PARAMS on the SSLContext.

OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ssl_version]
=> "SSLv23"

Since TLSv1.2, is a “higher” version than SSLv23, we were getting errors back because we a connection using TLSv1.2 would never be negotiated.

To get the ability to support TLS generically, instead of just hardcoding a specific version, e.g. TLSv1, or TLSv1_2, the ssl_version needs to be set to nil to tell Ruby’s OpenSSL components to use TLS instead of SSL.

This opened up the question of if we set OpenSSL to be TLS instead of SSL, would the TLS Protocol negotiate down to SSL if we happen to have an endpoint we need to talk to that doesn’t currently support TLS, but only SSL.

Playing around with Net::HTTP.start I was able to play with sending HTTPs requests using different settings for the ssl_version.

As I was also testing against a local instance of nginx that would only support SSLv3 using a self-signed certificate, I had the veryfy_mode to VERIFY_NONE for testing. Note that I do NOT recommend this for real use cases.

The first helper method I created doesn’t specify a ssl_version option, so it just uses the default ssl_version setting.

def test_url_no_version(url)
  Net::HTTP.start(url.hostname, nil,
                  use_ssl: url.scheme == "https",
                  verify_mode: OpenSSL::SSL::VERIFY_NONE ) do |http|
    response = http.request(Net::HTTP::Get.new(url))
    puts response.inspect
    response
  end
end
# => :test_url_no_version

The the next helper method I created sets the ssl_version option to nil to allow it to use TLS instead of SSL.

def test_url_ssl_version_is_nil(url)
  Net::HTTP.start(url.hostname, nil,
                  use_ssl: url.scheme == "https",
                  verify_mode: OpenSSL::SSL::VERIFY_NONE,
                  ssl_version: nil ) do |http|
    response = http.request(Net::HTTP::Get.new(url))
    puts response.inspect
    response
  end
end
# => :test_url

The the last helper method I created sets the ssl_version option to :SSLv3, which is the only version the webserver is setup to handle.

def test_url_ssl3(url)
  Net::HTTP.start(url.hostname, nil,
                  use_ssl: url.scheme == "https",
                  verify_mode: OpenSSL::SSL::VERIFY_NONE,
                  ssl_version: :SSLv3 ) do |http|
    response = http.request(Net::HTTP::Get.new(url))
    puts response.inspect
    response
  end
end
# => :test_url_ssl3

Now that we have these, we can test the results of asking the test webserver for a request and see what happens. We will also use Google as a baseline to compare against.

First we will test the verion where the ssl_version is set to nil. This would tell us if it would fall back to try a SSL variant.

test_url_ssl_version_is_nil(URI("https://www.google.com"))
# #<Net::HTTPOK 200 OK readbody=true>
# => #<Net::HTTPOK 200 OK readbody=true>
test_url_ssl_version_is_nil(URI("https://localhost/index.html"))
# OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv2/v3 read server hello A: unsupported protocol
# from /Users/proctor/.rvm/rubies/ruby-2.2.3/lib/ruby/2.2.0/net/http.rb:923:in `connect'

Google returns successfully, but the local test doesn’t so it doesn’t look to fall back to SSL from TLS.

Next we try with the default setting for ssl_version.

test_url_no_version(URI("https://www.google.com"))
# #<Net::HTTPOK 200 OK readbody=true>
# => #<Net::HTTPOK 200 OK readbody=true>
test_url_no_version(URI("https://localhost/index.html"))
# OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv2/v3 read server hello A: unsupported protocol
# from /Users/proctor/.rvm/rubies/ruby-2.2.3/lib/ruby/2.2.0/net/http.rb:923:in `connect'

Google still returns successfully, but the local test case still doesn’t work.

Finally we will test with specifying SSL v3 specifically.

test_url_ssl3(URI("https://www.google.com"))
# #<Net::HTTPOK 200 OK readbody=true>
# => #<Net::HTTPOK 200 OK readbody=true>
test_url_ssl3(URI("https://localhost/index.html"))
# #<Net::HTTPOK 200 OK readbody=true>
# => #<Net::HTTPOK 200 OK readbody=true>

And for this, Google and the local test both work. So we have shown that with the right ssl version specified, we do get a response back from our local test server, but the fallback from TLS to SSL doesn’t happen.

–Proctor