Skip to content

rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow - #9697

Open
junaruga wants to merge 6 commits into
ruby:masterfrom
junaruga:wip/rubygems-pqc-signed-gem
Open

rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow#9697
junaruga wants to merge 6 commits into
ruby:masterfrom
junaruga:wip/rubygems-pqc-signed-gem

Conversation

@junaruga

@junaruga junaruga commented Jul 15, 2026

Copy link
Copy Markdown
Member

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.

  • The document is here.
  • The script is here.
  • The CI result is here.

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 (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]

Notes & challenges

Differences between OpenSSL::PKey::RSA/DSA/EC and OpenSSL::PKey::PKey

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?.

The above situation makes the changes complicated. Ideally I want the new feature OpenSSL::PKey::ML-DSA class or OpenSSL::PKey::ML-DSA-NN classes, or OpenSSL::PKey::PKey to implement #to_pem and #private? methods in Ruby OpenSSL. I am thinking to implement the following classes in the meantime.

  • Gem::OpenSSL::PKey::ML_DSA class which is child class of OpenSSL::PKey::PKey for the compatibility with OpenSSL::PKey::RSA and etc. The class can have #to_pem and #private?.
  • Gem::OpenSSL::PKey::PKey class to implement self.generate_key which is wrapper of OpenSSL::PKey.generate_key, but can return Gem::OpenSSL::PKey::ML_DSA.

After OpenSSL::PKey::ML-DSA is implemented, we can use use OpenSSL::PKey::ML-DSA if OpenSSL::PKey::ML-DSA is defined. Otherwise can use Gem::OpenSSL::PKey::ML_DSA.

or

  • Gem::OpenSSL::PKey::PKey class which is child class of OpenSSL::PKey::PKey for the compatibility with OpenSSL::PKey::RSA and etc. The class can have #to_pem and #private?.
  • Gem::OpenSSL::PKey::PKey class to implement self.generate_key which is wrapper of OpenSSL::PKey.generate_key, but can return Gem::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.

  • test/rubygems/test_gem_commands_cert_command.rb
  • test/rubygems/test_gem_security.rb
  • test/rubygems/test_gem_security_policy.rb
  • test/rubygems/test_gem_security_signer.rb

These constants can be managed in one place.

omit_if_support_pqc

I added omit_if_support_pqc to 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 used omit_if_support_pqc for 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

@junaruga
junaruga force-pushed the wip/rubygems-pqc-signed-gem branch from 8157611 to a558720 Compare July 15, 2026 11:49
@junaruga junaruga changed the title Add PQC ML-DSA support for cryptographically signed gems workflow rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow Jul 15, 2026
@junaruga
junaruga force-pushed the wip/rubygems-pqc-signed-gem branch 3 times, most recently from acf46a0 to 23850b4 Compare July 15, 2026 12:46
@junaruga

junaruga commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

I added "Proof of concept" section to the first comment.

@junaruga

Copy link
Copy Markdown
Member Author

Differences between OpenSSL::PKey::RSA/DSA/EC and OpenSSL::PKey::PKey

For the above topic, I opened ruby/openssl#1085 for feature request.

Comment thread lib/rubygems/security/policy.rb
Comment thread lib/rubygems/commands/cert_command.rb Outdated
Comment thread lib/rubygems/security.rb Outdated
@junaruga

Copy link
Copy Markdown
Member Author

Differences between OpenSSL::PKey::RSA/DSA/EC and OpenSSL::PKey::PKey

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.

Comment thread lib/rubygems/commands/cert_command.rb Outdated
@junaruga
junaruga force-pushed the wip/rubygems-pqc-signed-gem branch 7 times, most recently from 10f2a05 to 4ac322a Compare August 5, 2026 09:46
@junaruga

junaruga commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

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.

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.

Comment thread tool/create_certs.rb Outdated
Comment on lines +148 to +151
def public_key_for(key)
OpenSSL::PKey.read(key.public_to_der)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be possible to avoid duplicating the key object entirely since OpenSSL::PKey::PKey#public_to_pem is now being used.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread tool/create_certs.rb Outdated
Comment thread test/rubygems/pqc_utilities.rb Outdated
Comment thread test/rubygems/pqc_utilities.rb Outdated
Comment on lines +13 to +20
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant if we check for probe_pqc_handshake anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]
junaruga added a commit to junaruga/rubygems that referenced this pull request Aug 11, 2026
…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]
junaruga added a commit to junaruga/rubygems that referenced this pull request Aug 11, 2026
…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]
@junaruga
junaruga force-pushed the wip/rubygems-pqc-signed-gem branch 2 times, most recently from a3e1780 to d0adbe6 Compare August 11, 2026 19:55
@junaruga
junaruga marked this pull request as ready for review August 11, 2026 19:59
@junaruga

junaruga commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

After the PR #9768 is merged, I can rebase this PR on the latest master branch.

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 text

This 1st commit is the same with the original 1st commit.

2. create_certs.rb: Don't create redundant 32-bit certificate

This 2nd commit is the same with the original 2nd commit.

3. Rename Gem::PemUtilities to Gem::PEMUtilities

This 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.rb

Renamed PqcUtilities to PQCUtilities addressed by the review.

5. rubygems: Add PQC ML-DSA support for cryptographically signed gems workflow

This is the main commit. As a change from the previous commit, I added self.load_public_key in test/rubygems/pem_utilities.rb. I needed to handle some error when loading ML-DSA public key in old OpenSSL or Ruby OpenSSL versions. Now manages all of the new MLDSA65_*constants in test/rubygems/pem_utilities.rb.

6. pqc_utilities.rb: Simplify PQC check

This 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.

@junaruga

Copy link
Copy Markdown
Member Author

The following failed CI case is not related to this PR.

https://github.com/ruby/rubygems/actions/runs/31530324637/job/93908679294?pr=9697

Comment thread test/rubygems/pqc_utilities.rb Outdated
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants