Skip to content

Commit

Permalink
ssl: add verify_hostname option to SSLContext
Browse files Browse the repository at this point in the history
If a client sets this to true and enables SNI with SSLSocket#hostname=,
the hostname verification on the server certificate is performed
automatically during the handshake using
OpenSSL::SSL.verify_certificate_identity().

Currently an user who wants to do the hostname verification needs to
call SSLSocket#post_connection_check explicitly after the TLS connection
is established.

This commit also enables the option in SSLContext::DEFAULT_PARAMS.
Applications using SSLContext#set_params may be affected by this.

[GH ruby#8]
  • Loading branch information
rhenium committed Jul 9, 2016
1 parent 6bad98e commit 0c7255d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 5 deletions.
11 changes: 11 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ Backward compatibility notes
for consistency with OpenSSL::PKey::{DH,DSA,RSA,EC}#new.
[Bug #11774] [GH ruby/openssl#55]

* OpenSSL::SSL::SSLContext#set_params enables verify_hostname option. With the
SNI hostname set by OpenSSL::SSL::SSLSocket#hostname=, the hostname
verification on the server certificate is automatically performed during the
handshake. [GH ruby/openssl#60]

Updates since Ruby 2.3
----------------------

Expand Down Expand Up @@ -114,3 +119,9 @@ Updates since Ruby 2.3

- RC4 cipher suites are removed from OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.
RC4 is now considered to be weak. [GH ruby/openssl#50]

- A new option 'verify_hostname' is added to OpenSSL::SSL::SSLContext. When it
is enabled, and the SNI hostname is also set, the hostname verification on
the server certificate is automatically performed during the handshake. Also
it is enabled by default when an user call OpenSSL::SSL::Context#set_params.
[GH ruby/openssl#60]
49 changes: 47 additions & 2 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ static VALUE eSSLErrorWaitWritable;
#define ossl_sslctx_get_client_cert_cb(o) rb_iv_get((o),"@client_cert_cb")
#define ossl_sslctx_get_tmp_ecdh_cb(o) rb_iv_get((o),"@tmp_ecdh_callback")
#define ossl_sslctx_get_sess_id_ctx(o) rb_iv_get((o),"@session_id_context")
#define ossl_sslctx_get_verify_hostname(o) rb_iv_get((o),"@verify_hostname")

#define ossl_ssl_get_io(o) rb_iv_get((o),"@io")
#define ossl_ssl_get_ctx(o) rb_iv_get((o),"@context")
#define ossl_ssl_get_hostname_v(o) rb_iv_get((o),"@hostname")
#define ossl_ssl_get_x509(o) rb_iv_get((o),"@x509")
#define ossl_ssl_get_key(o) rb_iv_get((o),"@key")

Expand Down Expand Up @@ -319,14 +321,48 @@ ossl_tmp_ecdh_callback(SSL *ssl, int is_export, int keylength)
}
#endif

static VALUE
call_verify_certificate_identity(VALUE ctx_v)
{
X509_STORE_CTX *ctx = (X509_STORE_CTX *)ctx_v;
SSL *ssl;
VALUE ssl_obj, hostname, cert_obj;

ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
hostname = ossl_ssl_get_hostname_v(ssl_obj);

if (!RTEST(hostname)) {
rb_warning("verify_hostname requires hostname to be set");
return Qtrue;
}

cert_obj = ossl_x509_new(X509_STORE_CTX_get_current_cert(ctx));
return rb_funcall(mSSL, rb_intern("verify_certificate_identity"), 2,
cert_obj, hostname);
}

static int
ossl_ssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
{
VALUE cb;
VALUE cb, ssl_obj, verify_hostname, ret;
SSL *ssl;
int status;

ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
cb = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_vcb_idx);
ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
verify_hostname = ossl_sslctx_get_verify_hostname(ossl_ssl_get_ctx(ssl_obj));

if (preverify_ok && RTEST(verify_hostname) && !SSL_is_server(ssl) &&
!X509_STORE_CTX_get_error_depth(ctx)) {
ret = rb_protect(call_verify_certificate_identity, (VALUE)ctx, &status);
if (status) {
rb_ivar_set(ssl_obj, ID_callback_state, INT2NUM(status));
return 0;
}
preverify_ok = ret == Qtrue;
}

return ossl_verify_cb_call(cb, preverify_ok, ctx);
}
Expand Down Expand Up @@ -2278,10 +2314,19 @@ Init_ossl_ssl(void)
* +store_context+ is an OpenSSL::X509::StoreContext containing the
* context used for certificate verification.
*
* If the callback returns false verification is stopped.
* If the callback returns false, the chain verification is immediately
* stopped and a bad_certificate alert is then sent.
*/
rb_attr(cSSLContext, rb_intern("verify_callback"), 1, 1, Qfalse);

/*
* Whether to check the server certificate is valid for the hostname.
*
* In order to make this work, verify_mode must be set to VERIFY_PEER and
* the server hostname must be given by OpenSSL::SSL::SSLSocket#hostname=.
*/
rb_attr(cSSLContext, rb_intern("verify_hostname"), 1, 1, Qfalse);

/*
* An OpenSSL::X509::Store used for certificate verification
*/
Expand Down
11 changes: 8 additions & 3 deletions lib/openssl/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class SSLContext
DEFAULT_PARAMS = {
:ssl_version => "SSLv23",
:verify_mode => OpenSSL::SSL::VERIFY_PEER,
:verify_hostname => true,
:ciphers => %w{
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-GCM-SHA256
Expand Down Expand Up @@ -71,7 +72,7 @@ class SSLContext
"session_get_cb", "session_new_cb", "session_remove_cb",
"tmp_ecdh_callback", "servername_cb", "npn_protocols",
"alpn_protocols", "alpn_select_cb",
"npn_select_cb"].map { |x| "@#{x}" }
"npn_select_cb", "verify_hostname"].map { |x| "@#{x}" }

# A callback invoked when DH parameters are required.
#
Expand Down Expand Up @@ -107,13 +108,17 @@ def initialize(version = nil)
end

##
# Sets the parameters for this SSL context to the values in +params+.
# call-seq:
# ctx.set_params(params = {}) -> params
#
# Sets saner defaults optimized for the use with HTTP-like protocols.
#
# If a Hash +params+ is given, the parameters are overridden with it.
# The keys in +params+ must be assignment methods on SSLContext.
#
# If the verify_mode is not VERIFY_NONE and ca_file, ca_path and
# cert_store are not set then the system default certificate store is
# used.

def set_params(params={})
params = DEFAULT_PARAMS.merge(params)
params.each{|name, value| self.__send__("#{name}=", value) }
Expand Down
47 changes: 47 additions & 0 deletions test/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,53 @@ def test_tlsext_hostname
end
end

def test_verify_hostname_on_connect
now = Time.now
exts = [
["keyUsage", "keyEncipherment,digitalSignature", true],
["subjectAltName", "DNS:a.example.com,DNS:*.b.example.com,DNS:c*.example.com,DNS:d.*.example.com"],
]
cert = issue_cert(@svr, @svr_key, 4, now, now+1800, exts,
@ca_cert, @ca_key, OpenSSL::Digest::SHA1.new)

ctx_proc = proc { |ctx|
ctx.cert = cert
ctx.key = @svr_key
}

start_server(OpenSSL::SSL::VERIFY_NONE, true, ctx_proc: ctx_proc, ignore_listener_error: true) do |svr, port|
ctx = OpenSSL::SSL::SSLContext.new
ctx.verify_hostname = true
ctx.cert_store = OpenSSL::X509::Store.new
ctx.cert_store.add_cert(@ca_cert)
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER

[
["a.example.com", true],
["A.Example.Com", true],
["x.example.com", false],
["b.example.com", false],
["x.b.example.com", true],
["cx.example.com", true],
["d.x.example.com", false],
].each do |name, expected_ok|
begin
sock = TCPSocket.new("127.0.0.1", port)
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
ssl.hostname = name
if expected_ok
ssl.connect
else
assert_raise(OpenSSL::SSL::SSLError) { ssl.connect }
end
ensure
sock.close if sock
ssl.close if ssl
end
end
end
end

def test_multibyte_read_write
#German a umlaut
auml = [%w{ C3 A4 }.join('')].pack('H*')
Expand Down

0 comments on commit 0c7255d

Please sign in to comment.