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
16 changes: 13 additions & 3 deletions lib/rubygems/security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,18 @@ class Exception < Gem::Exception; end
ML_DSA_65_NAME = "ML-DSA-65"
ML_DSA_87_NAME = "ML-DSA-87"

ML_DSA_NAMES = [ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME].freeze
private_constant :ML_DSA_NAMES
# ML-DSA SubjectPublicKeyInfo algorithm OIDs (RFC 9881 Sections 2 and 4).
# https://www.rfc-editor.org/rfc/rfc9881.html
# Matching the OID rather than the algorithm name keeps this working on SSL
# libraries that spell the name differently: AWS-LC registers
# 2.16.840.1.101.3.4.3.18 as "MLDSA65", not "ML-DSA-65".

ML_DSA_OIDS = %w[
2.16.840.1.101.3.4.3.17
2.16.840.1.101.3.4.3.18
2.16.840.1.101.3.4.3.19
].freeze
Comment thread
justsmth marked this conversation as resolved.
private_constant :ML_DSA_OIDS

##
# Cipher used to encrypt the key pair used to sign gems.
Expand Down Expand Up @@ -527,7 +537,7 @@ def self.create_ml_dsa_key(algorithm)

def self.ml_dsa_key?(key)
algorithm = OpenSSL::ASN1.decode(key.public_to_der).value.first.value.first
ML_DSA_NAMES.include?(algorithm.ln)
ML_DSA_OIDS.include?(algorithm.oid)
rescue OpenSSL::ASN1::ASN1Error, OpenSSL::PKey::PKeyError, NoMethodError
false
end
Expand Down
10 changes: 10 additions & 0 deletions test/rubygems/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,11 @@ def omit_unless_support_ml_dsa_key
Gem::PQCUtilities.support_ml_dsa_key?
end

def omit_unless_support_ml_dsa_key_load
omit "OpenSSL cannot load ML-DSA keys" unless
Gem::PQCUtilities.support_ml_dsa_key_load?
end

def omit_unless_support_ml_dsa_cert
omit "Ruby OpenSSL cannot sign a certificate with an ML-DSA key" unless
Gem::PQCUtilities.support_ml_dsa_cert?
Expand All @@ -1710,6 +1715,11 @@ def omit_if_support_ml_dsa_cert
def omit_if_support_ml_dsa_key
omit "OpenSSL supports ML-DSA" if Gem::PQCUtilities.support_ml_dsa_key?
end

def omit_if_support_ml_dsa_key_load
omit "OpenSSL loads ML-DSA keys" if
Gem::PQCUtilities.support_ml_dsa_key_load?
end
end

# https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192
Expand Down
23 changes: 23 additions & 0 deletions test/rubygems/pqc_utilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,29 @@ def self.support_ml_dsa_key?
end
end

##
# Returns whether the runtime OpenSSL can load an ML-DSA key. A library can
# read ML-DSA keys without being able to generate them: AWS-LC parses ML-DSA
# keys and certificates, and signs and verifies with them, but registers no
# keygen by algorithm name, so support_ml_dsa_key? is false there. Tests that
# assert the "no ML-DSA support" path for a key or certificate read from disk
# need this instead of support_ml_dsa_key?.

def self.support_ml_dsa_key_load?
return @support_ml_dsa_key_load unless @support_ml_dsa_key_load.nil?

@support_ml_dsa_key_load =
begin
!OpenSSL::PKey.read(
File.read(File.join(CERTS_DIR, "mldsa65_private_key.pem"))
).nil?
# Mirrors Gem::PEMUtilities.load_key, which rescues the same error when an
# unsupported key algorithm is read.
rescue OpenSSL::PKey::PKeyError
false
end
end

Comment thread
justsmth marked this conversation as resolved.
##
# Returns whether the runtime can sign an X.509 certificate with an ML-DSA
# key. Ruby OpenSSL rejects the nil digest that needs before 3.3, so
Expand Down
4 changes: 2 additions & 2 deletions test/rubygems/test_gem_commands_build_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ def test_build_signed_gem
def test_build_signed_gem_ml_dsa_65
pend "openssl is missing" unless Gem::HAVE_OPENSSL && !Gem.java_platform?

omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

trust_dir = Gem::Security.trust_dir

Expand Down Expand Up @@ -642,7 +642,7 @@ def test_build_signed_gem_ml_dsa_65
def test_build_signed_gem_ml_dsa_65_without_ml_dsa_support
pend "openssl is missing" unless Gem::HAVE_OPENSSL

omit_if_support_ml_dsa_key
omit_if_support_ml_dsa_key_load

spec = util_spec "some_gem" do |s|
s.signing_key = ML_DSA_65_PRIVATE_KEY_FILE
Expand Down
8 changes: 4 additions & 4 deletions test/rubygems/test_gem_commands_cert_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def test_execute_certificate
end

def test_execute_certificate_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

use_ui @ui do
@cmd.handle_options %W[--certificate #{ML_DSA_65_PUBLIC_CERT_FILE}]
Expand Down Expand Up @@ -513,7 +513,7 @@ def test_execute_encrypted_private_key
end

def test_execute_private_ml_dsa_65_key
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

use_ui @ui do
@cmd.send :handle_options, %W[--private-key #{ML_DSA_65_PRIVATE_KEY_FILE}]
Expand All @@ -527,7 +527,7 @@ def test_execute_private_ml_dsa_65_key
end

def test_execute_private_ml_dsa_65_key_without_ml_dsa_support
omit_if_support_ml_dsa_key
omit_if_support_ml_dsa_key_load

use_ui @ui do
e = assert_raise Gem::OptionParser::InvalidArgument do
Expand All @@ -539,7 +539,7 @@ def test_execute_private_ml_dsa_65_key_without_ml_dsa_support
end

def test_execute_encrypted_private_ml_dsa_65_key
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

use_ui @ui do
@cmd.send :handle_options,
Expand Down
8 changes: 4 additions & 4 deletions test/rubygems/test_gem_security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_check_data
end

def test_check_data_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

data = digest "hello"

Expand All @@ -78,7 +78,7 @@ def test_check_data_invalid
end

def test_check_data_invalid_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

data = digest "hello"

Expand Down Expand Up @@ -249,7 +249,7 @@ def test_check_trust
end

def test_check_trust_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

Gem::Security.trust_dir.trust_cert ML_DSA_65_PUBLIC_CERT

Expand Down Expand Up @@ -427,7 +427,7 @@ def test_verify_wrong_digest_type
end

def test_verify_ml_dsa_65_without_ml_dsa_support
omit_if_support_ml_dsa_key
omit_if_support_ml_dsa_key_load

e = assert_raise Gem::Security::Exception do
@high.verify [ML_DSA_65_PUBLIC_CERT], nil, *dummy_signatures
Expand Down
8 changes: 4 additions & 4 deletions test/rubygems/test_gem_security_signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_initialize_key_path
end

def test_initialize_key_path_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

key_file = ML_DSA_65_PRIVATE_KEY_FILE

Expand All @@ -80,7 +80,7 @@ def test_initialize_key_path_ml_dsa_65
end

def test_initialize_key_path_ml_dsa_65_without_ml_dsa_support
omit_if_support_ml_dsa_key
omit_if_support_ml_dsa_key_load

key_file = ML_DSA_65_PRIVATE_KEY_FILE

Expand All @@ -103,7 +103,7 @@ def test_initialize_encrypted_key_path
end

def test_initialize_encrypted_key_path_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

key_file = ML_DSA_65_ENCRYPTED_PRIVATE_KEY_FILE

Expand Down Expand Up @@ -153,7 +153,7 @@ def test_sign
end

def test_sign_ml_dsa_65
omit_unless_support_ml_dsa_key
omit_unless_support_ml_dsa_key_load

signer = Gem::Security::Signer.new ML_DSA_65_PRIVATE_KEY, [ML_DSA_65_PUBLIC_CERT]

Expand Down