From 81f35ff519e0cbf072cb0ba65979a6e12f4172d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:09:18 +0000 Subject: [PATCH 01/10] [ruby/rubygems] Bump the rb-sys group across 2 directories with 1 update Bumps the rb-sys group with 1 update in the /test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib directory: [rb-sys](https://github.com/oxidize-rb/rb-sys). Bumps the rb-sys group with 1 update in the /test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example directory: [rb-sys](https://github.com/oxidize-rb/rb-sys). Updates `rb-sys` from 0.9.128 to 0.9.130 - [Release notes](https://github.com/oxidize-rb/rb-sys/releases) - [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.128...v0.9.130) Updates `rb-sys` from 0.9.128 to 0.9.130 - [Release notes](https://github.com/oxidize-rb/rb-sys/releases) - [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.128...v0.9.130) Updates `rb-sys` from 0.9.128 to 0.9.130 - [Release notes](https://github.com/oxidize-rb/rb-sys/releases) - [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.128...v0.9.130) Updates `rb-sys` from 0.9.128 to 0.9.130 - [Release notes](https://github.com/oxidize-rb/rb-sys/releases) - [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.128...v0.9.130) --- updated-dependencies: - dependency-name: rb-sys dependency-version: 0.9.130 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rb-sys - dependency-name: rb-sys dependency-version: 0.9.130 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rb-sys ... Signed-off-by: dependabot[bot] https://github.com/ruby/rubygems/commit/929690eab0 --- .../custom_name/ext/custom_name_lib/Cargo.lock | 8 ++++---- .../rust_ruby_example/Cargo.lock | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock index d6c49c3de13f8b..77bd160146b759 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock @@ -153,18 +153,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.128" +version = "0.9.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" +checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.128" +version = "0.9.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" +checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694" dependencies = [ "bindgen", "lazy_static", diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock index 806d51d3a1c045..da53acedd01a65 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock @@ -146,18 +146,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.128" +version = "0.9.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" +checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.128" +version = "0.9.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" +checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694" dependencies = [ "bindgen", "lazy_static", From 0ad59eb0588e2b510d90d93e5035b28e06cda98f Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Wed, 8 Jul 2026 19:20:43 +0100 Subject: [PATCH 02/10] [ruby/rubygems] security.rb: Reorder case branches in create_key to match help text The following help text and error message in lib/rubygems/security.rb show that the order of algorithms is RSA, DSA, EC. Align the case branches in create_key with this order. ``` -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA. ``` ``` "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported." ``` Assisted-by: Claude:Opus 4.6 https://github.com/ruby/rubygems/commit/3f07a81aae --- lib/rubygems/security.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 6b46a483fd1c40..6012a7b5f15ffd 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -470,10 +470,10 @@ def self.create_digest(algorithm = DIGEST_NAME) 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) else From 76891fb3e6de389cd3e179bfd8695d2021c08f6d Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 11 Aug 2026 18:45:30 +0100 Subject: [PATCH 03/10] [ruby/rubygems] Rename Gem::PemUtilities to Gem::PEMUtilities Acronyms should be in all caps for naming convention. Assisted-by: Claude:Opus 4.6 https://github.com/ruby/rubygems/commit/7dc8e9e72d --- .../gem_remote_fetcher_local_ssl_server_spec.rb | 12 ++++++------ test/rubygems/helper.rb | 2 +- test/rubygems/local_ssl_server_utilities.rb | 3 ++- test/rubygems/pem_utilities.rb | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb b/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb index 2a287af19587b6..c68eabae9a6b9b 100644 --- a/spec/bundler/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb +++ b/spec/bundler/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 66e1e3e293459c..c278ef3faa4142 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1681,7 +1681,7 @@ def prefetch(reqs) # :nodoc: end end - include Gem::PemUtilities + include Gem::PEMUtilities 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 d0d73dbd1219bf..34c32aaca7479a 100644 --- a/test/rubygems/local_ssl_server_utilities.rb +++ b/test/rubygems/local_ssl_server_utilities.rb @@ -8,7 +8,8 @@ require_relative "pem_utilities" module Gem::LocalSSLServerUtilities - include Gem::PemUtilities + include Gem::PEMUtilities + def initialize_ssl_server @ssl_server_thread = nil @ssl_server = nil diff --git a/test/rubygems/pem_utilities.rb b/test/rubygems/pem_utilities.rb index bec08d9a38dd4f..f57c5adbdcbcc8 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/. From 49b460022fe8c95ac991f6e37ca643843dcb26fe Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Mon, 13 Jul 2026 12:26:32 +0100 Subject: [PATCH 04/10] [ruby/rubygems] rubygems: Move omit_unless_support_pqc to helper.rb * Move omit_unless_support_pqc from test_gem_remote_fetcher_local_ssl_server.rb to helper.rb so that all rubygems test files can use it * Create test/rubygems/pqc_utilities.rb to manage PQC utilities * Update local_ssl_server_utilities.rb to require and include pqc_utilities.rb Assisted-by: Claude:Opus 4.6 https://github.com/ruby/rubygems/commit/6ccce5d35f --- test/rubygems/helper.rb | 9 +++ test/rubygems/local_ssl_server_utilities.rb | 67 +--------------- test/rubygems/pqc_utilities.rb | 78 +++++++++++++++++++ ...est_gem_remote_fetcher_local_ssl_server.rb | 6 -- 4 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 test/rubygems/pqc_utilities.rb diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index c278ef3faa4142..e9480d687ac754 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -47,6 +47,7 @@ require_relative "mock_gem_ui" require_relative "pem_utilities" require_relative "fake_credential_backend" +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 @@ -1682,6 +1683,14 @@ def prefetch(reqs) # :nodoc: end 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 34c32aaca7479a..95785b2573d41c 100644 --- a/test/rubygems/local_ssl_server_utilities.rb +++ b/test/rubygems/local_ssl_server_utilities.rb @@ -6,9 +6,11 @@ require "socket" require "openssl" require_relative "pem_utilities" +require_relative "pqc_utilities" module Gem::LocalSSLServerUtilities include Gem::PEMUtilities + include Gem::PQCUtilities def initialize_ssl_server @ssl_server_thread = nil @@ -75,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/pqc_utilities.rb b/test/rubygems/pqc_utilities.rb new file mode 100644 index 00000000000000..139f91a5c06f5a --- /dev/null +++ b/test/rubygems/pqc_utilities.rb @@ -0,0 +1,78 @@ +# 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__ + + 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 + # 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 + 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::PQCUtilities.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/test_gem_remote_fetcher_local_ssl_server.rb b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb index 813e8e2f026567..144d80c134b05a 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 From 665ff92e14d56290c7df0553f18ac7e97814f9fd Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 14 Jul 2026 20:16:22 +0100 Subject: [PATCH 05/10] [ruby/rubygems] rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow These changes enable the full PQC ML-DSA cryptographically signed gems workflow: `gem cert --build` (key/cert generation), `gem build` (package signing), and `gem install -P HighSecurity` (signature verification). The `gem cert -A` accepts `ML-DSA-44`, `ML-DSA-65`, or `ML-DSA-87` to generate ML-DSA based cert and key. It raises `Gem::Security::Exception` if ML-DSA is not supported such as OpenSSL < 3.5. ``` $ gem cert --build your@email.com -A ML-DSA-44 $ gem cert --build your@email.com -A ML-DSA-65 $ gem cert --build your@email.com -A ML-DSA-87 ``` PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5. https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ The reason why it accepts all 3 ML-DSA parameter sets rather than one of them is because all 3 ML-DSA parameter sets suit different workflows. While ML-DSA-65 can be used commonly, a high security environment requires ML-DSA-87. 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) The ML-DSA signed gems workflow also includes * `gem cert -C cert.pem` * `gem cert -K private_key.pem` It raises `Gem::OptionParser::InvalidArgument` if ML-DSA is not supported. * `gem cert --add cert.pem` * `gem cert --remove filter` * `gem build gemname.gemspec` with `s.cert_chain` and `s.signing_key` to build signed gem. It raises `Gem::Security::Exception` if ML-DSA is not supported. * `gem install gemname -P HighSecurity` It raises `Gem::Security::Exception` on RSA server/client connection if ML-DSA is not supported. * `gem update gemname -P HighSecurity` It raises `Gem::Security::Exception` on RSA server/client connection if ML-DSA is not supported. The changes come from: * Ruby OpenSSL methods `#sign` (signing) and `#verify` don't accept digest algorithm, in ML-DSA cases because ML-DSA has a built-in digest * Unifying tool/create_certs.rb with tool/create_encrypted_key.rb to generate RSA and ML-DSA-65 keys/certificates. * Adding ML-DSA-65 related .pem files generated by tool/create_certs.rb. * Adding ML-DSA tests. We need many tests due to differences between `OpenSSL::PKey::RSA/DSA/EC` and `OpenSSL::PKey::PKey`. Note JRuby remains unsupported for the signed gems feature. The following tests have been skipped entirely in JRuby. * test/rubygems/test_gem_security.rb * test/rubygems/test_gem_commands_cert_command.rb See also https://guides.rubygems.org/security/ for the signed gems workflows. Assisted-by: Claude:Fable 5 https://github.com/ruby/rubygems/commit/8135567fdb --- lib/rubygems/commands/cert_command.rb | 8 +- lib/rubygems/security.rb | 57 +++- lib/rubygems/security/policy.rb | 10 +- lib/rubygems/security/signer.rb | 12 +- test/rubygems/helper.rb | 4 + .../mldsa65_encrypted_private_key.pem | 90 +++++++ test/rubygems/mldsa65_private_key.pem | 88 +++++++ test/rubygems/mldsa65_public_cert.pem | 118 +++++++++ test/rubygems/mldsa65_public_cert_32.pem | 118 +++++++++ test/rubygems/mldsa65_public_key.pem | 44 ++++ test/rubygems/pem_utilities.rb | 14 +- test/rubygems/pqc_utilities.rb | 22 ++ .../test_gem_commands_build_command.rb | 65 +++++ .../test_gem_commands_cert_command.rb | 165 +++++++++++- test/rubygems/test_gem_security.rb | 247 +++++++++++++++--- test/rubygems/test_gem_security_policy.rb | 51 +++- test/rubygems/test_gem_security_signer.rb | 46 ++++ 17 files changed, 1106 insertions(+), 53 deletions(-) create mode 100644 test/rubygems/mldsa65_encrypted_private_key.pem create mode 100644 test/rubygems/mldsa65_private_key.pem create mode 100644 test/rubygems/mldsa65_public_cert.pem create mode 100644 test/rubygems/mldsa65_public_cert_32.pem create mode 100644 test/rubygems/mldsa65_public_key.pem diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index 09ae43e35f683d..1f19b26b3349e2 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 6012a7b5f15ffd..f41798907aeefe 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,8 +480,8 @@ 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) @@ -476,13 +492,42 @@ def self.create_key(algorithm) OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH) when "ec" OpenSSL::PKey::EC.generate(EC_NAME) + when "ml-dsa-44" + create_ml_dsa_key(ML_DSA_44_NAME) + when "ml-dsa-65" + create_ml_dsa_key(ML_DSA_65_NAME) + when "ml-dsa-87" + create_ml_dsa_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 + ## + # Creates an ML-DSA key pair of the +algorithm+ such as ML-DSA-65. ML-DSA + # requires OpenSSL >= 3.5 or an SSL library supporting ML-DSA. + + def self.create_ml_dsa_key(algorithm) + OpenSSL::PKey.generate_key(algorithm) + rescue OpenSSL::PKey::PKeyError + raise Gem::Security::Exception, + "#{algorithm} key generation failed: #{algorithm} requires "\ + "OpenSSL >= 3.5 or an SSL library supporting ML-DSA." + end + private_class_method :create_ml_dsa_key + + ## + # 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 +607,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 03ddbe530e4820..80fae6a009d731 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 @@ -268,6 +270,12 @@ def verify(chain, key = nil, digests = {}, signatures = {}, full_name = "(unknow end true + # NotImplementedError: JRuby's Ruby OpenSSL raises it for ML-DSA. + rescue OpenSSL::X509::CertificateError, NotImplementedError + raise Gem::Security::Exception, + "certificate verification failed: The certificate may use an algorithm "\ + "such as ML-DSA that the installed OpenSSL does not support. ML-DSA "\ + "requires OpenSSL >= 3.5 or an SSL library supporting ML-DSA." end ## diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index c90f3a93301ddc..d6854a4a7014ef 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -85,7 +85,14 @@ def initialize(key, cert_chain, passphrase = nil, options = {}) @digest_algorithm = Gem::Security.create_digest(@digest_name) if @key && !@key.is_a?(OpenSSL::PKey::PKey) - @key = OpenSSL::PKey.read(File.read(@key), @passphrase) + begin + @key = OpenSSL::PKey.read(File.read(@key), @passphrase) + rescue OpenSSL::PKey::PKeyError + raise Gem::Security::Exception, + "private key could not be loaded: The key may use an algorithm "\ + "such as ML-DSA that the installed OpenSSL does not support. "\ + "ML-DSA requires OpenSSL >= 3.5 or an SSL library supporting ML-DSA." + end end if @cert_chain @@ -152,7 +159,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/test/rubygems/helper.rb b/test/rubygems/helper.rb index e9480d687ac754..05be858b9d3576 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1691,6 +1691,10 @@ def omit_unless_support_pqc omit message end end + + def omit_if_support_ml_dsa_key + omit "OpenSSL supports ML-DSA" if Gem::PQCUtilities.support_ml_dsa_key? + end end # https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192 diff --git a/test/rubygems/mldsa65_encrypted_private_key.pem b/test/rubygems/mldsa65_encrypted_private_key.pem new file mode 100644 index 00000000000000..9dbae3d4522be3 --- /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 00000000000000..686c961c231aa9 --- /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 00000000000000..510323d3d590fe --- /dev/null +++ b/test/rubygems/mldsa65_public_cert.pem @@ -0,0 +1,118 @@ +-----BEGIN CERTIFICATE----- +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 +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 00000000000000..ecf7f7e6ac5679 --- /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 00000000000000..61fbbb8e8b54e0 --- /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 f57c5adbdcbcc8..7c69c375b38159 100644 --- a/test/rubygems/pem_utilities.rb +++ b/test/rubygems/pem_utilities.rb @@ -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 index 139f91a5c06f5a..d5688ad3da8e05 100644 --- a/test/rubygems/pqc_utilities.rb +++ b/test/rubygems/pqc_utilities.rb @@ -35,6 +35,28 @@ def without_pqc_support(&block) end end + ## + # Returns whether the runtime OpenSSL can generate ML-DSA keys. + # Unlike support_pqc_handshake?, this only probes key generation. + # Gem::Security.create_key tests need it. + # Handshake cannot be used to judge ML-DSA key availability on + # OpenSSL >= 3.5 with Ruby OpenSSL < 4.0.0, where support_pqc_handshake? is + # false due to Ruby OpenSSL's missing methods but + # OpenSSL::PKey.generate_key succeeds. + + def self.support_ml_dsa_key? + return @support_ml_dsa_key unless @support_ml_dsa_key.nil? + + @support_ml_dsa_key = + begin + OpenSSL::PKey.generate_key("ML-DSA-65") + true + # NoMethodError: JRuby 10.1.0.0's Ruby OpenSSL lacks generate_key. + rescue OpenSSL::PKey::PKeyError, NoMethodError + false + 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. diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index cd88421c0754ff..36d4ed9d804c2d 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -606,6 +606,71 @@ 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_ml_dsa_65_without_ml_dsa_support + pend "openssl is missing" unless Gem::HAVE_OPENSSL + + omit_if_support_ml_dsa_key + + 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] + + use_ui @ui do + Dir.chdir @tempdir do + e = assert_raise Gem::Security::Exception do + @cmd.execute + end + + assert_match( + /^private key could not be loaded: .+ ML-DSA requires OpenSSL >= 3\.5/, + e.message + ) + end + end + 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 17fe3d789a6f8c..02ef4fe3ab5737 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -157,6 +157,66 @@ 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_key_algorithm_ml_dsa_65_key_without_ml_dsa_support + omit_if_support_ml_dsa_key + + 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 + e = assert_raise Gem::Security::Exception do + @cmd.execute + end + + assert_match( + /^ML-DSA-65 key generation failed: ML-DSA-65 requires OpenSSL >= 3\.5/, + e.message + ) + end + end + def test_execute_build_bad_email_address passphrase = "Foo bar" email = "nobody@" @@ -329,6 +389,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 +447,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 +511,47 @@ 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_private_ml_dsa_65_key_without_ml_dsa_support + omit_if_support_ml_dsa_key + + use_ui @ui do + e = assert_raise Gem::OptionParser::InvalidArgument do + @cmd.send :handle_options, %W[--private-key #{ML_DSA_65_PRIVATE_KEY_FILE}] + end + + assert_match(/invalid .+ key/, e.message) + end + 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 +972,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_security.rb b/test/rubygems/test_gem_security.rb index 20f4b9fa8951cb..351ed11aa504cd 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -99,15 +99,107 @@ 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_ml_dsa_44_without_ml_dsa_support + omit_if_support_ml_dsa_key + + e = assert_raise Gem::Security::Exception do + Gem::Security.create_key "ml-dsa-44" + end + + assert_match( + /^ML-DSA-44 key generation failed: ML-DSA-44 requires OpenSSL >= 3\.5/, + e.message + ) + end + + def test_class_create_key_ml_dsa_65_without_ml_dsa_support + omit_if_support_ml_dsa_key + + e = assert_raise Gem::Security::Exception do + Gem::Security.create_key "ml-dsa-65" + end + + assert_match( + /^ML-DSA-65 key generation failed: ML-DSA-65 requires OpenSSL >= 3\.5/, + e.message + ) + end + + def test_class_create_key_ml_dsa_87_without_ml_dsa_support + omit_if_support_ml_dsa_key + + e = assert_raise Gem::Security::Exception do + Gem::Security.create_key "ml-dsa-87" + end + + assert_match( + /^ML-DSA-87 key generation failed: ML-DSA-87 requires OpenSSL >= 3\.5/, + e.message + ) + 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 +212,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 +279,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 +322,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 +350,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 +382,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 +421,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 +457,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 30116ae9ef428d..6298be22264469 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 @@ -392,6 +426,19 @@ def test_verify_wrong_digest_type assert_equal "no digests provided (probable bug)", e.message end + def test_verify_ml_dsa_65_without_ml_dsa_support + omit_if_support_ml_dsa_key + + e = assert_raise Gem::Security::Exception do + @high.verify [ML_DSA_65_PUBLIC_CERT], nil, *dummy_signatures + end + + assert_match( + /^certificate verification failed: .+ ML-DSA requires OpenSSL >= 3\.5/, + e.message + ) + end + def test_verify_signatures_chain @spec.cert_chain = [PUBLIC_CERT, CHILD_CERT] @@ -509,8 +556,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 59a379322b6340..a06d57aab28176 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -69,6 +69,31 @@ 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_key_path_ml_dsa_65_without_ml_dsa_support + omit_if_support_ml_dsa_key + + key_file = ML_DSA_65_PRIVATE_KEY_FILE + + e = assert_raise Gem::Security::Exception do + Gem::Security::Signer.new key_file, nil + end + + assert_match( + /^private key could not be loaded: .+ ML-DSA requires OpenSSL >= 3\.5/, + e.message + ) + end + def test_initialize_encrypted_key_path key_file = ENCRYPTED_PRIVATE_KEY_FILE @@ -77,6 +102,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 +152,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] From 67b5d802f0d67d8bc6f1660d5bdb846e2bfda9a9 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 11 Aug 2026 19:47:19 +0100 Subject: [PATCH 06/10] [ruby/rubygems] pqc_utilities.rb: Simplify PQC check Simplify PQC check not to depend on OpenSSL and Ruby OpenSSL versions. https://github.com/ruby/rubygems/commit/303e1214a1 --- test/rubygems/pqc_utilities.rb | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/test/rubygems/pqc_utilities.rb b/test/rubygems/pqc_utilities.rb index d5688ad3da8e05..afea521c7ff6d2 100644 --- a/test/rubygems/pqc_utilities.rb +++ b/test/rubygems/pqc_utilities.rb @@ -9,21 +9,15 @@ 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) - # 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 - # 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 - 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 @@ -31,7 +25,8 @@ def without_pqc_support(&block) # 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 "PQC handshake is not available in this OpenSSL configuration" + yield "OpenSSL or Ruby OpenSSL is too old to support PQC, "\ + "or PQC handshake is not available in this OpenSSL configuration" end end @@ -71,6 +66,13 @@ def self.probe_pqc_handshake 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) From 151a401ba15be6a15600e0dab9c02e1bbddcd2bc Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 2 Sep 2026 18:22:29 +0900 Subject: [PATCH 07/10] [ruby/rubygems] Drop encipherment key usages from Gem::Security.create_cert defaults keyEncipherment should be set only when the subject public key is used for key transport, and dataEncipherment only when it is used to directly encrypt user data. RubyGems uses the key for neither purpose. These key usages are also not applicable to non-RSA keys. In preparation for ML-DSA support, remove them from the default settings. https://github.com/ruby/rubygems/commit/46c869633b --- lib/rubygems/security.rb | 6 ++---- test/rubygems/test_gem_security.rb | 12 ++++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index f41798907aeefe..bb9c39d666b380 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -387,14 +387,12 @@ class Exception < Gem::Exception; end # The default set of extensions are: # # * The certificate is not a certificate authority - # * The key for the certificate may be used for key and data encipherment - # and digital signatures + # * The key for the certificate may be used for digital signatures # * The certificate contains a subject key identifier EXTENSIONS = { "basicConstraints" => "CA:FALSE", - "keyUsage" => - "keyEncipherment,dataEncipherment,digitalSignature", + "keyUsage" => "digitalSignature", "subjectKeyIdentifier" => "hash", }.freeze diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 351ed11aa504cd..305f31b6d094c3 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -34,8 +34,7 @@ def test_class_create_cert assert_equal "CA:FALSE", constraints.value key_usage = cert.extensions.find {|ext| ext.oid == "keyUsage" } - assert_equal "Digital Signature, Key Encipherment, Data Encipherment", - key_usage.value + assert_include key_usage.value, "Digital Signature" key_ident = cert.extensions.find {|ext| ext.oid == "subjectKeyIdentifier" } assert_equal 59, key_ident.value.length @@ -80,8 +79,7 @@ def test_class_create_cert_email assert_equal "CA:FALSE", constraints.value key_usage = cert.extensions.find {|ext| ext.oid == "keyUsage" } - assert_equal "Digital Signature, Key Encipherment, Data Encipherment", - key_usage.value + assert_include key_usage.value, "Digital Signature" key_ident = cert.extensions.find {|ext| ext.oid == "subjectKeyIdentifier" } assert_equal 59, key_ident.value.length @@ -312,8 +310,7 @@ def test_class_sign_AltName 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 + assert_include key_usage.value, "Digital Signature" key_ident = signed.extensions.find {|ext| ext.oid == "subjectKeyIdentifier" } @@ -490,8 +487,7 @@ def assert_sign(signing_cert, signing_key) 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 + assert_include key_usage.value, "Digital Signature" key_ident = signed.extensions.find {|ext| ext.oid == "subjectKeyIdentifier" } From 98f9701e8ea15ac240f7e9a99d6cd149b4bd3031 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 11:28:58 +0900 Subject: [PATCH 08/10] [ruby/rubygems] Reject key algorithms RubyGems has not qualified for signing digest_required? treated every algorithm outside RSA, DSA and EC as digest-less, so a gemspec cert_chain could steer verification into a scheme nobody reviewed. Only ML-DSA belongs on that side of the branch. https://github.com/ruby/rubygems/commit/62a5df0d15 Co-Authored-By: Claude Opus 5 --- lib/rubygems/security.rb | 30 ++++++++++++++++++++++++++++-- test/rubygems/test_gem_security.rb | 14 ++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index bb9c39d666b380..d2d29f47192b59 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -367,6 +367,9 @@ 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 + ## # Cipher used to encrypt the key pair used to sign gems. # Must be in the list returned by OpenSSL::Cipher.ciphers @@ -517,13 +520,36 @@ def self.create_ml_dsa_key(algorithm) end private_class_method :create_ml_dsa_key + ## + # Returns whether +key+ uses ML-DSA. OpenSSL::PKey::PKey#oid raises for the + # provider-backed keys ML-DSA uses, so the algorithm is read from the + # SubjectPublicKeyInfo instead. + + def self.ml_dsa_key?(key) + algorithm = OpenSSL::ASN1.decode(key.public_to_der).value.first.value.first + ML_DSA_NAMES.include?(algorithm.ln) + rescue OpenSSL::ASN1::ASN1Error, OpenSSL::PKey::PKeyError, NoMethodError + false + end + private_class_method :ml_dsa_key? + ## # 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. + # Any other algorithm raises, since a gem carries no record of how its + # signature was produced and RubyGems must not guess. def self.digest_required?(key) - key.is_a?(OpenSSL::PKey::RSA) || key.is_a?(OpenSSL::PKey::DSA) || - key.is_a?(OpenSSL::PKey::EC) + case key + when OpenSSL::PKey::RSA, OpenSSL::PKey::DSA, OpenSSL::PKey::EC + true + else + return false if ml_dsa_key?(key) + + raise Gem::Security::Exception, + "unsupported key algorithm. RSA, DSA, EC, ML-DSA-44, ML-DSA-65, and "\ + "ML-DSA-87 keys are supported." + end end ## diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 305f31b6d094c3..20ae5ce147ac12 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -192,6 +192,20 @@ def test_class_digest_required_ec assert Gem::Security.digest_required?(Gem::Security.create_key("ec")) end + def test_class_digest_required_raises_unsupported_algorithm + key = begin + OpenSSL::PKey.generate_key("X25519") + rescue OpenSSL::PKey::PKeyError, NoMethodError + omit "X25519 is not available" + end + + e = assert_raise Gem::Security::Exception do + Gem::Security.digest_required?(key) + end + + assert_match(/^unsupported key algorithm\./, e.message) + end + def test_class_digest_required_ml_dsa_65 omit_unless_support_pqc From b7c5fc9d2a7aa39b657a7cfa0f0696f05da53b4c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 16:45:42 +0900 Subject: [PATCH 09/10] [ruby/rubygems] Gate ML-DSA tests on what they actually need The signing tests were gated on the PQC handshake probe, which needs SSLContext#groups=. On OpenSSL >= 3.5 with Ruby OpenSSL < 4.0 all 23 were omitted even though ML-DSA keys work there, so the core of this PR went unverified. They now gate on ML-DSA key support, and the five that build a certificate gate on nil-digest X509 signing, which Ruby OpenSSL only accepts from 3.3 on. The algorithm assertions read the SubjectPublicKeyInfo instead of PKey#inspect, whose type_name field only exists from Ruby OpenSSL 4.0. The handshake probe also returned nil on its early exits, so its memoization never took and it rebound a TCP socket per call. https://github.com/ruby/rubygems/commit/9a4353546d Co-Authored-By: Claude Opus 5 --- test/rubygems/helper.rb | 10 +++++ test/rubygems/pqc_utilities.rb | 39 ++++++++++++++++++- .../test_gem_commands_build_command.rb | 2 +- .../test_gem_commands_cert_command.rb | 15 +++---- test/rubygems/test_gem_security.rb | 24 ++++++------ test/rubygems/test_gem_security_policy.rb | 6 +-- test/rubygems/test_gem_security_signer.rb | 6 +-- 7 files changed, 74 insertions(+), 28 deletions(-) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 05be858b9d3576..5ffde523709639 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1692,6 +1692,16 @@ def omit_unless_support_pqc end end + def omit_unless_support_ml_dsa_key + omit "OpenSSL does not support ML-DSA" unless + Gem::PQCUtilities.support_ml_dsa_key? + 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? + end + def omit_if_support_ml_dsa_key omit "OpenSSL supports ML-DSA" if Gem::PQCUtilities.support_ml_dsa_key? end diff --git a/test/rubygems/pqc_utilities.rb b/test/rubygems/pqc_utilities.rb index afea521c7ff6d2..3b46daa9376682 100644 --- a/test/rubygems/pqc_utilities.rb +++ b/test/rubygems/pqc_utilities.rb @@ -52,6 +52,41 @@ def self.support_ml_dsa_key? 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 + # support_ml_dsa_key? alone does not cover certificate building. + + def self.support_ml_dsa_cert? + return @support_ml_dsa_cert unless @support_ml_dsa_cert.nil? + + @support_ml_dsa_cert = + begin + key = OpenSSL::PKey.generate_key("ML-DSA-65") + cert = OpenSSL::X509::Certificate.new + cert.subject = cert.issuer = OpenSSL::X509::Name.new([["CN", "probe"]]) + cert.public_key = OpenSSL::PKey.read(key.public_to_pem) + cert.not_before = Time.now + cert.not_after = Time.now + 60 + cert.sign(key, nil) + true + # NoMethodError: JRuby's Ruby OpenSSL lacks generate_key. + # TypeError: Ruby OpenSSL < 3.3 rejects a nil digest here. + rescue OpenSSL::PKey::PKeyError, OpenSSL::X509::CertificateError, + NoMethodError, TypeError + false + end + end + + ## + # Returns the algorithm named in the SubjectPublicKeyInfo of +key+, such as + # "ML-DSA-65". OpenSSL::PKey::PKey#inspect only names the algorithm on Ruby + # OpenSSL >= 4.0, and #oid raises for the provider-backed keys ML-DSA uses. + + def self.key_algorithm_name(key) + OpenSSL::ASN1.decode(key.public_to_der).value.first.value.first.ln + 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. @@ -68,10 +103,10 @@ def self.probe_pqc_handshake 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 + return false unless ctx.key # ctx.groups (OpenSSL::SSL::SSLContext#groups) requires Ruby OpenSSL >= 4.0. - return nil unless ctx.respond_to?(:groups=) + return false unless ctx.respond_to?(:groups=) ctx.groups = "X25519MLKEM768" ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index 36d4ed9d804c2d..03af9dfff2288d 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_pqc + omit_unless_support_ml_dsa_key trust_dir = Gem::Security.trust_dir diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index 02ef4fe3ab5737..e8063f76ac3c0e 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -158,7 +158,7 @@ def test_execute_build_key_algorithm_ec_key end def test_execute_build_key_algorithm_ml_dsa_65_key - omit_unless_support_pqc + omit_unless_support_ml_dsa_cert passphrase = "Foo bar" @@ -193,7 +193,8 @@ def test_execute_build_key_algorithm_ml_dsa_65_key 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) + assert_equal "ML-DSA-65", + Gem::PQCUtilities.key_algorithm_name(cert.public_key) end def test_execute_build_key_algorithm_ml_dsa_65_key_without_ml_dsa_support @@ -390,7 +391,7 @@ def test_execute_build_bad_key end def test_execute_build_ml_dsa_65_key - omit_unless_support_pqc + omit_unless_support_ml_dsa_cert @cmd.handle_options %W[ --build nobody@example.com @@ -414,7 +415,7 @@ def test_execute_build_ml_dsa_65_key end def test_execute_build_encrypted_ml_dsa_65_key - omit_unless_support_pqc + omit_unless_support_ml_dsa_cert @cmd.handle_options %W[ --build nobody@example.com @@ -448,7 +449,7 @@ def test_execute_certificate end def test_execute_certificate_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key use_ui @ui do @cmd.handle_options %W[--certificate #{ML_DSA_65_PUBLIC_CERT_FILE}] @@ -512,7 +513,7 @@ def test_execute_encrypted_private_key end def test_execute_private_ml_dsa_65_key - omit_unless_support_pqc + omit_unless_support_ml_dsa_key use_ui @ui do @cmd.send :handle_options, %W[--private-key #{ML_DSA_65_PRIVATE_KEY_FILE}] @@ -538,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_pqc + omit_unless_support_ml_dsa_key use_ui @ui do @cmd.send :handle_options, diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 20ae5ce147ac12..8fddcb74ba3eee 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -104,30 +104,30 @@ def test_class_create_key_ec end def test_class_create_key_ml_dsa_44 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key key = Gem::Security.create_key "ml-dsa-44" assert_kind_of OpenSSL::PKey::PKey, key - assert_match(/type_name=ML-DSA-44/, key.inspect) + assert_equal "ML-DSA-44", Gem::PQCUtilities.key_algorithm_name(key) end def test_class_create_key_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key key = Gem::Security.create_key "ml-dsa-65" assert_kind_of OpenSSL::PKey::PKey, key - assert_match(/type_name=ML-DSA-65/, key.inspect) + assert_equal "ML-DSA-65", Gem::PQCUtilities.key_algorithm_name(key) end def test_class_create_key_ml_dsa_87 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key key = Gem::Security.create_key "ml-dsa-87" assert_kind_of OpenSSL::PKey::PKey, key - assert_match(/type_name=ML-DSA-87/, key.inspect) + assert_equal "ML-DSA-87", Gem::PQCUtilities.key_algorithm_name(key) end def test_class_create_key_ml_dsa_44_without_ml_dsa_support @@ -207,7 +207,7 @@ def test_class_digest_required_raises_unsupported_algorithm end def test_class_digest_required_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key refute Gem::Security.digest_required?(Gem::Security.create_key("ml-dsa-65")) end @@ -225,7 +225,7 @@ def test_class_get_public_key_ec end def test_class_get_public_key_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key pkey = Gem::Security.get_public_key(ML_DSA_65_PRIVATE_KEY) @@ -334,7 +334,7 @@ def test_class_sign_AltName end def test_class_sign_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_cert assert_sign ML_DSA_65_PUBLIC_CERT, ML_DSA_65_PRIVATE_KEY end @@ -362,7 +362,7 @@ def test_class_write_private_key end def test_class_write_private_key_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key key = Gem::Security.create_key "ml-dsa-65" @@ -394,7 +394,7 @@ def test_class_write_private_key_encrypted end def test_class_write_private_key_encrypted_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key key = Gem::Security.create_key "ml-dsa-65" @@ -433,7 +433,7 @@ def test_class_write_private_key_encrypted_cipher end def test_class_write_private_key_encrypted_cipher_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key key = Gem::Security.create_key "ml-dsa-65" diff --git a/test/rubygems/test_gem_security_policy.rb b/test/rubygems/test_gem_security_policy.rb index 6298be22264469..ae264c0370c2ee 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_pqc + omit_unless_support_ml_dsa_key data = digest "hello" @@ -78,7 +78,7 @@ def test_check_data_invalid end def test_check_data_invalid_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key data = digest "hello" @@ -249,7 +249,7 @@ def test_check_trust end def test_check_trust_ml_dsa_65 - omit_unless_support_pqc + omit_unless_support_ml_dsa_key Gem::Security.trust_dir.trust_cert ML_DSA_65_PUBLIC_CERT diff --git a/test/rubygems/test_gem_security_signer.rb b/test/rubygems/test_gem_security_signer.rb index a06d57aab28176..be79909453a4eb 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_pqc + omit_unless_support_ml_dsa_key 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_pqc + omit_unless_support_ml_dsa_key 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_pqc + omit_unless_support_ml_dsa_key signer = Gem::Security::Signer.new ML_DSA_65_PRIVATE_KEY, [ML_DSA_65_PUBLIC_CERT] From 76ce036fda8d3b1a1ce9d220f462c3c086154981 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 17:26:00 +0900 Subject: [PATCH 10/10] [ruby/rubygems] Report the Ruby OpenSSL floor when ML-DSA certificate signing fails Signing a certificate for an ML-DSA key passes a nil digest, which Ruby OpenSSL rejects with a bare TypeError before 3.3. On Ruby 3.2 or 3.3 with OpenSSL >= 3.5 the key generates and only `gem cert --build` breaks, so the message has to name the missing piece. https://github.com/ruby/rubygems/commit/7817de95c2 Co-Authored-By: Claude Opus 5 --- lib/rubygems/security.rb | 6 ++++++ test/rubygems/helper.rb | 5 +++++ test/rubygems/test_gem_security.rb | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index d2d29f47192b59..1d86ecc909cc8b 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -638,6 +638,12 @@ def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions rescue OpenSSL::PKey::PKeyError, ArgumentError raise Gem::Security::Exception, "incorrect signing key for signing" + # Ruby OpenSSL only accepts the nil digest ML-DSA needs from 3.3 on. + rescue TypeError + raise if digest_name + + raise Gem::Security::Exception, + "certificate signing failed: ML-DSA requires Ruby OpenSSL >= 3.3." end end diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 5ffde523709639..cd50ed5ae1a8c3 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1702,6 +1702,11 @@ def omit_unless_support_ml_dsa_cert Gem::PQCUtilities.support_ml_dsa_cert? end + def omit_if_support_ml_dsa_cert + omit "Ruby OpenSSL can sign a certificate with an ML-DSA key" if + Gem::PQCUtilities.support_ml_dsa_cert? + end + def omit_if_support_ml_dsa_key omit "OpenSSL supports ML-DSA" if Gem::PQCUtilities.support_ml_dsa_key? end diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 8fddcb74ba3eee..28886cecb89b60 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -85,6 +85,18 @@ def test_class_create_cert_email assert_equal 59, key_ident.value.length end + def test_class_create_cert_email_ml_dsa_65_without_cert_support + omit_unless_support_ml_dsa_key + omit_if_support_ml_dsa_cert + + e = assert_raise Gem::Security::Exception do + Gem::Security.create_cert_email "nobody@example", + ML_DSA_65_PRIVATE_KEY, 60 + end + + assert_match(/^certificate signing failed: /, e.message) + end + def test_class_create_key key = Gem::Security.create_key "rsa"