diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 1d86ecc909cc..0a62c4a4ea83 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -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 + private_constant :ML_DSA_OIDS ## # Cipher used to encrypt the key pair used to sign gems. @@ -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 diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index cd50ed5ae1a8..a1466921b6af 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -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? @@ -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 diff --git a/test/rubygems/pqc_utilities.rb b/test/rubygems/pqc_utilities.rb index 3b46daa93766..0eb6ccf73d77 100644 --- a/test/rubygems/pqc_utilities.rb +++ b/test/rubygems/pqc_utilities.rb @@ -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 + ## # 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 diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index 03af9dfff228..1adb52170d44 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -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 @@ -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 diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index e8063f76ac3c..d678c9778580 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -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}] @@ -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}] @@ -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 @@ -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, diff --git a/test/rubygems/test_gem_security_policy.rb b/test/rubygems/test_gem_security_policy.rb index ae264c0370c2..a15c83b7ca1d 100644 --- a/test/rubygems/test_gem_security_policy.rb +++ b/test/rubygems/test_gem_security_policy.rb @@ -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" @@ -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" @@ -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 @@ -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 diff --git a/test/rubygems/test_gem_security_signer.rb b/test/rubygems/test_gem_security_signer.rb index be79909453a4..6d99be533d35 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -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 @@ -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 @@ -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 @@ -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]