ruby-changes:69448
From: Jenny <ko1@a...>
Date: Tue, 26 Oct 2021 08:02:17 +0900 (JST)
Subject: [ruby-changes:69448] 92ec010595 (master): [rubygems/rubygems] Add support to build and sign certificates with multiple key algorithms
https://git.ruby-lang.org/ruby.git/commit/?id=92ec010595 From 92ec010595bed29567fc08dd4d52d4c4518f0fd4 Mon Sep 17 00:00:00 2001 From: Jenny Shen <jenny.shen@s...> Date: Wed, 6 Oct 2021 17:39:23 -0400 Subject: [rubygems/rubygems] Add support to build and sign certificates with multiple key algorithms https://github.com/rubygems/rubygems/commit/967876f15d Co-Authored-By: Frederik Dudzik <frederik.dudzik@s...> --- lib/rubygems/commands/cert_command.rb | 19 ++++--- lib/rubygems/security.rb | 64 +++++++++++++++++------ lib/rubygems/security/policy.rb | 8 +-- lib/rubygems/security/signer.rb | 7 ++- test/rubygems/helper.rb | 4 +- test/rubygems/private_ec_key.pem | 9 ++++ test/rubygems/test_gem_commands_cert_command.rb | 67 +++++++++++++++++++++++-- test/rubygems/test_gem_security.rb | 36 +++++++++++-- 8 files changed, 176 insertions(+), 38 deletions(-) create mode 100644 test/rubygems/private_ec_key.pem diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index bdfeb0ba6e8..867cb07cca0 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -43,6 +43,11 @@ class Gem::Commands::CertCommand < Gem::Command https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/cert_command.rb#L43 options[:key] = open_private_key(key_file) end + add_option('-A', '--key-algorithm ALGORITHM', + 'Select which key algorithm to use for --build') do |algorithm, options| + options[:key_algorithm] = algorithm + end + add_option('-s', '--sign CERT', 'Signs CERT with the key from -K', 'and the certificate from -C') do |cert_file, options| @@ -89,14 +94,14 @@ class Gem::Commands::CertCommand < Gem::Command https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/cert_command.rb#L94 def open_private_key(key_file) check_openssl passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - key = OpenSSL::PKey::RSA.new File.read(key_file), passphrase + key = OpenSSL::PKey.read File.read(key_file), passphrase raise OptionParser::InvalidArgument, "#{key_file}: private key not found" unless key.private? key rescue Errno::ENOENT raise OptionParser::InvalidArgument, "#{key_file}: does not exist" - rescue OpenSSL::PKey::RSAError - raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA key" + rescue OpenSSL::PKey::PKeyError, ArgumentError + raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key" end def execute @@ -170,7 +175,8 @@ class Gem::Commands::CertCommand < Gem::Command https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/cert_command.rb#L175 raise Gem::CommandLineError, "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation - key = Gem::Security.create_key + algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM + key = Gem::Security.create_key(algorithm) key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase return key, key_path @@ -255,13 +261,14 @@ For further reading on signing gems see `ri Gem::Security`. https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/cert_command.rb#L261 key_file = File.join Gem.default_key_path key = File.read key_file passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - options[:key] = OpenSSL::PKey::RSA.new key, passphrase + options[:key] = OpenSSL::PKey.read key, passphrase + rescue Errno::ENOENT alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist" terminate_interaction 1 - rescue OpenSSL::PKey::RSAError + rescue OpenSSL::PKey::PKeyError alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem is not valid" diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 28f705549c8..8240a1a0591 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -152,6 +152,7 @@ require_relative 'openssl' https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L152 # 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. # -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 @@ -317,7 +318,6 @@ require_relative 'openssl' https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L318 # * Honor extension restrictions # * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 # file, instead of an array embedded in the metadata. -# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1. # # == Original author # @@ -337,17 +337,19 @@ module Gem::Security https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L337 DIGEST_NAME = 'SHA256' # :nodoc: ## - # Algorithm for creating the key pair used to sign gems + # Length of keys created by RSA and DSA keys - KEY_ALGORITHM = - if defined?(OpenSSL::PKey::RSA) - OpenSSL::PKey::RSA - end + RSA_DSA_KEY_LENGTH = 3072 ## - # Length of keys created by KEY_ALGORITHM + # Default algorithm to use when building a key pair - KEY_LENGTH = 3072 + DEFAULT_KEY_ALGORITHM = 'RSA' + + ## + # Named curve used for Elliptic Curve + + EC_NAME = 'secp384r1' ## # Cipher used to encrypt the key pair used to sign gems. @@ -400,7 +402,7 @@ module Gem::Security https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L402 serial = 1) cert = OpenSSL::X509::Certificate.new - cert.public_key = key.public_key + cert.public_key = get_public_key(key) cert.version = 2 cert.serial = serial @@ -418,6 +420,24 @@ module Gem::Security https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L420 cert end + ## + # Gets the right public key from a PKey instance + + def self.get_public_key(key) + return key.public_key unless key.is_a?(OpenSSL::PKey::EC) + + ec_key = OpenSSL::PKey::EC.new(key.group.curve_name) + ec_key.public_key = key.public_key + ec_key + end + + ## + # In Ruby 2.3 EC doesn't implement the private_key? but not the private? method + + if defined?(OpenSSL::PKey::EC) && Gem::Version.new(String.new(RUBY_VERSION)) < Gem::Version.new("2.4.0") + OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?) + end + ## # Creates a self-signed certificate with an issuer and subject from +email+, # a subject alternative name of +email+ and the given +extensions+ for the @@ -459,11 +479,25 @@ module Gem::Security https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L479 end ## - # Creates a new key pair of the specified +length+ and +algorithm+. The - # default is a 3072 bit RSA key. - - def self.create_key(length = KEY_LENGTH, algorithm = KEY_ALGORITHM) - algorithm.new length + # Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC + # 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 'ec' + domain_key = OpenSSL::PKey::EC.new(EC_NAME) + domain_key.generate_key + domain_key + else + raise Gem::Security::Exception, + "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported." + end + end end ## @@ -492,7 +526,7 @@ module Gem::Security https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security.rb#L526 raise Gem::Security::Exception, "incorrect signing key for re-signing " + "#{expired_certificate.subject}" unless - expired_certificate.public_key.to_pem == private_key.public_key.to_pem + expired_certificate.public_key.to_pem == get_public_key(private_key).to_pem unless expired_certificate.subject.to_s == expired_certificate.issuer.to_s diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 9683e55b327..3c3cb647ee3 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -115,9 +115,11 @@ class Gem::Security::Policy https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security/policy.rb#L115 raise Gem::Security::Exception, 'missing key or signature' end + public_key = Gem::Security.get_public_key(key) + raise Gem::Security::Exception, "certificate #{signer.subject} does not match the signing key" unless - signer.public_key.to_pem == key.public_key.to_pem + signer.public_key.to_pem == public_key.to_pem true end @@ -164,9 +166,9 @@ class Gem::Security::Policy https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security/policy.rb#L166 end save_cert = OpenSSL::X509::Certificate.new File.read path - save_dgst = digester.digest save_cert.public_key.to_s + save_dgst = digester.digest save_cert.public_key.to_pem - pkey_str = root.public_key.to_s + pkey_str = root.public_key.to_pem cert_dgst = digester.digest pkey_str raise Gem::Security::Exception, diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index c5c2c4f2200..968cf889730 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -83,8 +83,8 @@ class Gem::Security::Signer https://github.com/ruby/ruby/blob/trunk/lib/rubygems/security/signer.rb#L83 (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/