Skip to content

RubyGems: Adding post-quantum cryptography (PQC) support #9542

@junaruga

Description

@junaruga

This ticket is a sub task of the https://bugs.ruby-lang.org/issues/22068.

Summary and motivation

For the common topics which is not RubyGems specific, you can check this Ruby issue ticket. I write RubyGems related things on this ticket.

I want to add PQC support to RubyGems. That means RubyGems server (gem server), client (gem install, gem update and etc), and other features such as gem cert work in PQC environment where only PQC-supported algorithms ML-DSA and ML-KEM are enabled.

RubyGems server

First, I want to discuss about how to add PQC support to RubyGems (https://rubygems.org/) production server.

Added on 2026/05/19: According to @gotoken's investigation, https://rubygems.org already supports PQC (ML-KEM), non-PQC hybrid key exchange, while it possibly uses non-PQC signatures (RSA). According to my colleague at my company's OpenSSL team, implementing ML-KEM communication is important now.

Choice of the RubyGems server types

Here is the current production environment:

RubyGems HTTPS client => https://rubygems.org/: HTTPS Nginx reverse proxy (port 443) => RubyGems HTTP server

I created the proof-of-concept for possible RubyGems servers to support PQC by the following repository. The GitHub Actions rubygems.yml is also helpful to understand how a set of the RubyGems server/client works.
You can check the following testing matrix by the following page and rubygems.yml.

This proof-of-concept has the following testing matrix (1, 2, 3) x (a, b):

  1. RubyGems HTTPS client => HTTPS RubyGems server (WEBRick HTTPS server)
  2. RubyGems HTTPS client => HTTPS Ruby OpenSSL reverse proxy => RubyGems HTTP server
  3. RubyGems HTTPS client => HTTPS Nginx reverse proxy => RubyGems HTTP server

x

a. PQC (single), non-PQC (single): Running a PQC server (ML-DSA-65 only) on port 18443 and a non-PQC server (RSA only) on port 18444 at the same time.

Here is the production image:

RubyGems HTTPS client RSA => RubyGems HTTPS server (reverse proxy) RSA (https://rubygems.org) => RubyGems HTTP server
RubyGems HTTPS client ML-DSA => RubyGems HTTPS server (reverse proxy) ML-DSA (https://pqc.rubygems.org) => RubyGems HTTP server

I propose the host name pqc.rubygems.org to run HTTPS PQC (ML-DSA) server. We can discuss about the host name.

b. PQC, non-PQC (dual): Dual PQC (ML-DSA-65) and non-PQC server (RSA) running at one port 18443. The HTTPS server or reverse proxy can distribute client's connection based on the client's request to be used signature algorithms. ruby/openssl test/openssl/test_ssl.rb test_pqc_sigalg demonstrates this case and the HTTPS client requests the used signature algorithms by ctx.sigalgs.
https://github.com/ruby/openssl/blob/ee351a5783938fd1b3ee06f943029bef8d667e40/test/openssl/test_ssl.rb#L2146

Here is the production image:

RubyGems HTTPS client RSA => RubyGems HTTPS server (reverse proxy) RSA (https://rubygems.org) => RubyGems HTTP server
RubyGems HTTPS client ML-DSA => RubyGems HTTPS server (reverse proxy) ML-DSA (https://rubygems.org) => RubyGems HTTP server
Based on my investigation, I think the easiet way is choosing 3. using Nginx reverse proxy, and a. using PQC (single), non-PQC (single). Because * We currently use the 3. * b. PQC, non-PQC (dual): the demerit is that we need to implement the logic for RubyGems client to request signature algorithms by default RSA (such as rsa_pss_rsae_sha256) to keep the backward compability, and the migration risk is high. The use case using current cryptograpohy (non-PQC) is affected by the RubyGems server migration. * a. PQC (single), non-PQC (single): the RubyGems PQC server/client connection works without any changes. The current RubyGems server/client (RSA) is less affected by the PQC server migration. When we share the same RubyGems gems data or RubyGems HTTP server, we need to care about the affection by concurrent access to the data or server by the migration.

Migration plan

I would write the migration plan when choosing the 3. using Nginx reverse proxy, and a. using PQC (single), non-PQC (single). Here is the production image:
RubyGems HTTPS client RSA => RubyGems HTTPS server (Nginx reverse proxy) RSA (https://rubygems.org) => RubyGems HTTP server
RubyGems HTTPS client ML-DSA => RubyGems HTTPS server (Nginx reverse proxy) ML-DSA (https://pqc.rubygems.org) => RubyGems HTTP server

Phase 1:

Get the ML-DSA private CA (not public CA) from a vendor such as AWS and Digicert and etc that publishes the private CA. It seems that AWS and Digicert have the service to create the private CA for ML-DSA.

We can also create self private CA for ML-DSA rather than private CA created by vendor's service.

Put the private CA file on RubyGems website on the website https://rubygems.org/, and users can download and use RubyGems PQC HTTPS server by the following gemrc config.

Note the ssl_ca_cert config item is not documented in the following page.
https://docs.ruby-lang.org/en/master/Gem/ConfigFile.html

But the config items is available according to the source.

@ssl_ca_cert = @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert

gemrc

:ssl_ca_cert: /path/to/ca_file
:sources:
  - https://pqc.rubygems.org
$ export GEMRC=/path/to/gemrc

Phase 2:

Put the private CA file into the following path in the ruby/rubygems repository.

lib/rubygems/ssl_certs/pqc.rubygems.org/<IssuerName>-MLDSA65.pem

Note that ML-DSA has 3 types ML-DSA-44, ML-DSA-65, ML-DSA-87. Note that "NN" in "ML-DSA-NN" isn't bit length.

I found the following CA file was put in ruby/rubygems repository in the past.

lib/rubygems/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem

Users using the new RubyGems containing the CA file can use the gemrc config without ssl_ca_cert config item.

gemrc

:sources:
  - https://pqc.rubygems.org
$ export GEMRC=/path/to/gemrc

Phase 3:

Migrate Private CA to Merkle Tree Certificate (MTC)-based CA or Public CA.
First, there is no vendor which provide a service to create Public CA.
And there is a Google's movement not to use public CA for PQC (ML-DSA).
They are proposing a new CA mechanism called Merkle Tree Certificate (MTC) instead of public CA.
Maybe the MTC is still ML-DSA-based. Because ML-DSA is the only PQC-supported algorithm used for CA.

Cultivating a robust and efficient quantum-safe HTTPS - 2026/02/27
https://blog.google/security/cultivating-a-robust-and-efficient-quantum-safe-https/

We also expect to support “traditional” X.509 certificates with quantum-resistant algorithms for use only in private PKIs (i.e., those not included in the Chrome Root Store) later this year.

So, we need to prepare for the 2 possibilities MTC or public CA.

The following pages are also about this topic as a reference.

How Google's Merkle Tree Certificates Reshape Web Trust - 2026/03/03
https://www.digicert.com/blog/google-merkle-tree-certificates

Moving Forward, Together: initiatives to promote MTC in Google Chrome
https://googlechrome.github.io/chromerootprogram/moving-forward-together/

Phase 4:

After ML-DSA is commonly used, and RSA is a kind of outdate (I am not sure when), stop https://rubygems.org using RSA connection, then redirect https://pqc.rubygems.org into https://rubygems.org.

Then users using the following gemrc can remove the config file.

gemrc

:sources:
  - https://pqc.rubygems.org

Added on 2026/05/19: I was told by my colleagues that it's better to keep using non-PQC signatures, not implementing ML-DSA signature for the rubygems.org until (maybe ML-DSA basd) MTC or ML-DSA public CA is supported. Because the importance of using PQC (ML-DSA) is not very high, and the above migration step phase 3 from ML-DSA private costs users.

gem cert

I want to RubyGems gem cert to support PQC.

Files to modify

Below is a list of the files that we may modify to support PQC.

lib/rubygems/commands/cert_command.rb - gem cert
lib/rubygems/request.rb - https client request
lib/rubygems/security.rb - gem cert
lib/rubygems/security/signer.rb - gem cert
lib/rubygems/vendor/net-http/lib/net/http.rb
test/rubygems/*.pem - need to add ML-DSA-NN .pem files
test/rubygems/test_gem_commands_cert_command.rb
test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb
test/rubygems/test_gem_security.rb
test/rubygems/test_gem_specification.rb

Let me know what you think.

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions