-
Notifications
You must be signed in to change notification settings - Fork 27
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,15 @@ class SSLConfig | |||||
| # c.setup_certificate | ||||||
| # | ||||||
| def initialize(config) | ||||||
| @config = config | ||||||
| @cert = config[:SSLCertificate] | ||||||
| @pkey = config[:SSLPrivateKey] | ||||||
| @ssl_ctx = nil | ||||||
| @config = config | ||||||
| @certs = nil | ||||||
| if config[:SSLCertificate] && config[:SSLPrivateKey] | ||||||
| @certs = [[config[:SSLCertificate], config[:SSLPrivateKey]]] | ||||||
| end | ||||||
| @certs = config[:SSLCertificates] if config.key?(:SSLCertificates) | ||||||
| @pkey_algs = config[:SSLPrivateKeyAlgorithms] || | ||||||
| self[:SSLPrivateKeyAlgorithms] | ||||||
| @ssl_ctx = nil | ||||||
| end | ||||||
|
|
||||||
| # A convenience method to access the values like a Hash | ||||||
|
|
@@ -162,25 +198,121 @@ 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 | ||||||
| ctx = ::OpenSSL::SSL::SSLContext.new | ||||||
| @certs&.each do |cert, pkey| | ||||||
| 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] | ||||||
| # OpenSSL::SSL::SSLContext#sigalgs=, #client_sigalgs=, #groups= were | ||||||
| # added in openssl gem 4.0.0. | ||||||
| # https://github.com/ruby/openssl/blob/v4.0.0/History.md?plain=1#L25-L31 | ||||||
| # OpenSSL::SSL::SSLContext#sigalgs= is supported in OpenSSL >= 1.0.2. | ||||||
| # https://github.com/ruby/openssl/blob/v4.0.0/ext/openssl/extconf.rb#L142-L143 | ||||||
| unless ctx.respond_to?(:sigalgs=) | ||||||
| raise(DRbBadConfig, | ||||||
| unsupported_message( | ||||||
| ':SSLSignatureAlgorithms', '4.0.0', '1.0.2' | ||||||
| )) | ||||||
| end | ||||||
| ctx.sigalgs = self[:SSLSignatureAlgorithms] | ||||||
| end | ||||||
|
|
||||||
| if self[:SSLClientSignatureAlgorithms] | ||||||
| # OpenSSL::SSL::SSLContext#client_sigalgs= is supported in | ||||||
| # OpenSSL >= 1.0.2. | ||||||
| # https://github.com/ruby/openssl/blob/v4.0.0/ext/openssl/extconf.rb#L144-L145 | ||||||
| unless ctx.respond_to?(:client_sigalgs=) | ||||||
| raise(DRbBadConfig, | ||||||
| unsupported_message( | ||||||
| ':SSLClientSignatureAlgorithms', '4.0.0', '1.0.2' | ||||||
| )) | ||||||
| end | ||||||
| ctx.client_sigalgs = self[:SSLClientSignatureAlgorithms] | ||||||
| end | ||||||
|
|
||||||
| if self[:SSLGroups] | ||||||
| # OpenSSL::SSL::SSLContext#groups is supported in all OpenSSL | ||||||
| # versions. | ||||||
| # https://github.com/ruby/openssl/blob/v4.0.0/ext/openssl/ossl_ssl.c#L3031 | ||||||
| unless ctx.respond_to?(:groups=) | ||||||
| raise(DRbBadConfig, | ||||||
| unsupported_message( | ||||||
| ':SSLGroups', '4.0.0' | ||||||
| )) | ||||||
| end | ||||||
| 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) | ||||||
| cert = OpenSSL::X509::Certificate.new | ||||||
|
|
||||||
| case pkey_alg | ||||||
| when "RSA" | ||||||
| pkey = OpenSSL::PKey::RSA.new(2048) | ||||||
| cert.public_key = pkey.public_key | ||||||
|
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
I recognize this was taken from the original code, but the |
||||||
| digest = "SHA256" | ||||||
| when "ML-DSA-44", "ML-DSA-65", "ML-DSA-87" | ||||||
| # openssl gem >= 4.0.0 and OpenSSL >= 3.5.0 support ML-DSA. | ||||||
|
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. This should work with all ruby/openssl versions as long as it's linked with OpenSSL >= 3.5. Perhaps it's best to pass through the |
||||||
| # https://github.com/ruby/openssl/blob/v4.0.0/History.md#notable-changes | ||||||
| # https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ | ||||||
| begin | ||||||
| pkey = OpenSSL::PKey.generate_key(pkey_alg) | ||||||
| rescue OpenSSL::PKey::PKeyError | ||||||
| raise(DRbBadConfig, | ||||||
| unsupported_message( | ||||||
| "#{pkey_alg} algorithm", '4.0.0', '3.5.0' | ||||||
| )) | ||||||
| end | ||||||
| # 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(DRbBadConfig, | ||||||
| "#{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 +324,19 @@ def setup_certificate | |||||
| if comment = self[:SSLCertComment] | ||||||
| cert.add_extension(ef.create_extension("nsComment", comment)) | ||||||
| end | ||||||
| cert.sign(rsa, "SHA256") | ||||||
| cert.sign(pkey, digest) | ||||||
|
|
||||||
| @cert = cert | ||||||
| @pkey = rsa | ||||||
| [cert, pkey] | ||||||
| end | ||||||
|
|
||||||
| # 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 | ||||||
| def unsupported_message(name, ruby_openssl_version, | ||||||
| openssl_version=nil) | ||||||
| message = "#{name} not supported. "\ | ||||||
| "Install openssl gem >= #{ruby_openssl_version}" | ||||||
| if openssl_version | ||||||
| message += " building with OpenSSL >= #{openssl_version}" | ||||||
| end | ||||||
| message | ||||||
| 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.
OpenSSL 1.0.2 is the minimum version supported by ruby/openssl bundled with non-EOL Ruby versions. I think you can forget about even older versions.