fix(http): make Hackney trust the CAStore bundle

Hackney trusts its own certifi bundle, which still has the roots of the
compromised eMudhra intermediate CAs. Pass explicit ssl_options with the
CAStore bundle. Hackney drops all its TLS defaults when ssl_options are
given, so keep peer and hostname checks in the options.
This commit is contained in:
Lain Soykaf 2026-10-21 08:21:43 +04:00
commit d8c880e494
No known key found for this signature in database
6 changed files with 102 additions and 3 deletions

View file

@ -0,0 +1 @@
Hackney now trusts only the CAStore certificate bundle instead of its outdated certifi bundle, which still trusted the compromised eMudhra intermediate CAs

View file

@ -28,6 +28,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|> add_scheme_opts(uri)
|> maybe_add_with_body()
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
|> Pleroma.HTTP.TLS.put_hackney_ssl_options()
end
defp add_scheme_opts(opts, _), do: opts

35
lib/pleroma/http/tls.ex Normal file
View file

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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, and it drops all of its
default TLS options, including certificate checks, as soon as any
`ssl_options` are given.
"""
@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)]
],
overrides
)
end
@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

View file

@ -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)

View file

@ -39,5 +39,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

View file

@ -0,0 +1,47 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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 "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