Handle federation HTTPS CRL verification failures (#293)

This commit is contained in:
l5y
2025-10-12 09:22:54 +02:00
committed by GitHub
parent 80f2bbdb25
commit aba94b197d
2 changed files with 161 additions and 8 deletions
+42 -8
View File
@@ -99,10 +99,7 @@ module PotatoMesh
instance_uri_candidates(domain, "/api/instances").each do |uri|
begin
http = Net::HTTP.new(uri.host, uri.port)
http.open_timeout = PotatoMesh::Config.remote_instance_http_timeout
http.read_timeout = PotatoMesh::Config.remote_instance_http_timeout
http.use_ssl = uri.scheme == "https"
http = build_remote_http_client(uri)
response = http.start do |connection|
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
@@ -215,10 +212,7 @@ module PotatoMesh
end
def perform_instance_http_request(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.open_timeout = PotatoMesh::Config.remote_instance_http_timeout
http.read_timeout = PotatoMesh::Config.remote_instance_http_timeout
http.use_ssl = uri.scheme == "https"
http = build_remote_http_client(uri)
http.start do |connection|
response = connection.request(Net::HTTP::Get.new(uri))
case response
@@ -247,6 +241,46 @@ module PotatoMesh
[nil, errors]
end
# Build an HTTP client configured for communication with a remote instance.
#
# @param uri [URI::Generic] target URI describing the remote endpoint.
# @return [Net::HTTP] HTTP client ready to execute the request.
def build_remote_http_client(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.open_timeout = PotatoMesh::Config.remote_instance_http_timeout
http.read_timeout = PotatoMesh::Config.remote_instance_http_timeout
http.use_ssl = uri.scheme == "https"
return http unless http.use_ssl?
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.min_version = :TLS1_2 if http.respond_to?(:min_version=)
store = remote_instance_cert_store
http.cert_store = store if store
http
end
# Construct a certificate store that disables strict CRL enforcement.
#
# OpenSSL may fail remote requests when certificate revocation lists are
# unavailable from the issuing authority. The returned store mirrors the
# default system trust store while clearing CRL-related flags so that
# federation announcements gracefully succeed when CRLs cannot be fetched.
#
# @return [OpenSSL::X509::Store, nil] configured store or nil when setup fails.
def remote_instance_cert_store
return @remote_instance_cert_store if defined?(@remote_instance_cert_store) && @remote_instance_cert_store
store = OpenSSL::X509::Store.new
store.set_default_paths
store.flags = 0 if store.respond_to?(:flags=)
@remote_instance_cert_store = store
rescue OpenSSL::X509::StoreError => e
debug_log(
"Failed to initialize certificate store for federation HTTP: #{e.message}",
)
@remote_instance_cert_store = nil
end
def validate_well_known_document(document, domain, pubkey)
unless document.is_a?(Hash)
return [false, "document is not an object"]
+119
View File
@@ -0,0 +1,119 @@
# frozen_string_literal: true
require "spec_helper"
require "net/http"
require "openssl"
require "uri"
RSpec.describe PotatoMesh::App::Federation do
subject(:federation_helpers) do
Class.new do
extend PotatoMesh::App::Federation
class << self
def debug_messages
@debug_messages ||= []
end
def debug_log(message)
debug_messages << message
end
def reset_debug_messages
@debug_messages = []
end
end
end
end
before do
federation_helpers.instance_variable_set(:@remote_instance_cert_store, nil)
federation_helpers.reset_debug_messages
end
describe ".remote_instance_cert_store" do
it "initializes the store with default paths and disables CRL checks" do
store_double = Class.new do
attr_reader :default_paths_called, :assigned_flags
def set_default_paths
@default_paths_called = true
end
def flags=(value)
@assigned_flags = value
end
def respond_to_missing?(method_name, include_private = false)
method_name == :flags= || super
end
end.new
allow(OpenSSL::X509::Store).to receive(:new).and_return(store_double)
result = federation_helpers.remote_instance_cert_store
expect(result).to eq(store_double)
expect(store_double.default_paths_called).to be(true)
expect(store_double.assigned_flags).to eq(0)
end
it "memoizes the generated store" do
first = federation_helpers.remote_instance_cert_store
second = federation_helpers.remote_instance_cert_store
expect(second).to equal(first)
end
it "logs and returns nil when initialization fails" do
allow(OpenSSL::X509::Store).to receive(:new).and_raise(OpenSSL::X509::StoreError, "boom")
expect(federation_helpers.remote_instance_cert_store).to be_nil
expect(federation_helpers.debug_messages.last).to include("Failed to initialize certificate store")
end
end
describe ".build_remote_http_client" do
let(:timeout) { 15 }
before do
allow(PotatoMesh::Config).to receive(:remote_instance_http_timeout).and_return(timeout)
end
it "configures SSL settings for HTTPS endpoints" 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)
http = federation_helpers.build_remote_http_client(uri)
expect(http.use_ssl?).to be(true)
expect(http.open_timeout).to eq(timeout)
expect(http.read_timeout).to eq(timeout)
expect(http.cert_store).to eq(store)
expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
if http.respond_to?(:min_version)
expect(http.min_version).to eq(:TLS1_2)
end
end
it "omits SSL configuration for HTTP endpoints" do
uri = URI.parse("http://remote.example.com/api")
http = federation_helpers.build_remote_http_client(uri)
expect(http.use_ssl?).to be(false)
expect(http.cert_store).to be_nil
expect(http.open_timeout).to eq(timeout)
expect(http.read_timeout).to eq(timeout)
end
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)
http = federation_helpers.build_remote_http_client(uri)
expect(http.cert_store).to be_nil
end
end
end