diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index fe03841ddb44..2c6e6846658e 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 @@ -96,8 +99,19 @@ def open_private_key(key_file) check_openssl passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] key = OpenSSL::PKey.read File.read(key_file), passphrase - raise Gem::OptionParser::InvalidArgument, - "#{key_file}: private key not found" unless key.private? + # ML-DSA keys (OpenSSL::PKey::PKey) do not have private? method. + # Try private_to_pem instead to check if the key has private data. + if key.respond_to?(:private?) + raise Gem::OptionParser::InvalidArgument, + "#{key_file}: private key not found" unless key.private? + else + begin + key.private_to_pem + rescue OpenSSL::PKey::PKeyError + raise Gem::OptionParser::InvalidArgument, + "#{key_file}: private key not found" + end + end key rescue Errno::ENOENT raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist" diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 69ba87b07f8e..da175fdb17a2 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,7 +594,8 @@ 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 - signed.sign signing_key, Gem::Security::DIGEST_NAME + digest_name = Gem::Security::DIGEST_NAME if digest_required?(signing_key) + signed.sign signing_key, digest_name end ## @@ -584,20 +617,56 @@ def self.trusted_certificates(&block) trust_dir.each_certificate(&block) end + ## + # Converts +pemmable+ to PEM format string. The +pemmable+ can be a key + # (OpenSSL::PKey::RSA, DSA, EC) or certificate (OpenSSL::X509::Certificate). + # RSA/DSA/EC keys and certificates support to_pem. ML-DSA keys + # (OpenSSL::PKey::PKey) do not support to_pem, so private_to_pem or + # public_to_pem is used instead. + # If +cipher+ and +passphrase+ are provided, the PEM output is encrypted. + + def self.pemmable_to_pem(pemmable, passphrase = nil, cipher = KEY_CIPHER) + if pemmable.respond_to?(:to_pem) + # pemmable: OpenSSL::PKey::RSA, DSA, EC, OpenSSL::X509::Certificate + if cipher && passphrase + pemmable.to_pem cipher, passphrase + else + pemmable.to_pem + end + else + # pemmable: OpenSSL::PKey::PKey (ML-DSA) + if cipher && passphrase + # public_to_pem doesn't have cipher and passphrase arguments. + # So, given these arguments, only run private_to_pem. + pemmable.private_to_pem cipher, passphrase + else + # Check if the key is private or public by trying, and rescuing an + # error as OpenSSL::PKey::PKey doesn't have such a method. + begin + pemmable.private_to_pem + rescue OpenSSL::PKey::PKeyError + # public_to_pem doesn't have cipher and passphrase arguments + pemmable.public_to_pem + end + end + end + end + ## # Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be - # passed to +to_pem+. + # passed to +to_pem+. The +pemmable+ can be key or certificate. def self.write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER) path = File.expand_path path File.open path, "wb", permissions do |io| - if passphrase && cipher - io.write pemmable.to_pem cipher, passphrase + pem_str = if passphrase && cipher + pemmable_to_pem pemmable, passphrase, cipher else - io.write pemmable.to_pem + pemmable_to_pem pemmable end + io.write pem_str end path diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 128958ab802a..1a0559d5b406 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 @@ -165,10 +167,13 @@ def check_trust(chain, digester, trust_dir) end save_cert = OpenSSL::X509::Certificate.new File.read path - save_dgst = digester.digest save_cert.public_key.to_pem + save_pkey = save_cert.public_key + save_pkey_str = Gem::Security.pemmable_to_pem(save_pkey) + save_dgst = digester.digest(save_pkey_str) - pkey_str = root.public_key.to_pem - cert_dgst = digester.digest pkey_str + root_pkey = root.public_key + root_pkey_str = Gem::Security.pemmable_to_pem(root_pkey) + cert_dgst = digester.digest(root_pkey_str) raise Gem::Security::Exception, "trusted root certificate #{root.subject} checksum " \ diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index eeeeb52906cb..0033c5d415b6 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/test/rubygems/private_ec_key.pem b/test/rubygems/ec_private_key.pem similarity index 100% rename from test/rubygems/private_ec_key.pem rename to test/rubygems/ec_private_key.pem diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 4c47d46835e3..4484d15fd8fe 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1721,6 +1721,39 @@ def self.key_path(key_name) PUBLIC_KEY = nil PUBLIC_CERT = nil end if Gem::HAVE_OPENSSL + + begin + ML_DSA_65_PRIVATE_KEY = load_key "mldsa65_private" + ML_DSA_65_PRIVATE_KEY_PATH = key_path "mldsa65_private" + + # ML_DSA_65_ENCRYPTED_PRIVATE_KEY is ML_DSA_65_PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE + ML_DSA_65_ENCRYPTED_PRIVATE_KEY = load_key "mldsa65_encrypted_private", PRIVATE_KEY_PASSPHRASE + ML_DSA_65_ENCRYPTED_PRIVATE_KEY_PATH = key_path "mldsa65_encrypted_private" + + ML_DSA_65_PUBLIC_KEY = OpenSSL::PKey.read(ML_DSA_65_PRIVATE_KEY.public_to_der) + + ML_DSA_65_PUBLIC_CERT = load_cert "mldsa65_public" + ML_DSA_65_PUBLIC_CERT_PATH = cert_path "mldsa65_public" + # OpenSSL::PKey::PKeyError is raised when ML-DSA is not supported by the + # OpenSSL version (requires OpenSSL >= 3.5). + rescue Errno::ENOENT, OpenSSL::PKey::PKeyError + ML_DSA_65_PRIVATE_KEY = nil + ML_DSA_65_PUBLIC_KEY = nil + ML_DSA_65_PUBLIC_CERT = nil + end if Gem::HAVE_OPENSSL + + require_relative "pqc_utilities" + include Gem::PqcUtilities + + def omit_unless_support_pqc + without_pqc_support do |message| + omit message + end + end + + def omit_if_support_pqc + omit "PQC is supported" if Gem::PqcUtilities.support_pqc? + end end # https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192 diff --git a/test/rubygems/local_ssl_server_utilities.rb b/test/rubygems/local_ssl_server_utilities.rb index a068efa9642d..5fe4b66ec766 100644 --- a/test/rubygems/local_ssl_server_utilities.rb +++ b/test/rubygems/local_ssl_server_utilities.rb @@ -5,8 +5,11 @@ require "socket" require "openssl" +require_relative "pqc_utilities" module Gem::LocalSSLServerUtilities + include Gem::PqcUtilities + CERTS_DIR = __dir__ def certs_dir @@ -86,69 +89,4 @@ def cert(filename) def key(filename) OpenSSL::PKey.read(File.read(File.join(certs_dir, filename))) end - - def without_pqc_support(&block) - # PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5. - # https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ - unless OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30500000 - yield "PQC algorithms require OpenSSL >= 3.5" - return - end - # ctx.groups (OpenSSL::SSL::SSLContext#groups) used in start_ssl_server - # mode :pqc requires Ruby OpenSSL >= 4.0. - unless Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new("4.0") - yield "PQC test requires Ruby OpenSSL >= 4.0" - return - end - # Even with a new enough OpenSSL, the runtime may keep PQC groups and - # signature algorithms out of its default negotiation lists (for example - # RHEL's system-wide crypto policies). The PQC server forces both, while - # the gem fetcher connects with the default client configuration, so a - # real loopback handshake is the only reliable way to tell whether this - # environment can negotiate PQC at all. - unless Gem::LocalSSLServerUtilities.support_pqc_handshake? - yield "PQC handshake is not available in this OpenSSL configuration" - end - end - - # Probe an actual PQC handshake between a forced-PQC server and a - # default-configured client, mirroring what the integration tests exercise. - # Memoized so the probe runs at most once per process. - def self.support_pqc_handshake? - return @support_pqc_handshake unless @support_pqc_handshake.nil? - - @support_pqc_handshake = probe_pqc_handshake - end - - def self.probe_pqc_handshake - server = TCPServer.new("127.0.0.1", 0) - ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERTS_DIR, "mldsa65_ssl_cert.pem"))) - ctx.key = OpenSSL::PKey.read(File.read(File.join(CERTS_DIR, "mldsa65_ssl_key.pem"))) - ctx.groups = "X25519MLKEM768" - ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) - - port = server.addr[1] - server_thread = Thread.new do - client = ssl_server.accept - client.close - rescue OpenSSL::OpenSSLError - nil - end - - client_ctx = OpenSSL::SSL::SSLContext.new - client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE - socket = TCPSocket.new("127.0.0.1", port) - ssl = OpenSSL::SSL::SSLSocket.new(socket, client_ctx) - ssl.connect - ssl.close - true - rescue OpenSSL::OpenSSLError, SystemCallError - false - ensure - server_thread&.join(5) - server_thread&.kill if server_thread&.alive? - ssl_server&.close - server&.close - end end diff --git a/test/rubygems/mldsa65_encrypted_private_key.pem b/test/rubygems/mldsa65_encrypted_private_key.pem new file mode 100644 index 000000000000..2a5dc9bfc72a --- /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..4ed082e159e3 --- /dev/null +++ b/test/rubygems/mldsa65_private_key.pem @@ -0,0 +1,88 @@ +-----BEGIN PRIVATE KEY----- +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 +Cd87u43ecy3rA1B/njZMqpZB +-----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..de2163a48182 --- /dev/null +++ b/test/rubygems/mldsa65_public_cert.pem @@ -0,0 +1,118 @@ +-----BEGIN CERTIFICATE----- +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 +hYmOkpTf4QAAAAAAAAAAAAAAAAAABAkUGx8p +-----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..7b96b45644e4 --- /dev/null +++ b/test/rubygems/mldsa65_public_cert_32.pem @@ -0,0 +1,118 @@ +-----BEGIN CERTIFICATE----- +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 +mAAAAAAAAAAAAAAAAAAAAAAAAAYIEBYgJQ== +-----END CERTIFICATE----- diff --git a/test/rubygems/mldsa65_public_key.pem b/test/rubygems/mldsa65_public_key.pem new file mode 100644 index 000000000000..94549140383d --- /dev/null +++ b/test/rubygems/mldsa65_public_key.pem @@ -0,0 +1,44 @@ +-----BEGIN PUBLIC KEY----- +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 +ezxR5KJi +-----END PUBLIC KEY----- diff --git a/test/rubygems/pqc_utilities.rb b/test/rubygems/pqc_utilities.rb new file mode 100644 index 000000000000..ecd801f843ff --- /dev/null +++ b/test/rubygems/pqc_utilities.rb @@ -0,0 +1,93 @@ +# 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) + unless Gem::PqcUtilities.support_pqc_openssl? + yield "PQC algorithms require OpenSSL >= 3.5" + return + end + unless Gem::PqcUtilities.support_pqc_ruby_openssl? + 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 + + def self.support_pqc? + Gem::PqcUtilities.support_pqc_openssl? && + Gem::PqcUtilities.support_pqc_ruby_openssl? && + Gem::PqcUtilities.support_pqc_handshake? + end + + # PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5. + # https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ + def self.support_pqc_openssl? + OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30500000 + end + + # Ruby OpenSSL >= 4.0 supports OpenSSL::SSL::SSLContext#groups, + # OpenSSL::SSL::SSLSocket#sigalg, #peer_sigalg used in PQC cases, fixing the + # following issue. + # https://github.com/ruby/openssl/blob/v4.0.0/History.md#notable-changes + # https://github.com/ruby/openssl/commit/9614b7f67a629cc8d31b6fe5be5040c68263ca8b + def self.support_pqc_ruby_openssl? + Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new("4.0") + end + + # Probe an actual PQC handshake between a forced-PQC server and a + # default-configured client, mirroring what the integration tests exercise. + # Memoized so the probe runs at most once per process. + def self.support_pqc_handshake? + return @support_pqc_handshake unless @support_pqc_handshake.nil? + + @support_pqc_handshake = probe_pqc_handshake + end + + def self.probe_pqc_handshake + server = TCPServer.new("127.0.0.1", 0) + ctx = OpenSSL::SSL::SSLContext.new + ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERTS_DIR, "mldsa65_ssl_cert.pem"))) + ctx.key = OpenSSL::PKey.read(File.read(File.join(CERTS_DIR, "mldsa65_ssl_key.pem"))) + ctx.groups = "X25519MLKEM768" + ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) + + port = server.addr[1] + server_thread = Thread.new do + client = ssl_server.accept + client.close + rescue OpenSSL::OpenSSLError + nil + end + + client_ctx = OpenSSL::SSL::SSLContext.new + client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE + socket = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(socket, client_ctx) + ssl.connect + ssl.close + true + rescue OpenSSL::OpenSSLError, SystemCallError + false + ensure + server_thread&.join(5) + server_thread&.kill if server_thread&.alive? + ssl_server&.close + server&.close + end +end diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index 9339f41f7cb8..49186bf6e2d4 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -612,6 +612,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_PATH + s.cert_chain = [ML_DSA_65_PUBLIC_CERT_PATH] + 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_PATH) + ) + ) + + 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 ed1a1c8627bf..6ac7b051c083 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -15,15 +15,18 @@ class TestGemCommandsCertCommand < Gem::TestCase ALTERNATE_CERT = load_cert "alternate" EXPIRED_PUBLIC_CERT = load_cert "expired" - ALTERNATE_KEY_FILE = key_path "alternate" - PRIVATE_KEY_FILE = key_path "private" - PRIVATE_EC_KEY_FILE = key_path "private_ec" - PUBLIC_KEY_FILE = key_path "public" - - ALTERNATE_CERT_FILE = cert_path "alternate" - CHILD_CERT_FILE = cert_path "child" - PUBLIC_CERT_FILE = cert_path "public" - EXPIRED_PUBLIC_CERT_FILE = cert_path "expired" + ALTERNATE_KEY_FILE = key_path "alternate" + PRIVATE_KEY_FILE = key_path "private" + EC_PRIVATE_KEY_FILE = key_path "ec_private" + ML_DSA_65_PRIVATE_KEY_FILE = key_path "mldsa65_private" + PUBLIC_KEY_FILE = key_path "public" + ML_DSA_65_PUBLIC_KEY_FILE = key_path "mldsa65_public" + + ALTERNATE_CERT_FILE = cert_path "alternate" + CHILD_CERT_FILE = cert_path "child" + PUBLIC_CERT_FILE = cert_path "public" + ML_DSA_65_PUBLIC_CERT_FILE = cert_path "mldsa65_public" + EXPIRED_PUBLIC_CERT_FILE = cert_path "expired" def setup super @@ -172,6 +175,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@" @@ -286,6 +328,15 @@ def test_execute_build_key assert_path_not_exist File.join(@tempdir, "gem-private_key.pem") end + def test_execute_build_invalid_key + assert_raise Gem::OptionParser::InvalidArgument do + @cmd.handle_options %W[ + --build nobody@example.com + --private-key #{PUBLIC_KEY_FILE} + ] + end + end + def test_execute_build_encrypted_key @cmd.handle_options %W[ --build nobody@example.com @@ -310,7 +361,31 @@ def test_execute_build_encrypted_key def test_execute_build_ec_key @cmd.handle_options %W[ --build nobody@example.com - --private-key #{PRIVATE_EC_KEY_FILE} + --private-key #{EC_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_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 @@ -329,6 +404,40 @@ def test_execute_build_ec_key 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_PATH} + ] + + 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_build_invalid_ml_dsa_65_key + omit_unless_support_pqc + + assert_raise Gem::OptionParser::InvalidArgument do + @cmd.handle_options %W[ + --build nobody@example.com + --private-key #{ML_DSA_65_PUBLIC_KEY_FILE} + ] + end + end + def test_execute_certificate use_ui @ui do @cmd.handle_options %W[--certificate #{PUBLIC_CERT_FILE}] @@ -340,6 +449,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 +513,35 @@ def test_execute_encrypted_private_key assert_equal ENCRYPTED_PRIVATE_KEY.to_pem, @cmd.options[:key].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_PATH}] + 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_PATH}] + 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 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 95e6d3ac65a8..0ca8c5c4dcb9 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 82449a862617..a3f3fd8058c1 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -13,7 +13,7 @@ class TestGemSecurity < Gem::TestCase CHILD_KEY = load_key "child" - EC_KEY = load_key "private_ec", "Foo bar" + EC_PRIVATE_KEY = load_key "ec_private", "Foo bar" ALTERNATE_CERT = load_cert "child" CHILD_CERT = load_cert "child" @@ -110,15 +110,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.to_pem @@ -126,11 +179,19 @@ def test_class_get_public_key_rsa end def test_class_get_public_key_ec - pkey = Gem::Security.get_public_key(EC_KEY) + pkey = Gem::Security.get_public_key(EC_PRIVATE_KEY) assert_respond_to pkey, :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 @@ -190,45 +251,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.to_pem, signed.public_key.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_equal "B1:1A:54:09:67:45:60:02:02:D7:CE:F4:1D:60:4A:89:DF:E7:58:D9", - key_ident.value - - assert signed.verify key + assert_sign PUBLIC_CERT, PRIVATE_KEY end def test_class_sign_AltName @@ -273,6 +296,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 @@ -281,6 +310,76 @@ def test_class_trust_dir assert_equal expected, trust_dir.dir end + def test_class_pemmable_to_pem_rsa_private_key + pem = Gem::Security.pemmable_to_pem(PRIVATE_KEY) + + assert_equal PRIVATE_KEY.to_pem, pem + end + + # Compare PEM-encoding string between the original string, and encrypted and + # decrypted string. We cannot call PRIVATE_KEY.to_pem(cipher, passphrase) in + # test, because it generates a new different text. + def test_class_pemmable_to_pem_rsa_private_key_encrypted + passphrase = "It should be long." + cipher = OpenSSL::Cipher.new "AES-256-CBC" + pem = Gem::Security.pemmable_to_pem(PRIVATE_KEY, passphrase, cipher) + # Decrypt the encrypted key with passphrase + key_from_pem = OpenSSL::PKey::RSA.new pem, passphrase + + assert_equal PRIVATE_KEY.to_pem, key_from_pem.to_pem + end + + def test_class_pemmable_to_pem_rsa_public_key + pem = Gem::Security.pemmable_to_pem(PUBLIC_KEY) + + assert_equal PUBLIC_KEY.to_pem, pem + end + + def test_class_pemmable_to_pem_rsa_certificate + pem = Gem::Security.pemmable_to_pem(PUBLIC_CERT) + + assert_equal PUBLIC_CERT.to_pem, pem + end + + def test_class_pemmable_to_pem_ml_dsa_65_private_key + omit_unless_support_pqc + + pem = Gem::Security.pemmable_to_pem(ML_DSA_65_PRIVATE_KEY) + + assert_equal ML_DSA_65_PRIVATE_KEY.private_to_pem, pem + end + + # Compare PEM-encoding string between the original string, and encrypted and + # decrypted string. We cannot call ML_DSA_65_PRIVATE_KEY.private_to_pem(cipher, + # passphrase) in test, because it generates a new different text. + def test_class_pemmable_to_pem_ml_dsa_65_private_key_encrypted + omit_unless_support_pqc + + passphrase = "It should be long." + cipher = OpenSSL::Cipher.new "AES-256-CBC" + pem = Gem::Security.pemmable_to_pem(ML_DSA_65_PRIVATE_KEY, passphrase, cipher) + # Decrypt the encrypted key with passphrase + key_from_pem = OpenSSL::PKey.read pem, passphrase + + assert_equal ML_DSA_65_PRIVATE_KEY.private_to_pem, key_from_pem.private_to_pem + end + + def test_class_pemmable_to_pem_ml_dsa_65_public_key + omit_unless_support_pqc + + pem = Gem::Security.pemmable_to_pem(ML_DSA_65_PUBLIC_KEY) + + assert_equal ML_DSA_65_PUBLIC_KEY.public_to_pem, pem + end + + def test_class_pemmable_to_pem_ml_dsa_65_certificate + omit_unless_support_pqc + + pem = Gem::Security.pemmable_to_pem(ML_DSA_65_PUBLIC_CERT) + + assert_equal ML_DSA_65_PUBLIC_CERT.to_pem, pem + end + def test_class_write key = Gem::Security.create_key "rsa" @@ -295,6 +394,22 @@ def test_class_write assert_equal key.to_pem, key_from_file end + def test_class_write_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 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_encrypted key = Gem::Security.create_key "rsa" @@ -311,6 +426,24 @@ def test_class_write_encrypted assert_equal key.to_pem, key_from_file.to_pem end + def test_class_write_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 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_encrypted_cipher key = Gem::Security.create_key "rsa" @@ -332,4 +465,72 @@ def test_class_write_encrypted_cipher assert_equal key.to_pem, key_from_file.to_pem end + + def test_class_write_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 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 + + 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 + + cert.subject = signee + cert.public_key = Gem::Security.get_public_key(key) + + signed = Gem::Security.sign cert, key, signing_cert, 60 + + assert_equal Gem::Security.pemmable_to_pem(Gem::Security.get_public_key(key)), + Gem::Security.pemmable_to_pem(signed.public_key) + 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 2f4fb1ce2875..e337bce62aac 100644 --- a/test/rubygems/test_gem_security_policy.rb +++ b/test/rubygems/test_gem_security_policy.rb @@ -70,6 +70,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" @@ -84,6 +94,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] @@ -239,6 +265,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 @@ -526,8 +560,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 f4799cbd4686..cc7dd1a8d4bb 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -77,6 +77,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_PATH + + 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_PATH @@ -85,6 +95,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_PATH + + 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 @@ -120,16 +141,18 @@ def test_sign signature = signer.sign "hello" - expected = <<-EXPECTED -FmrCYxEXW3dgYYNMxPdS16VrdXT+d5nyXTVlRm64ZHSgMxMAaPtQJsVYv73m -DWHTzNnLhhINSpgBMLh5a4atM52yxVdkPUTgqIH+LeIPBXn8xaP5JLmfDcmI -tBpc/9DhS3v9iKCX40igAArFu7Gg3swbgQ61SP+U22LvG5nDQZQz3sudtsw3 -qKPykFVaYjrRwzvBdSdJ1PwlAsanSwcwS/GKPtmE/ykZ6X5XOx7wvCDL/zGy -B8khkB8hDKC6moCzebmUxCBmTmXD0Wjzon+bf4MOriVE3a0ySGRvpr1mKR2+ -9EaVo7pDJLEM487+xg1CAZHRhwshd6II00XEzG/jBQ== - EXPECTED + digest = OpenSSL::Digest.new(Gem::Security::DIGEST_NAME) + 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_equal expected, [signature].pack("m") + assert ML_DSA_65_PRIVATE_KEY.verify(nil, signature, "hello") end def test_sign_expired diff --git a/tool/create_certs.rb b/tool/create_certs.rb index 8bfdbae9b6f9..2b095548dacb 100644 --- a/tool/create_certs.rb +++ b/tool/create_certs.rb @@ -5,15 +5,68 @@ 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 = create_keys [ + :alternate, + :child, + :grandchild, + :invalid, + :invalidchild, + :private, + ] + + keys[:public] = public_key_for(keys[: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") + + write_keys keys + write_encrypted_key keys[:private] + write_certs 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 +78,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 +105,7 @@ def create_certificate(key, subject, issuer_key, issuer_cert, cert.serial = next_serial - cert.public_key = key.public_key + cert.public_key = public_key_for(key) cert.subject = OpenSSL::X509::Name.new [%W[CN #{subject}], %w[DC example]] cert.issuer = issuer_cert.subject @@ -66,17 +126,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,71 +158,119 @@ def validity_for(time) [validity, validity_32] end + + private + + def write_keys(keys) + keys.each do |name, key| + dest = File.join BASE_DIR, "#{@prefix}#{name}_key.pem" + File.write dest, key_to_pem(key) + end + end + + def write_encrypted_key(key) + # 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_private_key.pem" + cipher = OpenSSL::Cipher.new "AES-256-CBC" + File.write dest, encrypted_key_to_pem(key, cipher, "Foo bar") + end + + def write_certs(certs) + 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" - File.write dest, key.to_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 public_key_for(key) + key.public_key + end + + def digest_name + "SHA256" + end + + def key_to_pem(key) + key.to_pem + end + + def encrypted_key_to_pem(key, cipher, passphrase) + key.to_pem(cipher, passphrase) + 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].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 execute + keys = create_keys [ + :private, + ] + + keys[:public] = public_key_for(keys[:private]) + + certs = {} + certs[:public] = + create_certificates(keys[:private], "nobody", + is_ca: true) + + write_keys keys + write_encrypted_key keys[:private] + write_certs certs + end + + def create_key + puts "creating ML-DSA-65 key" + OpenSSL::PKey.generate_key("ML-DSA-65") + end + + # Unlike OpenSSL::PKey::RSA#public_key OpenSSL::PKey::PKey#public_key + # is undefined. + def public_key_for(key) + OpenSSL::PKey.read(key.public_to_der) + end -certs.each do |name, (cert, cert_32)| - dest = File.join base_dir, "#{name}_cert.pem" - File.write dest, cert.to_pem + # ML-DSA keys do not support explicit digest algorithms. + def digest_name + nil + end - dest = File.join base_dir, "#{name}_cert_32.pem" - File.write dest, cert_32.to_pem + # Unlike OpenSSL::PKey::RSA#to_pem OpenSSL::PKey::PKey#to_pem + # is undefined. + def key_to_pem(key) + key.private_to_pem + rescue OpenSSL::PKey::PKeyError + key.public_to_pem + end + + def encrypted_key_to_pem(key, cipher, passphrase) + key.private_to_pem(cipher, passphrase) + 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 829a750fbdff..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.to_pem cipher, "Foo bar" -end