Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,4 +4,4 @@ gemspec

gem "rake"
gem "test-unit"
gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core"
gem "test-unit-ruby-core"
163 changes: 117 additions & 46 deletions lib/drb/ssl.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,19 +25,25 @@ class SSLConfig
#
# See DRb::DRbSSLSocket::SSLConfig.new for more details
DEFAULT = {
:SSLCertificate => nil,
:SSLPrivateKey => nil,
:SSLClientCA => nil,
:SSLCACertificatePath => nil,
:SSLCACertificateFile => nil,
:SSLTmpDhCallback => nil,
:SSLVerifyMode => ::OpenSSL::SSL::VERIFY_NONE,
:SSLVerifyDepth => nil,
:SSLVerifyCallback => nil, # custom verification
:SSLCertificateStore => nil,
:SSLCertificates => nil,
:SSLCertificate => nil,
:SSLPrivateKey => nil,
:SSLPrivateKeyAlgorithms => ["RSA"],
:SSLClientCA => nil,
:SSLCACertificatePath => nil,
:SSLCACertificateFile => nil,
:SSLSignatureAlgorithms => nil,
:SSLClientSignatureAlgorithms => nil,
:SSLGroups => nil,
:SSLTmpDhCallback => nil,
:SSLVerifyMode => ::OpenSSL::SSL::VERIFY_NONE,
:SSLVerifyDepth => nil,
:SSLVerifyCallback => nil, # custom verification
:SSLCertificateStore => nil,
# Must specify if you use auto generated certificate.
:SSLCertName => nil, # e.g. [["CN","fqdn.example.com"]]
:SSLCertComment => "Generated by Ruby/OpenSSL"
# e.g. [["CN", "fqdn.example.com"]]
:SSLCertName => nil,
:SSLCertComment => "Generated by Ruby/OpenSSL"
}

# Create a new DRb::DRbSSLSocket::SSLConfig instance
Expand All@@ -51,13 +57,29 @@ class SSLConfig
#
# From +config+ Hash:
#
# :SSLCertificates ::
# An Array of [certificate, private_key] pairs. Each element
# is an Array of an OpenSSL::X509::Certificate and its
# corresponding private key. This option is prioritized over
# :SSLCertificate and :SSLPrivateKey. See
# OpenSSL::SSL::SSLContext#add_certificate
#
# :SSLCertificate ::
# An instance of OpenSSL::X509::Certificate. If this is not provided,
# then a generic X509 is generated, with a correspond :SSLPrivateKey
#
# :SSLPrivateKey ::
# A private key instance, like OpenSSL::PKey::RSA. This key must be
# the key that signed the :SSLCertificate
# A private key instance, like OpenSSL::PKey::RSA for RSA
# and OpenSSL::PKey::PKey for ML-DSA-44, ML-DSA-65,
# ML-DSA-87. This key must be the key that signed the
# :SSLCertificate
#
# :SSLPrivateKeyAlgorithms ::
# An Array of private key algorithms used to generate
# certificates when :SSLCertificates, :SSLCertificate and
# :SSLPrivateKey are not provided. Supported algorithms are
# RSA, ML-DSA-44, ML-DSA-65, and ML-DSA-87.
# Defaults to ["RSA"]
#
# :SSLClientCA ::
# An OpenSSL::X509::Certificate, or Array of certificates that will
Expand All@@ -70,6 +92,15 @@ class SSLConfig
# :SSLCACertificateFile ::
# A path to a CA certificate file, in PEM format.
#
# :SSLSignatureAlgorithms ::
# Signature algorithms. See OpenSSL::SSL::SSLContext#sigalgs=
#
# :SSLClientSignatureAlgorithms ::
# Client signature algorithms. See OpenSSL::SSL::SSLContext#client_sigalgs=
#
# :SSLGroups ::
# Key exchange groups. See OpenSSL::SSL::SSLContext#groups=
#
# :SSLTmpDhCallback ::
# A DH callback. See OpenSSL::SSL::SSLContext.tmp_dh_callback
#
Expand DownExpand Up@@ -133,10 +164,14 @@ class SSLConfig
# c.setup_certificate
#
def initialize(config)
@config = config
@cert = config[:SSLCertificate]
@pkey = config[:SSLPrivateKey]
@ssl_ctx = nil
@config = config
@certs = if config.key?(:SSLCertificates)
config[:SSLCertificates]
elsif config[:SSLCertificate] && config[:SSLPrivateKey]
[[config[:SSLCertificate], config[:SSLPrivateKey]]]
end
@pkey_algs = self[:SSLPrivateKeyAlgorithms]
@ssl_ctx = nil
end

