From 297a003651206e354334eb2e01e6f6df38495b62 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Tue, 30 Dec 2025 17:37:03 +0900 Subject: [PATCH 1/9] ssl: move tests for IO-like methods to test_pair.rb r8081 originally intended test_pair.rb for testing methods that behave like IO. Move tests for #{get,read}byte, #sys{read,write}, #close_write, and IO.copy_stream from test_ssl.rb to test_pair.rb. Similarly, move tests for methods that are specific to SSLSocket and not for IO compatibility to test_ssl.rb. --- test/openssl/test_pair.rb | 159 ++++++++++--------------------- test/openssl/test_ssl.rb | 192 ++++++++++++++++++++++---------------- 2 files changed, 160 insertions(+), 191 deletions(-) diff --git a/test/openssl/test_pair.rb b/test/openssl/test_pair.rb index 10942191d..02525c8c3 100644 --- a/test/openssl/test_pair.rb +++ b/test/openssl/test_pair.rb @@ -97,28 +97,36 @@ module OpenSSL::TestPairM def test_getc ssl_pair {|s1, s2| s1 << "a" + s1.close assert_equal(?a, s2.getc) + assert_nil(s2.getc) } end def test_getbyte ssl_pair {|s1, s2| s1 << "a" + s1.close assert_equal(97, s2.getbyte) + assert_nil(s2.getbyte) } end - def test_readbyte + def test_readchar ssl_pair {|s1, s2| s1 << "b" - assert_equal(98, s2.readbyte) + s1.close + assert_equal("b", s2.readchar) + assert_raise(EOFError) { s2.readchar } } end - def test_readbyte_eof + def test_readbyte ssl_pair {|s1, s2| - s2.close - assert_raise(EOFError) { s1.readbyte } + s1 << "b" + s1.close + assert_equal(98, s2.readbyte) + assert_raise(EOFError) { s2.readbyte } } end @@ -216,6 +224,25 @@ def test_multibyte_read_write } end + def test_sysread_and_syswrite + ssl_pair {|s1, s2| + str = "x" * 100 + "\n" + s1.syswrite(str) + newstr = s2.sysread(str.bytesize) + assert_equal(str, newstr) + + buf = String.new + s1.syswrite(str) + assert_same(buf, s2.sysread(str.size, buf)) + assert_equal(str, buf) + + obj = Object.new + obj.define_singleton_method(:to_str) { str } + s1.syswrite(obj) + assert_equal(str, s2.sysread(str.bytesize)) + } + end + def test_read_nonblock ssl_pair {|s1, s2| err = nil @@ -393,116 +420,28 @@ def test_write_multiple_arguments } end - def test_partial_tls_record_read_nonblock + def test_copy_stream ssl_pair { |s1, s2| - # the beginning of a TLS record - s1.io.write("\x17") - # should raise a IO::WaitReadable since a full TLS record is not available - # for reading - assert_raise(IO::WaitReadable) { s2.read_nonblock(1) } + IO.pipe do |r, w| + str = "hello world\n" + w.write(str) + IO.copy_stream(r, s1, str.bytesize) + IO.copy_stream(s2, w, str.bytesize) + assert_equal(str, r.read(str.bytesize)) + end } end - def tcp_pair - host = "127.0.0.1" - serv = TCPServer.new(host, 0) - port = serv.connect_address.ip_port - sock1 = TCPSocket.new(host, port) - sock2 = serv.accept - serv.close - [sock1, sock2] - ensure - serv.close if serv && !serv.closed? - end - - def test_connect_accept_nonblock_no_exception - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.cert = @svr_cert - ctx2.key = @svr_key - - sock1, sock2 = tcp_pair - - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - accepted = s2.accept_nonblock(exception: false) - assert_equal :wait_readable, accepted - - ctx1 = OpenSSL::SSL::SSLContext.new - s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - th = Thread.new do - rets = [] - begin - rv = s1.connect_nonblock(exception: false) - rets << rv - case rv - when :wait_writable - IO.select(nil, [s1], nil, 5) - when :wait_readable - IO.select([s1], nil, nil, 5) - end - end until rv == s1 - rets - end - - until th.join(0.01) - accepted = s2.accept_nonblock(exception: false) - assert_include([s2, :wait_readable, :wait_writable ], accepted) - end - - rets = th.value - assert_instance_of Array, rets - rets.each do |rv| - assert_include([s1, :wait_readable, :wait_writable ], rv) - end - ensure - th.join if th - s1.close if s1 - s2.close if s2 - sock1.close if sock1 - sock2.close if sock2 - accepted.close if accepted.respond_to?(:close) - end - - def test_connect_accept_nonblock - ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = @svr_cert - ctx.key = @svr_key - - sock1, sock2 = tcp_pair - - th = Thread.new { - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx) - 5.times { - begin - break s2.accept_nonblock - rescue IO::WaitReadable - IO.select([s2], nil, nil, 1) - rescue IO::WaitWritable - IO.select(nil, [s2], nil, 1) - end - sleep 0.2 - } - } - - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - 5.times { - begin - break s1.connect_nonblock - rescue IO::WaitReadable - IO.select([s1], nil, nil, 1) - rescue IO::WaitWritable - IO.select(nil, [s1], nil, 1) - end - sleep 0.2 + def test_close_write + ssl_pair { |s1, s2| + message = "abc"*1024 + s1.write(message) + s1.close_write + assert_equal(message, s2.read) + s2.write(message) + s2.close_write + assert_equal(message, s1.read) } - - s2 = th.value - - s1.print "a\ndef" - assert_equal("a\n", s2.gets) - ensure - sock1&.close - sock2&.close - th&.join end end diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 914b80623..a4f0d25e8 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -108,6 +108,96 @@ def test_ssl_with_server_cert } end + def test_connect_accept_nonblock_no_exception + ctx2 = OpenSSL::SSL::SSLContext.new + ctx2.cert = @svr_cert + ctx2.key = @svr_key + + sock1, sock2 = socketpair + + s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) + accepted = s2.accept_nonblock(exception: false) + assert_equal :wait_readable, accepted + + ctx1 = OpenSSL::SSL::SSLContext.new + s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) + th = Thread.new do + rets = [] + begin + rv = s1.connect_nonblock(exception: false) + rets << rv + case rv + when :wait_writable + IO.select(nil, [s1], nil, 5) + when :wait_readable + IO.select([s1], nil, nil, 5) + end + end until rv == s1 + rets + end + + until th.join(0.01) + accepted = s2.accept_nonblock(exception: false) + assert_include([s2, :wait_readable, :wait_writable ], accepted) + end + + rets = th.value + assert_instance_of Array, rets + rets.each do |rv| + assert_include([s1, :wait_readable, :wait_writable ], rv) + end + ensure + th.join if th + s1.close if s1 + s2.close if s2 + sock1.close if sock1 + sock2.close if sock2 + accepted.close if accepted.respond_to?(:close) + end + + def test_connect_accept_nonblock + ctx = OpenSSL::SSL::SSLContext.new + ctx.cert = @svr_cert + ctx.key = @svr_key + + sock1, sock2 = socketpair + + th = Thread.new { + s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx) + 5.times { + begin + break s2.accept_nonblock + rescue IO::WaitReadable + IO.select([s2], nil, nil, 1) + rescue IO::WaitWritable + IO.select(nil, [s2], nil, 1) + end + sleep 0.2 + } + } + + s1 = OpenSSL::SSL::SSLSocket.new(sock1) + 5.times { + begin + break s1.connect_nonblock + rescue IO::WaitReadable + IO.select([s1], nil, nil, 1) + rescue IO::WaitWritable + IO.select(nil, [s1], nil, 1) + end + sleep 0.2 + } + + s2 = th.value + + s1.print "a\ndef" + assert_equal("a\n", s2.gets) + ensure + sock1&.close + sock2&.close + th&.join + end + def test_socket_open start_server { |port| begin @@ -158,30 +248,6 @@ def test_socket_open_with_local_address_port_context } end - def test_socket_close_write - server_proc = proc do |ctx, ssl| - message = ssl.read - ssl.write(message) - ssl.close_write - ensure - ssl.close - end - - start_server(server_proc: server_proc) do |port| - ctx = OpenSSL::SSL::SSLContext.new - ssl = OpenSSL::SSL::SSLSocket.open("127.0.0.1", port, context: ctx) - ssl.sync_close = true - ssl.connect - - message = "abc"*1024 - ssl.write message - ssl.close_write - assert_equal message, ssl.read - ensure - ssl&.close - end - end - def test_add_certificate ctx_proc = -> ctx { # Unset values set by start_server @@ -270,27 +336,6 @@ def test_extra_chain_cert_auto_chain end end - def test_sysread_and_syswrite - start_server { |port| - server_connect(port) { |ssl| - str = +("x" * 100 + "\n") - ssl.syswrite(str) - newstr = ssl.sysread(str.bytesize) - assert_equal(str, newstr) - - buf = String.new - ssl.syswrite(str) - assert_same buf, ssl.sysread(str.size, buf) - assert_equal(str, buf) - - obj = Object.new - obj.define_singleton_method(:to_str) { str } - ssl.syswrite(obj) - assert_equal(str, ssl.sysread(str.bytesize)) - } - } - end - def test_read_with_timeout omit "does not support timeout" unless IO.method_defined?(:timeout) @@ -314,30 +359,29 @@ def test_read_with_timeout end end - def test_getbyte - start_server { |port| - server_connect(port) { |ssl| - str = +("x" * 100 + "\n") - ssl.syswrite(str) - newstr = str.bytesize.times.map { |i| - ssl.getbyte - }.pack("C*") - assert_equal(str, newstr) - } - } - end + def test_partial_tls_record_read_nonblock + written = Thread::Queue.new + read = Thread::Queue.new + server_proc = -> (ctx, ssl) { + str = ssl.gets + ssl.puts(str) - def test_readbyte - start_server { |port| - server_connect(port) { |ssl| - str = +("x" * 100 + "\n") - ssl.syswrite(str) - newstr = str.bytesize.times.map { |i| - ssl.readbyte - }.pack("C*") - assert_equal(str, newstr) - } + # the beginning of a TLS record + ssl.io.write("\x17") + written << :written + read.pop } + start_server(server_proc: server_proc) do |port| + server_connect(port) do |ssl| + ssl.puts("abc") + assert_equal("abc\n", ssl.gets) + written.pop + # should raise a IO::WaitReadable since a full TLS record is not available + # for reading + assert_raise(IO::WaitReadable) { ssl.send(:sysread_nonblock, 1) } + read << :done + end + end end def test_sync_close @@ -383,20 +427,6 @@ def test_sync_close_initialize_opt end end - def test_copy_stream - start_server do |port| - server_connect(port) do |ssl| - IO.pipe do |r, w| - str = "hello world\n" - w.write(str) - IO.copy_stream(r, ssl, str.bytesize) - IO.copy_stream(ssl, w, str.bytesize) - assert_equal str, r.read(str.bytesize) - end - end - end - end - def test_verify_mode_default ctx = OpenSSL::SSL::SSLContext.new assert_equal OpenSSL::SSL::VERIFY_NONE, ctx.verify_mode From 7063d04b43f68e8e937b4fc44beb533202577c46 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Tue, 30 Dec 2025 17:43:50 +0900 Subject: [PATCH 2/9] ssl: simplify test_pair.rb OpenSSL::SSL::SSLSocket only depends on T_FILE and a small number of methods defined on IO, so the difference between TCPSocket and Socket is not significant for these tests. Test only one of them to reduce the test run time by half. Add a simple client using Socket to test_ssl.rb to keep basic coverage. Also simplify ut_eof.rb to test only one direction, since the direction does not matter after the handshake. --- test/openssl/test_pair.rb | 119 ++++++-------------------------------- test/openssl/test_ssl.rb | 13 +++++ test/openssl/ut_eof.rb | 11 ++++ 3 files changed, 43 insertions(+), 100 deletions(-) diff --git a/test/openssl/test_pair.rb b/test/openssl/test_pair.rb index 02525c8c3..8cb1060e4 100644 --- a/test/openssl/test_pair.rb +++ b/test/openssl/test_pair.rb @@ -2,35 +2,34 @@ require_relative 'utils' require_relative 'ut_eof' -if defined?(OpenSSL::SSL) +return unless defined?(OpenSSL::SSL) -module OpenSSL::SSLPairM - def setup +module OpenSSL::SSLPair + def ssl_pair svr_dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=localhost") ee_exts = [ ["keyUsage", "keyEncipherment,digitalSignature", true], ] - @svr_key = OpenSSL::TestUtils::Fixtures.pkey("rsa-1") - @svr_cert = issue_cert(svr_dn, @svr_key, 1, ee_exts, nil, nil) - end + svr_key = OpenSSL::TestUtils::Fixtures.pkey("rsa-1") + svr_cert = issue_cert(svr_dn, svr_key, 1, ee_exts, nil, nil) - def ssl_pair host = "127.0.0.1" - tcps = create_tcp_server(host, 0) - port = tcps.connect_address.ip_port + svr = TCPServer.new(host, 0) + svr.setsockopt(:TCP, :NODELAY, 1) + port = svr.connect_address.ip_port + tcps = nil th = Thread.new { + tcps = svr.accept sctx = OpenSSL::SSL::SSLContext.new - sctx.cert = @svr_cert - sctx.key = @svr_key - sctx.options |= OpenSSL::SSL::OP_NO_COMPRESSION - ssls = OpenSSL::SSL::SSLServer.new(tcps, sctx) - ns = ssls.accept - ssls.close - ns + sctx.add_certificate(svr_cert, svr_key) + ssl = OpenSSL::SSL::SSLSocket.new(tcps, sctx) + ssl.accept + ssl } - tcpc = create_tcp_client(host, port) + tcpc = TCPSocket.new(host, port) + tcpc.setsockopt(:TCP, :NODELAY, 1) c = OpenSSL::SSL::SSLSocket.new(tcpc) c.connect s = th.value @@ -39,57 +38,7 @@ def ssl_pair ensure tcpc&.close tcps&.close - s&.close - end -end - -module OpenSSL::SSLPair - include OpenSSL::SSLPairM - - def create_tcp_server(host, port) - TCPServer.new(host, port) - end - - def create_tcp_client(host, port) - TCPSocket.new(host, port) - end -end - -module OpenSSL::SSLPairLowlevelSocket - include OpenSSL::SSLPairM - - def create_tcp_server(host, port) - Addrinfo.tcp(host, port).listen - end - - def create_tcp_client(host, port) - Addrinfo.tcp(host, port).connect - end -end - -module OpenSSL::TestEOF1M - def open_file(content) - ssl_pair { |s1, s2| - begin - th = Thread.new { s2 << content; s2.close } - yield s1 - ensure - th&.join - end - } - end -end - -module OpenSSL::TestEOF2M - def open_file(content) - ssl_pair { |s1, s2| - begin - th = Thread.new { s1 << content; s1.close } - yield s2 - ensure - th&.join - end - } + svr&.close end end @@ -445,38 +394,8 @@ def test_close_write end end -class OpenSSL::TestEOF1 < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPair - include OpenSSL::TestEOF1M -end - -class OpenSSL::TestEOF1LowlevelSocket < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPairLowlevelSocket - include OpenSSL::TestEOF1M -end - -class OpenSSL::TestEOF2 < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPair - include OpenSSL::TestEOF2M -end - -class OpenSSL::TestEOF2LowlevelSocket < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPairLowlevelSocket - include OpenSSL::TestEOF2M -end - -class OpenSSL::TestPair < OpenSSL::TestCase +class OpenSSL::TestSSLPair < OpenSSL::TestCase include OpenSSL::SSLPair include OpenSSL::TestPairM -end - -class OpenSSL::TestPairLowlevelSocket < OpenSSL::TestCase - include OpenSSL::SSLPairLowlevelSocket - include OpenSSL::TestPairM -end - + include OpenSSL::TestEOF end diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index a4f0d25e8..f350ab9df 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -198,6 +198,19 @@ def test_connect_accept_nonblock th&.join end + def test_low_level_socket + start_server do |port| + sock = Socket.tcp("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.connect + ssl.puts("abc") + assert_equal("abc\n", ssl.gets) + ensure + ssl&.close + sock&.close + end + end + def test_socket_open start_server { |port| begin diff --git a/test/openssl/ut_eof.rb b/test/openssl/ut_eof.rb index 06aa632a6..807bbd015 100644 --- a/test/openssl/ut_eof.rb +++ b/test/openssl/ut_eof.rb @@ -4,6 +4,17 @@ if defined?(OpenSSL) module OpenSSL::TestEOF + def open_file(content) + ssl_pair { |s1, s2| + begin + th = Thread.new { s2 << content; s2.close } + yield s1 + ensure + th&.join + end + } + end + def test_getbyte_eof open_file("") {|f| assert_nil f.getbyte } end From b67520ff55a99de432a01ba147375761a51d2541 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 28 Nov 2025 03:34:14 +0900 Subject: [PATCH 3/9] ssl: refactor test helper start_server Break it into multiple pieces and simplify: - Let callers pass a complete SSLContext object instead of a callback proc ctx_proc to mutate it. - Add a variant start_server_proc for tests that need finer control, and remove two callbacks server_proc and accept_proc. - Remove rescue for IOError, Errno::EBADF, Errno::EINVAL, and Errno::ENOTSOCK which as far as I can tell should not be possible. --- test/openssl/test_ssl.rb | 563 ++++++++++++++++--------------- test/openssl/test_ssl_session.rb | 113 ++++--- test/openssl/utils.rb | 134 ++++---- 3 files changed, 405 insertions(+), 405 deletions(-) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index f350ab9df..8dbd48145 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -75,24 +75,23 @@ def test_ctx_options_config end def test_ssl_with_server_cert - ctx_proc = -> ctx { - ctx.cert = @svr_cert - ctx.key = @svr_key - ctx.extra_chain_cert = [@ca_cert] - } - server_proc = -> (ctx, ssl) { + sctx = OpenSSL::SSL::SSLContext.new + sctx.cert = @svr_cert + sctx.key = @svr_key + sctx.extra_chain_cert = [@ca_cert] + + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept assert_equal @svr_cert.to_der, ssl.cert.to_der assert_equal nil, ssl.peer_cert - - readwrite_loop(ctx, ssl) - } - start_server(ctx_proc: ctx_proc, server_proc: server_proc) { |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) { |port| begin sock = TCPSocket.new("127.0.0.1", port) ctx = OpenSSL::SSL::SSLContext.new ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) ssl.connect - assert_equal sock, ssl.io assert_equal nil, ssl.cert assert_equal @svr_cert.to_der, ssl.peer_cert.to_der @@ -262,12 +261,10 @@ def test_socket_open_with_local_address_port_context end def test_add_certificate - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA + + start_server(sctx) do |port| server_connect(port) { |ssl| assert_equal @svr_cert.subject, ssl.peer_cert.subject assert_equal [@svr_cert.subject, @ca_cert.subject], @@ -294,13 +291,11 @@ def test_add_certificate_multiple_certs ecdsa_dn = OpenSSL::X509::Name.parse_rfc2253("CN=localhost2") ecdsa_cert = issue_cert(ecdsa_dn, ecdsa_key, 456, exts, ca2_cert, ca2_key) - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA - ctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert]) - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA + sctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert]) + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 # TODO: We need this to force certificate type ctx.ciphers = "aECDSA" @@ -332,14 +327,12 @@ def test_extra_chain_cert_auto_chain # AWS-LC enables SSL_MODE_NO_AUTO_CHAIN by default unless aws_lc? - ctx_proc = -> ctx { - # Sanity check: start_server won't set extra_chain_cert - assert_nil ctx.extra_chain_cert - ctx.cert_store = OpenSSL::X509::Store.new.tap { |store| - store.add_cert(@ca_cert) - } + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key) # no extra certs + sctx.cert_store = OpenSSL::X509::Store.new.tap { |store| + store.add_cert(@ca_cert) } - start_server(ctx_proc: ctx_proc) { |port| + start_server(sctx) { |port| server_connect(port) { |ssl| ssl.puts "abc"; assert_equal "abc\n", ssl.gets assert_equal @svr_cert.to_der, ssl.peer_cert.to_der @@ -375,7 +368,8 @@ def test_read_with_timeout def test_partial_tls_record_read_nonblock written = Thread::Queue.new read = Thread::Queue.new - server_proc = -> (ctx, ssl) { + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept str = ssl.gets ssl.puts(str) @@ -383,8 +377,8 @@ def test_partial_tls_record_read_nonblock ssl.io.write("\x17") written << :written read.pop - } - start_server(server_proc: server_proc) do |port| + end + start_server_proc(server_proc) do |port| server_connect(port) do |ssl| ssl.puts("abc") assert_equal("abc\n", ssl.gets) @@ -479,19 +473,24 @@ def test_verify_mode_server_cert def test_verify_mode_client_cert_required # Optional, client certificate not supplied - vflag = OpenSSL::SSL::VERIFY_PEER - accept_proc = -> ssl { + sctx = make_server_context + sctx.verify_mode = OpenSSL::SSL::VERIFY_PEER + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept assert_equal nil, ssl.peer_cert - } - start_server(verify_mode: vflag, accept_proc: accept_proc) { |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) { |port| assert_nothing_raised { server_connect(port) { |ssl| ssl.puts("abc"); ssl.gets } } } # Required, client certificate not supplied - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - start_server(verify_mode: vflag, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + start_server(sctx, ignore_listener_error: true) { |port| assert_handshake_error { server_connect(port) { |ssl| ssl.puts("abc"); ssl.gets } } @@ -499,16 +498,18 @@ def test_verify_mode_client_cert_required end def test_client_auth_success - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - ctx_proc = proc { |ctx| - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - ctx.cert_store = store - # LibreSSL doesn't support client_cert_cb in TLS 1.3 - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? - } - start_server(verify_mode: vflag, ctx_proc: ctx_proc) { |port| + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + sctx.cert_store = store + # LibreSSL doesn't support client_cert_cb in TLS 1.3 + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? + + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.key = @cli_key ctx.cert = @cli_cert @@ -534,8 +535,11 @@ def test_client_auth_success end def test_client_cert_cb_ignore_error - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - start_server(verify_mode: vflag, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + + start_server(sctx, ignore_listener_error: true) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.client_cert_cb = -> ssl { raise "exception in client_cert_cb must be suppressed" @@ -552,16 +556,16 @@ def test_client_cert_cb_ignore_error def test_client_ca pend "LibreSSL doesn't support certificate_authorities" if libressl? - ctx_proc = Proc.new do |ctx| - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - ctx.cert_store = store - ctx.client_ca = [@ca_cert] - end + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + sctx.cert_store = store + sctx.client_ca = [@ca_cert] - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - start_server(verify_mode: vflag, ctx_proc: ctx_proc) { |port| + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new client_ca_from_server = nil ctx.client_cert_cb = Proc.new do |sslconn| @@ -723,13 +727,14 @@ def test_finished_messages client_finished = nil client_peer_finished = nil - start_server(accept_proc: proc { |server| - server_finished = server.finished_message - server_peer_finished = server.peer_finished_message - }) { |port| - ctx = OpenSSL::SSL::SSLContext.new - ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE - server_connect(port, ctx) { |ssl| + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept + server_finished = ssl.finished_message + server_peer_finished = ssl.peer_finished_message + readwrite_loop(ssl) + end + start_server_proc(server_proc) { |port| + server_connect(port) { |ssl| ssl.puts "abc"; ssl.gets client_finished = ssl.finished_message @@ -760,14 +765,13 @@ def test_post_connect_check_with_anon_ciphers omit_on_fips omit "AWS-LC does not support DHE ciphersuites" if aws_lc? - ctx_proc = -> ctx { - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.ciphers = "aNULL" - ctx.tmp_dh = Fixtures.pkey("dh-1") - ctx.security_level = 0 - } + sctx = OpenSSL::SSL::SSLContext.new + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.ciphers = "aNULL" + sctx.tmp_dh = Fixtures.pkey("dh-1") + sctx.security_level = 0 - start_server(ctx_proc: ctx_proc) { |port| + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION ctx.ciphers = "aNULL" @@ -1093,19 +1097,18 @@ def test_tlsext_hostname fooctx.cert = @cli_cert fooctx.key = @cli_key - ctx_proc = proc { |ctx| - ctx.servername_cb = proc { |ssl, servername| - case servername - when "foo.example.com" - fooctx - when "bar.example.com" - nil - else - raise "unreachable" - end - } + sctx = make_server_context + sctx.servername_cb = proc { |ssl, servername| + case servername + when "foo.example.com" + fooctx + when "bar.example.com" + nil + else + raise "unreachable" + end } - start_server(ctx_proc: ctx_proc) do |port| + start_server(sctx) do |port| sock = TCPSocket.new("127.0.0.1", port) begin ssl = OpenSSL::SSL::SSLSocket.new(sock) @@ -1203,17 +1206,16 @@ def test_accept_errors_include_peeraddr end def test_verify_hostname_on_connect - ctx_proc = proc { |ctx| - 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, exts, @ca_cert, @ca_key) - ctx.key = @svr_key - } + 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, exts, @ca_cert, @ca_key) + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(cert, @svr_key) - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + start_server(sctx, ignore_listener_error: true) do |port| ctx = OpenSSL::SSL::SSLContext.new assert_equal false, ctx.verify_hostname ctx.verify_hostname = true @@ -1249,16 +1251,15 @@ def test_verify_hostname_on_connect end def test_verify_hostname_failure_error_code - ctx_proc = proc { |ctx| - exts = [ - ["keyUsage", "keyEncipherment,digitalSignature", true], - ["subjectAltName", "DNS:a.example.com"], - ] - ctx.cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key) - ctx.key = @svr_key - } + exts = [ + ["keyUsage", "keyEncipherment,digitalSignature", true], + ["subjectAltName", "DNS:a.example.com"], + ] + cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key) + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(cert, @svr_key) - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + start_server(sctx, ignore_listener_error: true) do |port| verify_callback_ok = verify_callback_err = nil ctx = OpenSSL::SSL::SSLContext.new @@ -1294,12 +1295,13 @@ def test_connect_certificate_verify_failed_exception_message } } - ctx_proc = proc { |ctx| - now = Time.now - ctx.cert = issue_cert(@svr, @svr_key, 30, [], @ca_cert, @ca_key, - not_before: now - 7200, not_after: now - 3600) - } - start_server(ignore_listener_error: true, ctx_proc: ctx_proc) { |port| + now = Time.now + cert = issue_cert(@svr, @svr_key, 30, [], @ca_cert, @ca_key, + not_before: now - 7200, not_after: now - 3600) + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(cert, @svr_key) + + start_server(sctx, ignore_listener_error: true) { |port| store = OpenSSL::X509::Store.new store.add_cert(@ca_cert) ctx = OpenSSL::SSL::SSLContext.new @@ -1318,16 +1320,16 @@ def check_supported_protocol_versions OpenSSL::SSL::TLS1_2_VERSION, OpenSSL::SSL::TLS1_3_VERSION, ] - supported = [] - ctx_proc = proc { |ctx| - # The default security level is 1 in OpenSSL <= 3.1, 2 in OpenSSL >= 3.2 - # In OpenSSL >= 3.0, TLS 1.1 or older is disabled at level 1 - ctx.security_level = 0 - # Explicitly reset them to avoid influenced by OPENSSL_CONF - ctx.min_version = ctx.max_version = nil - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + + sctx = make_server_context + # The default security level is 1 in OpenSSL <= 3.1, 2 in OpenSSL >= 3.2 + # In OpenSSL >= 3.0, TLS 1.1 or older is disabled at level 1 + sctx.security_level = 0 + # Explicitly reset them to avoid influenced by OPENSSL_CONF + sctx.min_version = sctx.max_version = nil + + start_server(sctx, ignore_listener_error: true) do |port| possible_versions.each do |ver| ctx = OpenSSL::SSL::SSLContext.new ctx.security_level = 0 @@ -1349,20 +1351,19 @@ def check_supported_protocol_versions def test_set_params_min_version supported = check_supported_protocol_versions - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) + return unless supported.include?(OpenSSL::SSL::SSL3_VERSION) - if supported.include?(OpenSSL::SSL::SSL3_VERSION) - # SSLContext#set_params properly disables SSL 3.0 by default - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::SSL3_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| - ctx = OpenSSL::SSL::SSLContext.new - ctx.set_params(cert_store: store, verify_hostname: false) - assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } - } - end + # SSLContext#set_params properly disables SSL 3.0 by default + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::SSL3_VERSION + + start_server(sctx, ignore_listener_error: true) { |port| + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + ctx = OpenSSL::SSL::SSLContext.new + ctx.set_params(cert_store: store, verify_hostname: false) + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } + } end def test_minmax_version @@ -1381,11 +1382,11 @@ def test_minmax_version # Server enables a single version supported.each do |ver| - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = ctx.max_version = ver - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = sctx.max_version = ver + + start_server(sctx, ignore_listener_error: true) { |port| supported.each do |cver| # Client enables a single version ctx1 = OpenSSL::SSL::SSLContext.new @@ -1434,11 +1435,11 @@ def test_minmax_version # Server sets min_version (earliest is disabled) sver = supported[1] - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = sver - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = sver + + start_server(sctx, ignore_listener_error: true) { |port| supported.each do |cver| # Client sets min_version ctx1 = OpenSSL::SSL::SSLContext.new @@ -1468,12 +1469,12 @@ def test_minmax_version # Server sets max_version (latest is disabled) sver = supported[-2] - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = 0 - ctx.max_version = sver - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = 0 + sctx.max_version = sver + + start_server(sctx, ignore_listener_error: true) { |port| supported.each do |cver| # Client sets min_version ctx1 = OpenSSL::SSL::SSLContext.new @@ -1563,10 +1564,9 @@ def test_respect_system_default_min EOF f.close - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + start_server(sctx, ignore_listener_error: true) do |port| assert_separately([{ "OPENSSL_CONF" => f.path }, "-ropenssl", "-", port.to_s], <<~"end;") sock = TCPSocket.new("127.0.0.1", ARGV[0].to_i) ctx = OpenSSL::SSL::SSLContext.new @@ -1579,10 +1579,9 @@ def test_respect_system_default_min end; end - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_3_VERSION + start_server(sctx, ignore_listener_error: true) do |port| assert_separately([{ "OPENSSL_CONF" => f.path }, "-ropenssl", "-", port.to_s], <<~"end;") sock = TCPSocket.new("127.0.0.1", ARGV[0].to_i) ctx = OpenSSL::SSL::SSLContext.new @@ -1609,12 +1608,11 @@ def test_options_disable_versions end # Server disables TLS 1.2 and earlier - ctx_proc = proc { |ctx| - ctx.options |= OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 | - OpenSSL::SSL::OP_NO_TLSv1 | OpenSSL::SSL::OP_NO_TLSv1_1 | - OpenSSL::SSL::OP_NO_TLSv1_2 - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.options |= OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 | + OpenSSL::SSL::OP_NO_TLSv1 | OpenSSL::SSL::OP_NO_TLSv1_1 | + OpenSSL::SSL::OP_NO_TLSv1_2 + start_server(sctx, ignore_listener_error: true) { |port| # Client only supports TLS 1.2 ctx1 = OpenSSL::SSL::SSLContext.new ctx1.min_version = ctx1.max_version = OpenSSL::SSL::TLS1_2_VERSION @@ -1627,10 +1625,9 @@ def test_options_disable_versions } # Server only supports TLS 1.2 - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + start_server(sctx, ignore_listener_error: true) { |port| # Client doesn't support TLS 1.2 ctx1 = OpenSSL::SSL::SSLContext.new ctx1.options |= OpenSSL::SSL::OP_NO_TLSv1_2 @@ -1656,9 +1653,13 @@ def test_ssl_methods_constant def test_renegotiation_cb num_handshakes = 0 - renegotiation_cb = Proc.new { |ssl| num_handshakes += 1 } - ctx_proc = Proc.new { |ctx| ctx.renegotiation_cb = renegotiation_cb } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.renegotiation_cb = -> ssl { + num_handshakes += 1 + assert_kind_of(OpenSSL::SSL::SSLSocket, ssl) + } + + start_server(sctx) { |port| server_connect(port) { |ssl| assert_equal(1, num_handshakes) ssl.puts "abc"; assert_equal "abc\n", ssl.gets @@ -1668,13 +1669,10 @@ def test_renegotiation_cb def test_alpn_protocol_selection_ary advertised = ["http/1.1", "spdy/2"] - ctx_proc = Proc.new { |ctx| - ctx.alpn_select_cb = -> (protocols) { - protocols.first - } - ctx.alpn_protocols = advertised - } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.alpn_select_cb = -> protocols { protocols.first } + + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.alpn_protocols = advertised server_connect(port, ctx) { |ssl| @@ -1715,8 +1713,10 @@ def test_npn_protocol_selection_ary return unless OpenSSL::SSL::SSLContext.method_defined?(:npn_select_cb) advertised = ["http/1.1", "spdy/2"] - ctx_proc = proc { |ctx| ctx.npn_protocols = advertised } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.npn_protocols = advertised + + start_server(sctx) { |port| selector = lambda { |which| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 @@ -1738,8 +1738,10 @@ def advertised.each yield "http/1.1" yield "spdy/2" end - ctx_proc = Proc.new { |ctx| ctx.npn_protocols = advertised } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.npn_protocols = advertised + + start_server(sctx) { |port| selector = lambda { |selected, which| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 @@ -1756,8 +1758,10 @@ def advertised.each def test_npn_protocol_selection_cancel return unless OpenSSL::SSL::SSLContext.method_defined?(:npn_select_cb) - ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["http/1.1"] } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.npn_protocols = ["http/1.1"] + + start_server(sctx, ignore_listener_error: true) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 ctx.npn_select_cb = -> (protocols) { raise RuntimeError.new } @@ -1778,8 +1782,10 @@ def test_npn_advertised_protocol_too_long def test_npn_selected_protocol_too_long return unless OpenSSL::SSL::SSLContext.method_defined?(:npn_select_cb) - ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["http/1.1"] } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.npn_protocols = ["http/1.1"] + + start_server(sctx, ignore_listener_error: true) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 ctx.npn_select_cb = -> (protocols) { "a" * 256 } @@ -1787,13 +1793,14 @@ def test_npn_selected_protocol_too_long } end - def readwrite_loop_safe(ctx, ssl) - readwrite_loop(ctx, ssl) - rescue OpenSSL::SSL::SSLError - end - def test_close_after_socket_close - start_server(server_proc: method(:readwrite_loop_safe)) { |port| + # The client closes the TCP socket without SSLSocket#stop + sctx = make_server_context + if defined?(OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF) + sctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF + end + + start_server(sctx) { |port| sock = TCPSocket.new("127.0.0.1", port) ssl = OpenSSL::SSL::SSLSocket.new(sock) ssl.connect @@ -1819,11 +1826,10 @@ def test_get_ephemeral_key omit_on_fips # kRSA - ctx_proc1 = proc { |ctx| - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.ciphers = "kRSA" - } - start_server(ctx_proc: ctx_proc1, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.ciphers = "kRSA" + start_server(sctx, ignore_listener_error: true) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION ctx.ciphers = "kRSA" @@ -1852,10 +1858,9 @@ def test_get_ephemeral_key end # ECDHE - ctx_proc3 = proc { |ctx| - ctx.groups = "P-256" - } - start_server(ctx_proc: ctx_proc3) do |port| + sctx = make_server_context + sctx.groups = "P-256" + start_server(sctx) do |port| server_connect(port) { |ssl| assert_instance_of OpenSSL::PKey::EC, ssl.tmp_key ssl.puts "abc"; assert_equal "abc\n", ssl.gets @@ -1879,12 +1884,11 @@ def test_fallback_scsv server_connect(port, ctx) end - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = 0 - ctx.max_version = OpenSSL::SSL::TLS1_1_VERSION - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = 0 + sctx.max_version = OpenSSL::SSL::TLS1_1_VERSION + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.enable_fallback_scsv ctx.security_level = 0 @@ -1940,15 +1944,16 @@ def test_tmp_dh_callback dh = Fixtures.pkey("dh-1") called = false - ctx_proc = -> ctx { - ctx.max_version = :TLS1_2 - ctx.ciphers = "DH:!NULL" - ctx.tmp_dh_callback = ->(*args) { - called = true - dh - } + + sctx = make_server_context + sctx.max_version = :TLS1_2 + sctx.ciphers = "DH:!NULL" + sctx.tmp_dh_callback = ->(*args) { + called = true + dh } - start_server(ctx_proc: ctx_proc) do |port| + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256" # Exclude RFC 7919 groups server_connect(port, ctx) { |ssl| @@ -2048,13 +2053,11 @@ def test_sigalgs ecdsa_key = Fixtures.pkey("p256") ecdsa_cert = issue_cert(@svr, ecdsa_key, 10, svr_exts, @ca_cert, @ca_key) - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA - ctx.add_certificate(ecdsa_cert, ecdsa_key, [@ca_cert]) # ECDSA - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA + sctx.add_certificate(ecdsa_cert, ecdsa_key, [@ca_cert]) # ECDSA + + start_server(sctx) do |port| ctx1 = OpenSSL::SSL::SSLContext.new ctx1.sigalgs = "rsa_pss_rsae_sha256" server_connect(port, ctx1) { |ssl| @@ -2091,15 +2094,16 @@ def test_client_sigalgs ecdsa_key = Fixtures.pkey("p256") ecdsa_cert = issue_cert(@cli, ecdsa_key, 10, cli_exts, @ca_cert, @ca_key) - ctx_proc = -> ctx { - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - ctx.cert_store = store - ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - ctx.client_sigalgs = "ECDSA+SHA256" - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + sctx = make_server_context + sctx.cert_store = store + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + sctx.client_sigalgs = "ECDSA+SHA256" + + start_server(sctx, ignore_listener_error: true) do |port| ctx1 = OpenSSL::SSL::SSLContext.new ctx1.add_certificate(@cli_cert, @cli_key) # RSA assert_handshake_error { @@ -2121,13 +2125,14 @@ def test_get_sigalg # SSL_get0_peer_signature_name() not supported return unless openssl?(3, 5, 0) - server_proc = -> (ctx, ssl) { + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept assert_equal('rsa_pss_rsae_sha256', ssl.sigalg) assert_nil(ssl.peer_sigalg) - readwrite_loop(ctx, ssl) - } - start_server(server_proc: server_proc) do |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) do |port| cli_ctx = OpenSSL::SSL::SSLContext.new server_connect(port, cli_ctx) do |ssl| assert_nil(ssl.sigalg) @@ -2149,20 +2154,18 @@ def test_pqc_sigalg digest: nil) rsa = Fixtures.pkey("rsa-1") rsa_cert = issue_cert(@svr, rsa, 61, [], @ca_cert, @ca_key) - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.sigalgs = "rsa_pss_rsae_sha256:mldsa65" - ctx.add_certificate(mldsa_cert, mldsa) - ctx.add_certificate(rsa_cert, rsa) - } - server_proc = -> (ctx, ssl) { - assert_equal('mldsa65', ssl.sigalg) + sctx = OpenSSL::SSL::SSLContext.new + sctx.sigalgs = "rsa_pss_rsae_sha256:mldsa65" + sctx.add_certificate(mldsa_cert, mldsa) + sctx.add_certificate(rsa_cert, rsa) - readwrite_loop(ctx, ssl) - } - start_server(ctx_proc: ctx_proc, server_proc: server_proc) do |port| + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept + assert_equal('mldsa65', ssl.sigalg) + readwrite_loop(ssl) + end + start_server_proc(server_proc) do |port| ctx = OpenSSL::SSL::SSLContext.new # Set signature algorithm because while OpenSSL may use ML-DSA by # default, the system OpenSSL configuration affects the used signature @@ -2174,12 +2177,12 @@ def test_pqc_sigalg } end - server_proc = -> (ctx, ssl) { + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept assert_equal('rsa_pss_rsae_sha256', ssl.sigalg) - - readwrite_loop(ctx, ssl) - } - start_server(ctx_proc: ctx_proc, server_proc: server_proc) do |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.sigalgs = 'rsa_pss_rsae_sha256' server_connect(port, ctx) { |ssl| @@ -2192,12 +2195,12 @@ def test_pqc_sigalg def test_connect_works_when_setting_dh_callback_to_nil omit "AWS-LC does not support DHE ciphersuites" if aws_lc? - ctx_proc = -> ctx { - ctx.max_version = :TLS1_2 - ctx.ciphers = "DH:!NULL" # use DH - ctx.tmp_dh_callback = nil - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.max_version = :TLS1_2 + sctx.ciphers = "DH:!NULL" # use DH + sctx.tmp_dh_callback = nil + + start_server(sctx) do |port| assert_nothing_raised { server_connect(port) { } } end end @@ -2208,12 +2211,12 @@ def test_tmp_dh omit "AWS-LC does not support DHE ciphersuites" if aws_lc? dh = Fixtures.pkey("dh-1") - ctx_proc = -> ctx { - ctx.max_version = :TLS1_2 - ctx.ciphers = "DH:!NULL" # use DH - ctx.tmp_dh = dh - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.max_version = :TLS1_2 + sctx.ciphers = "DH:!NULL" # use DH + sctx.tmp_dh = dh + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256" # Exclude RFC 7919 groups server_connect(port, ctx) { |ssl| @@ -2223,13 +2226,13 @@ def test_tmp_dh end def test_set_groups_tls12 - ctx_proc = -> ctx { - # Enable both ECDHE (~ TLS 1.2) cipher suites and TLS 1.3 - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.ciphers = "kEECDH" - ctx.groups = "P-384:P-521" - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + # Enable both ECDHE (~ TLS 1.2) cipher suites and TLS 1.3 + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.ciphers = "kEECDH" + sctx.groups = "P-384:P-521" + + start_server(sctx, ignore_listener_error: true) do |port| # Test 1: Client=P-256:P-384, Server=P-384:P-521 --> P-384 ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256:P-384" @@ -2267,11 +2270,11 @@ def test_set_groups_tls12 end def test_set_groups_tls13 - ctx_proc = -> ctx { - # Assume TLS 1.3 is enabled and chosen by default - ctx.groups = "P-384:P-521" - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + # Assume TLS 1.3 is enabled and chosen by default + sctx = make_server_context + sctx.groups = "P-384:P-521" + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256:P-384" # disable P-521 @@ -2294,10 +2297,10 @@ def test_pqc_group 'SecP256r1MLKEM768', 'SecP384r1MLKEM1024' ].each do |group| - ctx_proc = -> ctx { - ctx.groups = group - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.groups = group + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = group server_connect(port, ctx) { |ssl| diff --git a/test/openssl/test_ssl_session.rb b/test/openssl/test_ssl_session.rb index 37874ca27..ab6379385 100644 --- a/test/openssl/test_ssl_session.rb +++ b/test/openssl/test_ssl_session.rb @@ -5,10 +5,10 @@ class OpenSSL::TestSSLSession < OpenSSL::SSLTestCase def test_session - ctx_proc = proc { |ctx| - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + + start_server(sctx) do |port| server_connect_with_session(port, nil, nil) { |ssl| session = ssl.session assert(session == OpenSSL::SSL::Session.new(session.to_pem)) @@ -120,14 +120,15 @@ def test_resumption } } - ctx_proc = proc { |ctx| - ctx.options &= ~OpenSSL::SSL::OP_NO_TICKET - # Disable server-side session cache which is enabled by default - ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_OFF - # Session tickets must be retrieved via ctx.session_new_cb in TLS 1.3 in AWS-LC. - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? || aws_lc? - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.options &= ~OpenSSL::SSL::OP_NO_TICKET + # Disable server-side session cache which is enabled by default + sctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_OFF + # Session tickets must be retrieved via ctx.session_new_cb in TLS 1.3 in + # AWS-LC. + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? || aws_lc? + + start_server(sctx) do |port| sess1 = server_connect_with_session(port, nil, nil) { |ssl| ssl.puts("abc"); assert_equal "abc\n", ssl.gets assert_equal false, ssl.session_reused? @@ -147,14 +148,16 @@ def test_resumption end def test_server_session_cache - ctx_proc = Proc.new do |ctx| - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.options |= OpenSSL::SSL::OP_NO_TICKET - end + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.options |= OpenSSL::SSL::OP_NO_TICKET connections = nil saved_session = nil - server_proc = Proc.new do |ctx, ssl| + + server_proc = Proc.new do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept + ctx = ssl.context stats = ctx.session_cache_stats case connections @@ -194,10 +197,9 @@ def test_server_session_cache assert_equal true, ctx.session_add(saved_session.dup) end - readwrite_loop(ctx, ssl) + readwrite_loop(ssl) end - - start_server(ctx_proc: ctx_proc, server_proc: server_proc) do |port| + start_server_proc(server_proc) do |port| first_session = nil 10.times do |i| connections = i @@ -285,11 +287,12 @@ def test_ctx_client_session_cb_tls13 def test_ctx_client_session_cb_tls13_exception omit "LibreSSL does not call session_new_cb in TLS 1.3" if libressl? - server_proc = lambda do |ctx, ssl| - readwrite_loop(ctx, ssl) - rescue SystemCallError, OpenSSL::SSL::SSLError + sctx = make_server_context + if defined?(OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF) + sctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF end - start_server(server_proc: server_proc) do |port| + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.min_version = :TLS1_3 ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT @@ -308,42 +311,42 @@ def test_ctx_client_session_cb_tls13_exception def test_ctx_server_session_cb connections = nil called = {} + cctx = OpenSSL::SSL::SSLContext.new cctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - sctx = nil - ctx_proc = Proc.new { |ctx| - sctx = ctx - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.options |= OpenSSL::SSL::OP_NO_TICKET - - # get_cb is called whenever a client proposed to resume a session but - # the session could not be found in the internal session cache. - last_server_session = nil - ctx.session_get_cb = lambda { |ary| - _sess, data = ary - called[:get] = data - - if connections == 2 - last_server_session.dup - else - nil - end - } - ctx.session_new_cb = lambda { |ary| - _sock, sess = ary - called[:new] = sess - last_server_session = sess - } - - if TEST_SESSION_REMOVE_CB - ctx.session_remove_cb = lambda { |ary| - _ctx, sess = ary - called[:remove] = sess - } + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.options |= OpenSSL::SSL::OP_NO_TICKET + + # get_cb is called whenever a client proposed to resume a session but + # the session could not be found in the internal session cache. + last_server_session = nil + sctx.session_get_cb = lambda { |ary| + _sess, data = ary + called[:get] = data + + if connections == 2 + last_server_session.dup + else + nil end } - start_server(ctx_proc: ctx_proc) do |port| + + sctx.session_new_cb = lambda { |ary| + _sock, sess = ary + called[:new] = sess + last_server_session = sess + } + + if TEST_SESSION_REMOVE_CB + sctx.session_remove_cb = lambda { |ary| + _ctx, sess = ary + called[:remove] = sess + } + end + + start_server(sctx) do |port| connections = 0 sess0 = server_connect_with_session(port, cctx, nil) { |ssl| ssl.puts("abc"); assert_equal "abc\n", ssl.gets diff --git a/test/openssl/utils.rb b/test/openssl/utils.rb index 7e6fe8b16..fc02bcd51 100644 --- a/test/openssl/utils.rb +++ b/test/openssl/utils.rb @@ -190,93 +190,87 @@ def setup @server = nil end - def readwrite_loop(ctx, ssl) + def readwrite_loop(ssl) while line = ssl.gets ssl.write(line) end + ssl.close end - def start_server(verify_mode: OpenSSL::SSL::VERIFY_NONE, - ctx_proc: nil, server_proc: method(:readwrite_loop), - accept_proc: proc{}, - ignore_listener_error: false, &block) - IO.pipe {|stop_pipe_r, stop_pipe_w| - ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = @svr_cert - ctx.key = @svr_key - ctx.verify_mode = verify_mode - ctx_proc.call(ctx) if ctx_proc - - Socket.do_not_reverse_lookup = true + def make_server_context + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key) + sctx + end + + def start_server_proc(server_proc, &block) + IO.pipe do |stop_pipe_r, stop_pipe_w| tcps = TCPServer.new("127.0.0.1", 0) + tcps.setsockopt(:TCP, :NODELAY, 1) port = tcps.connect_address.ip_port - ssls = OpenSSL::SSL::SSLServer.new(tcps, ctx) - threads = [] - begin - server_thread = Thread.new do - Thread.current.report_on_exception = false - - begin - loop do - begin - readable, = IO.select([ssls, stop_pipe_r]) - break if readable.include? stop_pipe_r - ssl = ssls.accept - accept_proc.call(ssl) - rescue OpenSSL::SSL::SSLError, IOError, Errno::EBADF, Errno::EINVAL, - Errno::ECONNABORTED, Errno::ENOTSOCK, Errno::ECONNRESET - retry if ignore_listener_error - raise - end - - th = Thread.new do - Thread.current.report_on_exception = false - - begin - server_proc.call(ctx, ssl) - ensure - ssl.close - end - true - end - threads << th - end - ensure - tcps.close - end - end + server_thread = Thread.new do + Thread.current.report_on_exception = false + + loop do + readable, = IO.select([tcps, stop_pipe_r]) + break if readable.include? stop_pipe_r + sock = tcps.accept - client_thread = Thread.new do - Thread.current.report_on_exception = false + th = Thread.new do + Thread.current.report_on_exception = false - begin - block.call(port) + server_proc.call(sock) ensure - # Stop accepting new connection - stop_pipe_w.close - server_thread.join + sock.close end + threads << th end - threads.unshift client_thread ensure - # Terminate existing connections. If a thread did 'pend', re-raise it. - pend = nil - threads.each { |th| - begin - timeout = EnvUtil.apply_timeout_scale(30) - th.join(timeout) or - th.raise(RuntimeError, "[start_server] thread did not exit in #{timeout} secs") - rescue Test::Unit::PendedError - pend = $! - rescue Exception - end - } - raise pend if pend - assert_join_threads(threads) + tcps.close + end + + client_thread = Thread.new do + Thread.current.report_on_exception = false + + block.call(port) + ensure + # Stop accepting new connection + stop_pipe_w.close + server_thread.join + end + threads.unshift client_thread + ensure + # Terminate existing connections. If a thread did 'pend', re-raise it. + pend = nil + threads.each { |th| + begin + timeout = EnvUtil.apply_timeout_scale(30) + th.join(timeout) or + th.raise(RuntimeError, "[start_server] thread did not exit in #{timeout} secs") + rescue Test::Unit::PendedError + pend = $! + rescue Exception + end + } + raise pend if pend + assert_join_threads(threads) + end + end + + def start_server(ctx = make_server_context, ignore_listener_error: false, &block) + server_proc = -> sock { + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + begin + ssl.accept + rescue OpenSSL::SSL::SSLError, Errno::ECONNABORTED, Errno::ECONNRESET + next if ignore_listener_error + raise end + readwrite_loop(ssl) } + start_server_proc(server_proc, &block) end end From fc60c97ee334081917cf388275fc460204b302f3 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Tue, 2 Dec 2025 17:33:47 +0900 Subject: [PATCH 4/9] ssl: delay closing TCP sockets in start_server Some tests expect server-side SSLSocket#accept to fail for various reasons. On some systems, closing the underlying socket immediately with IO#close causes the TCP connection to be terminated with RST. Do not close it immediately so that the client can reliably receive the TLS alert. This allows writing more meaningful assertions. Also add a dedicated test case for the rb_sys_fail() path in SSLSocket#connect. --- test/openssl/test_ssl.rb | 38 ++++++++++++++++++++++++++------------ test/openssl/utils.rb | 6 +++--- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 8dbd48145..5db46bcc9 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -491,7 +491,8 @@ def test_verify_mode_client_cert_required sctx.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT start_server(sctx, ignore_listener_error: true) { |port| - assert_handshake_error { + # TLS 1.3 alert: certificate_required(116) + assert_raise_with_message(OpenSSL::SSL::SSLError, /alert number 116/) { server_connect(port) { |ssl| ssl.puts("abc"); ssl.gets } } } @@ -547,7 +548,7 @@ def test_client_cert_cb_ignore_error # 1. Exception in client_cert_cb is suppressed # 2. No client certificate will be sent to the server # 3. SSL_VERIFY_FAIL_IF_NO_PEER_CERT causes the handshake to fail - assert_handshake_error { + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) { |ssl| ssl.puts("abc"); ssl.gets } } end @@ -1286,6 +1287,27 @@ def test_verify_hostname_failure_error_code end end + def test_connect_systemcallerror + # SSL_connect() should fail with SSL_ERROR_SYSCALL and errno should be + # kept intact from the underlying recv(2)/send(2). + pend "AWS-LC does not preserve errno on SSL_ERROR_SYSCALL" if aws_lc? + + server_proc = proc do |sock| + sock.setsockopt(:SOCKET, :LINGER, [1, 0].pack("ii")) + sock.read(1) + sock.close + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + assert_raise(Errno::ECONNRESET, Errno::EPIPE) { + ssl.connect + } + ensure + sock&.close + end + end + def test_connect_certificate_verify_failed_exception_message start_server(ignore_listener_error: true) { |port| ctx = OpenSSL::SSL::SSLContext.new @@ -1338,7 +1360,7 @@ def check_supported_protocol_versions ssl.puts "abc"; assert_equal "abc\n", ssl.gets } supported << ver - rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET + rescue OpenSSL::SSL::SSLError end end @@ -2106,7 +2128,7 @@ def test_client_sigalgs start_server(sctx, ignore_listener_error: true) do |port| ctx1 = OpenSSL::SSL::SSLContext.new ctx1.add_certificate(@cli_cert, @cli_key) # RSA - assert_handshake_error { + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx1) { |ssl| ssl.puts("abc"); ssl.gets } @@ -2458,14 +2480,6 @@ def server_connect(port, ctx = nil) sock.close end end - - def assert_handshake_error - # different OpenSSL versions react differently when facing a SSL/TLS version - # that has been marked as forbidden, therefore any of these may be raised - assert_raise(OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::EPIPE) { - yield - } - end end end diff --git a/test/openssl/utils.rb b/test/openssl/utils.rb index fc02bcd51..7dd3a719d 100644 --- a/test/openssl/utils.rb +++ b/test/openssl/utils.rb @@ -210,20 +210,19 @@ def start_server_proc(server_proc, &block) port = tcps.connect_address.ip_port threads = [] + sockets = [] server_thread = Thread.new do Thread.current.report_on_exception = false loop do readable, = IO.select([tcps, stop_pipe_r]) break if readable.include? stop_pipe_r - sock = tcps.accept + sockets << sock = tcps.accept th = Thread.new do Thread.current.report_on_exception = false server_proc.call(sock) - ensure - sock.close end threads << th end @@ -254,6 +253,7 @@ def start_server_proc(server_proc, &block) rescue Exception end } + sockets.each(&:close) raise pend if pend assert_join_threads(threads) end From c30c576522f36578f701d8d1c99c616dcdb1e3fa Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 28 Nov 2025 03:34:37 +0900 Subject: [PATCH 5/9] ssl: use start_server_proc in more test cases Update more tests to use start_server_proc instead of UNIXSocket.pair. It runs threads with timeout and prevents "rake test" from hanging indefinitely. It also produces better error messages when a test fails. --- test/openssl/test_ssl.rb | 331 ++++++++++++++++----------------------- 1 file changed, 135 insertions(+), 196 deletions(-) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 5db46bcc9..a252a0e64 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -108,62 +108,52 @@ def test_ssl_with_server_cert end def test_connect_accept_nonblock_no_exception - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.cert = @svr_cert - ctx2.key = @svr_key - - sock1, sock2 = socketpair - - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - accepted = s2.accept_nonblock(exception: false) - assert_equal :wait_readable, accepted - - ctx1 = OpenSSL::SSL::SSLContext.new - s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - th = Thread.new do - rets = [] - begin - rv = s1.connect_nonblock(exception: false) - rets << rv + server_proc = proc do |sock| + s2 = OpenSSL::SSL::SSLSocket.new(sock, make_server_context) + accepted = s2.accept_nonblock(exception: false) + assert_equal(:wait_readable, accepted) + loop do + rv = s2.accept_nonblock(exception: false) case rv - when :wait_writable - IO.select(nil, [s1], nil, 5) when :wait_readable - IO.select([s1], nil, nil, 5) + IO.select([s2], nil, nil, 1) + when :wait_writable + IO.select(nil, [s2], nil, 1) + else + assert_same(s2, rv) + break end - end until rv == s1 - rets - end - - until th.join(0.01) - accepted = s2.accept_nonblock(exception: false) - assert_include([s2, :wait_readable, :wait_writable ], accepted) + end + assert_equal("abc\n", s2.gets) + s2.puts("abc") + s2.close end - - rets = th.value - assert_instance_of Array, rets - rets.each do |rv| - assert_include([s1, :wait_readable, :wait_writable ], rv) + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + s1 = OpenSSL::SSL::SSLSocket.new(sock) + loop do + rv = s1.connect_nonblock(exception: false) + case rv + when :wait_readable + IO.select([s1], nil, nil, 1) + when :wait_writable + IO.select(nil, [s1], nil, 1) + else + assert_same(s1, rv) + break + end + end + s1.puts("abc") + assert_equal("abc\n", s1.gets) + ensure + sock&.close end - ensure - th.join if th - s1.close if s1 - s2.close if s2 - sock1.close if sock1 - sock2.close if sock2 - accepted.close if accepted.respond_to?(:close) end def test_connect_accept_nonblock - ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = @svr_cert - ctx.key = @svr_key - - sock1, sock2 = socketpair - - th = Thread.new { - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx) - 5.times { + server_proc = proc do |sock| + s2 = OpenSSL::SSL::SSLSocket.new(sock, make_server_context) + rv = 5.times { begin break s2.accept_nonblock rescue IO::WaitReadable @@ -171,30 +161,29 @@ def test_connect_accept_nonblock rescue IO::WaitWritable IO.select(nil, [s2], nil, 1) end - sleep 0.2 } - } - - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - 5.times { - begin - break s1.connect_nonblock - rescue IO::WaitReadable - IO.select([s1], nil, nil, 1) - rescue IO::WaitWritable - IO.select(nil, [s1], nil, 1) - end - sleep 0.2 - } - - s2 = th.value - - s1.print "a\ndef" - assert_equal("a\n", s2.gets) - ensure - sock1&.close - sock2&.close - th&.join + assert_same(s2, rv) + assert_equal("a\n", s2.gets) + s2.puts("b") + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + s1 = OpenSSL::SSL::SSLSocket.new(sock) + rv = 5.times { + begin + break s1.connect_nonblock + rescue IO::WaitReadable + IO.select([s1], nil, nil, 1) + rescue IO::WaitWritable + IO.select(nil, [s1], nil, 1) + end + } + assert_same(s1, rv) + s1.print "a\ndef" + assert_equal("b\n", s1.gets) + ensure + sock&.close + end end def test_low_level_socket @@ -1039,14 +1028,6 @@ def create_null_byte_SAN_certificate(critical = false) cert end - def socketpair - if defined? UNIXSocket - UNIXSocket.pair - else - Socket.pair(Socket::AF_INET, Socket::SOCK_STREAM, 0) - end - end - def test_keylog_cb omit "Keylog callback is not supported" if libressl? @@ -1140,70 +1121,41 @@ def test_tlsext_hostname end def test_servername_cb_exception - sock1, sock2 = socketpair - - t = Thread.new { - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - s1.hostname = "localhost" + server_proc = proc do |sock| + sctx = make_server_context + sctx.servername_cb = lambda { |args| raise RuntimeError, "foo" } + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + assert_raise_with_message(RuntimeError, "foo") { ssl.accept } + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.hostname = "example.org" assert_raise_with_message(OpenSSL::SSL::SSLError, /unrecognized.name/i) { - s1.connect + ssl.connect } - } - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.servername_cb = lambda { |args| raise RuntimeError, "foo" } - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - assert_raise_with_message(RuntimeError, "foo") { s2.accept } - assert t.join - ensure - sock1.close - sock2.close - t.kill.join + ensure + sock&.close + end end def test_servername_cb_raises_an_exception_on_unknown_objects - sock1, sock2 = socketpair - - t = Thread.new { - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - s1.hostname = "localhost" - assert_raise(OpenSSL::SSL::SSLError) { s1.connect } - } - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.servername_cb = lambda { |args| Object.new } - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - assert_raise(ArgumentError) { s2.accept } - assert t.join - ensure - sock1.close - sock2.close - t.kill.join - end - - def test_accept_errors_include_peeraddr - context = OpenSSL::SSL::SSLContext.new - context.cert = @svr_cert - context.key = @svr_key - - server = TCPServer.new("127.0.0.1", 0) - port = server.connect_address.ip_port - - ssl_server = OpenSSL::SSL::SSLServer.new(server, context) - - t = Thread.new do - assert_raise_with_message(OpenSSL::SSL::SSLError, /peeraddr=127\.0\.0\.1/) do - ssl_server.accept - end + server_proc = proc do |sock| + sctx = make_server_context + sctx.servername_cb = lambda { |args| Object.new } + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + assert_raise(ArgumentError) { ssl.accept } + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.hostname = "example.org" + assert_raise_with_message(OpenSSL::SSL::SSLError, /unrecognized.name/i) { + ssl.connect + } + ensure + sock&.close end - - sock = TCPSocket.new("127.0.0.1", port) - sock << "\x00" * 1024 - - assert t.join - ensure - sock&.close - server.close end def test_verify_hostname_on_connect @@ -1334,6 +1286,16 @@ def test_connect_certificate_verify_failed_exception_message } end + def test_connect_exception_message_include_peeraddr + start_server(ignore_listener_error: true) do |port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER + assert_raise_with_message(OpenSSL::SSL::SSLError, /peeraddr=127\.0\.0\.1/) do + server_connect(port, ctx) { } + end + end + end + def check_supported_protocol_versions possible_versions = [ OpenSSL::SSL::SSL3_VERSION, @@ -1705,30 +1667,20 @@ def test_alpn_protocol_selection_ary end def test_alpn_protocol_selection_cancel - sock1, sock2 = socketpair - - ctx1 = OpenSSL::SSL::SSLContext.new - ctx1.cert = @svr_cert - ctx1.key = @svr_key - ctx1.alpn_select_cb = -> (protocols) { nil } - ssl1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.alpn_protocols = ["http/1.1"] - ssl2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - - t = Thread.new { - ssl2.connect_nonblock(exception: false) - } - assert_raise_with_message(TypeError, /nil/) { ssl1.accept } - t.join - ensure - sock1&.close - sock2&.close - ssl1&.close - ssl2&.close - t&.kill - t&.join + server_proc = proc do |sock| + sctx = make_server_context + sctx.alpn_select_cb = -> (protocols) { nil } + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + assert_raise_with_message(TypeError, /nil/) { ssl.accept } + end + start_server_proc(server_proc) do |port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.alpn_protocols = ["http/1.1"] + # no_application_protocol alert + assert_raise_with_message(OpenSSL::SSL::SSLError, /alert number 120/) { + server_connect(port, ctx) { } + } + end end def test_npn_protocol_selection_ary @@ -1925,8 +1877,7 @@ def test_fallback_scsv # Here is not OK # TLS1.2 is supported, fallback to TLS1.1 (downgrade attack) and signaling the fallback # Server support better, so refuse the connection - sock1, sock2 = socketpair - begin + server_proc = proc do |sock| # This test is for the downgrade protection mechanism of TLS1.2. # This is why ctx1 bounds max_version == TLS1.2. # Otherwise, this test fails when using openssl 1.1.1 (or later) that supports TLS1.3. @@ -1935,27 +1886,21 @@ def test_fallback_scsv ctx1.security_level = 0 ctx1.min_version = 0 ctx1.max_version = OpenSSL::SSL::TLS1_2_VERSION - s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - + s1 = OpenSSL::SSL::SSLSocket.new(sock, ctx1) + # AWS-LC has slightly different error messages in all-caps. + assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate.fallback/i) { + s1.accept + } + end + start_server_proc(server_proc) do |port| ctx2 = OpenSSL::SSL::SSLContext.new ctx2.enable_fallback_scsv ctx2.security_level = 0 ctx2.min_version = 0 ctx2.max_version = OpenSSL::SSL::TLS1_1_VERSION - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - # AWS-LC has slightly different error messages in all-caps. - t = Thread.new { - assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate fallback|INAPPROPRIATE_FALLBACK/) { - s2.connect - } + assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate.fallback/i) { + server_connect(port, ctx2) { } } - assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate fallback|INAPPROPRIATE_FALLBACK/) { - s1.accept - } - t.join - ensure - sock1.close - sock2.close end end @@ -2371,15 +2316,12 @@ def test_security_level def test_dup ctx = OpenSSL::SSL::SSLContext.new - sock1, sock2 = socketpair - ssl = OpenSSL::SSL::SSLSocket.new(sock1, ctx) + Socket.open(:INET, :STREAM) { |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) - assert_raise(NoMethodError) { ctx.dup } - assert_raise(NoMethodError) { ssl.dup } - ensure - ssl.close if ssl - sock1.close - sock2.close + assert_raise(NoMethodError) { ctx.dup } + assert_raise(NoMethodError) { ssl.dup } + } end def test_freeze_calls_setup @@ -2395,17 +2337,14 @@ def test_freeze_calls_setup end def test_fileno - ctx = OpenSSL::SSL::SSLContext.new - sock1, sock2 = socketpair - - socket = OpenSSL::SSL::SSLSocket.new(sock1) - server = OpenSSL::SSL::SSLServer.new(sock2, ctx) + Socket.open(:INET, :STREAM) { |sock| + ctx = OpenSSL::SSL::SSLContext.new + ssl = OpenSSL::SSL::SSLSocket.new(sock) + server = OpenSSL::SSL::SSLServer.new(sock, ctx) - assert_equal socket.fileno, socket.to_io.fileno - assert_equal server.fileno, server.to_io.fileno - ensure - sock1.close - sock2.close + assert_equal(sock.fileno, ssl.fileno) + assert_equal(sock.fileno, server.fileno) + } end def test_export_keying_material From 5d7309237bfbed114822d0619f0b740c4d4eda39 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Thu, 27 Aug 2026 20:56:29 +0900 Subject: [PATCH 6/9] ssl: implement blocking methods in Ruby The blocking methods #connect, #accept, #sysread, and #syswrite can be implemented on top of their *_nonblock counterpart, with hopefully negligible amount of overhead. Also, handle the exception keyword argument of the *_nonblock methods in Ruby as well. This further simplifies the extension code and also avoids a Hash object allocation. This is a preparatory change for the upcoming patch to improve timeout support in SSLSocket. This patch does not intend to introduce any visible behavior change. --- ext/openssl/ossl_ssl.c | 552 +++++++++++------------------------------ lib/openssl/ssl.rb | 183 ++++++++++++++ 2 files changed, 322 insertions(+), 413 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index fcbbec0b3..d0cea374c 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -37,7 +37,7 @@ static VALUE eSSLErrorWaitReadable; static VALUE eSSLErrorWaitWritable; static ID id_call, ID_callback_state, id_npn_protocols_encoded, id_each; -static VALUE sym_exception, sym_wait_readable, sym_wait_writable; +static VALUE sym_wait_readable, sym_wait_writable; static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode, id_i_verify_depth, id_i_verify_callback, id_i_client_ca, @@ -1757,255 +1757,99 @@ errno_mapped(void) #endif } -static void -write_would_block(int nonblock) -{ - if (nonblock) - ossl_raise(eSSLErrorWaitWritable, "write would block"); -} - -static void -read_would_block(int nonblock) -{ - if (nonblock) - ossl_raise(eSSLErrorWaitReadable, "read would block"); -} - -static int -no_exception_p(VALUE opts) -{ - if (RB_TYPE_P(opts, T_HASH) && - rb_hash_lookup2(opts, sym_exception, Qundef) == Qfalse) - return 1; - return 0; -} - -// Provided by Ruby 3.2.0 and later in order to support the default IO#timeout. -#ifndef RUBY_IO_TIMEOUT_DEFAULT -#define RUBY_IO_TIMEOUT_DEFAULT Qnil -#endif - -#ifdef HAVE_RB_IO_TIMEOUT -#define IO_TIMEOUT_ERROR rb_eIOTimeoutError -#else -#define IO_TIMEOUT_ERROR rb_eIOError -#endif - - -static void -io_wait_writable(VALUE io) -{ -#ifdef HAVE_RB_IO_MAYBE_WAIT - if (!rb_io_wait(io, INT2NUM(RUBY_IO_WRITABLE), RUBY_IO_TIMEOUT_DEFAULT)) { - rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!"); - } -#else - rb_io_t *fptr; - GetOpenFile(io, fptr); - rb_thread_fd_writable(fptr->fd); -#endif -} - -static void -io_wait_readable(VALUE io) -{ -#ifdef HAVE_RB_IO_MAYBE_WAIT - if (!rb_io_wait(io, INT2NUM(RUBY_IO_READABLE), RUBY_IO_TIMEOUT_DEFAULT)) { - rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!"); - } -#else - rb_io_t *fptr; - GetOpenFile(io, fptr); - rb_thread_wait_fd(fptr->fd); -#endif -} - static VALUE -ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) +ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname) { SSL *ssl; VALUE cb_state; - int nonblock = opts != Qfalse; rb_ivar_set(self, ID_callback_state, Qnil); GetSSL(self, ssl); - VALUE io = rb_attr_get(self, id_i_io); - for (;;) { - int ret = func(ssl); - int saved_errno = errno_mapped(); - - cb_state = rb_attr_get(self, ID_callback_state); - if (!NIL_P(cb_state)) { - /* must cleanup OpenSSL error stack before re-raising */ - ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); - } + int ret; +#ifdef __APPLE__ + retry: +#endif + ret = func(ssl); + int saved_errno = errno_mapped(); + + cb_state = rb_attr_get(self, ID_callback_state); + if (!NIL_P(cb_state)) { + /* must cleanup OpenSSL error stack before re-raising */ + ossl_clear_error(); + rb_jump_tag(NUM2INT(cb_state)); + } + + if (ret > 0) + return self; - if (ret > 0) - break; - - int code = SSL_get_error(ssl, ret); - switch (code) { - case SSL_ERROR_WANT_WRITE: - if (no_exception_p(opts)) { return sym_wait_writable; } - write_would_block(nonblock); - io_wait_writable(io); - continue; - case SSL_ERROR_WANT_READ: - if (no_exception_p(opts)) { return sym_wait_readable; } - read_would_block(nonblock); - io_wait_readable(io); - continue; - case SSL_ERROR_SYSCALL: + int code = SSL_get_error(ssl, ret); + switch (code) { + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: #ifdef __APPLE__ - /* See ossl_ssl_write_internal() */ - if (saved_errno == EPROTOTYPE) - continue; + /* See ossl_ssl_write_internal() */ + if (saved_errno == EPROTOTYPE) + goto retry; #endif - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, funcname)); - /* fallthrough */ - default: { - VALUE error_append = Qnil; + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, funcname)); + /* fallthrough */ + default: { + VALUE error_append = Qnil; #if defined(SSL_R_CERTIFICATE_VERIFY_FAILED) - unsigned long err = ERR_peek_last_error(); - if (ERR_GET_LIB(err) == ERR_LIB_SSL && - ERR_GET_REASON(err) == SSL_R_CERTIFICATE_VERIFY_FAILED) { - const char *err_msg = ERR_reason_error_string(err), - *verify_msg = X509_verify_cert_error_string(SSL_get_verify_result(ssl)); - if (!err_msg) - err_msg = "(null)"; - if (!verify_msg) - verify_msg = "(null)"; - ossl_clear_error(); /* let ossl_raise() not append message */ - error_append = rb_sprintf(": %s (%s)", err_msg, verify_msg); - } -#endif - ossl_raise(eSSLError, - "%s%s returned=%d errno=%d peeraddr=%"PRIsVALUE" state=%s%"PRIsVALUE, - funcname, - code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", - code, - saved_errno, - peeraddr_ip_str(io), - SSL_state_string_long(ssl), - error_append); + unsigned long err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_SSL && + ERR_GET_REASON(err) == SSL_R_CERTIFICATE_VERIFY_FAILED) { + const char *err_msg = ERR_reason_error_string(err), + *verify_msg = X509_verify_cert_error_string(SSL_get_verify_result(ssl)); + if (!err_msg) + err_msg = "(null)"; + if (!verify_msg) + verify_msg = "(null)"; + ossl_clear_error(); /* let ossl_raise() not append message */ + error_append = rb_sprintf(": %s (%s)", err_msg, verify_msg); } - } +#endif + VALUE io = rb_attr_get(self, id_i_io); + ossl_raise(eSSLError, + "%s%s returned=%d errno=%d peeraddr=%"PRIsVALUE" state=%s%"PRIsVALUE, + funcname, + code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", + code, + saved_errno, + peeraddr_ip_str(io), + SSL_state_string_long(ssl), + error_append); + } } - - return self; -} - -/* - * call-seq: - * ssl.connect => self - * - * Initiates an SSL/TLS handshake with a server. - */ -static VALUE -ossl_ssl_connect(VALUE self) -{ - ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_connect, "SSL_connect", Qfalse); -} - -/* - * call-seq: - * ssl.connect_nonblock([options]) => self - * - * Initiates the SSL/TLS handshake as a client in non-blocking manner. - * - * # emulates blocking connect - * begin - * ssl.connect_nonblock - * rescue IO::WaitReadable - * IO.select([s2]) - * retry - * rescue IO::WaitWritable - * IO.select(nil, [s2]) - * retry - * end - * - * By specifying a keyword argument _exception_ to +false+, you can indicate - * that connect_nonblock should not raise an IO::WaitReadable or - * IO::WaitWritable exception, but return the symbol +:wait_readable+ or - * +:wait_writable+ instead. - */ -static VALUE -ossl_ssl_connect_nonblock(int argc, VALUE *argv, VALUE self) -{ - VALUE opts; - rb_scan_args(argc, argv, "0:", &opts); - - ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_connect, "SSL_connect", opts); } -/* - * call-seq: - * ssl.accept => self - * - * Waits for a SSL/TLS client to initiate a handshake. - */ static VALUE -ossl_ssl_accept(VALUE self) +ossl_ssl_connect_nonblock(VALUE self) { ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_accept, "SSL_accept", Qfalse); + return ossl_start_ssl(self, SSL_connect, "SSL_connect"); } -/* - * call-seq: - * ssl.accept_nonblock([options]) => self - * - * Initiates the SSL/TLS handshake as a server in non-blocking manner. - * - * # emulates blocking accept - * begin - * ssl.accept_nonblock - * rescue IO::WaitReadable - * IO.select([s2]) - * retry - * rescue IO::WaitWritable - * IO.select(nil, [s2]) - * retry - * end - * - * By specifying a keyword argument _exception_ to +false+, you can indicate - * that accept_nonblock should not raise an IO::WaitReadable or - * IO::WaitWritable exception, but return the symbol +:wait_readable+ or - * +:wait_writable+ instead. - */ static VALUE -ossl_ssl_accept_nonblock(int argc, VALUE *argv, VALUE self) +ossl_ssl_accept_nonblock(VALUE self) { - VALUE opts; - - rb_scan_args(argc, argv, "0:", &opts); ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_accept, "SSL_accept", opts); + return ossl_start_ssl(self, SSL_accept, "SSL_accept"); } static VALUE -ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) +ossl_ssl_read_nonblock(VALUE self, VALUE len, VALUE str) { SSL *ssl; int ilen; - VALUE len, str, cb_state; - VALUE opts = Qnil; + VALUE cb_state; - if (nonblock) { - rb_scan_args(argc, argv, "11:", &len, &str, &opts); - } else { - rb_scan_args(argc, argv, "11", &len, &str); - } GetSSL(self, ssl); if (!ssl_started(ssl)) rb_raise(eSSLError, "SSL session is not started yet"); @@ -2026,224 +1870,112 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) return str; } - VALUE io = rb_attr_get(self, id_i_io); + rb_str_locktmp(str); + int nread = SSL_read(ssl, RSTRING_PTR(str), ilen); + int saved_errno = errno_mapped(); + rb_str_unlocktmp(str); - for (;;) { - rb_str_locktmp(str); - int nread = SSL_read(ssl, RSTRING_PTR(str), ilen); - int saved_errno = errno_mapped(); - rb_str_unlocktmp(str); - - cb_state = rb_attr_get(self, ID_callback_state); - if (!NIL_P(cb_state)) { - rb_ivar_set(self, ID_callback_state, Qnil); - ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); - } + cb_state = rb_attr_get(self, ID_callback_state); + if (!NIL_P(cb_state)) { + rb_ivar_set(self, ID_callback_state, Qnil); + ossl_clear_error(); + rb_jump_tag(NUM2INT(cb_state)); + } - switch (SSL_get_error(ssl, nread)) { - case SSL_ERROR_NONE: - rb_str_set_len(str, nread); - return str; - case SSL_ERROR_ZERO_RETURN: - if (no_exception_p(opts)) { return Qnil; } - rb_eof_error(); - case SSL_ERROR_WANT_WRITE: - if (nonblock) { - if (no_exception_p(opts)) { return sym_wait_writable; } - write_would_block(nonblock); - } - io_wait_writable(io); - break; - case SSL_ERROR_WANT_READ: - if (nonblock) { - if (no_exception_p(opts)) { return sym_wait_readable; } - read_would_block(nonblock); - } - io_wait_readable(io); - break; - case SSL_ERROR_SYSCALL: - if (!ERR_peek_error()) { - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); - else { - /* - * The underlying BIO returned 0. This is actually a - * protocol error. But unfortunately, not all - * implementations cleanly shutdown the TLS connection - * but just shutdown/close the TCP connection. So report - * EOF for now... - */ - if (no_exception_p(opts)) { return Qnil; } - rb_eof_error(); - } + switch (SSL_get_error(ssl, nread)) { + case SSL_ERROR_NONE: + rb_str_set_len(str, nread); + return str; + case SSL_ERROR_ZERO_RETURN: + return Qnil; + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: + if (!ERR_peek_error()) { + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); + else { + /* + * The underlying BIO returned 0. This is actually a + * protocol error. But unfortunately, not all + * implementations cleanly shutdown the TLS connection + * but just shutdown/close the TCP connection. So report + * EOF for now... + */ + return Qnil; } - /* fall through */ - default: - ossl_raise(eSSLError, "SSL_read"); } - - // Ensure the buffer is not modified during io_wait_*able() - rb_str_modify(str); - if (rb_str_capacity(str) < (size_t)ilen) - rb_raise(eSSLError, "read buffer was modified"); + /* fall through */ + default: + ossl_raise(eSSLError, "SSL_read"); } } -/* - * call-seq: - * ssl.sysread(length) => string - * ssl.sysread(length, buffer) => buffer - * - * Reads _length_ bytes from the SSL connection. If a pre-allocated _buffer_ - * is provided the data will be written into it. - */ -static VALUE -ossl_ssl_read(int argc, VALUE *argv, VALUE self) -{ - return ossl_ssl_read_internal(argc, argv, self, 0); -} - -/* - * call-seq: - * ssl.sysread_nonblock(length) => string - * ssl.sysread_nonblock(length, buffer) => buffer - * ssl.sysread_nonblock(length[, buffer [, opts]) => buffer - * - * A non-blocking version of #sysread. Raises an SSLError if reading would - * block. If "exception: false" is passed, this method returns a symbol of - * :wait_readable, :wait_writable, or nil, rather than raising an exception. - * - * Reads _length_ bytes from the SSL connection. If a pre-allocated _buffer_ - * is provided the data will be written into it. - */ -static VALUE -ossl_ssl_read_nonblock(int argc, VALUE *argv, VALUE self) -{ - return ossl_ssl_read_internal(argc, argv, self, 1); -} - static VALUE -ossl_ssl_write_internal_safe(VALUE _args) +ossl_ssl_write_nonblock(VALUE self, VALUE str) { - VALUE *args = (VALUE*)_args; - VALUE self = args[0]; - VALUE str = args[1]; - VALUE opts = args[2]; - SSL *ssl; rb_io_t *fptr; - int num, nonblock = opts != Qfalse; + int num, nwritten; VALUE cb_state; GetSSL(self, ssl); if (!ssl_started(ssl)) rb_raise(eSSLError, "SSL session is not started yet"); - + StringValue(str); VALUE io = rb_attr_get(self, id_i_io); GetOpenFile(io, fptr); +#ifdef __APPLE__ + retry: +#endif /* SSL_write(3ssl) manpage states num == 0 is undefined */ num = RSTRING_LENINT(str); if (num == 0) return INT2FIX(0); - - for (;;) { - int nwritten = SSL_write(ssl, RSTRING_PTR(str), num); - int saved_errno = errno_mapped(); - - cb_state = rb_attr_get(self, ID_callback_state); - if (!NIL_P(cb_state)) { - rb_ivar_set(self, ID_callback_state, Qnil); - ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); - } - - switch (SSL_get_error(ssl, nwritten)) { - case SSL_ERROR_NONE: - return INT2NUM(nwritten); - case SSL_ERROR_WANT_WRITE: - if (no_exception_p(opts)) { return sym_wait_writable; } - write_would_block(nonblock); - io_wait_writable(io); - continue; - case SSL_ERROR_WANT_READ: - if (no_exception_p(opts)) { return sym_wait_readable; } - read_would_block(nonblock); - io_wait_readable(io); - continue; - case SSL_ERROR_SYSCALL: -#ifdef __APPLE__ - /* - * It appears that send syscall can return EPROTOTYPE if the - * socket is being torn down. Retry to get a proper errno to - * make the error handling in line with the socket library. - * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 - */ - if (saved_errno == EPROTOTYPE) - continue; -#endif - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); - /* fallthrough */ - default: - ossl_raise(eSSLError, "SSL_write"); - } - } -} - - -static VALUE -ossl_ssl_write_internal(VALUE self, VALUE str, VALUE opts) -{ - StringValue(str); int frozen = RB_OBJ_FROZEN(str); if (!frozen) { rb_str_locktmp(str); } - int state; - VALUE args[3] = {self, str, opts}; - VALUE result = rb_protect(ossl_ssl_write_internal_safe, (VALUE)args, &state); + nwritten = SSL_write(ssl, RSTRING_PTR(str), num); if (!frozen) { rb_str_unlocktmp(str); } + int saved_errno = errno_mapped(); - if (state) { - rb_jump_tag(state); + cb_state = rb_attr_get(self, ID_callback_state); + if (!NIL_P(cb_state)) { + rb_ivar_set(self, ID_callback_state, Qnil); + ossl_clear_error(); + rb_jump_tag(NUM2INT(cb_state)); } - return result; -} -/* - * call-seq: - * ssl.syswrite(string) => Integer - * - * Writes _string_ to the SSL connection. - */ -static VALUE -ossl_ssl_write(VALUE self, VALUE str) -{ - return ossl_ssl_write_internal(self, str, Qfalse); -} - -/* - * call-seq: - * ssl.syswrite_nonblock(string) => Integer - * ssl.syswrite_nonblock(string, opts) => Integer - * - * Writes _string_ to the SSL connection in a non-blocking manner. Raises an - * SSLError if writing would block. If "exception: false" is passed, this - * method returns a symbol of :wait_readable or :wait_writable, rather than - * raising an exception. - */ -static VALUE -ossl_ssl_write_nonblock(int argc, VALUE *argv, VALUE self) -{ - VALUE str, opts; - - rb_scan_args(argc, argv, "1:", &str, &opts); - - return ossl_ssl_write_internal(self, str, opts); + switch (SSL_get_error(ssl, nwritten)) { + case SSL_ERROR_NONE: + return INT2NUM(nwritten); + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: +#ifdef __APPLE__ + /* + * It appears that send syscall can return EPROTOTYPE if the + * socket is being torn down. Retry to get a proper errno to + * make the error handling in line with the socket library. + * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 + */ + if (saved_errno == EPROTOTYPE) + goto retry; +#endif + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); + /* fallthrough */ + default: + ossl_raise(eSSLError, "SSL_write"); + } } /* @@ -3167,14 +2899,10 @@ Init_ossl_ssl(void) rb_define_alloc_func(cSSLSocket, ossl_ssl_s_alloc); rb_define_method(cSSLSocket, "initialize", ossl_ssl_initialize, -1); rb_undef_method(cSSLSocket, "initialize_copy"); - rb_define_method(cSSLSocket, "connect", ossl_ssl_connect, 0); - rb_define_method(cSSLSocket, "connect_nonblock", ossl_ssl_connect_nonblock, -1); - rb_define_method(cSSLSocket, "accept", ossl_ssl_accept, 0); - rb_define_method(cSSLSocket, "accept_nonblock", ossl_ssl_accept_nonblock, -1); - rb_define_method(cSSLSocket, "sysread", ossl_ssl_read, -1); - rb_define_private_method(cSSLSocket, "sysread_nonblock", ossl_ssl_read_nonblock, -1); - rb_define_method(cSSLSocket, "syswrite", ossl_ssl_write, 1); - rb_define_private_method(cSSLSocket, "syswrite_nonblock", ossl_ssl_write_nonblock, -1); + rb_define_private_method(cSSLSocket, "__connect_nonblock", ossl_ssl_connect_nonblock, 0); + rb_define_private_method(cSSLSocket, "__accept_nonblock", ossl_ssl_accept_nonblock, 0); + rb_define_private_method(cSSLSocket, "__sysread_nonblock", ossl_ssl_read_nonblock, 2); + rb_define_private_method(cSSLSocket, "__syswrite_nonblock", ossl_ssl_write_nonblock, 1); rb_define_private_method(cSSLSocket, "stop", ossl_ssl_stop, 0); rb_define_method(cSSLSocket, "cert", ossl_ssl_get_cert, 0); rb_define_method(cSSLSocket, "peer_cert", ossl_ssl_get_peer_cert, 0); @@ -3352,8 +3080,6 @@ Init_ossl_ssl(void) /* TLS 1.3 */ rb_define_const(mSSL, "TLS1_3_VERSION", INT2NUM(TLS1_3_VERSION)); - - sym_exception = ID2SYM(rb_intern_const("exception")); sym_wait_readable = ID2SYM(rb_intern_const("wait_readable")); sym_wait_writable = ID2SYM(rb_intern_const("wait_writable")); diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index dccc11a55..7b80bae9c 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -360,6 +360,189 @@ def sysclose io.close if sync_close end + # Ruby 3.2 + IO_TimeoutError = defined?(IO::TimeoutError) ? IO::TimeoutError : IOError + private_constant :IO_TimeoutError + + private def check_nonblock(ret) + case ret + when :wait_readable + raise SSLErrorWaitReadable, "read would block" + when :wait_writable + raise SSLErrorWaitWritable, "write would block" + when nil + raise EOFError, "end of file reached" + else + ret + end + end + + # :call-seq: + # ssl.connect -> self + # + # Initiates an SSL/TLS handshake with a server. + def connect + while true + case ret = __connect_nonblock + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + else + return ret + end + end + end + + # :call-seq: + # ssl.connect_nonblock -> self + # ssl.connect_nonblock(exception: false) -> self | :wait_readable | :wait_writable + # + # Initiates the SSL/TLS handshake as a client in non-blocking manner. + # + # # emulates blocking connect + # begin + # ssl.connect_nonblock + # rescue IO::WaitReadable + # IO.select([s2]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [s2]) + # retry + # end + # + # By specifying a keyword argument _exception_ to +false+, you can + # indicate that connect_nonblock should not raise an IO::WaitReadable or + # IO::WaitWritable exception, but return the symbol +:wait_readable+ or + # +:wait_writable+ instead. + def connect_nonblock(exception: true) + ret = __connect_nonblock + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.accept -> self + # + # Waits for a SSL/TLS client to initiate a handshake. + def accept + while true + case ret = __accept_nonblock + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + else + return ret + end + end + end + + # :call-seq: + # ssl.accept_nonblock -> self + # ssl.accept_nonblock(exception: false) -> self | :wait_readable | :wait_writable + # + # Initiates the SSL/TLS handshake as a server in non-blocking manner. + # + # # emulates blocking accept + # begin + # ssl.accept_nonblock + # rescue IO::WaitReadable + # IO.select([s2]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [s2]) + # retry + # end + # + # By specifying a keyword argument _exception_ to +false+, you can + # indicate that accept_nonblock should not raise an IO::WaitReadable or + # IO::WaitWritable exception, but return the symbol +:wait_readable+ or + # +:wait_writable+ instead. + def accept_nonblock(exception: true) + ret = __accept_nonblock + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.sysread(length) -> string + # ssl.sysread(length, buffer) -> buffer + # + # Reads _length_ bytes from the SSL connection. If a pre-allocated + # _buffer_ is provided the data will be written into it. + def sysread(length, buffer = nil) + while true + case ret = __sysread_nonblock(length, buffer) + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + when nil + raise EOFError, "end of file reached" + else + return ret + end + end + end + + # :call-seq: + # ssl.sysread_nonblock(length) -> string + # ssl.sysread_nonblock(length, buffer) -> buffer + # ssl.sysread_nonblock(length, buffer, exception: false) -> buffer | :wait_readable | :wait_writable | nil + # + # A non-blocking version of #sysread. Raises an SSLError if reading + # would block. If "exception: false" is passed, this method returns a + # symbol of :wait_readable, :wait_writable, or nil, rather than raising + # an exception. + # + # Reads _length_ bytes from the SSL connection. If a pre-allocated + # _buffer_ is provided the data will be written into it. + def sysread_nonblock(length, buffer = nil, exception: true) + ret = __sysread_nonblock(length, buffer) + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.syswrite(string) -> Integer + # + # Writes _string_ to the SSL connection. + def syswrite(string) + while true + case ret = __syswrite_nonblock(string) + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + else + return ret + end + end + end + + # :call-seq: + # ssl.syswrite_nonblock(string) -> Integer + # ssl.syswrite_nonblock(string, exception: false) -> Integer | :wait_readable | :wait_writable + # + # Writes _string_ to the SSL connection in a non-blocking manner. Raises + # an SSLError if writing would block. If "exception: false" is passed, + # this method returns a symbol of :wait_readable or :wait_writable, + # rather than raising an exception. + def syswrite_nonblock(string, exception: true) + ret = __syswrite_nonblock(string) + check_nonblock(ret) if exception + ret + end + # call-seq: # ssl.post_connection_check(hostname) -> true # From 0c2c4500eeea97af3fd728796c0ec983611be5bb Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 28 Aug 2026 00:53:31 +0900 Subject: [PATCH 7/9] ssl: support SSLSocket#connect and #accept with timeout Add a keyword argument timeout to specify the total time allowed for the TLS handshake to complete. Inspired by Addrinfo#connect(timeout:) and TCPSocket.open(connect_timeout:). --- lib/openssl/ssl.rb | 50 +++++++++++++++++++++++++++++----------- test/openssl/test_ssl.rb | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index 7b80bae9c..0e0b62332 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -377,19 +377,40 @@ def sysclose end end + private def with_timeout(timeout) + timeout ||= self.timeout + if timeout.nil? + while true + yield + end + else + remaining = timeout + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + while remaining >= 0 + yield remaining + now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + remaining = timeout - (now - start) + end + end + # IO::TimeoutError was added in Ruby 3.2 + raise (defined?(IO::TimeoutError) ? IO::TimeoutError : IOError), + "user specified timeout for SSL handshake" + end + # :call-seq: - # ssl.connect -> self + # ssl.connect(timeout: nil) -> self # # Initiates an SSL/TLS handshake with a server. - def connect - while true + # + # If _timeout_ is specified, and if the handshake does not complete + # within _timeout_ seconds, IO::TimeoutError is raised. + def connect(timeout: nil) + with_timeout(timeout) do |remaining| case ret = __connect_nonblock when :wait_readable - wait_readable or - raise IO_TimeoutError, "Timed out while waiting to become readable!" + wait_readable(remaining) when :wait_writable - wait_writable or - raise IO_TimeoutError, "Timed out while waiting to become writable!" + wait_writable(remaining) else return ret end @@ -424,18 +445,19 @@ def connect_nonblock(exception: true) end # :call-seq: - # ssl.accept -> self + # ssl.accept(timeout: nil) -> self # # Waits for a SSL/TLS client to initiate a handshake. - def accept - while true + # + # If _timeout_ is specified, and if the handshake does not complete + # within _timeout_ seconds, IO::TimeoutError is raised. + def accept(timeout: nil) + with_timeout(timeout) do |remaining| case ret = __accept_nonblock when :wait_readable - wait_readable or - raise IO_TimeoutError, "Timed out while waiting to become readable!" + wait_readable(remaining) when :wait_writable - wait_writable or - raise IO_TimeoutError, "Timed out while waiting to become writable!" + wait_writable(remaining) else return ret end diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index a252a0e64..13d458911 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -186,6 +186,49 @@ def test_connect_accept_nonblock end end + def test_connect_timeout + timeout_error = defined?(IO::TimeoutError) ? IO::TimeoutError : IOError + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_proc = proc do |sock| + # TLS 1.2 handshake takes 2 RTTs + ctx = make_server_context + ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + + case (sleep 0.1; ssl.accept_nonblock(exception: false)) + when :wait_readable then ssl.wait_readable + when :wait_writable then ssl.wait_writable + else break + end while true + readwrite_loop(ssl) + rescue OpenSSL::SSL::SSLError, SystemCallError + end + start_server_proc(server_proc) do |port| + th = [] + th << Thread.new do + sock = TCPSocket.new("127.0.0.1", port) + sock.setsockopt(:TCP, :NODELAY, 1) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + assert_raise(timeout_error) { ssl.connect(timeout: 0.05) } + taken = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + assert_operator(taken, :<, 0.2) + ensure + sock.close + end + th << Thread.new do + sock = TCPSocket.new("127.0.0.1", port) + sock.setsockopt(:TCP, :NODELAY, 1) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.connect(timeout: 1) + taken = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + assert_operator(taken, :>=, 0.2) + ensure + sock.close + end + assert_join_threads(th) + end + end + def test_low_level_socket start_server do |port| sock = Socket.tcp("127.0.0.1", port) From 8bf8a0e0f5275abba2f3ec1167ce1a14f1da311a Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 28 Aug 2026 02:43:29 +0900 Subject: [PATCH 8/9] fixup! ssl: support SSLSocket#connect and #accept with timeout --- lib/openssl/ssl.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index 0e0b62332..6c2096932 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -378,7 +378,10 @@ def sysclose end private def with_timeout(timeout) - timeout ||= self.timeout + begin + timeout ||= self.timeout + rescue NoMethodError + end if timeout.nil? while true yield @@ -392,9 +395,7 @@ def sysclose remaining = timeout - (now - start) end end - # IO::TimeoutError was added in Ruby 3.2 - raise (defined?(IO::TimeoutError) ? IO::TimeoutError : IOError), - "user specified timeout for SSL handshake" + raise IO_TimeoutError, "user specified timeout for SSL handshake" end # :call-seq: From 76bc4f0d8e4d3372cff7141b23c3f793d4952ea1 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 28 Aug 2026 02:51:57 +0900 Subject: [PATCH 9/9] fixup! fixup! ssl: support SSLSocket#connect and #accept with timeout --- lib/openssl/ssl.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index 6c2096932..ac273d0f7 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -378,10 +378,8 @@ def sysclose end private def with_timeout(timeout) - begin - timeout ||= self.timeout - rescue NoMethodError - end + # IO#timeout= was added in Ruby 3.2 + timeout ||= self.timeout if IO.method_defined?(:timeout) if timeout.nil? while true yield