fix(http): reject Hackney's certifi partial chains

Hackney 4 merges given ssl_options into its defaults and keeps its own
partial_chain, which trusts any certificate in the peer chain that is in
certifi. A peer could send the emSign ECC Root CA - G3 root in its chain
and pass. Set partial_chain so certifi is never a trust anchor.
This commit is contained in:
Lain Soykaf 2026-10-22 10:05:12 +04:00
commit 65076aa2f8
No known key found for this signature in database
2 changed files with 15 additions and 3 deletions

View file

@ -6,9 +6,11 @@ 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
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.
`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()
@ -18,12 +20,16 @@ defmodule Pleroma.HTTP.TLS do
verify: :verify_peer,
cacertfile: CAStore.file_path(),
depth: 20,
customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
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

View file

@ -17,6 +17,12 @@ defmodule Pleroma.HTTP.TLSTest do
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"])