问题
Updated: At first, my test code didn't adequately show ruby 2.4 sees the :SSLCiphers option whereas ruby 2.2 does not. I have edited the example code below to make that clear.
Updated: Since my question failed to elicit any help from the community, I forged on ahead and two days later found the solution, which I have included below.
I have a small Rails 3 application on Ruby 2.2 and Webrick that handles small loads and therefore does not need the complexity of a "real" web server. It has been patched to support https connections for secure logins, but by default, it accepts many old weak Ciphers, which I want to prohibit. While upgrading ruby to 2.4 offers new hardening options to achieve this, I can't immediately make the switch to the newer ruby. Therefore, I have attempted to monkey-patch ruby 2.2's webrick/ssl.rb to add a couple of these options, :SSLVersion and :SSLCiphers. I was only partially successful: while :SSLVersion appears to work, :SSLCiphers does not.
First, the following example shows successful hardening on Ruby 2.4, based on the answer here https://stackoverflow.com/a/23283909/6588873 but also incorporating some of the advice found here: https://gist.github.com/tam7t/86eb4793e8ecf3f55037 except initially we'll be over-aggressive and exclude AES128 just to show the :SSLCiphers option works (as the version of openssl in Ruby 2.4 disables all SHA1 ciphers by default anyway).
#!/usr/bin/env ruby.exe
# script/rails:
require 'rails/commands/server'
require 'rack'
require 'webrick'
require 'webrick/https'
module Rails
class Server < ::Rack::Server
SSL_ENABLED=true
def default_options
# Don't use SSLv3
no_ssl_3 = OpenSSL::SSL::OP_NO_SSLv3
# Don't use SSLv2
no_ssl_2 = OpenSSL::SSL::OP_NO_SSLv2
# Don't use compression (CRIME CVE-2012-4929)
no_ssl_compression = OpenSSL::SSL::OP_NO_COMPRESSION
ssl_options = no_ssl_2 + no_ssl_3 + no_ssl_compression
super.merge({
:Port => 3002,
:environment => (ENV['RAILS_ENV'] || "development").dup,
:daemonize => false,
:debugger => false,
:config => File.expand_path("config.ru"),
:SSLEnable => true,
:SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
:SSLPrivateKey => OpenSSL::PKey::RSA.new(
File.open(File.expand_path("../cert/test.cert.key",__FILE__)).read),
:SSLCertificate => OpenSSL::X509::Certificate.new(
File.open(File.expand_path("../cert/test.cert.crt",__FILE__)).read),
:SSLCertName => [["CN", WEBrick::Utils::getservername]],
:SSLOptions => ssl_options,
:SSLCiphers => 'TLSv1.2:!aNULL:!eNULL:!AES128',
:SSLVersion => :TLSv1_2,
})
end
end
end
APP_PATH = File.expand_path('../../config/application', __FILE__)
require File.expand_path('../../config/boot', __FILE__)
require 'rails/commands'
And here's an excerpt from sslscan output with the above patch applied, showing the short list of supported ciphers with insecure ciphers and obsolete TLS versions dropped (though they are dropped anyway by default, hence the !AES128 just for the proof of principle showing the option works).
Supported Server Cipher(s):
Preferred TLSv1.2 256 bits ECDHE-RSA-AES256-GCM-SHA384 Curve P-256 DHE 256
Accepted TLSv1.2 256 bits ECDHE-RSA-AES256-SHA384 Curve P-256 DHE 256
Accepted TLSv1.2 256 bits DHE-RSA-AES256-GCM-SHA384 DHE 1024 bits
Accepted TLSv1.2 256 bits DHE-RSA-AES256-SHA256 DHE 1024 bits
Accepted TLSv1.2 256 bits AES256-GCM-SHA384
Accepted TLSv1.2 256 bits AES256-SHA256
So next, I changed :SSLCiphers to the actual string I want, since I'm actually fine with AES128. I just want to exclude all the old SHA1 Ciphers that the older openssl doesn't exclude by default:
:SSLCiphers => 'TLSv1.2:!aNULL:!eNULL!SHA',
And then prepared my backport, inserted just after the Rails::Server patch shown above. The following is lifted directly from Ruby 2.2's webrick/ssl.rb, but with the two new options from Ruby 2.4 added and passed through to ruby-openssl via WEBrick::GenericServer#setup_ssl_context():
if RUBY_VERSION < '2.4'
module WEBrick
module Config
svrsoft = General[:ServerSoftware]
osslv = ::OpenSSL::OPENSSL_VERSION.split[1]
SSL = {
:ServerSoftware => "#{svrsoft} OpenSSL/#{osslv}",
:SSLEnable => false,
:SSLCertificate => nil,
:SSLPrivateKey => nil,
:SSLClientCA => nil,
:SSLExtraChainCert => nil,
:SSLCACertificateFile => nil,
:SSLCACertificatePath => nil,
:SSLCertificateStore => nil,
:SSLTmpDhCallback => nil,
:SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
:SSLVerifyDepth => nil,
:SSLVerifyCallback => nil, # custom verification
:SSLTimeout => nil,
:SSLOptions => nil,
:SSLCiphers => nil,
:SSLVersion => nil,
:SSLStartImmediately => true,
# Must specify if you use auto generated certificate.
:SSLCertName => nil,
:SSLCertComment => "Generated by Ruby/OpenSSL"
}
General.update(SSL)
end
class GenericServer
def setup_ssl_context(config) # :nodoc:
unless config[:SSLCertificate]
cn = config[:SSLCertName]
comment = config[:SSLCertComment]
cert, key = Utils::create_self_signed_cert(1024, cn, comment)
config[:SSLCertificate] = cert
config[:SSLPrivateKey] = key
end
ctx = OpenSSL::SSL::SSLContext.new
ctx.key = config[:SSLPrivateKey]
ctx.cert = config[:SSLCertificate]
ctx.client_ca = config[:SSLClientCA]
ctx.extra_chain_cert = config[:SSLExtraChainCert]
ctx.ca_file = config[:SSLCACertificateFile]
ctx.ca_path = config[:SSLCACertificatePath]
ctx.cert_store = config[:SSLCertificateStore]
ctx.tmp_dh_callback = config[:SSLTmpDhCallback]
ctx.verify_mode = config[:SSLVerifyClient]
ctx.verify_depth = config[:SSLVerifyDepth]
ctx.verify_callback = config[:SSLVerifyCallback]
ctx.timeout = config[:SSLTimeout]
ctx.options = config[:SSLOptions]
ctx.ciphers = config[:SSLCiphers]
ctx.ssl_version = config[:SSLVersion]
ctx
end
end
end
end
When started, I see the following redefinition warnings. The first, I expected. The second I'm not sure of, but I think is OK:
script/rails:47: warning: already initialized constant WEBrick::Config::SSL
C:/Ruby226/lib/ruby/2.2.0/webrick/ssl.rb:62: warning: previous definition of SSL was here
Now, while sslscan now indicates the SSLVersion is being obeyed, (i.e. no TLS 1.0 or 1.1 are accepted,) the SSLCiphers option doesn't appear to be doing anything at all, i.e. SHA ciphers are still accepted:
Supported Server Cipher(s):
Preferred TLSv1.2 256 bits DHE-RSA-AES256-GCM-SHA384 DHE 1024 bits
Accepted TLSv1.2 256 bits DHE-RSA-AES256-SHA256 DHE 1024 bits
Accepted TLSv1.2 256 bits DHE-RSA-AES256-SHA DHE 1024 bits
Accepted TLSv1.2 256 bits DHE-RSA-CAMELLIA256-SHA DHE 1024 bits
Accepted TLSv1.2 256 bits AES256-GCM-SHA384
Accepted TLSv1.2 256 bits AES256-SHA256
Accepted TLSv1.2 256 bits AES256-SHA
Accepted TLSv1.2 256 bits CAMELLIA256-SHA
Accepted TLSv1.2 128 bits DHE-RSA-AES128-GCM-SHA256 DHE 1024 bits
Accepted TLSv1.2 128 bits DHE-RSA-AES128-SHA256 DHE 1024 bits
Accepted TLSv1.2 128 bits DHE-RSA-AES128-SHA DHE 1024 bits
Accepted TLSv1.2 128 bits DHE-RSA-SEED-SHA DHE 1024 bits
Accepted TLSv1.2 128 bits DHE-RSA-CAMELLIA128-SHA DHE 1024 bits
Accepted TLSv1.2 128 bits AES128-GCM-SHA256
Accepted TLSv1.2 128 bits AES128-SHA256
Accepted TLSv1.2 128 bits AES128-SHA
Accepted TLSv1.2 128 bits SEED-SHA
Accepted TLSv1.2 128 bits CAMELLIA128-SHA
Whereas I expected:
Supported Server Cipher(s):
Preferred TLSv1.2 256 bits DHE-RSA-AES256-GCM-SHA384 DHE 1024 bits
Accepted TLSv1.2 256 bits DHE-RSA-AES256-SHA256 DHE 1024 bits
Accepted TLSv1.2 256 bits AES256-GCM-SHA384
Accepted TLSv1.2 256 bits AES256-SHA256
Accepted TLSv1.2 128 bits DHE-RSA-AES128-GCM-SHA256 DHE 1024 bits
Accepted TLSv1.2 128 bits DHE-RSA-AES128-SHA256 DHE 1024 bits
Accepted TLSv1.2 128 bits AES128-GCM-SHA256
Accepted TLSv1.2 128 bits AES128-SHA256
In fact, it doesn't matter what I change :SSLCiphers to, any valid or even invalid value. Clearly, just updating the ruby is not enough.
Where have I gone wrong with this backport? Is there anything else I can try to achieve the same result? I seem so close to a solution here, working within the constraints of our Ruby version and webrick, which would be difficult for me to move away from.
Solution:
I inserted debug print statements to see where the SSLCiphers value gets 'lost'. Although my testing showed everything was fine on the WEBrick side of things, in the OpenSSL classes, it appeared that when an SSL listener was set up, the wrong ciphers list was passed in (i.e. the default instead of the override).
When looking at the OpenSSL API for clues as to how to obtain more info about the failure, I found OpenSSL::SSL::SSLContext#set_params, a method taking a hash as an argument with any parameters to the SSLContext to set to new values. So I experimentally replaced my monkey-patched WEBrick::GenericServer#setup_ssl_context to use this and pass a hash of all values to change, instead replacing ctx.= config:[:] for each setting (lifted directly from the original WEBrick code; even in Ruby 2.4 this has not changed).
My outcome? Success! Using the alternate method of changing the SSLContext object, I was able to make the :SSLCiphers override value 'stick'
if RUBY_VERSION < '2.4'
require 'webrick/ssl'
module WEBrick
module Config
svrsoft = General[:ServerSoftware]
osslv = ::OpenSSL::OPENSSL_VERSION.split[1]
SSL = {
:ServerSoftware => "#{svrsoft} OpenSSL/#{osslv}",
:SSLEnable => false,
:SSLCertificate => nil,
:SSLPrivateKey => nil,
:SSLClientCA => nil,
:SSLExtraChainCert => nil,
:SSLCACertificateFile => nil,
:SSLCACertificatePath => nil,
:SSLCertificateStore => nil,
:SSLTmpDhCallback => nil,
:SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
:SSLVerifyDepth => nil,
:SSLVerifyCallback => nil, # custom verification
:SSLTimeout => nil,
:SSLOptions => nil,
:SSLCiphers => nil,
:SSLVersion => nil,
:SSLStartImmediately => true,
# Must specify if you use auto generated certificate.
:SSLCertName => nil,
:SSLCertComment => "Generated by Ruby/OpenSSL"
}
General.update(SSL)
end
class GenericServer
def setup_ssl_context(config) # :nodoc:
unless config[:SSLCertificate]
cn = config[:SSLCertName]
comment = config[:SSLCertComment]
cert, key = Utils::create_self_signed_cert(1024, cn, comment)
config[:SSLCertificate] = cert
config[:SSLPrivateKey] = key
end
ctx = OpenSSL::SSL::SSLContext.new
ctx.set_params({
key: config[:SSLPrivateKey],
cert: config[:SSLCertificate],
client_ca: config[:SSLClientCA],
extra_chain_cert: config[:SSLExtraChainCert],
ca_file: config[:SSLCACertificateFile],
ca_path: config[:SSLCACertificatePath],
cert_store: config[:SSLCertificateStore],
tmp_dh_callback: config[:SSLTmpDhCallback],
verify_mode: config[:SSLVerifyClient],
verify_depth: config[:SSLVerifyDepth],
verify_callback: config[:SSLVerifyCallback],
timeout: config[:SSLTimeout],
options: config[:SSLOptions],
ciphers: config[:SSLCiphers],
ssl_version: config[:SSLVersion],
})
ctx
end
end
end
end
With this patch in place, sslscan output matches my expected output shown above.
来源:https://stackoverflow.com/questions/45301637/how-to-harden-railswebrickhttps-with-insecure-ciphers-removed-on-ruby-2-2