I use Gem in a Box, a Ruby project that allows to create personal self-hosted gem repositories. Gem in a Box uses
httpclient to connect to the gem repository. After reviewing my server's TLS setup in the aftermath of the OpenSSL heartbleed bug, I decided only to allow protocol versions of TLSv1 and higher on my server. A decision quite a few fellow administrators made too, because of security vulnerabilities in SSLv3 and below.
Now after I did this change, Gem in a Box refused to work with the following message:
OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv3 read server hello A: sslv3 alert handshake failure
I traced this problem back until I found the source in
httpclient. Even though OpenSSL can find the best protocol version to use by itself, you set it only to use SSLv3 by default. I think this is a very bad idea and created a patch which lets OpenSSL do its own decision again by default.
As I am not so familiar with OpenSSL itself and the documentation of the Ruby binding is severely lacking I want to prevent introducing more problems than there were originally. Please review my changes and verify that it is doing what I intended.
It is quite hard to post the actual changes here because they are only meaningful if one knows the context. So now I will try to describe what I did:
httpclient has a class called
SSLConfig which seems to produce helper objects to configure OpenSSL sockets. It sets some default configurations in its #initialize method. Originally it had the
@ssl_version instance variable set to
"SSLv3", When using the library an instance of
SSLConfig is created and its
#set_context method is called which applies the aforementioned defaults onto an instance of
OpenSSL::SSL::SSLContext, which afterwards seems to be used to create OpenSSL sockets.
I now changed to initial value of
@ssl_version to the symbol
:auto and modified the
#set_context method to only set the actual value of
@ssl_version to the
SSLContext object, if its value is not
Manual testing so far resulted in the exact effect I wanted to achieve.
- Here's a link to the official GitHub repository of the
- See my full commit on GitHub for the mentioned changes.
- I also made a pull request to the original project.
Here are some relevant snippets from
class SSLConfig ... # Which TLS protocol version (also called method) will be used. Defaults # to :auto which means that OpenSSL decides (In my tests this resulted # with always the highest available protocol being used). # # See the content of OpenSSL::SSL::SSLContext::METHODS for a list of # available versions in your specific Ruby environment. attr_reader :ssl_version ... # Creates a SSLConfig. def initialize(client) return unless SSLEnabled @client = client @cert_store = X509::Store.new @client_cert = @client_key = @client_ca = nil @verify_mode = SSL::VERIFY_PEER | SSL::VERIFY_FAIL_IF_NO_PEER_CERT @verify_depth = nil @verify_callback = nil @dest = nil @timeout = nil @ssl_version = :auto @options = defined?(SSL::OP_ALL) ? SSL::OP_ALL | SSL::OP_NO_SSLv2 : nil # OpenSSL 0.9.8 default: "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH" @ciphers = "ALL:!aNULL:!eNULL:!SSLv2" # OpenSSL >1.0.0 default @cacerts_loaded = false end ... # interfaces for SSLSocketWrap. def set_context(ctx) # :nodoc: load_trust_ca unless @cacerts_loaded @cacerts_loaded = true # Verification: Use Store#verify_callback instead of SSLContext#verify*? ctx.cert_store = @cert_store ctx.verify_mode = @verify_mode ctx.verify_depth = @verify_depth if @verify_depth ctx.verify_callback = @verify_callback || method(:default_verify_callback) # SSL config ctx.cert = @client_cert ctx.key = @client_key ctx.client_ca = @client_ca ctx.timeout = @timeout ctx.options = @options ctx.ciphers = @ciphers ctx.ssl_version = @ssl_version unless @ssl_version == :auto end ... end
Here's what is using the
#set_context method in
... def create_openssl_socket(socket) ssl_socket = nil if OpenSSL::SSL.const_defined?("SSLContext") ctx = OpenSSL::SSL::SSLContext.new @context.set_context(ctx) ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx) else ssl_socket = OpenSSL::SSL::SSLSocket.new(socket) @context.set_context(ssl_socket) end ssl_socket end ...