-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow #9697
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
base: master
Are you sure you want to change the base?
Changes from all commits
23e9735
c5adfbb
0b3482f
7761fd3
3aa0ac2
23850b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rhenium Note this logic
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is a public pkey ever passed to this method?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, The method rubygems/lib/rubygems/security.rb Lines 592 to 604 in 7288145
I think I need to refactor the following things in in ruby/rubygems.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure
So this makes a lot of sense to me.
I also think this is a good idea. I think it's an acceptable change, but it's perhaps worth noting that |
||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
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 wonder if we can just remove this and instead check for an exception that may be raised by
OpenSSL::X509::Certificate#signlater.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.
This may be possible, by checking an exception raised at a later step, and raise new exception with actionable error message.