rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow#9697
Draft
junaruga wants to merge 6 commits into
Draft
rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow#9697junaruga wants to merge 6 commits into
junaruga wants to merge 6 commits into
Conversation
8157611 to
a558720
Compare
a558720 to
4116303
Compare
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:claude-opus-4-6[1m]
The create_certs.rb created test/rubygems/expired_cert_32.pem which was not used and managed in git repository. 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. Assisted-by: Claude:claude-opus-4-6[1m]
* 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:claude-opus-4-6[1m]
* Rename test/rubygems/private_ec_key.pem to test/rubygems/ec_private_key.pem to align with the pattern <algorithm>_key_name.pem (e.g., mldsa65_ssl_key.pem) * Rename EC_KEY to EC_PRIVATE_KEY in test_gem_security.rb * Rename PRIVATE_EC_KEY_FILE to EC_PRIVATE_KEY_FILE in test_gem_commands_cert_command.rb Assisted-by: Claude:claude-opus-4-6[1m]
4116303 to
acf46a0
Compare
…rkflow 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. ``` $ 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 ``` 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 workflow also includes * `gem cert -C cert.pem` * `gem cert -K private_key.pem` * `gem build gemname.gemspec` with `s.cert_chain` and `s.signing_key` to build signed gem. * `gem install gemname-version.gem -P HighSecurity` # or `-P MediumSecurity` * `gem update gemname-version.gem -P HighSecurity` # or `-P MediumSecurity` The changes come from: * `OpenSSL::PKey::RSA`, `OpenSSL::PKey::DSA`, `OpenSSL::PKey::EC` for RSA, DSA, EC cases, vs `OpenSSL::PKey::PKey` for ML-DSA-NN cases. `OpenSSL::PKey::PKey` doesn't have the methods `#to_pem`, `#private?`. * Ruby OpenSSL methods `#sign` (signing) and `#verify` don't accept digest algorithm, in ML-DSA cases because ML-DSA has a built-in digest * Unify tool/create_certs.rb with tool/create_encrypted_key.rb to generate both RSA and ML-DSA-65 certificates/keys. * ML-DSA-65 related .pem files generated by tool/create_certs.rb. * Adding ML-DSA tests. We need lots of tests because of differences between `OpenSSL::PKey::RSA/DSA/EC` and `OpenSSL::PKey::PKey` mentioned above. The method chains for the changes are below. * gem cert --build (key/cert generation) * lib/rubygems/security.rb self.sign * chain: execute -> build -> build_cert -> Gem::Security.create_cert_email -> create_cert_self_signed -> sign * lib/rubygems/security.rb self.write * chain: execute -> build -> build_key -> Gem::Security.write * gem cert -K (for --sign or --build) * lib/rubygems/commands/cert_command.rb open_private_key * chain: open_private_key * gem build (package signing) * lib/rubygems/security/signer.rb sign * chain: execute -> build_gem -> build_package -> Gem::Package.build -> Package#build -> add_metadata -> TarWriter#add_file_signed -> signer.sign * gem install -P HighSecurity (signature verification) * lib/rubygems/security/policy.rb check_trust * chain: execute -> install_gems -> install_gem -> RequestSet#install -> Installer#install -> pre_install_checks -> verify_spec -> Package#spec -> Package#verify -> Policy#verify_signatures -> Policy#verify -> check_trust * lib/rubygems/security/policy.rb check_data * chain: (same as above up to Policy#verify) -> check_data See also https://guides.rubygems.org/security/ for the signed gems workflows. Assisted-by: Claude:claude-opus-4-6[1m]
acf46a0 to
23850b4
Compare
Member
Author
|
I added "Proof of concept" section to the first comment. |
Member
Author
For the above topic, I opened ruby/openssl#1085 for feature request. |
rhenium
reviewed
Jul 15, 2026
Comment on lines
169
to
181
| 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 " \ | ||
| "does not match signing root certificate checksum" unless | ||
| save_dgst == cert_dgst |
Member
There was a problem hiding this comment.
Can this be simplified like this?
However this is clearly checking the subject public key, not the certificate itself. (I haven't read the rest of the code, so I don't know what this implies.)
raise Gem::Security::Exception,
"trusted root certificate #{root.subject} checksum " \
"does not match signing root certificate checksum" unless
save_cert.public_key.to_der == root.public_key.to_der
Comment on lines
-99
to
+114
| 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 |
Member
There was a problem hiding this comment.
I wonder if we can just remove this and instead check for an exception that may be raised by OpenSSL::X509::Certificate#sign later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR is related to #9542, and to add PQC ML-DSA support for cryptographically signed gems workflow.
The PR has 6 commits. The 1st - 5th commits are not related to PQC, but for preparation to implement the 6th commit, the main commit of this PR. The PR's 6th commit is in draft phase. I want to share my early phase of the implementation to get feedback and adjust the direction. I hope #9678 will be reviewed and merged before this PR.
Proof of concept
I prepared proof-of-concept scripts for signed gems workflow.
Commit message
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(signatureverification).
The
gem cert -AacceptsML-DSA-44,ML-DSA-65, orML-DSA-87to generateML-DSA based cert and key.
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 workflow also includes
gem cert -C cert.pemgem cert -K private_key.pemgem build gemname.gemspecwiths.cert_chainands.signing_keyto buildsigned gem.
gem install gemname-version.gem -P HighSecurity# or-P MediumSecuritygem update gemname-version.gem -P HighSecurity# or-P MediumSecurityThe changes come from:
OpenSSL::PKey::RSA,OpenSSL::PKey::DSA,OpenSSL::PKey::ECfor RSA, DSA, ECcases, vs
OpenSSL::PKey::PKeyfor ML-DSA-NN cases.OpenSSL::PKey::PKeydoesn't have the methods
#to_pem,#private?.#sign(signing) and#verifydon't accept digestalgorithm, in ML-DSA cases because ML-DSA has a built-in digest
to generate both RSA and ML-DSA-65 certificates/keys.
OpenSSL::PKey::RSA/DSA/ECandOpenSSL::PKey::PKeymentioned above.The method chains for the changes are below.
Gem::Security.create_cert_email -> create_cert_self_signed ->
sign
Gem::Package.build -> Package#build -> add_metadata ->
TarWriter#add_file_signed -> signer.sign
RequestSet#install -> Installer#install ->
pre_install_checks -> verify_spec -> Package#spec ->
Package#verify -> Policy#verify_signatures ->
Policy#verify -> check_trust
See also https://guides.rubygems.org/security/ for the signed gems workflows.
Assisted-by: Claude:claude-opus-4-6[1m]
Notes & challenges
Differences between
OpenSSL::PKey::RSA/DSA/ECandOpenSSL::PKey::PKeyThe above situation makes the changes complicated. Ideally I want the new feature
OpenSSL::PKey::ML-DSAclass orOpenSSL::PKey::ML-DSA-NNclasses, orOpenSSL::PKey::PKeyto implement#to_pemand#private?methods in Ruby OpenSSL. I am thinking to implement the following classes in the meantime.Gem::OpenSSL::PKey::ML_DSAclass which is child class ofOpenSSL::PKey::PKeyfor the compatibility withOpenSSL::PKey::RSAand etc. The class can have#to_pemand#private?.Gem::OpenSSL::PKey::PKeyclass to implementself.generate_keywhich is wrapper ofOpenSSL::PKey.generate_key, but can returnGem::OpenSSL::PKey::ML_DSA.After
OpenSSL::PKey::ML-DSAis implemented, we can use useOpenSSL::PKey::ML-DSAifOpenSSL::PKey::ML-DSAis defined. Otherwise can useGem::OpenSSL::PKey::ML_DSA.or
Gem::OpenSSL::PKey::PKeyclass which is child class ofOpenSSL::PKey::PKeyfor the compatibility withOpenSSL::PKey::RSAand etc. The class can have#to_pemand#private?.Gem::OpenSSL::PKey::PKeyclass to implementself.generate_keywhich is wrapper ofOpenSSL::PKey.generate_key, but can returnGem::OpenSSL::PKey::PKey.Constant management in test files
test/rubygems/helper.rb defines KEY/CERT constants.
However, for example, the following files also defines such constants. I also saw duplicated constants from the ones in helper.rb.
These constants can be managed in one place.
omit_if_support_pqc
I added
omit_if_support_pqcto be used in non-PQC specific tests. However, so far it is not used in any places. I will delete the method, minimizing the code changes if it is not used eventually. I usedomit_if_support_pqcfor an error case in non-PQC tests. However, it was hard to handle OpenSSL versions and Ruby OpenSSL versions in the error case. So, I deleted the non-PQC tests.What was the end-user or developer problem that led to this PR?
Users cannot use ML-DSA for signed gem workflow.
https://guides.rubygems.org/security/
What is your fix for the problem, implemented in this PR?
Added the implementation to add PQC ML-DSA support for cryptographically signed gems workflow.
Make sure the following tasks are checked