Skip to content

Commit f584b51

Browse files
committed
ssl: add verify_hostname option to SSLContext
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]
1 parent 5d73437 commit f584b51

File tree

4 files changed

+112
-5
lines changed

4 files changed

+112
-5
lines changed

NEWS

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ Backward compatibility notes
3939
for consistency with OpenSSL::PKey::{DH,DSA,RSA,EC}#new.
4040
[Bug #11774] [GH ruby/openssl#55]
4141

42+
* OpenSSL::SSL::SSLContext#set_params enables verify_hostname option. With the
43+
SNI hostname set by OpenSSL::SSL::SSLSocket#hostname=, the hostname
44+
verification on the server certificate is automatically performed during the
45+
handshake. [GH ruby/openssl#60]
46+
4247
Updates since Ruby 2.3
4348
----------------------
4449

@@ -114,3 +119,8 @@ Updates since Ruby 2.3
114119

115120
- RC4 cipher suites are removed from OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.
116121
RC4 is now considered to be weak. [GH ruby/openssl#50]
122+
123+
- A new option 'verify_hostname' is added to OpenSSL::SSL::SSLContext. When it
124+
is enabled, and the SNI hostname is also set, the hostname verification on
125+
the server certificate is automatically performed. It is now enabled by
126+
OpenSSL::SSL::Context#set_params. [GH ruby/openssl#60]

ext/openssl/ossl_ssl.c

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ static VALUE eSSLErrorWaitWritable;
6464
#define ossl_sslctx_get_client_cert_cb(o) rb_iv_get((o),"@client_cert_cb")
6565
#define ossl_sslctx_get_tmp_ecdh_cb(o) rb_iv_get((o),"@tmp_ecdh_callback")
6666
#define ossl_sslctx_get_sess_id_ctx(o) rb_iv_get((o),"@session_id_context")
67+
#define ossl_sslctx_get_verify_hostname(o) rb_iv_get((o),"@verify_hostname")
6768

6869
#define ossl_ssl_get_io(o) rb_iv_get((o),"@io")
6970
#define ossl_ssl_get_ctx(o) rb_iv_get((o),"@context")
71+
#define ossl_ssl_get_hostname_v(o) rb_iv_get((o),"@hostname")
7072
#define ossl_ssl_get_x509(o) rb_iv_get((o),"@x509")
7173
#define ossl_ssl_get_key(o) rb_iv_get((o),"@key")
7274

@@ -319,14 +321,48 @@ ossl_tmp_ecdh_callback(SSL *ssl, int is_export, int keylength)
319321
}
320322
#endif
321323

324+
static VALUE
325+
call_verify_certificate_identity(VALUE ctx_v)
326+
{
327+
X509_STORE_CTX *ctx = (X509_STORE_CTX *)ctx_v;
328+
SSL *ssl;
329+
VALUE ssl_obj, hostname, cert_obj;
330+
331+
ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
332+
ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
333+
hostname = ossl_ssl_get_hostname_v(ssl_obj);
334+
335+
if (!RTEST(hostname)) {
336+
rb_warning("verify_hostname requires hostname to be set");
337+
return Qtrue;
338+
}
339+
340+
cert_obj = ossl_x509_new(X509_STORE_CTX_get_current_cert(ctx));
341+
return rb_funcall(mSSL, rb_intern("verify_certificate_identity"), 2,
342+
cert_obj, hostname);
343+
}
344+
322345
static int
323346
ossl_ssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
324347
{
325-
VALUE cb;
348+
VALUE cb, ssl_obj, verify_hostname, ret;
326349
SSL *ssl;
350+
int status;
327351

328352
ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
329353
cb = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_vcb_idx);
354+
ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
355+
verify_hostname = ossl_sslctx_get_verify_hostname(ossl_ssl_get_ctx(ssl_obj));
356+
357+
if (preverify_ok && RTEST(verify_hostname) && !SSL_is_server(ssl) &&
358+
!X509_STORE_CTX_get_error_depth(ctx)) {
359+
ret = rb_protect(call_verify_certificate_identity, (VALUE)ctx, &status);
360+
if (status) {
361+
rb_ivar_set(ssl_obj, ID_callback_state, INT2NUM(status));
362+
return 0;
363+
}
364+
preverify_ok = ret == Qtrue;
365+
}
330366

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

2322+
/*
2323+
* Whether to check the server certificate is valid for the hostname.
2324+
*
2325+
* In order to make this work, verify_mode must be set to VERIFY_PEER and
2326+
* the server hostname must be given by OpenSSL::SSL::SSLSocket#hostname=.
2327+
*/
2328+
rb_attr(cSSLContext, rb_intern("verify_hostname"), 1, 1, Qfalse);
2329+
22852330
/*
22862331
* An OpenSSL::X509::Store used for certificate verification
22872332
*/

lib/openssl/ssl.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class SSLContext
1919
DEFAULT_PARAMS = {
2020
:ssl_version => "SSLv23",
2121
:verify_mode => OpenSSL::SSL::VERIFY_PEER,
22+
:verify_hostname => true,
2223
:ciphers => %w{
2324
ECDHE-ECDSA-AES128-GCM-SHA256
2425
ECDHE-RSA-AES128-GCM-SHA256
@@ -71,7 +72,7 @@ class SSLContext
7172
"session_get_cb", "session_new_cb", "session_remove_cb",
7273
"tmp_ecdh_callback", "servername_cb", "npn_protocols",
7374
"alpn_protocols", "alpn_select_cb",
74-
"npn_select_cb"].map { |x| "@#{x}" }
75+
"npn_select_cb", "verify_hostname"].map { |x| "@#{x}" }
7576

7677
# A callback invoked when DH parameters are required.
7778
#
@@ -107,13 +108,17 @@ def initialize(version = nil)
107108
end
108109

109110
##
110-
# Sets the parameters for this SSL context to the values in +params+.
111+
# call-seq:
112+
# ctx.set_params(params = {}) -> params
113+
#
114+
# Sets saner defaults optimized for the use with HTTP-like protocols.
115+
#
116+
# If a Hash +params+ is given, the parameters are overridden with it.
111117
# The keys in +params+ must be assignment methods on SSLContext.
112118
#
113119
# If the verify_mode is not VERIFY_NONE and ca_file, ca_path and
114120
# cert_store are not set then the system default certificate store is
115121
# used.
116-
117122
def set_params(params={})
118123
params = DEFAULT_PARAMS.merge(params)
119124
params.each{|name, value| self.__send__("#{name}=", value) }

test/test_ssl.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,53 @@ def test_tlsext_hostname
892892
end
893893
end
894894

895+
def test_verify_hostname_on_connect
896+
now = Time.now
897+
exts = [
898+
["keyUsage", "keyEncipherment,digitalSignature", true],
899+
["subjectAltName", "DNS:a.example.com,DNS:*.b.example.com,DNS:c*.example.com,DNS:d.*.example.com"],
900+
]
901+
cert = issue_cert(@svr, @svr_key, 4, now, now+1800, exts,
902+
@ca_cert, @ca_key, OpenSSL::Digest::SHA1.new)
903+
904+
ctx_proc = proc { |ctx|
905+
ctx.cert = cert
906+
ctx.key = @svr_key
907+
}
908+
909+
start_server(OpenSSL::SSL::VERIFY_NONE, true, ctx_proc: ctx_proc, ignore_listener_error: true) do |svr, port|
910+
ctx = OpenSSL::SSL::SSLContext.new
911+
ctx.verify_hostname = true
912+
ctx.cert_store = OpenSSL::X509::Store.new
913+
ctx.cert_store.add_cert(@ca_cert)
914+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
915+
916+
[
917+
["a.example.com", true],
918+
["A.Example.Com", true],
919+
["x.example.com", false],
920+
["b.example.com", false],
921+
["x.b.example.com", true],
922+
["cx.example.com", true],
923+
["d.x.example.com", false],
924+
].each do |name, expected_ok|
925+
begin
926+
sock = TCPSocket.new("127.0.0.1", port)
927+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
928+
ssl.hostname = name
929+
if expected_ok
930+
ssl.connect
931+
else
932+
assert_raise(OpenSSL::SSL::SSLError) { ssl.connect }
933+
end
934+
ensure
935+
sock.close if sock
936+
ssl.close if ssl
937+
end
938+
end
939+
end
940+
end
941+
895942
def test_multibyte_read_write
896943
#German a umlaut
897944
auml = [%w{ C3 A4 }.join('')].pack('H*')

0 commit comments

Comments
 (0)