Handle CRL lookup failures during federation TLS (#299)

This commit is contained in:
l5y
2025-10-12 09:56:53 +02:00
committed by GitHub
parent 4329605e6f
commit ee904633a8
2 changed files with 86 additions and 1 deletions
@@ -299,6 +299,8 @@ module PotatoMesh
http.min_version = :TLS1_2 if http.respond_to?(:min_version=)
store = remote_instance_cert_store
http.cert_store = store if store
callback = remote_instance_verify_callback
http.verify_callback = callback if callback
http
end
@@ -324,6 +326,56 @@ module PotatoMesh
@remote_instance_cert_store = nil
end
# Build a TLS verification callback that tolerates CRL availability failures.
#
# Some certificate authorities publish CRL endpoints that may occasionally be
# unreachable. When OpenSSL cannot download the CRL it raises the
# V_ERR_UNABLE_TO_GET_CRL error which would otherwise cause HTTPS federation
# announcements to abort. The generated callback accepts those specific
# failures while preserving strict verification for all other errors.
#
# @return [Proc, nil] verification callback or nil when creation fails.
def remote_instance_verify_callback
if defined?(@remote_instance_verify_callback) && @remote_instance_verify_callback
return @remote_instance_verify_callback
end
callback = lambda do |preverify_ok, store_context|
return true if preverify_ok
if store_context && crl_unavailable_error?(store_context.error)
debug_log(
"Ignoring TLS CRL retrieval failure during federation request",
context: "federation.announce",
)
true
else
false
end
end
@remote_instance_verify_callback = callback
rescue StandardError => e
debug_log(
"Failed to initialize federation TLS verify callback: #{e.message}",
context: "federation.announce",
)
@remote_instance_verify_callback = nil
end
# Determine whether the supplied OpenSSL verification error corresponds to a
# missing certificate revocation list.
#
# @param error_code [Integer, nil] OpenSSL verification error value.
# @return [Boolean] true when the error should be ignored.
def crl_unavailable_error?(error_code)
allowed_errors = [OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL]
if defined?(OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL_ISSUER)
allowed_errors << OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL_ISSUER
end
allowed_errors.include?(error_code)
end
def validate_well_known_document(document, domain, pubkey)
unless document.is_a?(Hash)
return [false, "document is not an object"]
+34 -1
View File
@@ -27,7 +27,7 @@ RSpec.describe PotatoMesh::App::Federation do
@debug_messages ||= []
end
def debug_log(message)
def debug_log(message, **_metadata)
debug_messages << message
end
@@ -40,6 +40,7 @@ RSpec.describe PotatoMesh::App::Federation do
before do
federation_helpers.instance_variable_set(:@remote_instance_cert_store, nil)
federation_helpers.instance_variable_set(:@remote_instance_verify_callback, nil)
federation_helpers.reset_debug_messages
end
@@ -84,6 +85,33 @@ RSpec.describe PotatoMesh::App::Federation do
end
end
describe ".remote_instance_verify_callback" do
let(:callback) { federation_helpers.remote_instance_verify_callback }
it "memoizes the generated callback" do
first = federation_helpers.remote_instance_verify_callback
second = federation_helpers.remote_instance_verify_callback
expect(second).to equal(first)
end
it "allows the handshake to continue when CRLs are unavailable" do
store_context = instance_double(OpenSSL::X509::StoreContext, error: OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL)
expect(callback.call(false, store_context)).to be(true)
expect(federation_helpers.debug_messages.last).to include("Ignoring TLS CRL retrieval failure")
end
it "rejects other verification failures" do
store_context = instance_double(OpenSSL::X509::StoreContext, error: OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED)
expect(callback.call(false, store_context)).to be(false)
end
it "falls back to the default behavior when the handshake is already valid" do
expect(callback.call(true, nil)).to be(true)
end
end
describe ".build_remote_http_client" do
let(:timeout) { 15 }
@@ -95,6 +123,8 @@ RSpec.describe PotatoMesh::App::Federation do
uri = URI.parse("https://remote.example.com/api")
store = OpenSSL::X509::Store.new
allow(federation_helpers).to receive(:remote_instance_cert_store).and_return(store)
callback = proc { true }
allow(federation_helpers).to receive(:remote_instance_verify_callback).and_return(callback)
http = federation_helpers.build_remote_http_client(uri)
@@ -103,6 +133,7 @@ RSpec.describe PotatoMesh::App::Federation do
expect(http.read_timeout).to eq(timeout)
expect(http.cert_store).to eq(store)
expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
expect(http.verify_callback).to eq(callback)
if http.respond_to?(:min_version)
expect(http.min_version).to eq(:TLS1_2)
end
@@ -122,10 +153,12 @@ RSpec.describe PotatoMesh::App::Federation do
it "leaves the certificate store unset when unavailable" do
uri = URI.parse("https://remote.example.com/api")
allow(federation_helpers).to receive(:remote_instance_cert_store).and_return(nil)
allow(federation_helpers).to receive(:remote_instance_verify_callback).and_return(nil)
http = federation_helpers.build_remote_http_client(uri)
expect(http.cert_store).to be_nil
expect(http.verify_callback).to be_nil
end
end
end