# A convenience method to access the values like a Hash
Expand All@@ -162,25 +197,81 @@ def accept(tcp)
ssl
end

# Ensures that :SSLCertificate and :SSLPrivateKey have been provided
# or that a new certificate is generated with the other parameters
# provided.
# Ensures that :SSLCertificates or :SSLCertificate and
# :SSLPrivateKey have been provided, or that new certificates
# are generated with the other parameters provided.
def setup_certificate
if @cert && @pkey
if @certs
return
end

rsa = OpenSSL::PKey::RSA.new(2048)
@certs = @pkey_algs.map { |pkey_alg| setup_certificate_one(pkey_alg) }
end

# Establish the OpenSSL::SSL::SSLContext with the configuration
# parameters provided.
def setup_ssl_context

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to make setup_ssl_context public? It's just for test? If so, we should not make it public. We can use __send__ in test.

@junarugajunarugaAug 12, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current master branch 2b002d4, the setup_ssl_context is already public method. I didn't change the method from private to public.

defsetup_ssl_context

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kou I am waiting for your response on this topic.

ctx = ::OpenSSL::SSL::SSLContext.new
@certs&.each do |cert, pkey|
ctx.add_certificate(cert, pkey)
end
ctx.min_version = self[:SSLMinVersion]
ctx.max_version = self[:SSLMaxVersion]
ctx.client_ca = self[:SSLClientCA]
ctx.ca_path = self[:SSLCACertificatePath]
ctx.ca_file = self[:SSLCACertificateFile]
if self[:SSLSignatureAlgorithms]
ctx.sigalgs = self[:SSLSignatureAlgorithms]
end
if self[:SSLClientSignatureAlgorithms]
ctx.client_sigalgs = self[:SSLClientSignatureAlgorithms]
end
if self[:SSLGroups]
ctx.groups = self[:SSLGroups]
end
ctx.tmp_dh_callback = self[:SSLTmpDhCallback]
ctx.verify_mode = self[:SSLVerifyMode]
ctx.verify_depth = self[:SSLVerifyDepth]
ctx.verify_callback = self[:SSLVerifyCallback]
ctx.cert_store = self[:SSLCertificateStore]
@ssl_ctx = ctx
end

private

def setup_certificate_one(pkey_alg)
cert = OpenSSL::X509::Certificate.new

case pkey_alg
when "RSA"
pkey = OpenSSL::PKey::RSA.new(2048)
cert.public_key = pkey
digest = "SHA256"
when "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"
# OpenSSL >= 3.5.0 support ML-DSA. OpenSSL::PKey.generate_key raises
# OpenSSL::PKey::PKeyError if ML-DSA algorithm is not supported.
# https://openssl-library.org/post/2025-04-08-openssl-35-final-release/
pkey = OpenSSL::PKey.generate_key(pkey_alg)
# OpenSSL::PKey::PKey#public_to_der was added in openssl gem 2.2.0.
# https://github.com/ruby/openssl/blob/v2.2.0/History.md?plain=1#L72-L75
cert.public_key = OpenSSL::PKey.read(pkey.public_to_der)
# OpenSSL::X509::Certificate#sign doesn't accept explicit digest
# algorithm, in ML-DSA because ML-DSA has a built-in digest.
digest = nil
else
raise(ArgumentError,
"#{pkey_alg} algorithm not found. "\
"RSA, ML-DSA-44, ML-DSA-65, and ML-DSA-87 "\
"algorithms are supported")
end

