New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support to build and sign certificates with multiple key algorithms #4991
Add support to build and sign certificates with multiple key algorithms #4991
Conversation
|
||
pkey_str = root.public_key.to_s | ||
pkey_str = root.public_key.to_pem |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
KEY_LENGTH = 3072 | ||
EC_NAME = 'secp384r1' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We choose this particular curve as the default because it is reasonably secure and widely adopted.
e3d46d8
to
79bebfd
Compare
lib/rubygems/security.rb
Outdated
def self.get_public_key(key) | ||
return key.public_key unless key.is_a?(OpenSSL::PKey::EC) | ||
|
||
ec_key = OpenSSL::PKey::EC.new(EC_NAME) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generated my own EC key file with a different curve, then tried to build a certificate on the constant_resolver
gem using my own key:
➜ constant_resolver git:(master) ✗ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-darwin20]
➜ constant_resolver git:(master) ✗ ls ~/.gem
➜ constant_resolver git:(master) ✗ openssl ecparam -name prime256v1 -genkey -out ~/.gem/ec_prime256v1_key.pem
➜ constant_resolver git:(master) ✗ gem cert --build roch.lefebvre@shopify.com -K ~/.gem/ec_prime256v1_key.pem
ERROR: While executing gem ... (OptionParser::InvalidArgument)
invalid argument: -K /Users/rochlefebvre/.gem/ec_prime256v1_key.pem: invalid RSA key
So far, so good. My stock rubygems
install doesn't accept EC keys. I'll try the same with this PR's branch:
➜ constant_resolver git:(master) ✗ ruby -I../../jenshenny/rubygems/lib ../../jenshenny/rubygems/bin/gem cert --build roch.lefebvre@shopify.com -K ~/.gem/ec_prime256v1_key.pem
ERROR: While executing gem ... (OpenSSL::PKey::ECError)
EC_KEY_set_public_key: incompatible objects
➜ constant_resolver git:(master) ✗ ruby -help
Usage: ruby [switches] [--] [programfile] [arguments]
-0[octal] specify record separator (\0, if no argument)
-a autosplit mode with -n or -p (splits $_ into $F)
-c check syntax only
-Cdirectory cd to directory before executing your script
-d set debugging flags (set $DEBUG to true)
[...]
➜ constant_resolver git:(master) ✗ ruby -d -I../../jenshenny/rubygems/lib ../../jenshenny/rubygems/bin/gem cert --build roch.lefebvre@shopify.com -K ~/.gem/ec_prime256v1_key.pem
[...]
Exception `OpenSSL::PKey::ECError' at /Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/security.rb:425 - EC_KEY_set_public_key: incompatible objects
ERROR: While executing gem ... (OpenSSL::PKey::ECError)
EC_KEY_set_public_key: incompatible objects
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/security.rb:425:in `public_key='
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/security.rb:425:in `get_public_key'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/security.rb:400:in `create_cert'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/security.rb:448:in `create_cert_self_signed'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/security.rb:439:in `create_cert_email'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/commands/cert_command.rb:157:in `build_cert'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/commands/cert_command.rb:143:in `build'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/commands/cert_command.rb:123:in `block in execute'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/commands/cert_command.rb:122:in `each'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/commands/cert_command.rb:122:in `execute'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/command.rb:323:in `invoke_with_build_args'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/command_manager.rb:178:in `process_args'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/command_manager.rb:147:in `run'
/Users/rochlefebvre/src/github.com/jenshenny/rubygems/lib/rubygems/gem_runner.rb:53:in `run'
../../jenshenny/rubygems/bin/gem:13:in `<main>'
If I go through the same test while using a self-generated secp384r1 key, then everything works.
I think we may be able to instantiate an EC key instance with the same curve name as the given private key. After changing this line, my test worked:
ec_key = OpenSSL::PKey::EC.new(key.group.curve_name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
lib/rubygems/security.rb
Outdated
@@ -462,8 +470,21 @@ def self.create_digest(algorithm = DIGEST_NAME) | |||
# Creates a new key pair of the specified +length+ and +algorithm+. The |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we decide to rework this method, then we'll need new documentation.
@@ -462,8 +470,21 @@ def self.create_digest(algorithm = DIGEST_NAME) | |||
# 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 | |||
def self.create_key(algorithm) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're changing a public helper method's signature, which may be a no-no.
I did a quick search on GitHub (not exhaustive, since not everything is indexed). Virtually all of the ~1500 hits are from cert_command.rb
in vendored snapshots of Rubygems or Ruby. If I exclude cert_command.rb
from my search, I get an insignificant set of hits.
If an incompatible change is unacceptable, consider creating a new method, e.g. create_private_key
.
0c7eb55
to
85928f7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few nits. One question I still have: I see we added tests for EC certs, do we exercise RSA and DSA certs as well?
rescue OpenSSL::PKey::RSAError | ||
raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA key" | ||
rescue OpenSSL::PKey::PKeyError | ||
raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA/DSA/EC key" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I'd suggest the text be "invalid RSA, DSA or EC key".
@@ -170,7 +175,8 @@ def build_key # :nodoc: | |||
raise Gem::CommandLineError, | |||
"Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation | |||
|
|||
key = Gem::Security.create_key | |||
algorithm = options[:key_algorithm] || 'RSA' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: 'RSA' could be a constant.
lib/rubygems/security.rb
Outdated
@@ -152,6 +152,7 @@ | |||
# certificate for EMAIL_ADDR | |||
# -C, --certificate CERT Signing certificate for --sign | |||
# -K, --private-key KEY Key for --sign or --build | |||
# --key-algorithm ALGORITHM Select which key algorithm to use for --build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems to be missing the -A
option which was added via add_option
in cert_command.rb
.
I'd also suggest that you give the alternatives and default in the help text. So:
Select key algorithm for --build from RSA, DSA or EC. Defaults to RSA.
lib/rubygems/security.rb
Outdated
@@ -337,17 +337,14 @@ class Exception < Gem::Exception; end | |||
DIGEST_NAME = 'SHA256' # :nodoc: | |||
|
|||
## | |||
# Algorithm for creating the key pair used to sign gems | |||
# Length of keys created by KEY_ALGORITHM |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does KEY_ALGORITHM
still exist after this PR is applied? I think it refers to RSA, so the comment might read
Length of RSA keys
. The constant might be renamed from KEY_LENGTH
to RSA_KEY_LENGTH
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I see below we use KEY_LENGTH
for DSA as well. So perhaps RSA_DSA_KEY_LENGTH
. Mostly I'm concerned that folks can see it's clearly for one set of options and is unrelated to the EC curve.
lib/rubygems/security.rb
Outdated
domain_key | ||
else | ||
raise Gem::Security::Exception, | ||
"#{algorithm} algorithm not found. RSA/DSA/EC algorithms are supported." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: suggest "RSA, DSA and EC algorithms are supported".
2d96121
to
bf48b1a
Compare
bf48b1a
to
2e89bcc
Compare
key | ||
rescue ArgumentError | ||
raise OptionParser::InvalidArgument, | ||
"#{key_file}: private key not found" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruby 2.3.8 raises an argument error when encountering an empty file for PKey.read, whereas newer ruby versions don't.
We also had to add a custom private key check, because ruby 2.3 doesn't implement PKey::EC#private?
.
It might be worthwhile dropping support for ruby 2.3 since it adds some complexity in this PR and has been discussed in the past.
lib/rubygems/security.rb
Outdated
|
||
## | ||
# Checks if the provided key is private. | ||
# In Ruby 2.3 EC doesn't implments the private_key? but not the private? method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "doesn't implements" -> "doesn't implement".
2e89bcc
to
4f4c617
Compare
Co-Authored-By: Frederik Dudzik <frederik.dudzik@shopify.com>
4f4c617
to
967876f
Compare
@@ -317,7 +318,6 @@ | |||
# * 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
\o/
/ping @simi @deivid-rodriguez Can we help to move this in some way? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey! I don't really know this area very well, and I've heard other maintainers feel skeptical about the security guarantees our current gem signing system provides. For example, see this answer by @indirect: #4682 (comment). That said, you seem the closest to security experts we can get right now, and I don't see how this PR could made anything worse, so I'm not blocking this, and I'm ready to approve it!
For context, our team (@rochlefebvre, @jenshenny and @doodzik + myself currently) are interested in improving or even renovating the gem signing system. This is one low hanging fruit we could identify to help get ourselves up to speed with the current codebase. |
I will merge this in a couple of days if there is no opposition. |
Add support to build and sign certificates with multiple key algorithms (cherry picked from commit 9892d1e)
What was the end-user or developer problem that led to this PR?
The RubyGems client is currently hard-coded to build and sign certificates with private keys as
OpenSSL::PKey::RSA
instances, which prevents the usage from non-rsa certifcates. Adding support for different key algorithms has been on the TODO list of Rubygems'Security
module for a long time.What is your fix for the problem implemented in this PR?
This PR approaches the problem by swapping out the hardcoded
OpenSSL::PKey::RSA
constructor for theOpenSSL::PKey
factory method, which can take an RSA/DSA/EC a key and instantiate the correct object.The user can choose which algorithm to use to build a new certificate through a
--key-algorithm ALGORITHM
flag.We ran into issues when using different
OpenSSL::PKey
subclasses because their interface wasn't consistent. Specifically how a private key gets retrieved and the lack of theto_s
method for EC keys.Make sure the following tasks are checked
@jennyshen has been the co-author of this PR.