diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 4f1613a07..2bfff6968 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
- Federator.incoming_ap_doc(%{
+ Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
req_headers: conn.req_headers,
request_path: conn.request_path,
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
index 676fc5137..90cd2e54a 100644
--- a/lib/pleroma/web/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
+ alias Pleroma.Workers.SignatureRetryWorker
require Logger
@@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do
end
# Client API
- def incoming_ap_doc(%{params: params, req_headers: req_headers}) do
- ReceiverWorker.new(
+ def incoming_failed_signature_ap_doc(%{
+ method: method,
+ params: params,
+ req_headers: req_headers,
+ request_path: request_path,
+ query_string: query_string
+ }) do
+ SignatureRetryWorker.new(
%{
- "op" => "incoming_ap_doc",
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => method,
"req_headers" => req_headers,
"params" => params,
+ "request_path" => request_path,
+ "query_string" => query_string,
"timeout" => :timer.seconds(20)
},
priority: 2
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index 507e099c2..3afbe138d 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -4,55 +4,36 @@
defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.Instances
- alias Pleroma.Signature
- alias Pleroma.User
alias Pleroma.Web.Federator
- alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
+ alias Pleroma.Workers.SignatureRetryWorker
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true
-
- def perform(%Job{
- args: %{
- "op" => "incoming_ap_doc",
- "method" => method,
- "params" => params,
- "req_headers" => req_headers,
- "request_path" => request_path,
- "query_string" => query_string
- }
- }) do
- # Oban's serialization converts our tuple headers to lists.
- # Revert it for the signature validation.
- req_headers = Enum.into(req_headers, [], &List.to_tuple(&1))
-
- conn_data = %Plug.Conn{
- assigns: %{valid_signature: true},
- method: method,
- params: params,
- req_headers: req_headers,
- request_path: request_path,
- query_string: query_string
- }
-
- with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
- {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
- {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
- {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)},
- {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
- unless Instances.reachable?(params["actor"]) do
- domain = URI.parse(params["actor"]).host
- Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
- end
-
- {:ok, res}
+ def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do
+ if signature_retry_job?(args) do
+ perform_signature_retry(job)
else
- e -> process_errors(e)
+ perform_incoming(params)
end
end
- def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
+ def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do
+ if signature_retry_job?(args) do
+ perform_signature_retry(job)
+ else
+ process_errors(:missing_incoming_ap_doc_params)
+ end
+ end
+
+ defp perform_signature_retry(%Job{args: args} = job) do
+ SignatureRetryWorker.perform(%Job{
+ job
+ | args: Map.put(args, "op", "incoming_failed_signature_ap_doc")
+ })
+ end
+
+ defp perform_incoming(params) do
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
unless Instances.reachable?(params["actor"]) do
domain = URI.parse(params["actor"]).host
@@ -65,21 +46,15 @@ defmodule Pleroma.Workers.ReceiverWorker do
end
end
+ defp signature_retry_job?(args) do
+ Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1))
+ end
+
@impl true
def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
def timeout(_job), do: :timer.seconds(5)
- defp validate_same_actor(conn_data) do
- case MappedSignatureToIdentityPlug.call(conn_data, []) do
- %Plug.Conn{assigns: %{valid_signature: true}} ->
- true
-
- _ ->
- false
- end
- end
-
defp process_errors({:error, {:error, _} = error}), do: process_errors(error)
defp process_errors(errors) do
@@ -103,6 +78,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
# Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
+ :missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params}
# Catchall
{:error, _} = e -> e
e -> {:error, e}
diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex
new file mode 100644
index 000000000..56673a514
--- /dev/null
+++ b/lib/pleroma/workers/signature_retry_worker.ex
@@ -0,0 +1,144 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.SignatureRetryWorker do
+ alias Pleroma.Instances
+ alias Pleroma.Signature
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Federator
+ alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
+
+ use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
+
+ @impl true
+ def perform(%Job{
+ args: %{
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => method,
+ "params" => params,
+ "req_headers" => req_headers,
+ "request_path" => request_path,
+ "query_string" => query_string
+ }
+ })
+ when is_binary(method) and is_map(params) and is_list(req_headers) and
+ is_binary(request_path) and is_binary(query_string) do
+ with {:ok, req_headers} <- normalize_req_headers(req_headers),
+ conn_data = %Plug.Conn{
+ assigns: %{valid_signature: true},
+ method: method,
+ params: params,
+ req_headers: req_headers,
+ request_path: request_path,
+ query_string: query_string
+ },
+ actor_id = Utils.get_ap_id(params["actor"]),
+ {:signature_actor, {:ok, signature_actor_id}} <-
+ {:signature_actor, signature_actor_id(conn_data)},
+ {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id},
+ {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id),
+ {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
+ {:signature, true} <- {:signature, validate_signature(conn_data)},
+ {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)},
+ {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ unless Instances.reachable?(params["actor"]) do
+ domain = URI.parse(params["actor"]).host
+ Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
+ end
+
+ {:ok, res}
+ else
+ e -> process_errors(e)
+ end
+ end
+
+ def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do
+ process_errors(:missing_signature_retry_metadata)
+ end
+
+ def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata)
+
+ @impl true
+ def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
+
+ def timeout(_job), do: :timer.seconds(5)
+
+ defp normalize_req_headers(req_headers) do
+ req_headers
+ |> Enum.reduce_while({:ok, []}, fn
+ {key, value}, {:ok, acc} when is_binary(key) and is_binary(value) ->
+ {:cont, {:ok, [{key, value} | acc]}}
+
+ [key, value], {:ok, acc} when is_binary(key) and is_binary(value) ->
+ {:cont, {:ok, [{key, value} | acc]}}
+
+ _, _ ->
+ {:halt, {:error, :invalid_signature_retry_metadata}}
+ end)
+ |> case do
+ {:ok, headers} -> {:ok, Enum.reverse(headers)}
+ error -> error
+ end
+ end
+
+ defp validate_same_actor(conn_data) do
+ case MappedSignatureToIdentityPlug.call(conn_data, []) do
+ %Plug.Conn{assigns: %{valid_signature: true}} ->
+ true
+
+ _ ->
+ false
+ end
+ end
+
+ defp validate_signature(conn_data) do
+ Signature.validate_signature(conn_data)
+ rescue
+ _ -> false
+ catch
+ _, _ -> false
+ end
+
+ defp signature_actor_id(conn_data) do
+ Signature.get_actor_id(conn_data)
+ rescue
+ _ -> {:error, :invalid_signature}
+ catch
+ _, _ -> {:error, :invalid_signature}
+ end
+
+ defp process_errors({:error, {:error, _} = error}), do: process_errors(error)
+
+ defp process_errors(errors) do
+ case errors do
+ # User fetch failures
+ {:error, :not_found} = reason -> {:cancel, reason}
+ {:error, :forbidden} = reason -> {:cancel, reason}
+ # Inactive user
+ {:error, {:user_active, false} = reason} -> {:cancel, reason}
+ # Validator will error and return a changeset error
+ # e.g., duplicate activities or if the object was deleted
+ {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason}
+ # Duplicate detection during Normalization
+ {:error, :already_present} -> {:cancel, :already_present}
+ # MRFs will return a reject
+ {:error, {:reject, _} = reason} -> {:cancel, reason}
+ # HTTP Sigs
+ {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature}
+ {:signature, false} -> {:cancel, :invalid_signature}
+ {:same_actor, false} -> {:cancel, :actor_signature_mismatch}
+ # Origin / URL validation failed somewhere possibly due to spoofing
+ {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
+ # Unclear if this can be reached
+ {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
+ # Fail closed if the retry cannot reconstruct the original request.
+ :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata}
+ {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata}
+ # Catchall
+ {:error, _} = e -> e
+ e -> {:error, e}
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index 2be5ca6df..62c1dd830 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Workers.ReceiverWorker
+ alias Pleroma.Workers.SignatureRetryWorker
import Pleroma.Factory
@@ -36,6 +37,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
setup do: clear_config([:instance, :federating], true)
+ defp expect_signature_retry_from(%User{} = signer) do
+ signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
+
+ Tesla.Mock.mock(fn
+ %{url: url} when url == signer.ap_id ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(signer_json),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
+ end
+
describe "/relay" do
setup do: clear_config([:instance, :allow_relay])
@@ -727,6 +746,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
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")
object_id = "https://two.com/objects/inbox-forged-note"
@@ -750,6 +770,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
}
}
+ expect_signature_retry_from(alice)
+
conn =
conn
|> assign(:valid_signature, false)
@@ -760,13 +782,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
- ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
end
test "does not create a forged like 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")
note = insert(:note)
@@ -779,6 +802,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
"object" => note.data["id"]
}
+ expect_signature_retry_from(alice)
+
conn =
conn
|> assign(:valid_signature, false)
@@ -789,7 +814,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
- ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
end
@@ -820,7 +845,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
}
}
- Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
+ expect_signature_retry_from(alice)
conn =
conn
@@ -837,7 +862,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
- ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
@@ -858,7 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
"object" => note.data["id"]
}
- Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
+ expect_signature_retry_from(alice)
conn =
conn
@@ -875,7 +900,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
- ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
end
diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs
index ec630eb7b..67a3f902e 100644
--- a/test/pleroma/workers/receiver_worker_test.exs
+++ b/test/pleroma/workers/receiver_worker_test.exs
@@ -11,11 +11,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.ActivityPub.UserView
- alias Pleroma.Web.Federator
alias Pleroma.Workers.ReceiverWorker
- defp mismatched_signature_headers do
+ defp signature_headers_for(%User{} = signer) do
[
{"host", "local.test"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
@@ -23,39 +21,15 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
{"content-type", "application/activity+json"},
{
"signature",
- "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
}
]
end
- defp expect_signature_from(%User{} = signer) do
- signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
-
- Tesla.Mock.mock(fn
- %{url: url} when url == signer.ap_id ->
- %Tesla.Env{
- status: 200,
- body: Jason.encode!(signer_json),
- headers: HttpRequestMock.activitypub_object_headers()
- }
- end)
-
- Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
- end
-
- defp assert_mismatched_signature_cancelled(params, signer) do
- expect_signature_from(signer)
-
- assert {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: mismatched_signature_headers(),
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job)
+ defp perform_incoming(params) do
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{"op" => "incoming_ap_doc", "params" => params}
+ })
end
test "it does not retry MRF reject" do
@@ -125,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/bart")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:error, :forbidden}} = perform_incoming(params)
end
test "when request returns a 404" do
@@ -142,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/troymcclure")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:error, :not_found}} = perform_incoming(params)
end
test "when request returns a 410" do
@@ -159,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/hankscorpio")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:error, :not_found}} = perform_incoming(params)
end
test "when user account is disabled" do
@@ -182,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
{:ok, %User{}} = User.set_activation(user, false)
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:user_active, false}} = perform_incoming(params)
end
end
- test "it can validate the signature" do
- Tesla.Mock.mock(fn
- %{url: "https://phpc.social/users/denniskoch"} ->
- %Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/denniskoch.json"),
- headers: [{"content-type", "application/activity+json"}]
- }
-
- %{url: "https://phpc.social/users/denniskoch/collections/featured"} ->
- %Tesla.Env{
- status: 200,
- headers: [{"content-type", "application/activity+json"}],
- body:
- File.read!("test/fixtures/users_mock/masto_featured.json")
- |> String.replace("{{domain}}", "phpc.social")
- |> String.replace("{{nickname}}", "denniskoch")
- }
- end)
-
- params =
- File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!()
-
- req_headers = [
- ["accept-encoding", "gzip"],
- ["content-length", "5184"],
- ["content-type", "application/activity+json"],
- ["date", "Thu, 25 Jul 2024 13:33:31 GMT"],
- ["digest", "SHA-256=ouge/6HP2/QryG6F3JNtZ6vzs/hSwMk67xdxe87eH7A="],
- ["host", "bikeshed.party"],
- [
- "signature",
- "keyId=\"https://mastodon.social/users/bastianallgeier#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"ymE3vn5Iw50N6ukSp8oIuXJB5SBjGAGjBasdTDvn+ahZIzq2SIJfmVCsIIzyqIROnhWyQoTbavTclVojEqdaeOx+Ejz2wBnRBmhz5oemJLk4RnnCH0lwMWyzeY98YAvxi9Rq57Gojuv/1lBqyGa+rDzynyJpAMyFk17XIZpjMKuTNMCbjMDy76ILHqArykAIL/v1zxkgwxY/+ELzxqMpNqtZ+kQ29znNMUBB3eVZ/mNAHAz6o33Y9VKxM2jw+08vtuIZOusXyiHbRiaj2g5HtN2WBUw1MzzfRfHF2/yy7rcipobeoyk5RvP5SyHV3WrIeZ3iyoNfmv33y8fxllF0EA==\""
- ],
- [
- "user-agent",
- "http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-07-25; +https://mastodon.social/)"
- ]
- ]
-
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: req_headers,
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job)
- end
-
test "cancels due to origin containment" do
params =
insert(:note_activity).data
|> Map.put("id", "https://notorigindomain.com/activity")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, :origin_containment_failed} = perform_incoming(params)
end
test "canceled due to deleted object" do
@@ -277,16 +154,98 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
}
end)
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
+ assert {:cancel, _} = perform_incoming(params)
+ end
- assert {:cancel, _} = ReceiverWorker.perform(oban_job)
+ test "delegates legacy failed-signature metadata jobs instead of processing them as trusted" 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")
+ object_id = "https://two.com/objects/legacy-forged-note"
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/legacy-forged-create",
+ "context" => "https://two.com/contexts/legacy-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/legacy-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert {:cancel, :actor_signature_mismatch} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "method" => "POST",
+ "params" => create,
+ "req_headers" => signature_headers_for(alice),
+ "request_path" => "/inbox",
+ "query_string" => ""
+ }
+ })
+
+ refute Pleroma.Activity.get_by_ap_id(create["id"])
+ refute Pleroma.Object.get_by_ap_id(object_id)
+ end
+
+ test "fails closed for the old persisted failed-signature job shape" 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")
+ object_id = "https://two.com/objects/old-shape-forged-note"
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/old-shape-forged-create",
+ "context" => "https://two.com/contexts/old-shape-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/old-shape-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert {:cancel, :missing_signature_retry_metadata} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "params" => create,
+ "req_headers" => signature_headers_for(alice),
+ "timeout" => 20_000
+ }
+ })
+
+ refute Pleroma.Activity.get_by_ap_id(create["id"])
+ refute Pleroma.Object.get_by_ap_id(object_id)
+ end
+
+ test "fails closed for malformed legacy metadata jobs without params" do
+ assert {:cancel, :missing_signature_retry_metadata} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "req_headers" => [],
+ "timeout" => 20_000
+ }
+ })
end
describe "Server reachability:" do
@@ -346,262 +305,4 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
end
end
end
-
- test "cancels when signature actor does not match payload actor" 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")
-
- note =
- insert(:note,
- user: bob,
- object_local: false,
- data: %{"id" => "https://two.com/objects/malicious-update-note"}
- )
-
- update = %{
- "type" => "Update",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/malicious-update",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => note.data
- }
-
- req_headers = [
- ["host", "local.test"],
- ["date", "Thu, 25 Jul 2024 13:33:31 GMT"],
- ["digest", "SHA-256=fake-digest"],
- ["content-type", "application/activity+json"],
- [
- "signature",
- "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
- ]
- ]
-
- oban_job = %Oban.Job{
- args: %{
- "op" => "incoming_ap_doc",
- "method" => "POST",
- "params" => update,
- "req_headers" => req_headers,
- "request_path" => "/inbox",
- "query_string" => ""
- }
- }
-
- expect_signature_from(alice)
-
- assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job)
- end
-
- test "Federator preserves request metadata needed for ReceiverWorker signature checks" do
- params = insert(:note_activity).data
-
- req_headers = [
- {"host", "local.test"},
- {"signature", "keyId=\"https://one.com/users/alice#main-key\""}
- ]
-
- assert {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: req_headers,
- request_path: "/inbox",
- params: params,
- query_string: "foo=bar"
- })
-
- assert %{
- "method" => "POST",
- "req_headers" => ^req_headers,
- "request_path" => "/inbox",
- "params" => ^params,
- "query_string" => "foo=bar"
- } = oban_job.args
- end
-
- test "cancels signature actor mismatch through Federator-created jobs" 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")
-
- note =
- insert(:note,
- user: bob,
- object_local: false,
- data: %{"id" => "https://two.com/objects/federator-malicious-note"}
- )
-
- update = %{
- "type" => "Update",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/federator-malicious-update",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => note.data
- }
-
- assert_mismatched_signature_cancelled(update, alice)
- end
-
- test "cancels signature actor mismatch before processing a forged Create" 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")
-
- create = %{
- "type" => "Create",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/forged-create",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => %{
- "type" => "Note",
- "id" => "https://two.com/objects/forged-note",
- "actor" => bob.ap_id,
- "attributedTo" => bob.ap_id,
- "content" => "forged post",
- "published" => "2024-07-25T13:33:31Z",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => []
- }
- }
-
- assert_mismatched_signature_cancelled(create, alice)
- end
-
- test "cancels signature actor mismatch before actually creating a forged post" 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")
-
- object_id = "https://two.com/objects/actually-forged-note"
-
- create = %{
- "type" => "Create",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/actually-forged-create",
- "context" => "https://two.com/contexts/actually-forged-create",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => %{
- "type" => "Note",
- "id" => object_id,
- "actor" => bob.ap_id,
- "attributedTo" => bob.ap_id,
- "context" => "https://two.com/contexts/actually-forged-create",
- "content" => "forged post",
- "published" => "2024-07-25T13:33:31Z",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => []
- }
- }
-
- expect_signature_from(alice)
-
- assert {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: mismatched_signature_headers(),
- request_path: "/inbox",
- params: create,
- query_string: ""
- })
-
- assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job)
- refute Pleroma.Object.get_by_ap_id(object_id)
- end
-
- test "cancels signature actor mismatch before processing a forged Like" 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")
- note = insert(:note)
-
- like = %{
- "type" => "Like",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/forged-like",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => note.data["id"]
- }
-
- assert_mismatched_signature_cancelled(like, alice)
- end
-
- test "cancels signature actor mismatch before actually creating a forged Like" 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")
- note = insert(:note)
-
- like = %{
- "type" => "Like",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/actually-forged-like",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => note.data["id"]
- }
-
- expect_signature_from(alice)
-
- assert {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: mismatched_signature_headers(),
- request_path: "/inbox",
- params: like,
- query_string: ""
- })
-
- assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job)
- refute Pleroma.Activity.get_by_ap_id(like["id"])
- end
-
- test "cancels signature actor mismatch before processing a forged Announce" 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")
- note = insert(:note)
-
- announce = %{
- "type" => "Announce",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/forged-announce",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => note.data["id"]
- }
-
- assert_mismatched_signature_cancelled(announce, alice)
- end
-
- test "cancels signature actor mismatch before processing a forged Follow" 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")
- followed = insert(:user)
-
- follow = %{
- "type" => "Follow",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/forged-follow",
- "to" => [followed.ap_id],
- "cc" => [],
- "object" => followed.ap_id
- }
-
- assert_mismatched_signature_cancelled(follow, alice)
- end
-
- test "cancels signature actor mismatch before processing a forged Undo" 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")
-
- undo = %{
- "type" => "Undo",
- "actor" => bob.ap_id,
- "id" => "https://two.com/activities/forged-undo",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"],
- "cc" => [],
- "object" => "https://two.com/activities/existing-bob-activity"
- }
-
- assert_mismatched_signature_cancelled(undo, alice)
- end
end
diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs
new file mode 100644
index 000000000..02706ebad
--- /dev/null
+++ b/test/pleroma/workers/signature_retry_worker_test.exs
@@ -0,0 +1,469 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.SignatureRetryWorkerTest do
+ use Pleroma.DataCase, async: false
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Pleroma.Factory
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Signature
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Web.Federator
+ alias Pleroma.Workers.SignatureRetryWorker
+
+ defp signature_headers_for(%User{} = signer) do
+ [
+ {"host", "local.test"},
+ {"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
+ {"digest", "SHA-256=fake-digest"},
+ {"content-type", "application/activity+json"},
+ {
+ "signature",
+ "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ }
+ ]
+ end
+
+ defp stub_actor_fetch(%User{} = signer) do
+ signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
+
+ Tesla.Mock.mock(fn
+ %{url: url} when url == signer.ap_id ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(signer_json),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+ end)
+ end
+
+ defp expect_signature_from(%User{} = signer) do
+ stub_actor_fetch(signer)
+ Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
+ end
+
+ defp enqueue_failed_signature(params, signer) do
+ Federator.incoming_failed_signature_ap_doc(%{
+ method: "POST",
+ req_headers: signature_headers_for(signer),
+ request_path: "/inbox",
+ params: params,
+ query_string: ""
+ })
+ end
+
+ defp failed_signature_job(params, req_headers, opts \\ []) do
+ %Oban.Job{
+ args: %{
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => Keyword.get(opts, :method, "POST"),
+ "req_headers" => req_headers,
+ "request_path" => Keyword.get(opts, :request_path, "/inbox"),
+ "params" => params,
+ "query_string" => Keyword.get(opts, :query_string, "")
+ }
+ }
+ end
+
+ defp assert_mismatched_signature_cancelled(params, signer) do
+ assert {:ok, oban_job} = enqueue_failed_signature(params, signer)
+
+ assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job)
+ end
+
+ test "Federator preserves request metadata for failed-signature retry jobs" do
+ params = insert(:note_activity).data
+
+ req_headers = [
+ {"host", "local.test"},
+ {"signature", "keyId=\"https://one.com/users/alice#main-key\""}
+ ]
+
+ assert {:ok, oban_job} =
+ Federator.incoming_failed_signature_ap_doc(%{
+ method: "POST",
+ req_headers: req_headers,
+ request_path: "/inbox",
+ params: params,
+ query_string: "foo=bar"
+ })
+
+ assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker"
+
+ assert %{
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => "POST",
+ "req_headers" => ^req_headers,
+ "request_path" => "/inbox",
+ "params" => ^params,
+ "query_string" => "foo=bar"
+ } = oban_job.args
+ end
+
+ test "cancels retry jobs without request metadata" do
+ params = insert(:note_activity).data
+
+ assert {:cancel, :missing_signature_retry_metadata} =
+ SignatureRetryWorker.perform(%Oban.Job{
+ args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params}
+ })
+ end
+
+ test "cancels retry jobs with malformed serialized request headers" do
+ params = insert(:note_activity).data
+
+ assert {:cancel, :invalid_signature_retry_metadata} =
+ SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]]))
+ end
+
+ test "cancels retry jobs without a signature header" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity, user: alice).data
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}]))
+ end
+
+ test "cancels missing signature before fetching an unavailable payload actor" do
+ params =
+ insert(:note_activity).data
+ |> Map.put("actor", "https://unavailable.example/users/bob")
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}]))
+ end
+
+ test "cancels signer mismatch before fetching an unavailable payload actor" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ params =
+ insert(:note_activity).data
+ |> Map.put("actor", "https://unavailable.example/users/bob")
+
+ assert {:cancel, :actor_signature_mismatch} =
+ SignatureRetryWorker.perform(
+ failed_signature_job(params, signature_headers_for(alice))
+ )
+ end
+
+ test "cancels retry jobs with a signature header without keyId" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity, user: alice).data
+
+ req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}]
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
+ end
+
+ test "cancels retry jobs with an unparsable signature keyId" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity, user: alice).data
+ req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}]
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
+ end
+
+ test "cancels when the refetched key still cannot validate the signature" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/invalid-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/invalid-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ stub_actor_fetch(alice)
+
+ assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
+ assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job)
+ refute Activity.get_by_ap_id(create["id"])
+ end
+
+ test "processes the activity after refetching a valid matching signature" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/valid-signature-create",
+ "context" => "https://one.com/contexts/valid-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/valid-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "context" => "https://one.com/contexts/valid-signature-create",
+ "content" => "valid post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ expect_signature_from(alice)
+
+ assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
+ assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job)
+ assert Activity.get_by_ap_id(create["id"])
+ end
+
+ test "processes the activity when a real signature validates with a query string" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/valid-query-signature-create",
+ "context" => "https://one.com/contexts/valid-query-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/valid-query-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "context" => "https://one.com/contexts/valid-query-signature-create",
+ "content" => "valid signed post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ stub_actor_fetch(alice)
+
+ date = "Thu, 25 Jul 2024 13:33:31 GMT"
+ digest = "SHA-256=fake-digest"
+
+ signature =
+ Signature.sign(alice, %{
+ "(request-target)" => "post /inbox?foo=bar",
+ "content-type" => "application/activity+json",
+ date: date,
+ digest: digest,
+ host: "local.test"
+ })
+
+ req_headers = [
+ ["host", "local.test"],
+ ["date", date],
+ ["digest", digest],
+ ["content-type", "application/activity+json"],
+ ["signature", signature]
+ ]
+
+ assert {:ok, %Activity{}} =
+ SignatureRetryWorker.perform(
+ failed_signature_job(create, req_headers, query_string: "foo=bar")
+ )
+
+ assert Activity.get_by_ap_id(create["id"])
+ end
+
+ test "cancels when signature actor does not match payload actor" 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")
+
+ note =
+ insert(:note,
+ user: bob,
+ object_local: false,
+ data: %{"id" => "https://two.com/objects/malicious-update-note"}
+ )
+
+ update = %{
+ "type" => "Update",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/malicious-update",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data
+ }
+
+ assert_mismatched_signature_cancelled(update, alice)
+ end
+
+ test "cancels signature actor mismatch through Federator-created jobs" 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")
+
+ note =
+ insert(:note,
+ user: bob,
+ object_local: false,
+ data: %{"id" => "https://two.com/objects/federator-malicious-note"}
+ )
+
+ update = %{
+ "type" => "Update",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/federator-malicious-update",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data
+ }
+
+ assert_mismatched_signature_cancelled(update, alice)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Create" 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")
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://two.com/objects/forged-note",
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert_mismatched_signature_cancelled(create, alice)
+ end
+
+ test "cancels signature actor mismatch before actually creating a forged post" 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")
+
+ object_id = "https://two.com/objects/actually-forged-note"
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/actually-forged-create",
+ "context" => "https://two.com/contexts/actually-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/actually-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert_mismatched_signature_cancelled(create, alice)
+ refute Object.get_by_ap_id(object_id)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Like" 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")
+ note = insert(:note)
+
+ like = %{
+ "type" => "Like",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-like",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ assert_mismatched_signature_cancelled(like, alice)
+ end
+
+ test "cancels signature actor mismatch before actually creating a forged Like" 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")
+ note = insert(:note)
+
+ like = %{
+ "type" => "Like",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/actually-forged-like",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ assert_mismatched_signature_cancelled(like, alice)
+ refute Activity.get_by_ap_id(like["id"])
+ end
+
+ test "cancels signature actor mismatch before processing a forged Announce" 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")
+ note = insert(:note)
+
+ announce = %{
+ "type" => "Announce",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-announce",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ assert_mismatched_signature_cancelled(announce, alice)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Follow" 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")
+ followed = insert(:user)
+
+ follow = %{
+ "type" => "Follow",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-follow",
+ "to" => [followed.ap_id],
+ "cc" => [],
+ "object" => followed.ap_id
+ }
+
+ assert_mismatched_signature_cancelled(follow, alice)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Undo" 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")
+
+ undo = %{
+ "type" => "Undo",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-undo",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => "https://two.com/activities/existing-bob-activity"
+ }
+
+ assert_mismatched_signature_cancelled(undo, alice)
+ end
+end