Skip to content
Draft
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
8 changes: 6 additions & 2 deletions lib/rubygems/commands/cert_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ def initialize
end

add_option("-A", "--key-algorithm ALGORITHM",
"Select which key algorithm to use for --build") do |algorithm, options|
"Select key algorithm for --build from",
"RSA, DSA, EC, ML-DSA-44, ML-DSA-65,",
"or ML-DSA-87. Defaults to "\
"#{Gem::Security::DEFAULT_KEY_ALGORITHM}.") do |algorithm, options|
options[:key_algorithm] = algorithm
end

Expand Down Expand Up @@ -100,7 +103,8 @@ def open_private_key(key_file)
rescue Errno::ENOENT
raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist"
rescue OpenSSL::PKey::PKeyError, ArgumentError
raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key"
raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid "\
"RSA, DSA, EC, ML-DSA-44, ML-DSA-65, or ML-DSA-87 key"
end

def execute
Expand Down
48 changes: 41 additions & 7 deletions lib/rubygems/security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@
# certificate for EMAIL_ADDR
# -C, --certificate CERT Signing certificate for --sign
# -K, --private-key KEY Key for --sign or --build
# -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA.
# -A, --key-algorithm ALGORITHM Select key algorithm for --build from
# RSA, DSA, EC, ML-DSA-44, ML-DSA-65,
# or ML-DSA-87. Defaults to RSA.
# -s, --sign CERT Signs CERT with the key from -K
# and the certificate from -C
# -d, --days NUMBER_OF_DAYS Days before the certificate expires
Expand Down Expand Up @@ -351,6 +353,20 @@ class Exception < Gem::Exception; end

EC_NAME = "secp384r1"

##
# ML-DSA algorithm names to use when building a key pair.
# ML-DSA-44: NIST security strength category 2, signature size 2420 bytes
# ML-DSA-65: NIST security strength category 3, signature size 3309 bytes
# ML-DSA-87: NIST security strength category 5, signature size 4627 bytes
# See NIST FIPS 204 Section 4 (Parameter Sets).
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf
# See Security (Evaluation Criteria) - 4.A.5 Security Strength Categories.
# https://csrc.nist.gov/projects/post-quantum-cryptography/post-quantum-cryptography-standardization/evaluation-criteria/security-(evaluation-criteria)

ML_DSA_44_NAME = "ML-DSA-44"
ML_DSA_65_NAME = "ML-DSA-65"
ML_DSA_87_NAME = "ML-DSA-87"

##
# Cipher used to encrypt the key pair used to sign gems.
# Must be in the list returned by OpenSSL::Cipher.ciphers
Expand Down Expand Up @@ -464,25 +480,41 @@ def self.create_digest(algorithm = DIGEST_NAME)
end

##
# Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC
# are supported.
# Creates a new key pair of the specified +algorithm+. RSA, DSA, EC,
# ML-DSA-44, ML-DSA-65, and ML-DSA-87 are supported.

def self.create_key(algorithm)
if defined?(OpenSSL::PKey)
case algorithm.downcase
when "dsa"
OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH)
when "rsa"
OpenSSL::PKey::RSA.new(RSA_DSA_KEY_LENGTH)
when "dsa"
OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH)
when "ec"
OpenSSL::PKey::EC.generate(EC_NAME)
when "ml-dsa-44"
OpenSSL::PKey.generate_key(ML_DSA_44_NAME)
when "ml-dsa-65"
OpenSSL::PKey.generate_key(ML_DSA_65_NAME)
when "ml-dsa-87"
OpenSSL::PKey.generate_key(ML_DSA_87_NAME)
else
raise Gem::Security::Exception,
"#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported."
"#{algorithm} algorithm not found. RSA, DSA, EC, ML-DSA-44, "\
"ML-DSA-65, and ML-DSA-87 algorithms are supported."
end
end
end

##
# Returns whether the +key+ requires an explicit digest algorithm for signing
# and verification. ML-DSA has a built-in digest and does not accept one.

def self.digest_required?(key)
key.is_a?(OpenSSL::PKey::RSA) || key.is_a?(OpenSSL::PKey::DSA) ||
key.is_a?(OpenSSL::PKey::EC)
end

##
# Turns +email_address+ into an OpenSSL::X509::Name

Expand Down Expand Up @@ -562,8 +594,10 @@ def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions
signed = create_cert signee_subject, signee_key, age, extensions, serial
signed.issuer = signing_cert.subject

digest_name = Gem::Security::DIGEST_NAME if digest_required?(signing_key)

