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
3 changes: 3 additions & 0 deletions lib/drb/drb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ def initialize(error)
attr_reader :reason
end

# Error raised when an error occurs on the bad configuration.
class DRbBadConfig < DRbError; end

# Class wrapping a marshalled object whose type is unknown locally.
#
# If an object is returned by a method invoked over drb, but the
Expand Down
210 changes: 166 additions & 44 deletions lib/drb/ssl.rb
Original file line number Diff line number Diff 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 Down Expand Up @@ -133,10 +164,15 @@ class SSLConfig
# c.setup_certificate
#
def initialize(config)
@config = config
@cert = config[:SSLCertificate]
@pkey = config[:SSLPrivateKey]
@ssl_ctx = nil
@config = config
@certs = nil
if config[:SSLCertificate] && config[:SSLPrivateKey]
@certs = [[config[:SSLCertificate], config[:SSLPrivateKey]]]
end
@certs = config[:SSLCertificates] if config.key?(:SSLCertificates)
@pkey_algs = config[:SSLPrivateKeyAlgorithms] ||
self[:SSLPrivateKeyAlgorithms]
@ssl_ctx = nil
end

# A convenience method to access the values like a Hash
Expand All @@ -162,25 +198,121 @@ 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
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]
# OpenSSL::SSL::SSLContext#sigalgs=, #client_sigalgs=, #groups= were
# added in openssl gem 4.0.0.
# https://github.com/ruby/openssl/blob/v4.0.0/History.md?plain=1#L25-L31
# OpenSSL::SSL::SSLContext#sigalgs= is supported in OpenSSL >= 1.0.2.
# https://github.com/ruby/openssl/blob/v4.0.0/ext/openssl/extconf.rb#L142-L143
unless ctx.respond_to?(:sigalgs=)
raise(DRbBadConfig,
unsupported_message(
':SSLSignatureAlgorithms', '4.0.0', '1.0.2'
))
end
ctx.sigalgs = self[:SSLSignatureAlgorithms]
end

if self[:SSLClientSignatureAlgorithms]
# OpenSSL::SSL::SSLContext#client_sigalgs= is supported in
# OpenSSL >= 1.0.2.
# https://github.com/ruby/openssl/blob/v4.0.0/ext/openssl/extconf.rb#L144-L145
unless ctx.respond_to?(:client_sigalgs=)
raise(DRbBadConfig,
unsupported_message(
':SSLClientSignatureAlgorithms', '4.0.0', '1.0.2'
))
end
ctx.client_sigalgs = self[:SSLClientSignatureAlgorithms]
end

if self[:SSLGroups]
# OpenSSL::SSL::SSLContext#groups is supported in all OpenSSL
# versions.
# https://github.com/ruby/openssl/blob/v4.0.0/ext/openssl/ossl_ssl.c#L3031
unless ctx.respond_to?(:groups=)
raise(DRbBadConfig,
unsupported_message(
':SSLGroups', '4.0.0'
))
end
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.public_key
digest = "SHA256"
when "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"
# openssl gem >= 4.0.0 and OpenSSL >= 3.5.0 support ML-DSA.
# https://github.com/ruby/openssl/blob/v4.0.0/History.md#notable-changes
# https://openssl-library.org/post/2025-04-08-openssl-35-final-release/
begin
pkey = OpenSSL::PKey.generate_key(pkey_alg)
rescue OpenSSL::PKey::PKeyError
raise(DRbBadConfig,
unsupported_message(
"#{pkey_alg} algorithm", '4.0.0', '3.5.0'
))
end
# 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(DRbBadConfig,
"#{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 +324,19 @@ def setup_certificate
if comment = self[:SSLCertComment]
cert.add_extension(ef.create_extension("nsComment", comment))
end
cert.sign(rsa, "SHA256")
cert.sign(pkey, digest)

@cert = cert
@pkey = rsa
[cert, pkey]
end

# 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
def unsupported_message(name, ruby_openssl_version,
openssl_version=nil)
message = "#{name} not supported. "\
"Install openssl gem >= #{ruby_openssl_version}"
if openssl_version
message += " building with OpenSSL >= #{openssl_version}"
end
message
end
end

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

module DRbTests

Expand Down Expand Up @@ -393,4 +394,90 @@ 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/
def support_pqc_openssl?
OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30500000
end

# Ruby OpenSSL >= 4.0 supports OpenSSL::SSL::SSLContext#groups,
# OpenSSL::SSL::SSLSocket#sigalg, #peer_sigalg used in PQC cases, fixing the
# following issue.
# https://github.com/ruby/openssl/blob/v4.0.0/History.md#notable-changes
# https://github.com/ruby/openssl/commit/9614b7f67a629cc8d31b6fe5be5040c68263ca8b
def support_pqc_ruby_openssl?
Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new('4.0')
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
ctx.add_certificate(
Fixtures.read_cert('mldsa65_server.crt'),
Fixtures.read_pkey('mldsa65_server.key'))
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::OpenSSLError, SystemCallError
false
ensure
server_thread&.join(5)
server_thread&.kill if server_thread&.alive?
ssl_server&.close
server&.close
end

def omit_unless_support_pqc
unless support_pqc_openssl?
yield if block_given?
omit 'PQC algorithms require OpenSSL >= 3.5'
return
end
unless support_pqc_ruby_openssl?
yield if block_given?
omit 'PQC test requires Ruby OpenSSL >= 4.0'
return
end
unless support_pqc_handshake?
yield if block_given?
omit 'PQC handshake is not available in this OpenSSL configuration'
end
end
end

module DRbPQC
include DRbBase
include DRbPQCUtilities

def test_01_hello
assert_equal('hello', @there.hello)
end
end

end
Loading