Skip to content

Commit 9a16b81

Browse files
committed
set verify_result to V_ERR_HOSTNAME_MISMATCH on hostname fail
CRuby calls verify_certificate_identity in the verify callback and sets V_ERR_HOSTNAME_MISMATCH only when it returns `false`. Mirror that logic post-handshake. The verify_callback doesn't see this error on JRuby (JSSE limitation, documented in skipped test).
1 parent c2601b9 commit 9a16b81

3 files changed

Lines changed: 94 additions & 7 deletions

File tree

lib/openssl/ssl.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,14 @@ def post_connection_check(hostname)
449449
return true
450450
end
451451

452+
# @private JRuby due having to call this outside of post_connection_check
453+
def verify_certificate_identity_internal(hostname)
454+
peer_cert = self.peer_cert
455+
return nil if peer_cert.nil?
456+
OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
457+
end
458+
private :verify_certificate_identity_internal
459+
452460
# call-seq:
453461
# ssl.session -> aSession
454462
#

src/main/java/org/jruby/ext/openssl/SSLSocket.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,19 @@ private void verifyHostnameConnectionCheck(final ThreadContext context) {
424424
final IRubyObject hostname = getInstanceVariable("@hostname");
425425
if (hostname == null || hostname.isNil()) return;
426426

427-
// delegates to post_connection_check (defined in Ruby) which calls
428-
// OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
429-
callMethod(context, "post_connection_check", hostname);
430-
431-
// stash verified hostname so a subsequent explicit post_connection_check
432-
// (e.g. from net/http) does not execute the check twice (to match MRI)
433-
setInstanceVariable("@verified_hostname", hostname);
427+
// CRuby calls verify_certificate_identity inside the OpenSSL verify
428+
// callback and sets V_ERR_HOSTNAME_MISMATCH only when it returns false.
429+
IRubyObject result = callMethod(context, "verify_certificate_identity_internal", hostname);
430+
431+
if (result.isTrue()) {
432+
// stash verified hostname so a subsequent explicit post_connection_check
433+
// (e.g. from net/http) does not execute the check twice
434+
setInstanceVariable("@verified_hostname", hostname);
435+
} else if (!result.isNil()) { // false
436+
sslContext.setLastVerifyResult(verifyResult = X509Utils.V_ERR_HOSTNAME_MISMATCH);
437+
throw newSSLError(context.runtime, // same as `post_connection_check(hostname)`
438+
"hostname \"" + hostname + "\" does not match the server certificate");
439+
}
434440
}
435441

436442
final IRubyObject verify_mode(final ThreadContext context) {

src/test/ruby/ssl/test_ssl.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,79 @@ def test_verify_result_with_verify_peer_self_signed
274274
end
275275
end
276276

277+
# verify_result should report V_ERR_HOSTNAME_MISMATCH when hostname
278+
# verification fails during connect (matches CRuby behavior).
279+
def test_verify_result_hostname_mismatch
280+
now = Time.now
281+
exts = [
282+
["keyUsage", "keyEncipherment,digitalSignature", true],
283+
["subjectAltName", "DNS:a.example.com", false],
284+
]
285+
@svr_cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key,
286+
not_before: now, not_after: now + 1800)
287+
288+
start_server0(PORT, OpenSSL::SSL::VERIFY_NONE, true) do |server, port|
289+
ctx = OpenSSL::SSL::SSLContext.new
290+
ctx.verify_hostname = true
291+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
292+
ctx.cert_store = OpenSSL::X509::Store.new
293+
ctx.cert_store.add_cert(@ca_cert)
294+
295+
sock = TCPSocket.new("127.0.0.1", port)
296+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
297+
ssl.hostname = "b.example.com"
298+
assert_raise(OpenSSL::SSL::SSLError) { ssl.connect }
299+
assert_equal OpenSSL::X509::V_ERR_HOSTNAME_MISMATCH, ssl.verify_result
300+
ensure
301+
ssl&.close rescue nil
302+
sock&.close rescue nil
303+
end
304+
end
305+
306+
# Ported from CRuby's test_verify_hostname_failure_error_code.
307+
# CRuby invokes verify_callback with V_ERR_HOSTNAME_MISMATCH because the
308+
# hostname check runs inside OpenSSL's verify callback during handshake.
309+
# JRuby checks hostname post-handshake (JSSE limitation), so the callback
310+
# doesn't see the error. Skipped on JRuby; runs on CRuby for parity check.
311+
def test_verify_hostname_failure_error_code_via_callback
312+
skip 'verify_callback not invoked for hostname mismatch (JSSE limitation)' if defined?(JRUBY_VERSION)
313+
314+
now = Time.now
315+
exts = [
316+
["keyUsage", "keyEncipherment,digitalSignature", true],
317+
["subjectAltName", "DNS:a.example.com", false],
318+
]
319+
@svr_cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key,
320+
not_before: now, not_after: now + 1800)
321+
322+
start_server0(PORT, OpenSSL::SSL::VERIFY_NONE, true) do |server, port|
323+
verify_callback_ok = verify_callback_err = nil
324+
325+
ctx = OpenSSL::SSL::SSLContext.new
326+
ctx.verify_hostname = true
327+
ctx.cert_store = OpenSSL::X509::Store.new
328+
ctx.cert_store.add_cert(@ca_cert)
329+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
330+
ctx.verify_callback = -> (preverify_ok, store_ctx) {
331+
verify_callback_ok = preverify_ok
332+
verify_callback_err = store_ctx.error
333+
preverify_ok
334+
}
335+
336+
begin
337+
sock = TCPSocket.new("127.0.0.1", port)
338+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
339+
ssl.hostname = "b.example.com"
340+
assert_raise(OpenSSL::SSL::SSLError) { ssl.connect }
341+
assert_equal false, verify_callback_ok
342+
assert_equal OpenSSL::X509::V_ERR_HOSTNAME_MISMATCH, verify_callback_err
343+
ensure
344+
ssl&.close rescue nil
345+
sock&.close rescue nil
346+
end
347+
end
348+
end
349+
277350
def test_post_connect_check_with_anon_ciphers
278351
unless OpenSSL::ExtConfig::TLS_DH_anon_WITH_AES_256_GCM_SHA384
279352
return skip('OpenSSL::ExtConfig::TLS_DH_anon_WITH_AES_256_GCM_SHA384 not enabled')

0 commit comments

Comments
 (0)