begin
signed.sign signing_key, Gem::Security::DIGEST_NAME
signed.sign signing_key, digest_name
rescue OpenSSL::PKey::PKeyError, ArgumentError
raise Gem::Security::Exception,
"incorrect signing key for signing"
Expand Down
4 changes: 3 additions & 1 deletion lib/rubygems/security/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ def check_chain(chain, time)
# the +digest+ algorithm.

def check_data(public_key, digest, signature, data)
filtered_digest = digest if Gem::Security.digest_required?(public_key)

raise Gem::Security::Exception, "invalid signature" unless
public_key.verify digest, signature, data.digest
public_key.verify filtered_digest, signature, data.digest

true
end
Expand Down
3 changes: 2 additions & 1 deletion lib/rubygems/security/signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ def sign(data)

Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name

@key.sign @digest_algorithm.new, data
digest = @digest_algorithm.new if Gem::Security.digest_required?(@key)
@key.sign digest, data
end

##
Expand Down
File renamed without changes.
33 changes: 33 additions & 0 deletions test/rubygems/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,39 @@ def self.key_path(key_name)
PUBLIC_KEY = nil
PUBLIC_CERT = nil
end if Gem::HAVE_OPENSSL

begin
ML_DSA_65_PRIVATE_KEY = load_key "mldsa65_private"
ML_DSA_65_PRIVATE_KEY_PATH = key_path "mldsa65_private"

# ML_DSA_65_ENCRYPTED_PRIVATE_KEY is ML_DSA_65_PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE
ML_DSA_65_ENCRYPTED_PRIVATE_KEY = load_key "mldsa65_encrypted_private", PRIVATE_KEY_PASSPHRASE
ML_DSA_65_ENCRYPTED_PRIVATE_KEY_PATH = key_path "mldsa65_encrypted_private"

ML_DSA_65_PUBLIC_KEY = OpenSSL::PKey.read(ML_DSA_65_PRIVATE_KEY.public_to_der)

ML_DSA_65_PUBLIC_CERT = load_cert "mldsa65_public"
ML_DSA_65_PUBLIC_CERT_PATH = cert_path "mldsa65_public"
# OpenSSL::PKey::PKeyError is raised when ML-DSA is not supported by the
# OpenSSL version (requires OpenSSL >= 3.5).
rescue Errno::ENOENT, OpenSSL::PKey::PKeyError
ML_DSA_65_PRIVATE_KEY = nil
ML_DSA_65_PUBLIC_KEY = nil
ML_DSA_65_PUBLIC_CERT = nil
end if Gem::HAVE_OPENSSL

require_relative "pqc_utilities"
include Gem::PqcUtilities

def omit_unless_support_pqc
without_pqc_support do |message|
omit message
end
end

def omit_if_support_pqc
omit "PQC is supported" if Gem::PqcUtilities.support_pqc?
end
end

# https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192
Expand Down
68 changes: 3 additions & 65 deletions test/rubygems/local_ssl_server_utilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

require "socket"
require "openssl"
require_relative "pqc_utilities"

module Gem::LocalSSLServerUtilities
include Gem::PqcUtilities

CERTS_DIR = __dir__

def certs_dir
Expand Down Expand Up @@ -86,69 +89,4 @@ def cert(filename)
def key(filename)
OpenSSL::PKey.read(File.read(File.join(certs_dir, filename)))
end

def without_pqc_support(&block)
# PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5.
# https://openssl-library.org/post/2025-04-08-openssl-35-final-release/
unless OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30500000
yield "PQC algorithms require OpenSSL >= 3.5"
return
end
# ctx.groups (OpenSSL::SSL::SSLContext#groups) used in start_ssl_server
# mode :pqc requires Ruby OpenSSL >= 4.0.
unless Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new("4.0")
yield "PQC test requires Ruby OpenSSL >= 4.0"
return
end
# 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 Gem::LocalSSLServerUtilities.support_pqc_handshake?
yield "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 self.support_pqc_handshake?
return @support_pqc_handshake unless @support_pqc_handshake.nil?

@support_pqc_handshake = probe_pqc_handshake
end

def self.probe_pqc_handshake
server = TCPServer.new("127.0.0.1", 0)
ctx = OpenSSL::SSL::SSLContext.new
ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERTS_DIR, "mldsa65_ssl_cert.pem")))
ctx.key = OpenSSL::PKey.read(File.read(File.join(CERTS_DIR, "mldsa65_ssl_key.pem")))
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
end
90 changes: 90 additions & 0 deletions test/rubygems/mldsa65_encrypted_private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
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
-----END ENCRYPTED PRIVATE KEY-----
Loading