Merge pull request 'Federation: reject stale and incomplete HTTP signatures' (#8033) from lambadalambda/pleroma:fix/signature-freshness into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/8033
This commit is contained in:
commit
ca44028928
12 changed files with 563 additions and 59 deletions
|
|
@ -95,6 +95,84 @@ defmodule Pleroma.Signature do
|
|||
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks the parts of an incoming signature that the cryptographic check does
|
||||
not cover, so that a captured request cannot be replayed later or with a
|
||||
different body.
|
||||
|
||||
The signature must cover `date` or `(created)`, and the signed time must be
|
||||
within the configured window. Requests other than GET and HEAD must also
|
||||
sign `digest`, and DigestPlug must have hashed their body. Based on Akkoma's http_signatures fork and Mastodon's
|
||||
SignedRequest.
|
||||
"""
|
||||
@spec check_signature_policy(Plug.Conn.t(), DateTime.t()) :: :ok | {:error, atom()}
|
||||
def check_signature_policy(%Plug.Conn{} = conn, now \\ DateTime.utc_now()) do
|
||||
signature = HTTPSignatures.signature_for_conn(conn)
|
||||
signed = Map.get(signature, "headers", [])
|
||||
|
||||
cond do
|
||||
"date" not in signed and "(created)" not in signed ->
|
||||
{:error, :date_not_signed}
|
||||
|
||||
conn.method not in ["GET", "HEAD"] and "digest" not in signed ->
|
||||
{:error, :digest_not_signed}
|
||||
|
||||
# The plug puts the digest computed by DigestPlug into the header. With
|
||||
# no computed digest (e.g. multipart), nothing checked the body.
|
||||
conn.method not in ["GET", "HEAD"] and is_nil(conn.assigns[:digest]) ->
|
||||
{:error, :body_not_hashed}
|
||||
|
||||
true ->
|
||||
with :ok <- check_signed_date(conn, signed, now) do
|
||||
check_signed_created(signature, signed, now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_signed_date(conn, signed, now) do
|
||||
if "date" in signed do
|
||||
# Exactly one header: a second one could make this check and the
|
||||
# signature check see different dates.
|
||||
with [date] <- Plug.Conn.get_req_header(conn, "date"),
|
||||
{:ok, %DateTime{} = date} <- Timex.parse(date, "{RFC1123}") do
|
||||
check_time_window(date, now)
|
||||
else
|
||||
_ -> {:error, :invalid_date}
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_signed_created(signature, signed, now) do
|
||||
if "(created)" in signed do
|
||||
with created when is_binary(created) <- signature["created"],
|
||||
{created, ""} <- Integer.parse(created),
|
||||
{:ok, created} <- DateTime.from_unix(created) do
|
||||
check_time_window(created, now)
|
||||
else
|
||||
_ -> {:error, :invalid_date}
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_time_window(signed_at, now) do
|
||||
age = DateTime.diff(now, signed_at)
|
||||
|
||||
cond do
|
||||
age > Pleroma.Config.get([:activitypub, :signature_max_age], 7200) ->
|
||||
{:error, :date_too_old}
|
||||
|
||||
-age > Pleroma.Config.get([:activitypub, :signature_max_clock_skew], 3600) ->
|
||||
{:error, :date_in_future}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_signature(Plug.Conn.t(), String.t()) :: boolean()
|
||||
def validate_signature(%Plug.Conn{} = conn, request_target) do
|
||||
# Newer drafts for HTTP signatures now use @request-target instead of the
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue