Search code examples
rubyopensslubuntu-14.04net-http

Net:HTTP SSL negotiation timeout on Ubuntu 14.04


After a long day I managed to get to the bottom of what I believe is a SSL/TLS cipher negotiation issue with a server that doesn't support the latest and greatest versions.

Stack:

  • Ubuntu 14.04 fully patched
  • OpenSSL 1.0.1f 6 Jan 2014
  • irb 0.9.6(09/06/30)
  • ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] (using rbenv)

After 60 seconds the snippet below gives me an error:

require 'net/http'
require 'openssl'
uri = URI.parse('https://some_old_server/my/path')
http = Net::HTTP.new('some_old_server', 443)
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.use_ssl = true
response = http.request(Net::HTTP::Get.new(uri.request_uri))

Errno::ECONNRESET: Connection reset by peer - SSL_connect

If I add this to the code, it works:

(...)
http.ciphers = ['AES128-SHA']
(...)

 => #<Net::HTTPOK 200 OK readbody=true>

This isn't a ruby-specific issue but ideally there's a ruby solution. I can't lock the ciphers to 'AES128-SHA' because the same code handles a number of sites that may or may not support this cipher.

Has anyone ever come across this and found a generic solution?

EDIT: this seems to be caused by the "TLS hang bug" and was fixed in openssl 1.0.1g.

New question: is there a work-around that can be implemented on the ruby side?

More information.

A Gentoo server running OpenSSL 1.0.1j 15 Oct 2014 doesn't have this issue. I tried installing 1.0.1j on the Ubuntu 14.04 server, recompiling ruby (rbenv install 2.2.2) and the error was still present.

I've tried to monkey patch ext/openssl but that didn't work.

Using the whole cipher list from the link above doesn't work. However, using a small subset does work:

require 'net/http'
require 'openssl'
uri = URI.parse('https://some_old_server/my/path')
http = Net::HTTP.new('some_old_server', 443)
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.use_ssl = true
http.ciphers = %w{
          AES128-GCM-SHA256
          AES256-GCM-SHA384
          AES128-SHA256
          AES256-SHA256
          AES128-SHA
          AES256-SHA
          ECDHE-ECDSA-RC4-SHA
          ECDHE-RSA-RC4-SHA
          RC4-SHA
        }.join(":")
response = http.request(Net::HTTP::Get.new(uri.request_uri))

Openssl agrees with ruby (as it should). Running these, on the same system, replicates the issue as I see them in ruby:

openssl s_client -connect some_old_server:443
CONNECTED(00000003)
(...)
  write:errno=104
  ---
  no peer certificate available
  ---
  No client certificate CA names sent
  ---
  SSL handshake has read 0 bytes and written 295 bytes
  ---
  New, (NONE), Cipher is (NONE)
  Secure Renegotiation IS NOT supported
  Compression: NONE
  Expansion: NONE
  ---

Passing the cipher:

openssl s_client -cipher AES128-SHA -connect some_old_server:443
CONNECTED(00000003)
(...)
  ---
  No client certificate CA names sent
  ---
  SSL handshake has read 2721 bytes and written 425 bytes
  ---
  New, TLSv1/SSLv3, Cipher is AES128-SHA
  Server public key is 2048 bit
  Secure Renegotiation IS NOT supported
  Compression: NONE
  Expansion: NONE
  SSL-Session:
      Protocol  : TLSv1
      Cipher    : AES128-SHA
      Session-ID: removed
      Session-ID-ctx:
      Master-Key: removed
      Key-Arg   : None
      PSK identity: None
      PSK identity hint: None
      SRP username: None
      Start Time: 1454394952
      Timeout   : 300 (sec)
      Verify return code: 20 (unable to get local issuer certificate)
  ---

I read somewhere to use

http.ssl_options = OpenSSL::SSL::OP_ALL

but ssl_options isn't available in Net::HTTP on ruby 2.2.2.


Solution

  • After spending more time on this than I'd care to admit, my solution was to upgrade from Ubuntu 14.04 to 15.10 which comes with OpenSSL 1.0.2d 9 Jul 2015.

    While the TLS negotiation still hangs using the openssl CLI, in Ruby it does not:

    require 'net/http'
    require 'openssl'
    require 'pp'
    
    uri = URI.parse('https://broken_server/my/path')
    http = Net::HTTP.new('broken_server', 443)
    
    http.instance_eval {
      @ssl_context = OpenSSL::SSL::SSLContext.new
      @ssl_context.set_params({:options=>OpenSSL::SSL::OP_ALL})
    }
    
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    http.use_ssl = true
    pp response = http.request(Net::HTTP::Get.new(uri.request_uri))
    

    SSL context code above courtesy of @vinhboy.

    The CLI equivalent of the above is turned-on with the -bugs option:

    openssl s_client -bugs -connect broken_server:443