rubygems-pq-tls-policy
RubyGems plugin that checks negotiated TLS properties for RubyGems gem-server HTTPS connections when explicitly enabled.
This repository is an experimental starting point for testing post-quantum TLS policy enforcement around RubyGems operations.
Typical uses
Observe negotiated TLS groups:
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_TRACE=1 \
gem install rake
Require PQ TLS key exchange:
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_POLICY=pq_required \
RUBYGEMS_GEM_SERVER_TLS_ALLOWED_GROUPS=X25519MLKEM768 \
gem install rake
Observe certificate signatures:
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_POLICY=pq_observe \
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_SCOPE=leaf \
gem install rake
Require ML-DSA certificate signatures:
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_POLICY=pq_required \
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_SCOPE=chain_all \
gem install rake
Scope
When enabled, this plugin extends the Gem::Net::HTTP instances created by RubyGems' HTTPS connection pool and checks each connection after Net::HTTP#connect completes.
The intended scope is RubyGems HTTPS communication with configured gem servers and gem push hosts, such as https://rubygems.org or a private gem server.
The hook is installed on RubyGems' HTTPS pool and on the individual RubyGems HTTP connection instances it creates; it is not installed globally on OpenSSL::SSL::SSLSocket or Net::HTTP.
This is not a sandbox. It does not restrict network connections made by:
- gemspec evaluation
- native extension build scripts
- installed gems
- RubyGems or Bundler plugins
- git, ssh, curl, make, compiler toolchains, or other subprocesses
- non-Ruby OpenSSL implementations
- JRuby Java TLS internals, unless they expose the same Ruby OpenSSL behavior
For full egress control, use OS/container-level network policy.
Configuration
The plugin is disabled by default.
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_POLICY=pq_required \
gem install rake
Supported environment variables:
| Variable | Example | Meaning |
|---|---|---|
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_POLICY |
pq_required |
Enables policy enforcement. default, off, disabled, or unset disables it. |
RUBYGEMS_GEM_SERVER_TLS_ALLOWED_GROUPS |
X25519MLKEM768 |
Colon- or comma-separated list of allowed negotiated TLS groups. |
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_POLICY |
pq_observe |
Enables certificate signature observation or enforcement. default, off, disabled, or unset disables it. pq_observe only traces; pq_required rejects non-compliant chains. |
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_SCOPE |
leaf |
Certificate signature check scope: leaf, chain_any, or chain_all. Defaults to leaf. |
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_TRACE |
1 |
Prints certificate signature policy observations. pq_observe also prints these observations. |
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_TRACE |
1 |
Prints observed TLS version, cipher, and negotiated group. |
Default allowed group:
X25519MLKEM768
Default allowed certificate signature algorithms:
ML-DSA-44
ML-DSA-65
ML-DSA-87
The ML-DSA X.509/PKIX OIDs are also recognized:
2.16.840.1.101.3.4.3.17 # ML-DSA-44
2.16.840.1.101.3.4.3.18 # ML-DSA-65
2.16.840.1.101.3.4.3.19 # ML-DSA-87
Advanced option:
| Variable | Example | Meaning |
|---|---|---|
RUBYGEMS_GEM_SERVER_TLS_ALLOWED_CERT_SIGNATURE_ALGORITHMS |
ML-DSA-44:2.16.840.1.101.3.4.3.18 |
Colon- or comma-separated list of allowed certificate signature algorithm names or OIDs. This is intended for experiments with future algorithms, hybrid/composite identifiers, or vendor OIDs. |
Example
gem install rubygems-pq-tls-policy
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_POLICY=pq_required \
RUBYGEMS_GEM_SERVER_TLS_ALLOWED_GROUPS=X25519MLKEM768 \
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_POLICY=pq_observe \
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_SCOPE=leaf \
RUBYGEMS_GEM_SERVER_TLS_CERT_SIGNATURE_TRACE=1 \
RUBYGEMS_GEM_SERVER_TLS_KEY_EXCHANGE_TRACE=1 \
gem install rake
A compliant connection prints trace output similar to:
[rubygems:tls] host=rubygems.org version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 group="X25519MLKEM768"
[rubygems:tls] host=rubygems.org cert_signature_scope=leaf cert_signature_algorithms=["ML-DSA-44"] cert_pq=true
A non-compliant connection raises Gem::PqTlsPolicy::Violation before the RubyGems HTTP request proceeds.
The certificate signature policy is independent from the TLS key exchange policy. TLS key exchange checks inspect the negotiated TLS group, while certificate signature checks inspect the X.509 chain returned by SSLSocket#peer_cert_chain after the handshake. peer_cert_chain does not include the trust anchor/root certificate.
If the policy is enabled on an unsupported runtime, the RubyGems plugin entrypoint exits the gem command before the requested operation runs.
This is intentional because RubyGems treats ordinary plugin load exceptions as warnings and otherwise continues.
Development
Install dependencies:
bundle install
Run unit tests:
bundle exec rake test
or:
script/test
Run a local diagnostic:
script/diagnose-tls
The diagnostic prints whether the current Ruby/OpenSSL exposes the APIs used by this plugin, especially:
OpenSSL::SSL::SSLSocket#groupGem::Request::HTTPSPool#setup_connectionOpenSSL::SSL::SSLContext#groups=
OpenSSL::SSL::SSLContext#groups= is used only by this plugin's development test server to force specific TLS groups during local real-TLS tests.
Local real-TLS integration test
If your local Ruby is linked against an OpenSSL version with the required TLS group support, run:
script/integration
The integration script:
- builds a fixture gem,
- creates a static RubyGems repository,
- generates a localhost certificate,
- starts a localhost HTTPS gem server with
X25519MLKEM768, - runs
gem fetch,gem install,bundle install, andgem push --host, - verifies certificate signature
pq_observeandpq_requiredbehavior with classic and ML-DSA self-signed certificates, - restarts the server with
X25519, and - verifies that non-PQ TLS group usage is rejected.
It also builds and installs this plugin into a temporary GEM_HOME, clears RUBYOPT, and verifies RubyGems plugin auto-loading from the installed gem.
The script loads the plugin from the checkout using RUBYOPT=-Ilib -rrubygems_plugin, so you can test changes before packaging or installing the gem.
Docker real-TLS integration test
If your local Ruby/OpenSSL is not suitable, use Docker:
script/integration-docker
This runs script/integration in the official ruby:4.0.5-trixie image, which currently provides Ruby 4.0.5 linked against OpenSSL 3.5.
It also verifies that an installed plugin auto-load on ruby:4.0.5-bookworm fails closed before the requested gem operation runs.
To use another image:
RUBY_DOCKER_IMAGE=ruby:4.0.5-trixie script/integration-docker
To run only the certificate signature policy matrix used by GitHub Actions:
MATRIX=gha script/cert-signature-integration-docker
To run one certificate signature case:
CERT_SIG_CASE=pq-leaf-classic-chain \
CERT_SIG_POLICY=pq_required \
CERT_SIG_SCOPE=leaf \
CERT_SIG_EXPECT=pass \
script/cert-signature-integration-docker
For an interactive shell:
script/shell-docker
Docker runtime condition test
To verify the plugin's runtime checks against both supported and unsupported Ruby/OpenSSL combinations:
script/runtime-check-docker
By default this expects ruby:4.0.5-trixie to pass and ruby:4.0.5-bookworm to fail before installation with Gem::PqTlsPolicy::UnsupportedRuntime.
To use different images:
SUPPORTED_RUBY_DOCKER_IMAGE=ruby:4.0.5-trixie \
UNSUPPORTED_RUBY_DOCKER_IMAGE=ruby:4.0.5-bookworm \
script/runtime-check-docker
Docker compatibility probes
To run source-checkout probes on JRuby and TruffleRuby:
script/compatibility-docker
These probes load the plugin code directly from the checkout, print diagnostics, and verify that enabling the policy either installs cleanly or fails with Gem::PqTlsPolicy::UnsupportedRuntime.
To use different images:
JRUBY_DOCKER_IMAGE=jruby:latest \
TRUFFLERUBY_DOCKER_IMAGE=ghcr.io/flavorjones/truffleruby:latest \
script/compatibility-docker
GitHub Actions
This repository includes three workflows:
| Workflow | Purpose |
|---|---|
CI |
Unit tests and gem build on MRI Ruby. |
Compatibility |
Docker probes on JRuby and TruffleRuby. |
PQ TLS Integration |
Runs unit tests, localhost real-TLS command tests, certificate signature policy matrix cases, and unsupported-runtime fail-closed checks. |
Compatibility results should be treated as observed behavior, not a compatibility guarantee. Docker latest tags are mutable, so JRuby and TruffleRuby rows record the runtime versions observed by the compatibility probe.
Observed compatibility:
| Runtime | Policy enablement | SSLSocket#group |
Real TLS integration | Notes |
|---|---|---|---|---|
ruby:4.0.5-trixie (MRI Ruby 4.0.5 + OpenSSL 3.5) |
✅ | ✅ | ✅ | Default Docker integration/runtime-check image and PQ TLS Integration container. |
ruby:4.0.5-bookworm |
❌ | ❌ | N/A | Expected to fail with Gem::PqTlsPolicy::UnsupportedRuntime. |
| JRuby 10.1.0.0 (Ruby 4.0.0), JRuby-OpenSSL 0.15.6 | ❌ | ❌ | N/A | Observed by source-checkout compatibility probe on 2026-05-22. |
| TruffleRuby 24.2.2 (Ruby 3.3.7), OpenSSL 3.5.1 | ❌ | ❌ | N/A | Observed by source-checkout compatibility probe on 2026-05-22; below the gemspec Ruby requirement. |
Command coverage
The real-TLS integration currently exercises:
| Command | Path |
|---|---|
gem fetch |
download/read |
gem install |
download/read |
bundle install |
Bundler resolution/install |
gem push --host |
upload/write |
The gem push test uses a local fake RubyGems-compatible HTTPS endpoint. It does not publish anything to RubyGems.org.
Security model
This plugin checks the negotiated TLS group after TLS handshake and hostname verification, but before RubyGems continues with the HTTPS request. When certificate signature policy is enabled, it also checks the peer certificate chain after TLS handshake and hostname verification.
It does not prevent a non-PQ TLS handshake from happening. It rejects the connection after observing the negotiated group. Likewise, certificate signature enforcement rejects the connection only after the server certificate chain has already been received and verified by the TLS stack.
For a stronger design, combine this plugin with client-side TLS group configuration, an internal gem mirror, or container/network-level egress policy.
Packaging
Build the gem:
gem build rubygems-pq-tls-policy.gemspec
Install locally:
gem install ./rubygems-pq-tls-policy-1.2.0.gem
License
MIT