Merge pull request 'Reject unsupported HTTP Message Signatures' (#7918) from activitypub-bot-federation into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7918
This commit is contained in:
commit
d115b41848
3 changed files with 189 additions and 11 deletions
1
changelog.d/activitypub-bot-rfc9421-fallback.fix
Normal file
1
changelog.d/activitypub-bot-rfc9421-fallback.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Reject unsupported or ambiguous inbox signatures instead of queueing a doomed failed-signature retry, allowing clients to fall back to legacy HTTP Signatures.
|
||||
|
|
@ -309,7 +309,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
} = conn,
|
||||
%{"nickname" => nickname} = params
|
||||
) do
|
||||
with {:recipient_exists, %User{} = recipient} <-
|
||||
with false <- unsupported_http_message_signature?(conn),
|
||||
{:recipient_exists, %User{} = recipient} <-
|
||||
{:recipient_exists, User.get_cached_by_nickname(nickname)},
|
||||
{:sender_exists, {:ok, %User{} = actor}} <-
|
||||
{:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])},
|
||||
|
|
@ -320,6 +321,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
else
|
||||
true ->
|
||||
reject_unsupported_http_message_signature(conn)
|
||||
|
||||
{:recipient_exists, _} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|
|
@ -348,20 +352,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
if unsupported_http_message_signature?(conn) do
|
||||
reject_unsupported_http_message_signature(conn)
|
||||
else
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
end
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
|
||||
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
|
||||
})
|
||||
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
|
||||
})
|
||||
|
||||
json(conn, "ok")
|
||||
json(conn, "ok")
|
||||
end
|
||||
end
|
||||
|
||||
# POST /relay/inbox -or- POST /internal/fetch/inbox
|
||||
|
|
@ -381,6 +393,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> json("error, missing HTTP Signature")
|
||||
end
|
||||
|
||||
defp reject_unsupported_http_message_signature(conn) do
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> json("error, unsupported HTTP Message Signature")
|
||||
end
|
||||
|
||||
defp unsupported_http_message_signature?(conn) do
|
||||
signature_headers = Plug.Conn.get_req_header(conn, "signature")
|
||||
|
||||
multiple_signature_headers?(signature_headers) or
|
||||
(Plug.Conn.get_req_header(conn, "signature-input") != [] and
|
||||
not cavage_signature?(signature_headers))
|
||||
end
|
||||
|
||||
defp multiple_signature_headers?([_, _ | _]), do: true
|
||||
defp multiple_signature_headers?(_), do: false
|
||||
|
||||
defp cavage_signature?(signature_headers) do
|
||||
Enum.any?(signature_headers, &Regex.match?(~r/(^|,)\s*keyId\s*=/, &1))
|
||||
end
|
||||
|
||||
defp post_inbox_relayed_create(conn, params) do
|
||||
Logger.debug(
|
||||
"Signature missing or not from author, relayed Create message, fetching object from source"
|
||||
|
|
|
|||
|
|
@ -729,6 +729,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
assert Activity.get_by_ap_id(data["id"])
|
||||
end
|
||||
|
||||
test "rejects duplicate Signature headers before processing signed inbox requests", %{
|
||||
conn: conn
|
||||
} do
|
||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign_valid_signature_for_actor(data["actor"])
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> Map.update!(:req_headers, &[{"signature", "sig1=:fake-signature:"} | &1])
|
||||
|> post("/inbox", data)
|
||||
|
||||
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
|
||||
assert [] == all_enqueued(worker: ReceiverWorker)
|
||||
end
|
||||
|
||||
test "it inserts an incoming activity into the database" <>
|
||||
"even if we can't fetch the user but have it in our db",
|
||||
%{conn: conn} do
|
||||
|
|
@ -757,6 +773,120 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
assert Activity.get_by_ap_id(data["id"])
|
||||
end
|
||||
|
||||
test "rejects unsupported HTTP Message Signatures so clients can fall back", %{conn: conn} do
|
||||
data = %{
|
||||
"type" => "Create",
|
||||
"actor" => "https://activitypubbot.example/user/ok",
|
||||
"id" => "https://activitypubbot.example/activities/rfc9421-create",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => "https://activitypubbot.example/objects/rfc9421-note",
|
||||
"actor" => "https://activitypubbot.example/user/ok",
|
||||
"attributedTo" => "https://activitypubbot.example/user/ok",
|
||||
"content" => "hello from activitypub-bot",
|
||||
"published" => "2026-05-29T13:33:31Z",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|
||||
|> put_req_header("content-digest", "sha-256=:fake-digest:")
|
||||
|> put_req_header(
|
||||
"signature-input",
|
||||
"sig1=(\"@method\" \"@target-uri\" \"date\" \"content-type\" \"content-digest\");keyid=\"https://activitypubbot.example/user/ok/publickey\";alg=\"rsa-v1_5-sha256\";created=1780061611"
|
||||
)
|
||||
|> put_req_header("signature", "sig1=:fake-signature:")
|
||||
|> post("/inbox", data)
|
||||
|
||||
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
|
||||
assert [] == all_enqueued(worker: SignatureRetryWorker)
|
||||
end
|
||||
|
||||
test "rejects duplicate Signature headers so clients can fall back", %{conn: conn} do
|
||||
data = %{
|
||||
"type" => "Create",
|
||||
"actor" => "https://activitypubbot.example/user/ok",
|
||||
"id" => "https://activitypubbot.example/activities/duplicate-signature-create",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => "https://activitypubbot.example/objects/duplicate-signature-note",
|
||||
"actor" => "https://activitypubbot.example/user/ok",
|
||||
"attributedTo" => "https://activitypubbot.example/user/ok",
|
||||
"content" => "hello from activitypub-bot",
|
||||
"published" => "2026-05-29T13:33:31Z",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
legacy_signature =
|
||||
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
|
||||
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header("signature", "sig1=:fake-signature:")
|
||||
|> Map.update!(:req_headers, &[{"signature", legacy_signature} | &1])
|
||||
|> post("/inbox", data)
|
||||
|
||||
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
|
||||
assert [] == all_enqueued(worker: SignatureRetryWorker)
|
||||
end
|
||||
|
||||
test "keeps failed-signature retry for legacy signatures with Signature-Input", %{conn: conn} do
|
||||
data = %{
|
||||
"type" => "Create",
|
||||
"actor" => "https://activitypubbot.example/user/ok",
|
||||
"id" => "https://activitypubbot.example/activities/legacy-create",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => "https://activitypubbot.example/objects/legacy-note",
|
||||
"actor" => "https://activitypubbot.example/user/ok",
|
||||
"attributedTo" => "https://activitypubbot.example/user/ok",
|
||||
"content" => "hello from activitypub-bot",
|
||||
"published" => "2026-05-29T13:33:31Z",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header(
|
||||
"signature-input",
|
||||
"sig1=(\"@method\" \"@target-uri\" \"date\" \"content-type\" \"digest\");keyid=\"https://activitypubbot.example/user/ok/publickey\";alg=\"rsa-v1_5-sha256\";created=1780061611"
|
||||
)
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
)
|
||||
|> post("/inbox", data)
|
||||
|
||||
assert "ok" == json_response(conn, 200)
|
||||
assert [_] = all_enqueued(worker: SignatureRetryWorker)
|
||||
end
|
||||
|
||||
test "does not create a forged post after failed signature retry", %{conn: conn} do
|
||||
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
|
||||
|
|
@ -1320,6 +1450,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
assert Activity.get_by_ap_id(data["id"])
|
||||
end
|
||||
|
||||
test "rejects duplicate Signature headers", %{conn: conn, data: data} do
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign_valid_signature_for_actor(data["actor"])
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> Map.update!(:req_headers, &[{"signature", "sig1=:fake-signature:"} | &1])
|
||||
|> post("/users/#{user.nickname}/inbox", data)
|
||||
|
||||
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
|
||||
assert [] == all_enqueued(worker: ReceiverWorker)
|
||||
end
|
||||
|
||||
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue