From 7635e364a49f3939f3cef4b036406cfdc7e58ea4 Mon Sep 17 00:00:00 2001 From: Mario Visic Date: Wed, 5 Nov 2025 14:55:52 +1100 Subject: [PATCH 1/2] Support local certificates and HTTP proxies - Allow specifying the local certificate to check and the CA bundle rather than looking up the certificates via URL. - Support providing a proxy host and port --- lib/ssl-test.rb | 49 ++++++++++++++++++++++++++++++-------------- lib/ssl-test/crl.rb | 6 +++--- lib/ssl-test/ocsp.rb | 6 +++--- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/lib/ssl-test.rb b/lib/ssl-test.rb index 41fcd18..a70a3c2 100644 --- a/lib/ssl-test.rb +++ b/lib/ssl-test.rb @@ -10,30 +10,49 @@ module SSLTest extend OCSP extend CRL - VERSION = -"1.4.1" + VERSION = -"1.4.1f" class << self - def test url, open_timeout: 5, read_timeout: 5, redirection_limit: 5 - uri = URI.parse(url) - return if uri.scheme != 'https' and uri.scheme != 'tcps' - cert = failed_cert_reason = chain = nil - - @logger&.info { "SSLTest #{url} started" } - http = Net::HTTP.new(uri.host, uri.port) - http.open_timeout = open_timeout - http.read_timeout = read_timeout - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.verify_callback = -> (verify_ok, store_context) { + def test url, open_timeout: 5, read_timeout: 5, proxy_host:nil, proxy_port: nil, redirection_limit: 5, client_cert: nil, ca_bundle_path: nil + + cert = failed_cert_reason = chain = store = nil + + if url.nil? && (client_cert.nil? || ca_bundle_path.nil?) + raise ArgumentError, "A url must be provided as the first argument, OR ... client_cert: AND ca_bundle_path: must both be specified" + end + + certificate_verify_callback = -> (verify_ok, store_context) { cert = store_context.current_cert chain = store_context.chain failed_cert_reason = [store_context.error, store_context.error_string] if store_context.error != 0 verify_ok } + if !client_cert.nil? && !ca_bundle_path.nil? + store = OpenSSL::X509::Store.new + store.add_file(ca_bundle_path) + store.verify_callback = certificate_verify_callback + else + uri = URI.parse(url) + return if uri.scheme != 'https' and uri.scheme != 'tcps' + + @logger&.info { "SSLTest #{url} started" } + http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port) + http.open_timeout = open_timeout + http.read_timeout = read_timeout + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.verify_callback = certificate_verify_callback + end + begin - http.start { } - revoked, message, revocation_date = test_chain_revocation(chain, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit) + if store.nil? + http.start { } + else + store.verify(client_cert) + end + + revoked, message, revocation_date = test_chain_revocation(chain, open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit) @logger&.info { "SSLTest #{url} finished: revoked=#{revoked} #{message}" } return [false, "SSL certificate revoked: #{message} (revocation date: #{revocation_date})", cert] if revoked return [true, "Revocation test couldn't be performed: #{message}", cert] if message diff --git a/lib/ssl-test/crl.rb b/lib/ssl-test/crl.rb index 3c0da70..35e1036 100644 --- a/lib/ssl-test/crl.rb +++ b/lib/ssl-test/crl.rb @@ -51,7 +51,7 @@ def test_crl_revocation cert, issuer:, chain:, **options end # Returns an array with [response, error_message] - def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limit: 5) + def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limit: 5, proxy_host: nil, proxy_port: nil) return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0 # Return file from cache if not expired @@ -61,7 +61,7 @@ def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limi @logger&.debug { "SSLTest + CRL: fetch URI #{uri}" } path = uri.path == "" ? "/" : uri.path - http = Net::HTTP.new(uri.hostname, uri.port) + http = Net::HTTP.new(uri.hostname, uri.port, proxy_host, proxy_port) http.open_timeout = open_timeout http.read_timeout = read_timeout @@ -92,7 +92,7 @@ def follow_crl_redirects(uri, open_timeout: 5, read_timeout: 5, redirection_limi } [http_response.body, nil] when Net::HTTPRedirection - follow_crl_redirects(URI(http_response["location"]), open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1) + follow_crl_redirects(URI(http_response["location"]), open_timeout: open_timeout, read_timeout: read_timeout, proxy_host: proxy_host, proxy_port: proxy_port, redirection_limit: redirection_limit - 1) else @logger&.debug { "SSLTest + CRL: Error: #{http_response.class}" } [nil, "Wrong response type (#{http_response.class})"] diff --git a/lib/ssl-test/ocsp.rb b/lib/ssl-test/ocsp.rb index 41b1cc6..607295d 100644 --- a/lib/ssl-test/ocsp.rb +++ b/lib/ssl-test/ocsp.rb @@ -67,12 +67,12 @@ def test_ocsp_revocation cert, issuer:, chain:, **options end # Returns an array with [response, error_message] - def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirection_limit: 5) + def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirection_limit: 5, proxy_host: nil, proxy_port: nil) return [nil, "Too many redirections (> #{redirection_limit})"] if redirection_limit == 0 @logger&.debug { "SSLTest + OCSP: fetch URI #{uri}" } path = uri.path == "" ? "/" : uri.path - http = Net::HTTP.new(uri.hostname, uri.port) + http = Net::HTTP.new(uri.hostname, uri.port, proxy_host, proxy_port) http.open_timeout = open_timeout http.read_timeout = read_timeout @@ -82,7 +82,7 @@ def follow_ocsp_redirects(uri, data, open_timeout: 5, read_timeout: 5, redirecti @logger&.debug { "SSLTest + OCSP: 200 OK (#{http_response.body.bytesize} bytes)" } [http_response.body, nil] when Net::HTTPRedirection - follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, redirection_limit: redirection_limit - 1) + follow_ocsp_redirects(URI(http_response["location"]), data, open_timeout: open_timeout, read_timeout: read_timeout, rproxy_host: proxy_host, proxy_port: proxy_port, edirection_limit: redirection_limit - 1) else @logger&.debug { "SSLTest + OCSP: Error: #{http_response.class}" } [nil, "Wrong response type (#{http_response.class})"] From 1fbdcb6ee907b6799a7c98ad20dfe9b07afed6d6 Mon Sep 17 00:00:00 2001 From: Mario Visic Date: Thu, 6 Nov 2025 10:00:57 +1100 Subject: [PATCH 2/2] Accept an array of CA certs instead of a file path for added flexibility --- lib/ssl-test.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/ssl-test.rb b/lib/ssl-test.rb index a70a3c2..56905a8 100644 --- a/lib/ssl-test.rb +++ b/lib/ssl-test.rb @@ -13,12 +13,11 @@ module SSLTest VERSION = -"1.4.1f" class << self - def test url, open_timeout: 5, read_timeout: 5, proxy_host:nil, proxy_port: nil, redirection_limit: 5, client_cert: nil, ca_bundle_path: nil - + def test url, open_timeout: 5, read_timeout: 5, proxy_host:nil, proxy_port: nil, redirection_limit: 5, client_cert: nil, ca_certs: [] cert = failed_cert_reason = chain = store = nil - if url.nil? && (client_cert.nil? || ca_bundle_path.nil?) - raise ArgumentError, "A url must be provided as the first argument, OR ... client_cert: AND ca_bundle_path: must both be specified" + if url.nil? && (client_cert.nil? || ca_certs.empty?) + raise ArgumentError, "A url must be provided as the first argument, OR ... client_cert: AND ca_certs: must both be specified" end certificate_verify_callback = -> (verify_ok, store_context) { @@ -28,9 +27,9 @@ def test url, open_timeout: 5, read_timeout: 5, proxy_host:nil, proxy_port: nil, verify_ok } - if !client_cert.nil? && !ca_bundle_path.nil? + if !client_cert.nil? && !ca_certs.empty? store = OpenSSL::X509::Store.new - store.add_file(ca_bundle_path) + ca_certs.each { store.add_cert(_1) } store.verify_callback = certificate_verify_callback else uri = URI.parse(url)