-
Notifications
You must be signed in to change notification settings - Fork 26
Add post-quantum cryptography (PQC) support to SSLConfig #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -25,19 +25,25 @@ class SSLConfig | |||||
| # | ||||||
| # See DRb::DRbSSLSocket::SSLConfig.new for more details | ||||||
| DEFAULT = { | ||||||
| :SSLCertificate => nil, | ||||||
| :SSLPrivateKey => nil, | ||||||
| :SSLClientCA => nil, | ||||||
| :SSLCACertificatePath => nil, | ||||||
| :SSLCACertificateFile => nil, | ||||||
| :SSLTmpDhCallback => nil, | ||||||
| :SSLVerifyMode => ::OpenSSL::SSL::VERIFY_NONE, | ||||||
| :SSLVerifyDepth => nil, | ||||||
| :SSLVerifyCallback => nil, # custom verification | ||||||
| :SSLCertificateStore => nil, | ||||||
| :SSLCertificates => nil, | ||||||
| :SSLCertificate => nil, | ||||||
| :SSLPrivateKey => nil, | ||||||
| :SSLPrivateKeyAlgorithms => ["RSA"], | ||||||
| :SSLClientCA => nil, | ||||||
| :SSLCACertificatePath => nil, | ||||||
| :SSLCACertificateFile => nil, | ||||||
| :SSLSignatureAlgorithms => nil, | ||||||
| :SSLClientSignatureAlgorithms => nil, | ||||||
| :SSLGroups => nil, | ||||||
| :SSLTmpDhCallback => nil, | ||||||
| :SSLVerifyMode => ::OpenSSL::SSL::VERIFY_NONE, | ||||||
| :SSLVerifyDepth => nil, | ||||||
| :SSLVerifyCallback => nil, # custom verification | ||||||
| :SSLCertificateStore => nil, | ||||||
| # Must specify if you use auto generated certificate. | ||||||
| :SSLCertName => nil, # e.g. [["CN","fqdn.example.com"]] | ||||||
| :SSLCertComment => "Generated by Ruby/OpenSSL" | ||||||
| # e.g. [["CN", "fqdn.example.com"]] | ||||||
| :SSLCertName => nil, | ||||||
| :SSLCertComment => "Generated by Ruby/OpenSSL" | ||||||
| } | ||||||
|
|
||||||
| # Create a new DRb::DRbSSLSocket::SSLConfig instance | ||||||
|
|
@@ -51,13 +57,29 @@ class SSLConfig | |||||
| # | ||||||
| # From +config+ Hash: | ||||||
| # | ||||||
| # :SSLCertificates :: | ||||||
| # An Array of [certificate, private_key] pairs. Each element | ||||||
| # is an Array of an OpenSSL::X509::Certificate and its | ||||||
| # corresponding private key. This option is prioritized over | ||||||
| # :SSLCertificate and :SSLPrivateKey. See | ||||||
| # OpenSSL::SSL::SSLContext#add_certificate | ||||||
| # | ||||||
| # :SSLCertificate :: | ||||||
| # An instance of OpenSSL::X509::Certificate. If this is not provided, | ||||||
| # then a generic X509 is generated, with a correspond :SSLPrivateKey | ||||||
| # | ||||||
| # :SSLPrivateKey :: | ||||||
| # A private key instance, like OpenSSL::PKey::RSA. This key must be | ||||||
| # the key that signed the :SSLCertificate | ||||||
| # A private key instance, like OpenSSL::PKey::RSA for RSA | ||||||
| # and OpenSSL::PKey::PKey for ML-DSA-44, ML-DSA-65, | ||||||
| # ML-DSA-87. This key must be the key that signed the | ||||||
| # :SSLCertificate | ||||||
| # | ||||||
| # :SSLPrivateKeyAlgorithms :: | ||||||
| # An Array of private key algorithms used to generate | ||||||
| # certificates when :SSLCertificates, :SSLCertificate and | ||||||
| # :SSLPrivateKey are not provided. Supported algorithms are | ||||||
| # RSA, ML-DSA-44, ML-DSA-65, and ML-DSA-87. | ||||||
| # Defaults to ["RSA"] | ||||||
| # | ||||||
| # :SSLClientCA :: | ||||||
| # An OpenSSL::X509::Certificate, or Array of certificates that will | ||||||
|
|
@@ -70,6 +92,15 @@ class SSLConfig | |||||
| # :SSLCACertificateFile :: | ||||||
| # A path to a CA certificate file, in PEM format. | ||||||
| # | ||||||
| # :SSLSignatureAlgorithms :: | ||||||
| # Signature algorithms. See OpenSSL::SSL::SSLContext#sigalgs= | ||||||
| # | ||||||
| # :SSLClientSignatureAlgorithms :: | ||||||
| # Client signature algorithms. See OpenSSL::SSL::SSLContext#client_sigalgs= | ||||||
| # | ||||||
| # :SSLGroups :: | ||||||
| # Key exchange groups. See OpenSSL::SSL::SSLContext#groups= | ||||||
| # | ||||||
| # :SSLTmpDhCallback :: | ||||||
| # A DH callback. See OpenSSL::SSL::SSLContext.tmp_dh_callback | ||||||
| # | ||||||
|
|
@@ -133,10 +164,14 @@ class SSLConfig | |||||
| # c.setup_certificate | ||||||
| # | ||||||
| def initialize(config) | ||||||
| @config = config | ||||||
| @cert = config[:SSLCertificate] | ||||||
| @pkey = config[:SSLPrivateKey] | ||||||
| @ssl_ctx = nil | ||||||
| @config = config | ||||||
| @certs = if config.key?(:SSLCertificates) | ||||||
| config[:SSLCertificates] | ||||||
| elsif config[:SSLCertificate] && config[:SSLPrivateKey] | ||||||
| [[config[:SSLCertificate], config[:SSLPrivateKey]]] | ||||||
| end | ||||||
| @pkey_algs = self[:SSLPrivateKeyAlgorithms] | ||||||
| @ssl_ctx = nil | ||||||
| end | ||||||
|
|
||||||
| # A convenience method to access the values like a Hash | ||||||
|
|
@@ -162,25 +197,81 @@ def accept(tcp) | |||||
| ssl | ||||||
| end | ||||||
|
|
||||||
| # Ensures that :SSLCertificate and :SSLPrivateKey have been provided | ||||||
| # or that a new certificate is generated with the other parameters | ||||||
| # provided. | ||||||
| # Ensures that :SSLCertificates or :SSLCertificate and | ||||||
| # :SSLPrivateKey have been provided, or that new certificates | ||||||
| # are generated with the other parameters provided. | ||||||
| def setup_certificate | ||||||
| if @cert && @pkey | ||||||
| if @certs | ||||||
| return | ||||||
| end | ||||||
|
|
||||||
| rsa = OpenSSL::PKey::RSA.new(2048) | ||||||
| @certs = @pkey_algs.map { |pkey_alg| setup_certificate_one(pkey_alg) } | ||||||
| end | ||||||
|
|
||||||
| # Establish the OpenSSL::SSL::SSLContext with the configuration | ||||||
| # parameters provided. | ||||||
| def setup_ssl_context | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you want to make
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kou I am waiting for your response on this topic.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, sorry. |
||||||
| ctx = ::OpenSSL::SSL::SSLContext.new | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| @certs&.each do |cert, pkey| | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need |
||||||
| ctx.add_certificate(cert, pkey) | ||||||
| end | ||||||
| ctx.min_version = self[:SSLMinVersion] | ||||||
| ctx.max_version = self[:SSLMaxVersion] | ||||||
| ctx.client_ca = self[:SSLClientCA] | ||||||
| ctx.ca_path = self[:SSLCACertificatePath] | ||||||
| ctx.ca_file = self[:SSLCACertificateFile] | ||||||
| if self[:SSLSignatureAlgorithms] | ||||||
| ctx.sigalgs = self[:SSLSignatureAlgorithms] | ||||||
| end | ||||||
| if self[:SSLClientSignatureAlgorithms] | ||||||
| ctx.client_sigalgs = self[:SSLClientSignatureAlgorithms] | ||||||
| end | ||||||
| if self[:SSLGroups] | ||||||
| ctx.groups = self[:SSLGroups] | ||||||
| end | ||||||
| ctx.tmp_dh_callback = self[:SSLTmpDhCallback] | ||||||
| ctx.verify_mode = self[:SSLVerifyMode] | ||||||
| ctx.verify_depth = self[:SSLVerifyDepth] | ||||||
| ctx.verify_callback = self[:SSLVerifyCallback] | ||||||
| ctx.cert_store = self[:SSLCertificateStore] | ||||||
| @ssl_ctx = ctx | ||||||
| end | ||||||
|
|
||||||
| private | ||||||
|
|
||||||
| def setup_certificate_one(pkey_alg) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you rename this to |
||||||
| cert = OpenSSL::X509::Certificate.new | ||||||
|
|
||||||
| case pkey_alg | ||||||
| when "RSA" | ||||||
| pkey = OpenSSL::PKey::RSA.new(2048) | ||||||
| cert.public_key = pkey | ||||||
| digest = "SHA256" | ||||||
| when "ML-DSA-44", "ML-DSA-65", "ML-DSA-87" | ||||||
| # OpenSSL >= 3.5.0 support ML-DSA. OpenSSL::PKey.generate_key raises | ||||||
| # OpenSSL::PKey::PKeyError if ML-DSA algorithm is not supported. | ||||||
| # https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ | ||||||
| pkey = OpenSSL::PKey.generate_key(pkey_alg) | ||||||
| # OpenSSL::PKey::PKey#public_to_der was added in openssl gem 2.2.0. | ||||||
| # https://github.com/ruby/openssl/blob/v2.2.0/History.md?plain=1#L72-L75 | ||||||
| cert.public_key = OpenSSL::PKey.read(pkey.public_to_der) | ||||||
| # OpenSSL::X509::Certificate#sign doesn't accept explicit digest | ||||||
| # algorithm, in ML-DSA because ML-DSA has a built-in digest. | ||||||
| digest = nil | ||||||
| else | ||||||
| raise(ArgumentError, | ||||||
| "#{pkey_alg} algorithm not found. "\ | ||||||
| "RSA, ML-DSA-44, ML-DSA-65, and ML-DSA-87 "\ | ||||||
| "algorithms are supported") | ||||||
| end | ||||||
|
|
||||||
| cert.version = 2 # This means v3 | ||||||
| cert.serial = 0 | ||||||
| name = OpenSSL::X509::Name.new(self[:SSLCertName]) | ||||||
| cert.subject = name | ||||||
| cert.issuer = name | ||||||
| cert.not_before = Time.now | ||||||
| cert.not_after = Time.now + (365*24*60*60) | ||||||
| cert.public_key = rsa.public_key | ||||||
|
|
||||||
| ef = OpenSSL::X509::ExtensionFactory.new(nil,cert) | ||||||
| cert.extensions = [ | ||||||
|
|
@@ -192,29 +283,9 @@ def setup_certificate | |||||
| if comment = self[:SSLCertComment] | ||||||
| cert.add_extension(ef.create_extension("nsComment", comment)) | ||||||
| end | ||||||
| cert.sign(rsa, "SHA256") | ||||||
|
|
||||||
| @cert = cert | ||||||
| @pkey = rsa | ||||||
| end | ||||||
| cert.sign(pkey, digest) | ||||||
|
|
||||||
| # Establish the OpenSSL::SSL::SSLContext with the configuration | ||||||
| # parameters provided. | ||||||
| def setup_ssl_context | ||||||
| ctx = ::OpenSSL::SSL::SSLContext.new | ||||||
| ctx.cert = @cert | ||||||
| ctx.key = @pkey | ||||||
| ctx.min_version = self[:SSLMinVersion] | ||||||
| ctx.max_version = self[:SSLMaxVersion] | ||||||
| ctx.client_ca = self[:SSLClientCA] | ||||||
| ctx.ca_path = self[:SSLCACertificatePath] | ||||||
| ctx.ca_file = self[:SSLCACertificateFile] | ||||||
| ctx.tmp_dh_callback = self[:SSLTmpDhCallback] | ||||||
| ctx.verify_mode = self[:SSLVerifyMode] | ||||||
| ctx.verify_depth = self[:SSLVerifyDepth] | ||||||
| ctx.verify_callback = self[:SSLVerifyCallback] | ||||||
| ctx.cert_store = self[:SSLCertificateStore] | ||||||
| @ssl_ctx = ctx | ||||||
| [cert, pkey] | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |||||||
| require 'drb/drb' | ||||||||
| require 'drb/extservm' | ||||||||
| require 'timeout' | ||||||||
| require_relative 'drbtest_utils' | ||||||||
|
|
||||||||
| module DRbTests | ||||||||
|
|
||||||||
|
|
@@ -393,4 +394,75 @@ def test_07_break_18 | |||||||
|
|
||||||||
| end | ||||||||
|
|
||||||||
| # A PQC support module | ||||||||
| # Inspired by ruby/rubygems omit_unless_support_pqc | ||||||||
| module DRbPQC | ||||||||
| # PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5. | ||||||||
| # https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ | ||||||||
| # Ruby OpenSSL >= 4.0 has useful methods in PQC use cases. | ||||||||
| # https://github.com/ruby/openssl/blob/v4.0.0/History.md?plain=1#L25-L35 | ||||||||
| # And fixed the following bug related to PQC. | ||||||||
| # https://github.com/ruby/openssl/pull/898 | ||||||||
| # However, we don't check OpenSSL and Ruby OpenSSL versions here | ||||||||
| # for a flexible check for other SSL libraries such as LibreSSL and AWS-LC. | ||||||||
| def omit_unless_support_pqc | ||||||||
| # Even with a new enough OpenSSL, the runtime may keep PQC groups and | ||||||||
| # signature algorithms out of its default negotiation lists (for example | ||||||||
| # RHEL's system-wide crypto policies). The PQC server forces both, while | ||||||||
| # the gem fetcher connects with the default client configuration, so a | ||||||||
| # real loopback handshake is the only reliable way to tell whether this | ||||||||
| # environment can negotiate PQC at all. | ||||||||
| unless support_pqc_handshake? | ||||||||
| omit 'OpenSSL or Ruby OpenSSL is too old to support PQC, '\ | ||||||||
| 'or PQC handshake is not available in this OpenSSL configuration' | ||||||||
| end | ||||||||
| end | ||||||||
|
|
||||||||
| # Probe an actual PQC handshake between a forced-PQC server and a | ||||||||
| # default-configured client, mirroring what the integration tests exercise. | ||||||||
| # Memoized so the probe runs at most once per process. | ||||||||
| def support_pqc_handshake? | ||||||||
| return @support_pqc_handshake unless @support_pqc_handshake.nil? | ||||||||
|
|
||||||||
| @support_pqc_handshake = probe_pqc_handshake | ||||||||
| end | ||||||||
|
|
||||||||
| def probe_pqc_handshake | ||||||||
| server = TCPServer.new('127.0.0.1', 0) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you use |
||||||||
| ctx = OpenSSL::SSL::SSLContext.new | ||||||||
| cert = Fixtures.read_cert('mldsa65_server.crt') | ||||||||
| pkey = Fixtures.read_pkey('mldsa65_server.key') | ||||||||
| ctx.add_certificate(cert, pkey) | ||||||||
|
|
||||||||
| # ctx.groups (OpenSSL::SSL::SSLContext#groups) requires Ruby OpenSSL >= 4.0. | ||||||||
| return nil unless ctx.respond_to?(:groups=) | ||||||||
|
|
||||||||
| ctx.groups = 'X25519MLKEM768' | ||||||||
| ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) | ||||||||
|
|
||||||||
| port = server.addr[1] | ||||||||
| server_thread = Thread.new do | ||||||||
| client = ssl_server.accept | ||||||||
| client.close | ||||||||
| rescue OpenSSL::OpenSSLError | ||||||||
| nil | ||||||||
| end | ||||||||
|
|
||||||||
| client_ctx = OpenSSL::SSL::SSLContext.new | ||||||||
| client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE | ||||||||
| socket = TCPSocket.new('127.0.0.1', port) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you use |
||||||||
| ssl = OpenSSL::SSL::SSLSocket.new(socket, client_ctx) | ||||||||
| ssl.connect | ||||||||
| ssl.close | ||||||||
| true | ||||||||
| rescue OpenSSL::PKey::PKeyError, OpenSSL::OpenSSLError, SystemCallError | ||||||||
| false | ||||||||
| ensure | ||||||||
| server_thread&.join(5) | ||||||||
| server_thread&.kill if server_thread&.alive? | ||||||||
|
Comment on lines
+461
to
+462
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can simplify this.
Suggested change
|
||||||||
| ssl_server&.close | ||||||||
| server&.close | ||||||||
| end | ||||||||
| end | ||||||||
|
|
||||||||
| end | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about renaming this to |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # frozen_string_literal: false | ||
|
|
||
| # This file does not require test/unit unlike drbtest.rb because it can | ||
| # be loaded by child processes (ut_*.rb) where | ||
| # test-unit's at_exit autorunner would cause errors. | ||
|
|
||
| require 'openssl' | ||
|
|
||
| module DRbTests | ||
|
|
||
| module Fixtures | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you indent this? |
||
| module_function | ||
|
|
||
| def file_path(name) | ||
| File.join(__dir__, 'fixtures', name) | ||
| end | ||
|
|
||
| def read_file(name) | ||
| @file_cache ||= {} | ||
| @file_cache[name] ||= File.read(file_path(name)) | ||
| end | ||
|
|
||
| # Raises OpenSSL::PKey::PKeyError when reading an ML-DSA-65 key file | ||
| # with old OpenSSL versions. | ||
| def read_pkey(name) | ||
| OpenSSL::PKey.read(read_file(name)) | ||
| end | ||
|
|
||
| def read_cert(name) | ||
| OpenSSL::X509::Certificate.new(read_file(name)) | ||
| end | ||
| end | ||
|
|
||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you revert this?