rubygems-pq-tls-policy
RubyGems plugin that checks the negotiated TLS key exchange group for RubyGems gem-server HTTPS connections when explicitly enabled.
This repository is an experimental starting point for testing post-quantum TLS key exchange policy enforcement around RubyGems operations.
Scope
When enabled, this plugin installs a process-local Ruby OpenSSL hook and checks TLS connections that pass through OpenSSL::SSL::SSLSocket#post_connection_check.
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.
Because the hook is process-local, it may also affect other Ruby OpenSSL HTTPS connections made in the same Ruby process while the policy is enabled.
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_KEY_EXCHANGE_TRACE |
1 |
Prints observed TLS version, cipher, and negotiated group. |
Default allowed group:
X25519MLKEM768
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_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"
A non-compliant connection raises Gem::PqTlsPolicy::Violation before the RubyGems HTTP request proceeds.
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#groupOpenSSL::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, - 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
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 and localhost real-TLS command tests in ruby:4.0.5-trixie. |
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.
It does not prevent a non-PQ TLS handshake from happening. It rejects the connection after observing the negotiated group.
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.0.1.gem
License
MIT