diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index 09ae43e35f68..1f19b26b3349 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -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 @@ -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 diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 6b46a483fd1c..d1fc7d09f1cc 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -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 @@ -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 @@ -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 @@ -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" diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 03ddbe530e48..7415f778c8bc 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -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 diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index 044562137cb8..5fdbb6feb1f5 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -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 ## diff --git a/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb b/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb index 2a287af19587..c68eabae9a6b 100644 --- a/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb +++ b/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb @@ -20,7 +20,7 @@ it "connects" do ssl_server = start_ssl_server allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::CA_CERT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::CA_CERT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end @@ -30,8 +30,8 @@ verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT ) allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::CA_CERT_FILE) - allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PemUtilities::CLIENT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::CA_CERT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PEMUtilities::CLIENT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end @@ -45,7 +45,7 @@ it "connects" do ssl_server = start_ssl_server(mode: :pqc) allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::MLDSA65_CA_CERT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::MLDSA65_CA_CERT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end @@ -56,8 +56,8 @@ verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT ) allow(Bundler.settings).to receive(:[]).and_call_original - allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::MLDSA65_CA_CERT_FILE) - allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PemUtilities::MLDSA65_CLIENT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::MLDSA65_CA_CERT_FILE) + allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PEMUtilities::MLDSA65_CLIENT_FILE) response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml") expect(response.code).to eq("200") end diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 174bd258168b..d40888fb7a7f 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -47,6 +47,7 @@ require "zlib" require_relative "mock_gem_ui" require_relative "pem_utilities" +require_relative "pqc_utilities" # JRuby on Windows raises TypeError inside File.symlink (the wincode helper # trips on a nil path), so any test that exercises Gem::Installer's symlink @@ -1659,7 +1660,15 @@ def prefetch(reqs) # :nodoc: end end - include Gem::PemUtilities + include Gem::PEMUtilities + + include Gem::PQCUtilities + + def omit_unless_support_pqc + without_pqc_support do |message| + omit message + end + end end # https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192 diff --git a/test/rubygems/local_ssl_server_utilities.rb b/test/rubygems/local_ssl_server_utilities.rb index d0d73dbd1219..95785b2573d4 100644 --- a/test/rubygems/local_ssl_server_utilities.rb +++ b/test/rubygems/local_ssl_server_utilities.rb @@ -6,9 +6,12 @@ require "socket" require "openssl" require_relative "pem_utilities" +require_relative "pqc_utilities" module Gem::LocalSSLServerUtilities - include Gem::PemUtilities + include Gem::PEMUtilities + include Gem::PQCUtilities + def initialize_ssl_server @ssl_server_thread = nil @ssl_server = nil @@ -74,69 +77,4 @@ def handle_request(client) client.print "HTTP/1.1 404 Not Found\r\n\r\n" end 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 = Gem::PemUtilities::MLDSA65_SSL_CERT - ctx.key = Gem::PemUtilities::MLDSA65_SSL_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 end diff --git a/test/rubygems/mldsa65_encrypted_private_key.pem b/test/rubygems/mldsa65_encrypted_private_key.pem new file mode 100644 index 000000000000..9dbae3d4522b --- /dev/null +++ b/test/rubygems/mldsa65_encrypted_private_key.pem @@ -0,0 +1,90 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/rubygems/mldsa65_private_key.pem b/test/rubygems/mldsa65_private_key.pem new file mode 100644 index 000000000000..686c961c231a --- /dev/null +++ b/test/rubygems/mldsa65_private_key.pem @@ -0,0 +1,88 @@ +-----BEGIN PRIVATE KEY----- +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 +K217eS//q+3kNLi5FMCDRxX1 +-----END PRIVATE KEY----- diff --git a/test/rubygems/mldsa65_public_cert.pem b/test/rubygems/mldsa65_public_cert.pem new file mode 100644 index 000000000000..510323d3d590 --- /dev/null +++ b/test/rubygems/mldsa65_public_cert.pem @@ -0,0 +1,118 @@ +-----BEGIN CERTIFICATE----- +MIIVpzCCCKSgAwIBAgIBADALBglghkgBZQMEAxIwKjEPMA0GA1UEAwwGbm9ib2R5 +MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTAgFw0xMjAxMDEwMDAwMDBaGA85OTk5 +MTIzMTIzNTk1OVowKjEPMA0GA1UEAwwGbm9ib2R5MRcwFQYKCZImiZPyLGQBGRYH +ZXhhbXBsZTCCB7IwCwYJYIZIAWUDBAMSA4IHoQBTF/mgjzpQn7G2YapfGoVAsa4p +qU901UYehmjYRLfRP+BDuGvg4G7Rm5K5kXJIew47XZ3NI4GU7aF924V8aHEfk+EZ +SH8GnN4MRKOF4Fs0lTagZiVQV+mVvFsuNXYQ1OnDtDPe0jSlgG6HYAXLD7hTAWYs +u2xmQU0THQjY0FKZyUy1zo5y3+MsNrI3f3vmfliHFfGGzWrmNiUqpBdJg/bB5Y2C +fJMQQyui8ijJrdimV2t7jHMlsQgefxiZmDbqwH5seg7iUNI+/RBAi39ipKo/C0TU +bwZoM31aCtpzGiewKLFuPrpBDMJuoMYVqb18bh9ogXfVD80RW78lUuhNVaXWXPaD +UofSWikgJNq42DDYcSMLDewbUbZpe62nQJAdMc8zdrUhPIVAbwFpTkXQdDfJXzuq +10Zgtlh3IrrUVSe74t/Mc0oqoIL4u6e5/PwOnFyv5v9MRFXIb15ISePuo/BrS/rv +B+NY9TsCpsq6XpdHB+E0zpPE/kxHGXqWmcat7urWb5/kGv3rd9uym82It5pstoPB +vZ5r3Sx2XR/vS/v/10haw80nLRRT0vtOd8T3/I7LU3Nc8yrSLF5X1pLwnQxNKDmY +28lPeNYHQ3dz5Lc7lpyqjss/rTRS3Pjy5OaoyZ4t5Q2iVrsM4A77kcOZt0oi3dHE +q7GjCL0exko+k/yCW2hLVvs9F1V2UNyOG861E/fRmQ9Pga93Mo9a8yrkE46WqdJD +p7b93ZOOuD4TSD13u254xtmZH5rAHSMHhdwLQ/AdQaLi/T9wwFDLIiI/q8ZdQCAZ +9pgO1TnnuKJt0mhnyzijXmMa1tDW7JdH9iudUu8UAg/WT2aiarycrRigpa/9Ex9U +JhGxAYcqQtgsprANA/cXhWZxaHM23S/ryP+K7gc/40c/Kn7qgkxh47j5f8mLlI0F +zLAwzuHtyHusShOzuW3+mICYPHL6lw33D6yQBAgliiOWxTjONuF8DyYJDzW4PCl1 +fF1rC805vLYbN7uQtPZZMUzyxpLC0/w7+pYIdC3MekZok+mp+6X58fzL6gBRNRe5 +jdzo00BLSg9cJjLb9iTQjB7rzxea0r4m9ap2iypKjv9izLtDOYHLAePIPHy9WwuH +nSbz7zIGX6t4eOCWO+koeoMf/SW6F0iLsPCzNyT5PVHNrxlReTu52yxk9X31fIaP +83CAGKoLD1MWVm5sDqI417yLD3eQsUQB75dtNOBTnuNt0lYTzhUe2R3LSnV6rFPN +7uKmSnSySi9HasZU2AMjBU0ezDS46y48J7h3yk4wv624y1lcYYQYCGpg27lmdhPZ +frMvp0n3k4oii2udHOW4sFLCooZyF2vRCFAm74mWKWBbtWAXAsxCmOEFfyNMySCM +rnrH0cuu0XhEFGzsm5PS419K8lUeDVMytL1ETfFUiy6BFQK98PA0KCpVWwGK0HlC +tK/9b/Z5ZX9GYRPCXbicjiRn5bh8ozhpZWEqAU75z6E8veGAE93+kMGPOj5WzzHT +Q6ZGJ7R5GDq7xwQoJ3jvH6/8YifQ6xsRlY4Sa5Nk9HAaapisyoM2GBL9vcKKG01r +0NvK4E6Rmz0B5ZTQlbucaeeiphNOqgaMW8bl/fLtOslheF6+uwyUGmQH9MUlDrGq +NZ5e1mcZGSLVT8eYmcCOVY91RVgm9o3FhR4qgPgPvVO/ERfgDPxjMUWTGTC15Ofr +6/myndJqYe/5RE1A+G4tzOzCHtKpQTE+S6f1/8d6anmXfilxUsp+amDdFJxdbuH+ +CWgO2z78Cro62NJHnJg+8PDMEvjJQEvzkOy5L+hDUwE3x4Xcih66QagPzC963CeB +NhKHGHMexaK2InVeCGKoR3YNORAN9dm0SeiYU1mCHOEqT1c2HUzPn8xOhfoq1Nnn +vy1fXvuxSB9tZVUOzsgzO62TRJ6+Mlpi3cdHFTE9wYVRj7BwOv2NWG5isx5uChcL +VVc1az1i3Up88ZiDHEPt2N9iI6TiwQmgxio3prx5miq5T4Gl3poUzxmZ+H7amAzK +OJXeRiRrzr08MN7g0t8y9Gz83KCGbRCtl6cYN2SweVLZuirL/HM8V0hYeTV/fKE0 +iOw24qrUsCCy/7nLR4EDlgK4CEx1+jwkFIfZr8oQi4LtbeyvNtg5zM1knzNY9vAl +4HEVSEMyE9NVX2iYdkLnbTtbOM9pJ4U1+/RRiB7kXgo9bghjkmknW5Uad73Hqvnm +BmE2nbfaiFxMYV4Ze+aFzxEwxqyfxjBcYBB08lzLjoLFvc0k0LMvJmrczIqrhkiu +uftLL6tKj+zbFa03EtNih5HSnQdpc+Uz8z2WgaPPR7NaE7hzWWSUt8We/jqsVp0z +Kaas/YStqMzcSWZyd+boiTZb8oHv/C4kV3BuefvRIqBwj9PWNTCnPKaiZOChGf4P +rb4YRGjF6wrLRby1kK3bnkueGA5HySOgyc7Hp89Rf1fAJtgickm6kdUx2xcHyVg5 +KwMRNBVOC7/NkCM05W0Jtt9rshozq0DUIqJ3SJUZrgX7LH1UMienwGDSm3LAQ26f +42s6xM4t1prqgq45O3dfZ0PCfdv0z+cEnjkD2mKXcpHQ7PpL0EH3NquyIdwUUEQg +4ugdmEJezOU+agAwkKNdMFswGQYDVR0RBBIwEIEObm9ib2R5QGV4YW1wbGUwHQYD +VR0OBBYEFATYBlv4POttAuYMOehOwhdw4kRGMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgIEMAsGCWCGSAFlAwQDEgOCDO4AvRrx7ammIhNRSmHGQb5J73mE +17mlSdnxgKTJYCTfqHFQjdzHo3seKN/O0zw3fxUL6/k/Mf0+X6HYN3+93/h+IdXv +9MB2nWl3SkwXHp63zPIVCzf7cLBq0ipzD7NsengVQe5F/t46+VuF6Rm8qxEQzgVP +Bd17QIlT1wzcML5g5JZkjm87ysOC9KaQfxcYZ9K3WHpWxXHHHo3pfy0jeeqbv9g+ +cojSGoD+6/RuJjEpPQPUvSsM1CrAePcSeCxv+8McCH8tjI54sZtEe7QJgClqUU+4 +Z2VHQi0UdCRlteRLPpWjhDS1vWBFeDdGSZqENPmzTs2oY4EqaBRHhMDH0wx3Zenh +ERLTtW0LTQ8NgUD0bp2AFqJA8MMoB9rydFHdueoGI/hu4REWsDG8TZvzlPwm8LvG +NA2tElwLn6uw5gWuh5oymU5ZEqV5cPwtV5ITiqjr5UGdLNyobIdCVrqWsGRUZSjq +BDErflCwCf5i+LxyoQvxaa4FhFpn3Dj5ELed0h38wFUA5Iu56xXxgjt9vDZ5ZGkE +fPoY04bI1Gdsx+1psg0TEmTUr5XKvn1TvqcMPgOG8B+PpDIiZe0Yw0TT/HJGSFAz +JabIbBXhywoIQuSZVBUKiHNv4uxX2BkY1b9f2Ph3KW/J+moihcVmSH9iB3Q3Pxp0 +HUNaO7nTZ1e99FMHMaP4POhLjIOFdwFaVUFFozspZzQ9Yh3K1pSChY3M7prbamxV +8NOEg8964nT8+bynzSFJLaCw8Mw9Xo2QhzsIr5D+VKRQJSmeqqs33zvzzj0OPn7n +kFpd1zSNGU3ldXFnHM7Lui3CyS0YJ8kXt9bJOsS4dtXk1oohaIxfsbGhhqNJBQGF +5rQ8/GEfZn0sxH6bcpV5Njhf3i02KPejZJAcrNrCYsZjyDdDDpdJie5qzznl4BH4 +UbXrZ26q9eYQbU8aI4nRL1nQvPnOqc610Gz3vcW1mDdgIQTomIYo/LFctmJRG0Fx +Dh9YjMQ8NM4EaCjgFt2zkfx7tyIlUXiBiiEz0A0nCmAvjOV5U4YHo9UqgCAMDYCM +3cwKQMScyeNhcdSP0DKqQ5fTX5zm8Sd53t7mR3K/hG95pD0cQqEOy83/JVPJ2ImD +VvrhSm+N4JZts6181Zrljhm7CgZesgVwX/xBGjfGNsF/vlnsNCCnqLMcQvF758xD +TU8cN+PVptuPBwGnelg1uP3T7y7uHFtybWG833mnL2MCdbtTGHnffdUO7QcE/mlp +NF2SgPvjtrtr+vlj/IG+lhVYheH2ihcfvqZEdlbaBqQaWEioF0oPQ1CI3zG5OCK8 +l7R/r2yjP4Ye4ImcHncIy2ezuYRw9tDVJEV53OZFr3mI45vgjI6CZ1V052Cl9h1G +xPeueTjZZyOY7apWGLHXPHzw3Cgu+QE09hzR9Fu9Oh3REo9BR8WadqMuPT1QiLnb +4qrq/Oc4CcMEXZrQTGSmkoH/PyRs5Ka9YCc1pVR4U5apb6CTU8wib3e4ieyRI1bR +bljlOi4wzSPxTEWErSF5rX559hBYUPtiXC3gRQeMgTmsq0BhT4kmXCZwsRUH9H8t +a1gopEPIBOP4xxuIESMWyCZ1UllMPqoOIEpeTVyCZ7H0eLDQdR6VscJB1JIMDQVf +S2QEJSd9BnwSGmvX8Ns2oi5C8uszTGiFaM9hQUXzQLtqOQn0Khh73GMtumM6tiiD +kRfaTVI2AkkCVcWnvQj2vsAc1m6YiNLNqNxNwNtoqYmNnxmb7cGwELbZIugkEBwq +L5XxWom6aPAvzCdOLzgPXnxVo7iZ1oKeKBxUKrwTB92klaUFepv5b8V1NUQa+8qF +ng3Te8DYquh/ithRWW371OhLRNWeAv9KiYSqoXEmWB9gCLMVh2wHg6W23OSUoDWC +f0DFg9eRPSJMo5Bp0uOOBamfE+F5nPR7Eub5n5xelKrg60yZ7xkEZ+6xgZ62Ns5J +HLFhRi93gIeebQI3xWxCu5MmnLxJ6lQL3ZGiXNNe0ppA4i0e7Hlat4QtUQiZAuLm +tQ+e1um14mKSbyNAbcp9xddkvOcMSxkM1Yzi/Ko2ejKtseugTW7JTJm8tZJAiXLP +KwTQFSe//JJy5cpLe/0J6uupoZERPxPAEHqC+eg9VodmJpXAhbnu2JVCAs+tkLc/ +K1kzuR1gMagP8qeoZmanKN2MCP4TWTJRoJIdARAA/joDYfmCjg7XIR5kY2jRCgSY +PDRonZF8bxc0nF6h2anWeeOdAKaMKtOiRRmqLTEn3dazOCNTKQ1n4Mw8vzi1aqxC +f78HY4I+jWQyK6P7ZMITW0gY3FQu+mtjPjXqc1i/uVlLdHu27l1PjuXCQ2ra8w27 +lY4xzDju3Sm0RqZzTBzja/BCCC9M1m/CglfnORofiwHiUC+H8JnVO68IqmtHHlrq +xKYF0mQryi86APlRuNAgA/DeElaGaN3AfDKk4ZOJag3GxiXXGIu1wgEoL6Tabr/N +m6rEmQD4yy590FV1s0fbltOdKFlw1znctaO3uTYtOeJxViycZs10mY2XlkRFDkeM +HjDut/GTF0WNvaepnPOZb7G1ZGb0xhzcawNSKkvUaQnOvHK1KGEEAsha4HYRoCSp +j6TEmdg30mApL+Q8Vu00XTOQYJHlrITXofxk+wy2dmu3vfExpFTXEIJhq6jnAdNE +CPRtQRK+bKETM1pnzd6SImJc2uF/251emYCsIZfLXpWcwqEVljEtO2JcjXSTwteW +3b9o7TkJ6vZxYX5nhgXIWV8lqziXRHNu5D74fpWvFXV9mnoJWX/iHK1TKEr+R4gm +IPW9f7sKRqLEgHO/zks2yDu57jl5xziKwT+xVd1CXLjQ4AV2azBUuwagaI1rst8t +1TflYBhcLTdw8ROT1oiFeUE4Epwl4GD/G3qnfIbj3MW1jvmie47R7CAmo43+I/QI +kmg9/d30eRstC+q58gSKfuEq+hU11La1lvmp441bqJyHW3J3v7w7UQdWN0ZveH1k +ibRX2dl2YgLRMHylmo79XwmUjghTbBpQ7cbPIKR1ZexctFtzZp/FL8op/88qZrVo +nnqPffR90Xpauq4GkqgDw9tOnYpkRWwD4q5FZMoNnulRBXVnlD71FyrlBly6WcsN +GImQ1zwkPX5jZ6TC+PvV5PqzrlkErFXLfQCMwCSW11F81xRZhecpIixJLKNAPuQZ +lpTnk7hQSzXLhsvHv6+pM2F6Sim7aunwAm1gzJOhI3pVWwash1CnwVoHJVxozugm +rJj3tlgM+Syi3MqvVRRG+bCyJz3/KqEOkp9Mu5Dqph6WEBr/ZW1C/Rf+R6LU7aSL +6vFwMwoPb4hm9SnPU9GRgdWQ4IZ73MwFS+M4qU2S0ez4Gc6zagg0epW/9zhgq44q +CIvw78rBAndsCuRqvEk2v4CMcJAgTVdXGD394ZViyzRwh4xiEmPRjBzfwxst2xw3 +iugoZSx725hLnSH2Tyqd/6pwgGOKoq/JUpgJt7EfqVe2tGV65qhtqfUIj2k9ERX9 +y8rnRzS+njm34TtTvokGMXng7jvLTjYneRIYAPVyrUWaxm5CCwXXm9xDlJksVzbD +8f0bDfdatgVbFsVz+lU3Hvq5XcSccY9UOkfOup2z1CKRRcEdlBB59XXLDrz1BBiK +CcuaGkPtaJ5NIEfgZImLFtpBrjGzYfR7dbVbz4by8AT8FWlfD2/u1DDveb7y1Ze/ +yo/3YneQG42rNGKDB951/pc6mnILA4SXU7+kqdh+/WlFQAvltLjwrOloKjw+3IAy +JW5eoXb8cL0HmrlEeKuZ8RZCZH6j5XB30KHQ2KS45RpDjjExDyIqfkveIWZOL5AV +KPVCOKXGAmCqjvkc2VpT1pSgL/MFUKlVZN3EeBODriotCRd4tTSjdZlzam43F2GI +j7WbD7OIWikCGPeF9dJwT+gK7+sgpGS5vrts66rwsUIF5GnBWnClGuOJ9jC2935I +Qzs7KYQqT9gOdkRpsDraouf67rTNhMnvEZ75xOCtmwtk6ChZxBgI/inYEfn4RoRU +K/Ntkb3vxWl2FNEV//U+HZj2WZvfIZMAFW/Kd/y9wQTc0WqpiaQTGzGCfuEsX8mr +45wolB+WuCdXzhgXU1gpq3Op2Uf4O+RJlnmeTZFeCEBk6+E4EOypb9VULl/ipqjm +p5LnkXr6svfOvRb0M2+UOSEGB4A+qOO+brKZQw3FCTijR8ijtN9jUFzYaJE+NTKF +jZmUf+tIfZhfx0CCffay0KsZXV1syd8KwL2Brtmz0lBnEsFyYIKN+VFXTdySxZwU +6EM0Xl67LDtc2Bj5xq+JYfUp1GIqWueWZggT30nbEYZuObXqqBRnEPtk0VhLuwf9 +PdZr8yC1bCkRs43QwKhjcZai4PP/M01/seQlKs3a6A0WGbbG1+LoBDyNpcECBCUq +eL8AAAAAAAAAAAAAAAAAAAAAAAAABwwRGR4k +-----END CERTIFICATE----- diff --git a/test/rubygems/mldsa65_public_cert_32.pem b/test/rubygems/mldsa65_public_cert_32.pem new file mode 100644 index 000000000000..ecf7f7e6ac56 --- /dev/null +++ b/test/rubygems/mldsa65_public_cert_32.pem @@ -0,0 +1,118 @@ +-----BEGIN CERTIFICATE----- +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 +n7/nAAAAAAAAAAAAAAAAAAAAAAYNFRshJw== +-----END CERTIFICATE----- diff --git a/test/rubygems/mldsa65_public_key.pem b/test/rubygems/mldsa65_public_key.pem new file mode 100644 index 000000000000..61fbbb8e8b54 --- /dev/null +++ b/test/rubygems/mldsa65_public_key.pem @@ -0,0 +1,44 @@ +-----BEGIN PUBLIC KEY----- +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 +5T5qADCQ +-----END PUBLIC KEY----- diff --git a/test/rubygems/pem_utilities.rb b/test/rubygems/pem_utilities.rb index bec08d9a38dd..7c69c375b381 100644 --- a/test/rubygems/pem_utilities.rb +++ b/test/rubygems/pem_utilities.rb @@ -5,7 +5,7 @@ require "rubygems/openssl" -module Gem::PemUtilities +module Gem::PEMUtilities ## # Loads certificate named +cert_name+ from test/rubygems/. @@ -33,7 +33,8 @@ def self.cert_file(cert_name) end ## - # Loads a private key named +key_name+ with +passphrase+ in test/rubygems/ + # Loads a private key named +key_name+ with +passphrase+ or a public key + # named +key_name+ in test/rubygems/ def self.load_key(key_name, passphrase = nil) key = File.read(key_file(key_name)) @@ -98,7 +99,7 @@ def self.pem_file(pem_name) # ENCRYPTED_PRIVATE_KEY is PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE ENCRYPTED_PRIVATE_KEY = load_key "encrypted_private", PRIVATE_KEY_PASSPHRASE ENCRYPTED_PRIVATE_KEY_FILE = key_file "encrypted_private" - PUBLIC_KEY = PRIVATE_KEY.public_key + PUBLIC_KEY = load_key "public" PUBLIC_KEY_FILE = key_file "public" PUBLIC_CERT = load_cert "public" PUBLIC_CERT_FILE = cert_file "public" @@ -117,6 +118,15 @@ def self.pem_file(pem_name) EXPIRED_CERT_FILE = cert_file "expired" FUTURE_CERT = load_cert "future" WRONG_KEY_CERT = load_cert "wrong_key" + # ML-DSA-65 + ML_DSA_65_PRIVATE_KEY = load_key "mldsa65_private" + ML_DSA_65_PRIVATE_KEY_FILE = key_file "mldsa65_private" + ML_DSA_65_ENCRYPTED_PRIVATE_KEY = load_key "mldsa65_encrypted_private", PRIVATE_KEY_PASSPHRASE + ML_DSA_65_ENCRYPTED_PRIVATE_KEY_FILE = key_file "mldsa65_encrypted_private" + ML_DSA_65_PUBLIC_KEY = load_key "mldsa65_public" + ML_DSA_65_PUBLIC_KEY_FILE = key_file "mldsa65_public" + ML_DSA_65_PUBLIC_CERT = load_cert "mldsa65_public" + ML_DSA_65_PUBLIC_CERT_FILE = cert_file "mldsa65_public" # Keys and certificates manually created without script # RSA 3072 bits diff --git a/test/rubygems/pqc_utilities.rb b/test/rubygems/pqc_utilities.rb new file mode 100644 index 000000000000..f9295f7f4849 --- /dev/null +++ b/test/rubygems/pqc_utilities.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# This file can be loaded by RubyGems test-unit files and Bundler rspec files. +# Don't add test-unit or rspec dependent logic in this file. + +require "socket" +require "openssl" + +module Gem::PQCUtilities + CERTS_DIR = __dir__ + + # 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 without_pqc_support(&block) + # 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::PQCUtilities.support_pqc_handshake? + yield "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 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 = Gem::PEMUtilities::MLDSA65_SSL_CERT + ctx.key = Gem::PEMUtilities::MLDSA65_SSL_KEY + # ctx.key is nil when unsupported ML-DSA-65 algorithm's file is read with + # old OpenSSL versions. + return nil unless ctx.key + + # 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::OpenSSLError, SystemCallError + false + ensure + server_thread&.join(5) + server_thread&.kill if server_thread&.alive? + ssl_server&.close + server&.close + end +end diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index cd88421c0754..2c424aa0df6e 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -606,6 +606,39 @@ def test_build_signed_gem assert gem.verify end + def test_build_signed_gem_ml_dsa_65 + pend "openssl is missing" unless Gem::HAVE_OPENSSL && !Gem.java_platform? + + omit_unless_support_pqc + + trust_dir = Gem::Security.trust_dir + + spec = util_spec "some_gem" do |s| + s.signing_key = ML_DSA_65_PRIVATE_KEY_FILE + s.cert_chain = [ML_DSA_65_PUBLIC_CERT_FILE] + end + + gemspec_file = File.join(@tempdir, spec.spec_name) + + File.open gemspec_file, "w" do |gs| + gs.write spec.to_ruby + end + + @cmd.options[:args] = [gemspec_file] + + util_test_build_gem spec + + trust_dir.trust_cert( + OpenSSL::X509::Certificate.new( + File.read(ML_DSA_65_PUBLIC_CERT_FILE) + ) + ) + + gem = Gem::Package.new(File.join(@tempdir, spec.file_name), + Gem::Security::HighSecurity) + assert gem.verify + end + def test_build_signed_gem_with_cert_expiration_length_days pend "openssl is missing" unless Gem::HAVE_OPENSSL && !Gem.java_platform? diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index 17fe3d789a6f..ae7385a05af2 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -157,6 +157,45 @@ def test_execute_build_key_algorithm_ec_key assert cert.public_key.is_a? OpenSSL::PKey::EC end + def test_execute_build_key_algorithm_ml_dsa_65_key + omit_unless_support_pqc + + passphrase = "Foo bar" + + @cmd.handle_options %W[--build nobody@example.com --key-algorithm ml-dsa-65] + + @build_ui = Gem::MockGemUi.new "#{passphrase}\n#{passphrase}" + + use_ui @build_ui do + @cmd.execute + end + + output = @build_ui.output.squeeze("\n").split "\n" + + assert_equal "Passphrase for your Private Key: ", + output.shift + assert_equal "Please repeat the passphrase for your Private Key: ", + output.shift + assert_equal "Certificate: #{File.join @tempdir, "gem-public_cert.pem"}", + output.shift + assert_equal "Private Key: #{File.join @tempdir, "gem-private_key.pem"}", + output.shift + + assert_equal "Don't forget to move the key file to somewhere private!", + output.shift + + assert_empty output + assert_empty @build_ui.error + + assert_path_exist File.join(@tempdir, "gem-private_key.pem") + + cert_path = File.join(@tempdir, "gem-public_cert.pem") + assert_path_exist cert_path + cert = OpenSSL::X509::Certificate.new(File.read(cert_path)) + assert cert.public_key.is_a? OpenSSL::PKey::PKey + assert_match(/type_name=ML-DSA-65/, cert.public_key.inspect) + end + def test_execute_build_bad_email_address passphrase = "Foo bar" email = "nobody@" @@ -329,6 +368,53 @@ def test_execute_build_bad_key assert_equal "incorrect signing key for signing", e.message end + def test_execute_build_ml_dsa_65_key + omit_unless_support_pqc + + @cmd.handle_options %W[ + --build nobody@example.com + --private-key #{ML_DSA_65_PRIVATE_KEY_FILE} + ] + + use_ui @ui do + @cmd.execute + end + + output = @ui.output.split "\n" + + assert_equal "Certificate: #{File.join @tempdir, "gem-public_cert.pem"}", + output.shift + + assert_empty output + assert_empty @ui.error + + assert_path_exist File.join(@tempdir, "gem-public_cert.pem") + assert_path_not_exist File.join(@tempdir, "gem-private_key.pem") + end + + def test_execute_build_encrypted_ml_dsa_65_key + omit_unless_support_pqc + + @cmd.handle_options %W[ + --build nobody@example.com + --private-key #{ML_DSA_65_ENCRYPTED_PRIVATE_KEY_FILE} + ] + + use_ui @ui do + @cmd.execute + end + + output = @ui.output.split "\n" + + assert_equal "Certificate: #{File.join @tempdir, "gem-public_cert.pem"}", + output.shift + + assert_empty output + assert_empty @ui.error + + assert_path_exist File.join(@tempdir, "gem-public_cert.pem") + end + def test_execute_certificate use_ui @ui do @cmd.handle_options %W[--certificate #{PUBLIC_CERT_FILE}] @@ -340,6 +426,19 @@ def test_execute_certificate assert_equal PUBLIC_CERT.to_pem, @cmd.options[:issuer_cert].to_pem end + def test_execute_certificate_ml_dsa_65 + omit_unless_support_pqc + + use_ui @ui do + @cmd.handle_options %W[--certificate #{ML_DSA_65_PUBLIC_CERT_FILE}] + end + + assert_equal "", @ui.output + assert_equal "", @ui.error + + assert_equal ML_DSA_65_PUBLIC_CERT.to_pem, @cmd.options[:issuer_cert].to_pem + end + def test_execute_list @trust_dir.trust_cert PUBLIC_CERT @trust_dir.trust_cert ALTERNATE_CERT @@ -391,6 +490,35 @@ def test_execute_encrypted_private_key assert_equal ENCRYPTED_PRIVATE_KEY.private_to_pem, @cmd.options[:key].private_to_pem end + def test_execute_private_ml_dsa_65_key + omit_unless_support_pqc + + use_ui @ui do + @cmd.send :handle_options, %W[--private-key #{ML_DSA_65_PRIVATE_KEY_FILE}] + end + + assert_equal "", @ui.output + assert_equal "", @ui.error + + assert_equal ML_DSA_65_PRIVATE_KEY.private_to_pem, + @cmd.options[:key].private_to_pem + end + + def test_execute_encrypted_private_ml_dsa_65_key + omit_unless_support_pqc + + use_ui @ui do + @cmd.send :handle_options, + %W[--private-key #{ML_DSA_65_ENCRYPTED_PRIVATE_KEY_FILE}] + end + + assert_equal "", @ui.output + assert_equal "", @ui.error + + assert_equal ML_DSA_65_ENCRYPTED_PRIVATE_KEY.private_to_pem, + @cmd.options[:key].private_to_pem + end + def test_execute_remove @trust_dir.trust_cert PUBLIC_CERT @@ -811,7 +939,9 @@ def test_handle_options_key_bad @cmd.handle_options %W[--private-key #{bad}] end - assert_equal "invalid argument: --private-key #{bad}: invalid RSA, DSA, or EC key", + assert_equal "invalid argument: "\ + "--private-key #{bad}: "\ + "invalid RSA, DSA, EC, ML-DSA-44, ML-DSA-65, or ML-DSA-87 key", e.message end diff --git a/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb index 813e8e2f0265..144d80c134b0 100644 --- a/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb +++ b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb @@ -155,10 +155,4 @@ def with_configured_fetcher(config_str = nil, &block) fetcher.close_all Gem.configuration = nil end - - def omit_unless_support_pqc - without_pqc_support do |message| - omit message - end - end end if Gem::HAVE_OPENSSL diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 20f4b9fa8951..114cbaa7ee5a 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -99,15 +99,68 @@ def test_class_create_key_downcases assert_kind_of OpenSSL::PKey::DSA, key end + def test_class_create_key_ec + key = Gem::Security.create_key "ec" + + assert_kind_of OpenSSL::PKey::EC, key + end + + def test_class_create_key_ml_dsa_44 + omit_unless_support_pqc + + key = Gem::Security.create_key "ml-dsa-44" + + assert_kind_of OpenSSL::PKey::PKey, key + assert_match(/type_name=ML-DSA-44/, key.inspect) + end + + def test_class_create_key_ml_dsa_65 + omit_unless_support_pqc + + key = Gem::Security.create_key "ml-dsa-65" + + assert_kind_of OpenSSL::PKey::PKey, key + assert_match(/type_name=ML-DSA-65/, key.inspect) + end + + def test_class_create_key_ml_dsa_87 + omit_unless_support_pqc + + key = Gem::Security.create_key "ml-dsa-87" + + assert_kind_of OpenSSL::PKey::PKey, key + assert_match(/type_name=ML-DSA-87/, key.inspect) + end + def test_class_create_key_raises_unknown_algorithm e = assert_raise Gem::Security::Exception do Gem::Security.create_key "NOT_RSA" end - assert_equal "NOT_RSA algorithm not found. RSA, DSA, and EC algorithms are supported.", + assert_equal "NOT_RSA algorithm not found. RSA, DSA, EC, "\ + "ML-DSA-44, ML-DSA-65, and ML-DSA-87 "\ + "algorithms are supported.", e.message end + def test_class_digest_required_rsa + assert Gem::Security.digest_required?(Gem::Security.create_key("rsa")) + end + + def test_class_digest_required_dsa + assert Gem::Security.digest_required?(Gem::Security.create_key("dsa")) + end + + def test_class_digest_required_ec + assert Gem::Security.digest_required?(Gem::Security.create_key("ec")) + end + + def test_class_digest_required_ml_dsa_65 + omit_unless_support_pqc + + refute Gem::Security.digest_required?(Gem::Security.create_key("ml-dsa-65")) + end + def test_class_get_public_key_rsa pkey_pem = PRIVATE_KEY.public_key.public_to_pem @@ -120,6 +173,14 @@ def test_class_get_public_key_ec assert_respond_to pkey, :public_to_pem end + def test_class_get_public_key_ml_dsa_65 + omit_unless_support_pqc + + pkey = Gem::Security.get_public_key(ML_DSA_65_PRIVATE_KEY) + + assert_respond_to pkey, :public_to_pem + end + def test_class_email_to_name assert_equal "/CN=nobody/DC=example", Gem::Security.email_to_name("nobody@example").to_s @@ -179,43 +240,7 @@ def test_class_reset end def test_class_sign - issuer = PUBLIC_CERT.subject - signee = OpenSSL::X509::Name.new([["CN", "signee"], ["DC", "example"]]) - - key = PRIVATE_KEY - cert = OpenSSL::X509::Certificate.new - cert.subject = signee - - cert.subject = signee - cert.public_key = key.public_key - - signed = Gem::Security.sign cert, key, PUBLIC_CERT, 60 - - assert_equal key.public_key.public_to_pem, signed.public_key.public_to_pem - assert_equal signee.to_s, signed.subject.to_s - assert_equal issuer.to_s, signed.issuer.to_s - - assert_in_delta Time.now, signed.not_before, 10 - assert_in_delta Time.now + 60, signed.not_after, 10 - - assert_equal 4, signed.extensions.length, - signed.extensions.map {|e| e.to_a.first } - - constraints = signed.extensions.find {|ext| ext.oid == "issuerAltName" } - assert_equal "email:nobody@example", constraints.value, "issuerAltName" - - constraints = signed.extensions.find {|ext| ext.oid == "basicConstraints" } - assert_equal "CA:FALSE", constraints.value - - key_usage = signed.extensions.find {|ext| ext.oid == "keyUsage" } - assert_equal "Digital Signature, Key Encipherment, Data Encipherment", - key_usage.value - - key_ident = - signed.extensions.find {|ext| ext.oid == "subjectKeyIdentifier" } - assert_equal 59, key_ident.value.length - - assert signed.verify key + assert_sign PUBLIC_CERT, PRIVATE_KEY end def test_class_sign_AltName @@ -258,6 +283,12 @@ def test_class_sign_AltName assert signed.verify PUBLIC_KEY end + def test_class_sign_ml_dsa_65 + omit_unless_support_pqc + + assert_sign ML_DSA_65_PUBLIC_CERT, ML_DSA_65_PRIVATE_KEY + end + def test_class_trust_dir trust_dir = Gem::Security.trust_dir @@ -280,6 +311,22 @@ def test_class_write_private_key assert_equal key.private_to_pem, key_from_file end + def test_class_write_private_key_ml_dsa_65 + omit_unless_support_pqc + + key = Gem::Security.create_key "ml-dsa-65" + + path = File.join @tempdir, "test-ml-dsa-private_key.pem" + + Gem::Security.write_private_key key, path + + assert_path_exist path + + key_from_file = File.read path + + assert_equal key.private_to_pem, key_from_file + end + def test_class_write_private_key_encrypted key = Gem::Security.create_key "rsa" @@ -296,6 +343,24 @@ def test_class_write_private_key_encrypted assert_equal key.private_to_pem, key_from_file.private_to_pem end + def test_class_write_private_key_encrypted_ml_dsa_65 + omit_unless_support_pqc + + key = Gem::Security.create_key "ml-dsa-65" + + path = File.join @tempdir, "test-ml-dsa-private_encrypted_key.pem" + + passphrase = "It should be long." + + Gem::Security.write_private_key key, path, 0o600, passphrase + + assert_path_exist path + + key_from_file = OpenSSL::PKey.read File.read(path), passphrase + + assert_equal key.private_to_pem, key_from_file.private_to_pem + end + def test_class_write_private_key_encrypted_cipher key = Gem::Security.create_key "rsa" @@ -317,6 +382,31 @@ def test_class_write_private_key_encrypted_cipher assert_equal key.private_to_pem, key_from_file.private_to_pem end + def test_class_write_private_key_encrypted_cipher_ml_dsa_65 + omit_unless_support_pqc + + key = Gem::Security.create_key "ml-dsa-65" + + path = File.join @tempdir, + "test-ml-dsa-private_encrypted_with_non_default_cipher_key.pem" + + passphrase = "It should be long." + + cipher = OpenSSL::Cipher.new "AES-192-CBC" + + Gem::Security.write_private_key key, path, 0o600, passphrase, cipher + + assert_path_exist path + + # Unlike RSA's traditional PEM format where the cipher name appears in a + # plaintext DEK-Info header, ML-DSA uses private_to_pem which outputs PKCS#8 + # format where there is no header, and the cipher name doesn't appear as a + # text. So we cannot assert the cipher name in the PEM text. + key_from_file = OpenSSL::PKey.read File.read(path), passphrase + + assert_equal key.private_to_pem, key_from_file.private_to_pem + end + def test_class_write_certificate path = File.join @tempdir, "test-public_cert.pem" @@ -328,4 +418,46 @@ def test_class_write_certificate assert_equal PUBLIC_CERT.to_pem, cert_from_file end + + private + + def assert_sign(signing_cert, signing_key) + issuer = signing_cert.subject + signee = OpenSSL::X509::Name.new([["CN", "signee"], ["DC", "example"]]) + + key = signing_key + cert = OpenSSL::X509::Certificate.new + cert.subject = signee + public_key = Gem::Security.get_public_key(key) + cert.public_key = public_key + + signed = Gem::Security.sign cert, key, signing_cert, 60 + signed_public_key = Gem::Security.get_public_key(signed) + + assert_equal public_key.public_to_pem, signed_public_key.public_to_pem + assert_equal signee.to_s, signed.subject.to_s + assert_equal issuer.to_s, signed.issuer.to_s + + assert_in_delta Time.now, signed.not_before, 10 + assert_in_delta Time.now + 60, signed.not_after, 10 + + assert_equal 4, signed.extensions.length, + signed.extensions.map {|e| e.to_a.first } + + constraints = signed.extensions.find {|ext| ext.oid == "issuerAltName" } + assert_equal "email:nobody@example", constraints.value, "issuerAltName" + + constraints = signed.extensions.find {|ext| ext.oid == "basicConstraints" } + assert_equal "CA:FALSE", constraints.value + + key_usage = signed.extensions.find {|ext| ext.oid == "keyUsage" } + assert_equal "Digital Signature, Key Encipherment, Data Encipherment", + key_usage.value + + key_ident = + signed.extensions.find {|ext| ext.oid == "subjectKeyIdentifier" } + assert_equal 59, key_ident.value.length + + assert signed.verify key + end end if Gem::HAVE_OPENSSL && !Gem.java_platform? diff --git a/test/rubygems/test_gem_security_policy.rb b/test/rubygems/test_gem_security_policy.rb index 30116ae9ef42..6925e77ebcdc 100644 --- a/test/rubygems/test_gem_security_policy.rb +++ b/test/rubygems/test_gem_security_policy.rb @@ -53,6 +53,16 @@ def test_check_data assert @almost_no.check_data(PUBLIC_KEY, @digest, signature, data) end + def test_check_data_ml_dsa_65 + omit_unless_support_pqc + + data = digest "hello" + + signature = sign data, ML_DSA_65_PRIVATE_KEY, nil + + assert @almost_no.check_data(ML_DSA_65_PUBLIC_KEY, @digest, signature, data) + end + def test_check_data_invalid data = digest "hello" @@ -67,6 +77,22 @@ def test_check_data_invalid assert_equal "invalid signature", e.message end + def test_check_data_invalid_ml_dsa_65 + omit_unless_support_pqc + + data = digest "hello" + + signature = sign data, ML_DSA_65_PRIVATE_KEY, nil + + invalid = digest "hello!" + + e = assert_raise Gem::Security::Exception do + @almost_no.check_data ML_DSA_65_PUBLIC_KEY, @digest, signature, invalid + end + + assert_equal "invalid signature", e.message + end + def test_check_chain chain = [PUBLIC_CERT, CHILD_CERT, GRANDCHILD_CERT] @@ -222,6 +248,14 @@ def test_check_trust assert @high.check_trust [PUBLIC_CERT], @digest, @trust_dir end + def test_check_trust_ml_dsa_65 + omit_unless_support_pqc + + Gem::Security.trust_dir.trust_cert ML_DSA_65_PUBLIC_CERT + + assert @high.check_trust [ML_DSA_65_PUBLIC_CERT], @digest, @trust_dir + end + def test_check_trust_child Gem::Security.trust_dir.trust_cert PUBLIC_CERT @@ -509,8 +543,8 @@ def digest(data) digester end - def sign(data, key = PRIVATE_KEY) - key.sign @digest.new, data.digest + def sign(data, key = PRIVATE_KEY, digest = @digest.new) + key.sign digest, data.digest end def dummy_signatures(key = PRIVATE_KEY) diff --git a/test/rubygems/test_gem_security_signer.rb b/test/rubygems/test_gem_security_signer.rb index 59a379322b63..c3494f478a31 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -69,6 +69,16 @@ def test_initialize_key_path assert_equal PRIVATE_KEY.to_s, signer.key.to_s end + def test_initialize_key_path_ml_dsa_65 + omit_unless_support_pqc + + key_file = ML_DSA_65_PRIVATE_KEY_FILE + + signer = Gem::Security::Signer.new key_file, nil + + assert_equal ML_DSA_65_PRIVATE_KEY.private_to_pem, signer.key.private_to_pem + end + def test_initialize_encrypted_key_path key_file = ENCRYPTED_PRIVATE_KEY_FILE @@ -77,6 +87,17 @@ def test_initialize_encrypted_key_path assert_equal ENCRYPTED_PRIVATE_KEY.to_s, signer.key.to_s end + def test_initialize_encrypted_key_path_ml_dsa_65 + omit_unless_support_pqc + + key_file = ML_DSA_65_ENCRYPTED_PRIVATE_KEY_FILE + + signer = Gem::Security::Signer.new key_file, nil, PRIVATE_KEY_PASSPHRASE + + assert_equal ML_DSA_65_ENCRYPTED_PRIVATE_KEY.private_to_pem, + signer.key.private_to_pem + end + def test_extract_name signer = Gem::Security::Signer.new nil, nil @@ -116,6 +137,16 @@ def test_sign assert PRIVATE_KEY.verify(digest, signature, "hello") end + def test_sign_ml_dsa_65 + omit_unless_support_pqc + + signer = Gem::Security::Signer.new ML_DSA_65_PRIVATE_KEY, [ML_DSA_65_PUBLIC_CERT] + + signature = signer.sign "hello" + + assert ML_DSA_65_PRIVATE_KEY.verify(nil, signature, "hello") + end + def test_sign_expired signer = Gem::Security::Signer.new PRIVATE_KEY, [EXPIRED_CERT] diff --git a/tool/create_certs.rb b/tool/create_certs.rb index 1a13950a5f74..f09e6ecfb453 100644 --- a/tool/create_certs.rb +++ b/tool/create_certs.rb @@ -5,15 +5,69 @@ class CertificateBuilder attr_reader :start - def initialize(key_size = 2048) + BASE_DIR = "test/rubygems" + + def initialize(prefix: "") + @prefix = prefix @start = Time.utc 2012, 1, 1, 0, 0, 0 @end_of_time = Time.utc 9999, 12, 31, 23, 59, 59 @end_of_time_32 = Time.utc 2038, 1, 19, 3, 14, 7 - @key_size = key_size @serial = 0 end + def execute + keys, certs = create_keys_and_certificates + write_keys_and_certificates(keys, certs) + end + + def create_keys_and_certificates + keys = create_keys [ + :alternate, + :child, + :grandchild, + :invalid, + :invalidchild, + :private, + ] + + certs = {} + certs[:public] = + create_certificates(keys[:private], "nobody", + is_ca: true) + certs[:child] = + create_certificates(keys[:child], "child", + keys[:private], certs[:public], + is_ca: true) + certs[:alternate] = + create_certificates(keys[:alternate], "alternate") + certs[:expired] = + create_certificates(keys[:private], "nobody", + not_before: Time.at(0), + not_after: Time.at(0)) + certs[:future] = + create_certificates(keys[:private], "nobody", + not_before: :end_of_time, + not_after: :end_of_time) + certs[:invalid_issuer] = + create_certificates(keys[:invalid], "invalid", + keys[:invalid], certs[:public], + is_ca: true) + certs[:grandchild] = + create_certificates(keys[:grandchild], "grandchild", + keys[:child], certs[:child]) + certs[:invalid_signer] = + create_certificates(keys[:invalid], "invalid", + keys[:private], certs[:invalid]) + certs[:invalidchild] = + create_certificates(keys[:invalidchild], "invalidchild", + keys[:invalid], certs[:child]) + certs[:wrong_key] = + create_certificates(keys[:alternate], "nobody") + + [keys, certs] + end + def create_certificates(key, subject, issuer_key = key, issuer_cert = nil, not_before: @start, not_after: :end_of_time, is_ca: false) certificates = [] @@ -25,9 +79,16 @@ def create_certificates(key, subject, issuer_key = key, issuer_cert = nil, certificates << create_certificate(key, subject, issuer_key, issuer_cert, not_before, not_after, is_ca) - certificates << - create_certificate(key, subject, issuer_key, issuer_cert_32, - not_before_32, not_after_32, is_ca) + + # When not_before and not_after are the same between the regular and 32-bit + # certificates, don't create 32-bit certificate. Because it is redundant. + if not_before == not_before_32 && not_after == not_after_32 + certificates << nil + else + certificates << + create_certificate(key, subject, issuer_key, issuer_cert_32, + not_before_32, not_after_32, is_ca) + end certificates end @@ -45,7 +106,7 @@ def create_certificate(key, subject, issuer_key, issuer_cert, cert.serial = next_serial - cert.public_key = key.public_key + cert.public_key = OpenSSL::PKey.read(key.public_to_pem) cert.subject = OpenSSL::X509::Name.new [%W[CN #{subject}], %w[DC example]] cert.issuer = issuer_cert.subject @@ -66,17 +127,12 @@ def create_certificate(key, subject, issuer_key, issuer_cert, cert.add_extension ef.create_extension("keyUsage", "keyCertSign", true) end - cert.sign issuer_key, "SHA256" + cert.sign issuer_key, digest_name puts "created cert - subject: #{cert.subject}, issuer: #{cert.issuer}" cert end - def create_key - puts "creating key" - OpenSSL::PKey::RSA.new @key_size - end - def create_keys(names) keys = {} @@ -103,72 +159,87 @@ def validity_for(time) [validity, validity_32] end + + def write_keys_and_certificates(keys, certs) + cipher = "aes-256-cbc" + passphrase = "Foo bar" + + keys.each do |name, key| + dest = File.join BASE_DIR, "#{@prefix}#{name}_key.pem" + File.write dest, key.private_to_pem + + next unless name == :private + + # Write the public key PEM from the private key + dest = File.join BASE_DIR, "#{@prefix}public_key.pem" + File.write dest, key.public_to_pem + + # Create an encrypted private key protected by a passphrase + # it has to be the same as is in test/rubygems/helper.rb in PRIVATE_KEY_PASSPHRASE + dest = File.join BASE_DIR, "#{@prefix}encrypted_#{name}_key.pem" + File.write dest, key.private_to_pem(cipher, passphrase) + end + + certs.each do |name, (cert, cert_32)| + dest = File.join BASE_DIR, "#{@prefix}#{name}_cert.pem" + File.write dest, cert.to_pem + + next unless cert_32 + + dest = File.join BASE_DIR, "#{@prefix}#{name}_cert_32.pem" + File.write dest, cert_32.to_pem + end + end end -cb = CertificateBuilder.new - -keys = cb.create_keys [ - :alternate, - :child, - :grandchild, - :invalid, - :invalidchild, - :private, -] - -keys[:public] = keys[:private].public_key - -certs = {} -certs[:public] = - cb.create_certificates(keys[:private], "nobody", - is_ca: true) -certs[:child] = - cb.create_certificates(keys[:child], "child", - keys[:private], certs[:public], - is_ca: true) -certs[:alternate] = - cb.create_certificates(keys[:alternate], "alternate") -certs[:expired] = - cb.create_certificates(keys[:private], "nobody", - not_before: Time.at(0), - not_after: Time.at(0)) -certs[:future] = - cb.create_certificates(keys[:private], "nobody", - not_before: :end_of_time, - not_after: :end_of_time) -certs[:invalid_issuer] = - cb.create_certificates(keys[:invalid], "invalid", - keys[:invalid], certs[:public], - is_ca: true) -certs[:grandchild] = - cb.create_certificates(keys[:grandchild], "grandchild", - keys[:child], certs[:child]) -certs[:invalid_signer] = - cb.create_certificates(keys[:invalid], "invalid", - keys[:private], certs[:invalid]) -certs[:invalidchild] = - cb.create_certificates(keys[:invalidchild], "invalidchild", - keys[:invalid], certs[:child]) -certs[:wrong_key] = - cb.create_certificates(keys[:alternate], "nobody") - -base_dir = "test/rubygems" - -keys.each do |name, key| - dest = File.join base_dir, "#{name}_key.pem" - pem = name == :public ? key.public_to_pem : key.private_to_pem - File.write dest, pem +class RSACertificateBuilder < CertificateBuilder + def initialize(key_size: 2048) + super(prefix: "") + @key_size = key_size + end + + def create_key + puts "creating RSA key" + OpenSSL::PKey::RSA.new @key_size + end + + def digest_name + "SHA256" + end end -# Create an encrypted private key protected by a passphrase from the new keys[:private] -# it has to be the same as is in # test/rubygems/helper.rb in PRIVATE_KEY_PASSPHRASE -dest = File.join base_dir, "encrypted_private_key.pem" -File.write dest, keys[:private].private_to_pem(OpenSSL::Cipher.new("aes-256-cbc"), "Foo bar") +class MLDSA65CertificateBuilder < CertificateBuilder + def initialize + super(prefix: "mldsa65_") + end + + # Only create the private key and public cert needed for current tests. + def create_keys_and_certificates + keys = create_keys [ + :private, + ] + + certs = {} + certs[:public] = + create_certificates(keys[:private], "nobody", + is_ca: true) -certs.each do |name, (cert, cert_32)| - dest = File.join base_dir, "#{name}_cert.pem" - File.write dest, cert.to_pem + [keys, certs] + end - dest = File.join base_dir, "#{name}_cert_32.pem" - File.write dest, cert_32.to_pem + def create_key + puts "creating ML-DSA-65 key" + OpenSSL::PKey.generate_key("ML-DSA-65") + end + + # ML-DSA keys do not support explicit digest algorithms. + def digest_name + nil + end end + +# Create RSA and ML-DSA-65 certificates +[ + RSACertificateBuilder.new, + MLDSA65CertificateBuilder.new, +].each(&:execute) diff --git a/tool/create_encrypted_key.rb b/tool/create_encrypted_key.rb deleted file mode 100644 index 3f7f7f500a22..000000000000 --- a/tool/create_encrypted_key.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "openssl" - -test_path = File.expand_path("../test/rubygems", __dir__) - -private_key_path = "#{test_path}/private_key.pem" - -key = OpenSSL::PKey::RSA.new File.read private_key_path - -cipher = OpenSSL::Cipher.new "AES-256-CBC" - -encrypted_key_path = "#{test_path}/encrypted_private_key.pem" - -File.open encrypted_key_path, "w" do |io| - io.write key.private_to_pem cipher, "Foo bar" -end