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 #8]
  • Loading branch information
rhenium committed Jul 23, 2016
1 parent 6c387d4 commit 028e495
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 5 deletions.
10 changes: 10 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,8 @@ 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. It is now enabled by
OpenSSL::SSL::Context#set_params. [GH ruby/openssl#60]
48 changes: 46 additions & 2 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ 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")
Expand Down Expand Up @@ -319,14 +320,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 = rb_attr_get(ssl_obj, rb_intern("@hostname"));

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 +2313,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 @@ -889,6 +889,53 @@ def test_tlsext_hostname
end
end

def test_verify_hostname_on_connect
ctx_proc = proc { |ctx|
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"],
]
ctx.cert = issue_cert(@svr, @svr_key, 4, now, now+1800, exts,
@ca_cert, @ca_key, OpenSSL::Digest::SHA1.new)
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
assert_nothing_raised { ssl.connect }
else
assert_raise(OpenSSL::SSL::SSLError) { ssl.connect }
end
ensure
ssl.close if ssl
sock.close if sock
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 028e495

Please sign in to comment.