cert.version = 2 # This means v3
cert.serial = 0
name = OpenSSL::X509::Name.new(self[:SSLCertName])
cert.subject = name
cert.issuer = name
cert.not_before = Time.now
cert.not_after = Time.now + (365*24*60*60)
cert.public_key = rsa.public_key

ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
cert.extensions = [
Expand All@@ -192,29 +283,9 @@ def setup_certificate
if comment = self[:SSLCertComment]
cert.add_extension(ef.create_extension("nsComment", comment))
end
cert.sign(rsa, "SHA256")

@cert = cert
@pkey = rsa
end
cert.sign(pkey, digest)

# Establish the OpenSSL::SSL::SSLContext with the configuration
# parameters provided.
def setup_ssl_context
ctx = ::OpenSSL::SSL::SSLContext.new
ctx.cert = @cert
ctx.key = @pkey
ctx.min_version = self[:SSLMinVersion]
ctx.max_version = self[:SSLMaxVersion]
ctx.client_ca = self[:SSLClientCA]
ctx.ca_path = self[:SSLCACertificatePath]
ctx.ca_file = self[:SSLCACertificateFile]
ctx.tmp_dh_callback = self[:SSLTmpDhCallback]
ctx.verify_mode = self[:SSLVerifyMode]
ctx.verify_depth = self[:SSLVerifyDepth]
ctx.verify_callback = self[:SSLVerifyCallback]
ctx.cert_store = self[:SSLCertificateStore]
@ssl_ctx = ctx
[cert, pkey]
end
end

Expand Down
72 changes: 72 additions & 0 deletions test/drb/drbtest.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
require 'drb/drb'
require 'drb/extservm'
require 'timeout'
require_relative 'drbtest_utils'

module DRbTests

Expand DownExpand Up@@ -393,4 +394,75 @@ def test_07_break_18

end

# A PQC Utilities module inspired from ruby/rubygems omit_unless_support_pqc
module DRbPQCUtilities
# PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5.
# https://openssl-library.org/post/2025-04-08-openssl-35-final-release/
# Ruby OpenSSL >= 4.0 has useful methods in PQC use cases.
# https://github.com/ruby/openssl/blob/v4.0.0/History.md?plain=1#L25-L35
# And fixed the following bug related to PQC.
# https://github.com/ruby/openssl/pull/898
# However, we don't check OpenSSL and Ruby OpenSSL versions here
# for a flexible check for other SSL libraries such as LibreSSL and AWS-LC.
def omit_unless_support_pqc
# Even with a new enough OpenSSL, the runtime may keep PQC groups and
# signature algorithms out of its default negotiation lists (for example
# RHEL's system-wide crypto policies). The PQC server forces both, while
# the gem fetcher connects with the default client configuration, so a
# real loopback handshake is the only reliable way to tell whether this
# environment can negotiate PQC at all.
unless support_pqc_handshake?
yield if block_given?
omit 'OpenSSL or Ruby OpenSSL is too old to support PQC, '\
'or PQC handshake is not available in this OpenSSL configuration'
end
end

# Probe an actual PQC handshake between a forced-PQC server and a
# default-configured client, mirroring what the integration tests exercise.
# Memoized so the probe runs at most once per process.
def support_pqc_handshake?
return @support_pqc_handshake unless @support_pqc_handshake.nil?

@support_pqc_handshake = probe_pqc_handshake
end

def probe_pqc_handshake
server = TCPServer.new('127.0.0.1', 0)
ctx = OpenSSL::SSL::SSLContext.new
cert = Fixtures.read_cert('mldsa65_server.crt')
pkey = Fixtures.read_pkey('mldsa65_server.key')
ctx.add_certificate(cert, pkey)

# ctx.groups (OpenSSL::SSL::SSLContext#groups) requires Ruby OpenSSL >= 4.0.
return nil unless ctx.respond_to?(:groups=)

ctx.groups = 'X25519MLKEM768'
ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx)

port = server.addr[1]
server_thread = Thread.new do
client = ssl_server.accept
client.close
rescue OpenSSL::OpenSSLError
nil
end

client_ctx = OpenSSL::SSL::SSLContext.new
client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
socket = TCPSocket.new('127.0.0.1', port)
ssl = OpenSSL::SSL::SSLSocket.new(socket, client_ctx)
ssl.connect
ssl.close
true
rescue OpenSSL::PKey::PKeyError, OpenSSL::OpenSSLError, SystemCallError
false
ensure
server_thread&.join(5)
server_thread&.kill if server_thread&.alive?
ssl_server&.close
server&.close
end
end

end
34 changes: 34 additions & 0 deletions test/drb/drbtest_utils.rb
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: false

# This file does not require test/unit unlike drbtest.rb because it can
# be loaded by child processes (ut_*.rb) where
# test-unit's at_exit autorunner would cause errors.

require 'openssl'

module DRbTests

module Fixtures
module_function

def file_path(name)
File.join(__dir__, 'fixtures', name)
end

def read_file(name)
@file_cache ||= {}
@file_cache[name] ||= File.read(file_path(name))
end

# Raises OpenSSL::PKey::PKeyError when reading an ML-DSA-65 key file
# with old OpenSSL versions.
def read_pkey(name)
OpenSSL::PKey.read(read_file(name))
end

def read_cert(name)
OpenSSL::X509::Certificate.new(read_file(name))
end
end

end
Loading