rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow - #9697
rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow#9697junaruga wants to merge 6 commits into
Conversation
8157611 to
a558720
Compare
acf46a0 to
23850b4
Compare
|
I added "Proof of concept" section to the first comment. |
For the above topic, I opened ruby/openssl#1085 for feature request. |
For the above challenge, I sent the PR #9701 based on the discussion on this PR. I think the PR #9701 can be a solution for the challenge. I hope the PR #9701 is reviewed before this PR. Then I can rebase this PR on the latest master branch. |
10f2a05 to
4ac322a
Compare
For the above issue addressed on notes and challenges section on the first comment, I sent the PR #9768. After the PR #9768 is merged, I can rebase this PR on the latest master branch. |
| def public_key_for(key) | ||
| OpenSSL::PKey.read(key.public_to_der) | ||
| end | ||
|
|
There was a problem hiding this comment.
I think it should be possible to avoid duplicating the key object entirely since OpenSSL::PKey::PKey#public_to_pem is now being used.
There was a problem hiding this comment.
Sorry I don't understand your suggestion. Are you suggesting removing keys[:public] to avoid duplicated objects between keys[:private] and keys[:public], and write public key in write_keys_and_certificates method directly from keys[:private], right?
There was a problem hiding this comment.
I fixed this on the 5th commit "rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow" on the rebase on the latest master branch, removing keys[:public]. What do you think?
| 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 |
There was a problem hiding this comment.
This seems redundant if we check for probe_pqc_handshake anyway.
There was a problem hiding this comment.
If we don't use the guards Gem::PqcUtilities.support_pqc_openssl? and Gem::PqcUtilities.support_pqc_ruby_openssl?, we need to rescue OpenSSL::PKey.read raising error when reading ML-DSA keys and ctx.groups undefined method case in self.probe_pqc_handshake. Do you still want to remove the OpenSSL version check and Ruby OpenSSL version check?
There was a problem hiding this comment.
I fixed this on the 6th commit "pqc_utilities.rb: Simplify PQC check", removing OpenSSL and Ruby OpenSSL version checks. What do you think?
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]
…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 signed gems 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: * Ruby OpenSSL methods `#sign` (signing) and `#verify` don't accept digest algorithm, in ML-DSA cases because ML-DSA has a built-in digest * Unifying tool/create_certs.rb with tool/create_encrypted_key.rb to generate RSA and ML-DSA-65 keys/certificates. * Adding ML-DSA-65 related .pem files generated by tool/create_certs.rb. * Adding ML-DSA tests. We need many tests due to differences between `OpenSSL::PKey::RSA/DSA/EC` and `OpenSSL::PKey::PKey`. See also https://guides.rubygems.org/security/ for the signed gems workflows. Assisted-by: Claude:claude-opus-4-6[1m] create_certs.rb: Remove public key object duplication * Remove keys[:public] - write public key PEM directly from the private key using public_to_pem instead of creating a separate public key object * Remove public_key_for method, inline OpenSSL::PKey.read with public_to_pem in create_certificate * Simplify write_keys_and_certificates Per review feedback from rhenium on PR ruby#9697. Assisted-by: Claude:claude-opus-4-6[1m]
Acronyms should be in all caps for naming convention. Assisted-by: Claude:claude-opus-4-6[1m]
…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 signed gems 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: * Ruby OpenSSL methods `#sign` (signing) and `#verify` don't accept digest algorithm, in ML-DSA cases because ML-DSA has a built-in digest * Unifying tool/create_certs.rb with tool/create_encrypted_key.rb to generate RSA and ML-DSA-65 keys/certificates. * Adding ML-DSA-65 related .pem files generated by tool/create_certs.rb. * Adding ML-DSA tests. We need many tests due to differences between `OpenSSL::PKey::RSA/DSA/EC` and `OpenSSL::PKey::PKey`. See also https://guides.rubygems.org/security/ for the signed gems workflows. Assisted-by: Claude:claude-opus-4-6[1m] create_certs.rb: Remove public key object duplication * Remove keys[:public] - write public key PEM directly from the private key using public_to_pem instead of creating a separate public key object * Remove public_key_for method, inline OpenSSL::PKey.read with public_to_pem in create_certificate * Simplify write_keys_and_certificates Per review feedback from rhenium on PR ruby#9697. Assisted-by: Claude:claude-opus-4-6[1m]
a3e1780 to
d0adbe6
Compare
Now I rebased this PR with the following updated 6 commits after merging the above refactoring PR #9768. 1. security.rb: Reorder case branches in create_key to match help textThis 1st commit is the same with the original 1st commit. 2. create_certs.rb: Don't create redundant 32-bit certificateThis 2nd commit is the same with the original 2nd commit. 3. Rename Gem::PemUtilities to Gem::PEMUtilitiesThis commit is the fix related to the PqcUtilities to rename the class name mentioned on the PR review. PEM is also acronyms. 4. rubygems: Move omit_unless_support_pqc to helper.rbRenamed PqcUtilities to PQCUtilities addressed by the review. 5. rubygems: Add PQC ML-DSA support for cryptographically signed gems workflowThis is the main commit. As a change from the previous commit, I added 6. pqc_utilities.rb: Simplify PQC checkThis is also the fix mentioned on the PR review. The PQC check doesn't depend on the OpenSSL and Ruby OpenSSL versions. That may be good when other SSL libraries such as LibreSSL and AWS-LC supports PQC in the future. |
|
The following failed CI case is not related to this PR. https://github.com/ruby/rubygems/actions/runs/31530324637/job/93908679294?pr=9697 |
* 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]
…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 signed gems 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: * Ruby OpenSSL methods `#sign` (signing) and `#verify` don't accept digest algorithm, in ML-DSA cases because ML-DSA has a built-in digest * Unifying tool/create_certs.rb with tool/create_encrypted_key.rb to generate RSA and ML-DSA-65 keys/certificates. * Adding ML-DSA-65 related .pem files generated by tool/create_certs.rb. * Adding ML-DSA tests. We need many tests due to differences between `OpenSSL::PKey::RSA/DSA/EC` and `OpenSSL::PKey::PKey`. See also https://guides.rubygems.org/security/ for the signed gems workflows. Assisted-by: Claude:claude-opus-4-6[1m]
Simplify PQC check not to depend on OpenSSL and Ruby OpenSSL versions. Signed-off-by: Jun Aruga <jaruga@redhat.com>
d0adbe6 to
5bb132c
Compare
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