fix(federation): enforce the signature policy on incoming requests

HTTPSignaturePlug checks the policy before the cryptographic check. A
signed GET that fails it counts as unsigned; the inbox answers 401 to a
POST that fails it instead of queueing a failed-signature retry, which
only re-checks the signature. signature_policy: :log only logs.

Based on Akkoma's HTTP signature hardening.
This commit is contained in:
Lain Soykaf 2026-10-27 21:46:40 +04:00
commit e5481b4c28
No known key found for this signature in database
7 changed files with 329 additions and 59 deletions

View file

@ -386,18 +386,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
if unsupported_http_message_signature?(conn) do
reject_unsupported_http_message_signature(conn)
else
Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
req_headers: conn.req_headers,
request_path: conn.request_path,
params: params,
query_string: conn.query_string
})
cond do
unsupported_http_message_signature?(conn) ->
reject_unsupported_http_message_signature(conn)
json(conn, "ok")
reason = conn.assigns[:signature_error] ->
conn
|> put_status(:unauthorized)
|> json("error, invalid HTTP signature (#{reason})")
true ->
Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
req_headers: conn.req_headers,
request_path: conn.request_path,
params: params,
query_string: conn.query_string
})
json(conn, "ok")
end
end

View file

@ -45,20 +45,52 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
defp maybe_assign_valid_signature(conn) do
if has_signature_header?(conn) do
# we replace the digest header with the one we computed in DigestPlug
conn =
case conn do
%{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
conn -> conn
end
conn = put_computed_digest(conn)
assign(conn, :valid_signature, Signature.validate_signature(conn))
case check_signature_policy(conn) do
:ok ->
assign(conn, :valid_signature, Signature.validate_signature(conn))
{:error, reason} ->
reject_signature(conn, reason)
end
else
Logger.debug("No signature header!")
conn
end
end
# we replace the digest header with the one we computed in DigestPlug
defp put_computed_digest(%{assigns: %{digest: digest}} = conn),
do: put_req_header(conn, "digest", digest)
defp put_computed_digest(conn), do: conn
defp check_signature_policy(conn) do
with {:error, reason} <- Signature.check_signature_policy(conn) do
if Pleroma.Config.get([:activitypub, :signature_policy], :enforce) == :log do
Logger.warning(
"HTTP signature policy violation (#{reason}) for #{conn.method} #{conn.request_path}, allowed by config"
)
:ok
else
{:error, reason}
end
end
end
# The request is treated as badly signed. The reason is kept so the inbox
# can refuse it instead of queueing a failed-signature retry, which only
# checks the cryptographic signature.
defp reject_signature(conn, reason) do
Logger.debug("Rejecting HTTP signature: #{reason}")
conn
|> assign(:valid_signature, false)
|> assign(:signature_error, reason)
end
defp maybe_assign_actor_id(%{assigns: %{valid_signature: true}} = conn) do
adapter = Application.get_env(:http_signatures, :adapter)