diff --git a/changelog.d/hackney-castore.security b/changelog.d/hackney-castore.security
new file mode 100644
index 000000000..44cb90ce6
--- /dev/null
+++ b/changelog.d/hackney-castore.security
@@ -0,0 +1 @@
+Hackney and Finch now trust only the CAStore certificate bundle, which rejects the compromised eMudhra intermediate CAs
diff --git a/changelog.d/mailer-tls-options.fix b/changelog.d/mailer-tls-options.fix
new file mode 100644
index 000000000..fe17eea6d
--- /dev/null
+++ b/changelog.d/mailer-tls-options.fix
@@ -0,0 +1 @@
+Fix Swoosh HTTP mail adapters failing with `cacerts: :undefined` on OTP 26+
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 353e91f72..adc054f3b 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -66,6 +66,7 @@ defmodule Pleroma.Application do
if match?({Tesla.Adapter.Finch, _}, adapter) do
Logger.info("Starting Finch")
+ use_castore_for_otp_cacerts()
Finch.start_link(name: MyFinch)
end
@@ -304,6 +305,15 @@ defmodule Pleroma.Application do
end
end
+ # Mint trusts the OTP CA store, which comes from the OS. Make it the CAStore
+ # bundle that the other adapters trust. OTP 24 has no such store and Mint
+ # uses CAStore there anyway.
+ defp use_castore_for_otp_cacerts do
+ if Code.ensure_loaded?(:public_key) and function_exported?(:public_key, :cacerts_load, 1) do
+ :ok = :public_key.cacerts_load(CAStore.file_path())
+ end
+ end
+
defp http_children_hackney do
pools = [:federation, :media]
diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex
index 2a80f8547..fa6fd8020 100644
--- a/lib/pleroma/emails/mailer.ex
+++ b/lib/pleroma/emails/mailer.ex
@@ -36,10 +36,11 @@ defmodule Pleroma.Emails.Mailer do
def deliver(email, config \\ [])
def deliver(email, config) do
- # temporary hackney fix until hackney max_connections bug is fixed
- # https://git.pleroma.social/pleroma/pleroma/-/issues/2101
+ # Swoosh HTTP adapters use Hackney, which otherwise trusts its certifi bundle
email =
- Swoosh.Email.put_private(email, :hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
+ Swoosh.Email.put_private(email, :hackney_options,
+ ssl_options: Pleroma.HTTP.TLS.ssl_options()
+ )
case enabled?() do
true -> Swoosh.Mailer.deliver(email, parse_config(config))
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
index 314f9e8b1..32a186e8f 100644
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -30,6 +30,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
|> Keyword.put(:follow_redirect, false)
|> Keyword.put(:force_redirect, false)
+ |> Pleroma.HTTP.TLS.put_hackney_ssl_options()
end
defp add_scheme_opts(opts, _), do: opts
diff --git a/lib/pleroma/http/tls.ex b/lib/pleroma/http/tls.ex
new file mode 100644
index 000000000..e1fbc7caa
--- /dev/null
+++ b/lib/pleroma/http/tls.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.TLS do
+ @moduledoc """
+ TLS client options that trust the CAStore bundle.
+
+ Hackney otherwise trusts its own certifi bundle. Hackney 1 drops all of its
+ default TLS options, including certificate checks, as soon as any
+ `ssl_options` are given. Hackney 4 merges them, so `partial_chain` must be
+ set too: Hackney's own one trusts any certificate in the peer chain that is
+ in certifi.
+ """
+
+ @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
+ )
+ end
+
+ @doc false
+ def reject_partial_chain(_certs), do: :unknown_ca
+
+ @spec put_hackney_ssl_options(keyword()) :: keyword()
+ def put_hackney_ssl_options(opts) do
+ if opts[:insecure] do
+ opts
+ else
+ Keyword.put(opts, :ssl_options, ssl_options(Keyword.get(opts, :ssl_options, [])))
+ end
+ end
+end
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
index 948787497..b4d2b5e01 100644
--- a/lib/pleroma/reverse_proxy/client/hackney.ex
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -28,9 +28,9 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
def request(method, url, headers, body, opts \\ []) do
opts =
- Keyword.put_new(opts, :path_encode_fun, fn path ->
- path
- end)
+ opts
+ |> Keyword.put_new(:path_encode_fun, fn path -> path end)
+ |> Pleroma.HTTP.TLS.put_hackney_ssl_options()
if opts[:follow_redirect] != false do
{_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end)
diff --git a/test/pleroma/emails/mailer_test.exs b/test/pleroma/emails/mailer_test.exs
index 7a718c653..2c2b5a4af 100644
--- a/test/pleroma/emails/mailer_test.exs
+++ b/test/pleroma/emails/mailer_test.exs
@@ -52,4 +52,13 @@ defmodule Pleroma.Emails.MailerTest do
subject: "Pleroma test email"
)
end
+
+ test "keeps certificate checks for HTTP API adapters" do
+ Mailer.deliver(@email)
+
+ assert_received {:email, %Swoosh.Email{private: %{hackney_options: hackney_options}}}
+
+ assert hackney_options[:ssl_options] ==
+ Pleroma.HTTP.TLS.ssl_options()
+ end
end
diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs
index cf60d994b..42d0aaf6b 100644
--- a/test/pleroma/http/adapter_helper/hackney_test.exs
+++ b/test/pleroma/http/adapter_helper/hackney_test.exs
@@ -46,5 +46,20 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
assert opts[:b] == 1
refute Keyword.has_key?(opts, :proxy)
end
+
+ test "trusts the CAStore bundle", %{uri: uri} do
+ opts = Hackney.options([], uri)
+
+ assert opts[:ssl_options] == Pleroma.HTTP.TLS.ssl_options()
+ end
+
+ test "keeps the CAStore bundle with configured ssl_options", %{uri: uri} do
+ clear_config([:http, :adapter], ssl_options: [versions: [:"tlsv1.2"]])
+
+ opts = Hackney.options([], uri)
+
+ assert opts[:ssl_options][:versions] == [:"tlsv1.2"]
+ assert opts[:ssl_options][:cacertfile] == CAStore.file_path()
+ end
end
end
diff --git a/test/pleroma/http/tls_test.exs b/test/pleroma/http/tls_test.exs
new file mode 100644
index 000000000..490950b50
--- /dev/null
+++ b/test/pleroma/http/tls_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.TLSTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.HTTP.TLS
+
+ describe "ssl_options/1" do
+ test "verifies peers against the CAStore bundle" do
+ opts = TLS.ssl_options()
+
+ assert opts[:verify] == :verify_peer
+ assert opts[:cacertfile] == CAStore.file_path()
+ assert [match_fun: match_fun] = opts[:customize_hostname_check]
+ assert is_function(match_fun, 2)
+ end
+
+ test "does not accept certificates from the peer chain as trust anchors" do
+ partial_chain = TLS.ssl_options()[:partial_chain]
+
+ assert partial_chain.([:certificate]) == :unknown_ca
+ end
+
+ test "keeps the CA bundle when other options are given" do
+ opts = TLS.ssl_options(versions: [:"tlsv1.2"])
+
+ assert opts[:versions] == [:"tlsv1.2"]
+ assert opts[:cacertfile] == CAStore.file_path()
+ assert opts[:verify] == :verify_peer
+ end
+ end
+
+ describe "put_hackney_ssl_options/1" do
+ test "adds the TLS options" do
+ opts = TLS.put_hackney_ssl_options(pool: :media)
+
+ assert opts[:pool] == :media
+ assert opts[:ssl_options] == TLS.ssl_options()
+ end
+
+ test "merges given ssl_options into the TLS options" do
+ opts = TLS.put_hackney_ssl_options(ssl_options: [versions: [:"tlsv1.2"]])
+
+ 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]
+ end
+ end
+end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 63c0c2bb8..ec5a2a12a 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -531,9 +531,9 @@ defmodule Pleroma.UserTest do
ObanHelpers.perform_all()
Pleroma.Emails.UserEmail.account_confirmation_email(registered_user)
- # temporary hackney fix until hackney max_connections bug is fixed
- # https://git.pleroma.social/pleroma/pleroma/-/issues/2101
- |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
+ |> Swoosh.Email.put_private(:hackney_options,
+ ssl_options: Pleroma.HTTP.TLS.ssl_options()
+ )
|> assert_email_sent()
end
diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
index fc578d828..aa38a7454 100644
--- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
@@ -1042,9 +1042,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
ObanHelpers.perform_all()
Pleroma.Emails.UserEmail.account_confirmation_email(first_user)
- # temporary hackney fix until hackney max_connections bug is fixed
- # https://git.pleroma.social/pleroma/pleroma/-/issues/2101
- |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
+ |> Swoosh.Email.put_private(:hackney_options,
+ ssl_options: Pleroma.HTTP.TLS.ssl_options()
+ )
|> assert_email_sent()
end