From 302039e43783865440cac504055710d54a5a9fc3 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 22 Oct 2026 15:31:30 +0400 Subject: [PATCH] fix(http): keep admin CAs, secure HTTPS proxies, make insecure explicit - An admin-set cacerts no longer gets the CAStore cacertfile added; Hackney drops cacerts when both are set. - Set proxy_ssl_options too: with an HTTPS proxy, Hackney 4 uses them for the proxy connection and falls back to certifi. - Hackney 4 ignores the top-level insecure option and uses certifi. Turn it into an explicit verify_none. --- lib/pleroma/http/tls.ex | 32 ++++++++++++------- .../adapter_helper/tesla_adapter_test.exs | 5 +-- test/pleroma/http/tls_test.exs | 21 ++++++++++-- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/http/tls.ex b/lib/pleroma/http/tls.ex index e1fbc7caa..bb90f8e4e 100644 --- a/lib/pleroma/http/tls.ex +++ b/lib/pleroma/http/tls.ex @@ -15,16 +15,21 @@ defmodule Pleroma.HTTP.TLS do @spec ssl_options(keyword()) :: keyword() def ssl_options(overrides \\ []) do - Keyword.merge( - [ - verify: :verify_peer, - cacertfile: CAStore.file_path(), - depth: 20, - customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)], - partial_chain: &__MODULE__.reject_partial_chain/1 - ], - overrides - ) + defaults = [ + verify: :verify_peer, + cacertfile: CAStore.file_path(), + depth: 20, + customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)], + partial_chain: &__MODULE__.reject_partial_chain/1 + ] + + # Hackney drops cacerts when cacertfile is set too. + defaults = + if Keyword.has_key?(overrides, :cacerts), + do: Keyword.delete(defaults, :cacertfile), + else: defaults + + Keyword.merge(defaults, overrides) end @doc false @@ -32,10 +37,13 @@ defmodule Pleroma.HTTP.TLS do @spec put_hackney_ssl_options(keyword()) :: keyword() def put_hackney_ssl_options(opts) do + # Hackney 4 ignores the top-level insecure option and falls back to certifi. if opts[:insecure] do - opts + Keyword.put(opts, :ssl_options, verify: :verify_none) else - Keyword.put(opts, :ssl_options, ssl_options(Keyword.get(opts, :ssl_options, []))) + opts + |> Keyword.put(:ssl_options, ssl_options(Keyword.get(opts, :ssl_options, []))) + |> Keyword.put(:proxy_ssl_options, ssl_options(Keyword.get(opts, :proxy_ssl_options, []))) end end end diff --git a/test/pleroma/http/adapter_helper/tesla_adapter_test.exs b/test/pleroma/http/adapter_helper/tesla_adapter_test.exs index 1ee2f13ba..96b68d352 100644 --- a/test/pleroma/http/adapter_helper/tesla_adapter_test.exs +++ b/test/pleroma/http/adapter_helper/tesla_adapter_test.exs @@ -16,8 +16,9 @@ defmodule Pleroma.HTTP.AdapterHelper.TeslaAdapterTest do test "adds the TLS options to Hackney" do Application.put_env(:tesla, :adapter, Tesla.Adapter.Hackney) - assert AdapterHelper.tesla_adapter() == - {Tesla.Adapter.Hackney, ssl_options: TLS.ssl_options()} + assert {Tesla.Adapter.Hackney, opts} = AdapterHelper.tesla_adapter() + assert opts[:ssl_options] == TLS.ssl_options() + assert opts[:proxy_ssl_options] == TLS.ssl_options() end test "turns on certificate checks for Gun" do diff --git a/test/pleroma/http/tls_test.exs b/test/pleroma/http/tls_test.exs index 490950b50..a8701038c 100644 --- a/test/pleroma/http/tls_test.exs +++ b/test/pleroma/http/tls_test.exs @@ -23,6 +23,13 @@ defmodule Pleroma.HTTP.TLSTest do assert partial_chain.([:certificate]) == :unknown_ca end + test "keeps CA certificates that the admin set" do + opts = TLS.ssl_options(cacerts: [:private_ca]) + + assert opts[:cacerts] == [:private_ca] + refute Keyword.has_key?(opts, :cacertfile) + end + test "keeps the CA bundle when other options are given" do opts = TLS.ssl_options(versions: [:"tlsv1.2"]) @@ -38,6 +45,7 @@ defmodule Pleroma.HTTP.TLSTest do assert opts[:pool] == :media assert opts[:ssl_options] == TLS.ssl_options() + assert opts[:proxy_ssl_options] == TLS.ssl_options() end test "merges given ssl_options into the TLS options" do @@ -46,8 +54,17 @@ defmodule Pleroma.HTTP.TLSTest do assert opts[:ssl_options] == TLS.ssl_options(versions: [:"tlsv1.2"]) end - test "leaves insecure requests alone" do - assert TLS.put_hackney_ssl_options(insecure: true) == [insecure: true] + test "turns insecure into an explicit verify_none" do + opts = TLS.put_hackney_ssl_options(insecure: true) + + assert opts[:ssl_options] == [verify: :verify_none] + refute Keyword.has_key?(opts, :proxy_ssl_options) + end + + test "secures the connection to an HTTPS proxy" do + opts = TLS.put_hackney_ssl_options(proxy_ssl_options: [versions: [:"tlsv1.3"]]) + + assert opts[:proxy_ssl_options] == TLS.ssl_options(versions: [:"tlsv1.3"]) end end end