Split failed-signature inbox retries
Route failed-signature ActivityPub inbox retries through a dedicated worker so legacy and malformed retry jobs fail closed before processing.
This commit is contained in:
parent
bd45704dba
commit
7756f491d5
7 changed files with 786 additions and 461 deletions
|
|
@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
|
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
|
||||||
Federator.incoming_ap_doc(%{
|
Federator.incoming_failed_signature_ap_doc(%{
|
||||||
method: conn.method,
|
method: conn.method,
|
||||||
req_headers: conn.req_headers,
|
req_headers: conn.req_headers,
|
||||||
request_path: conn.request_path,
|
request_path: conn.request_path,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Workers.PublisherWorker
|
alias Pleroma.Workers.PublisherWorker
|
||||||
alias Pleroma.Workers.ReceiverWorker
|
alias Pleroma.Workers.ReceiverWorker
|
||||||
|
alias Pleroma.Workers.SignatureRetryWorker
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|
@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Client API
|
# Client API
|
||||||
def incoming_ap_doc(%{params: params, req_headers: req_headers}) do
|
def incoming_failed_signature_ap_doc(%{
|
||||||
ReceiverWorker.new(
|
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,
|
"req_headers" => req_headers,
|
||||||
"params" => params,
|
"params" => params,
|
||||||
|
"request_path" => request_path,
|
||||||
|
"query_string" => query_string,
|
||||||
"timeout" => :timer.seconds(20)
|
"timeout" => :timer.seconds(20)
|
||||||
},
|
},
|
||||||
priority: 2
|
priority: 2
|
||||||
|
|
|
||||||
|
|
@ -4,55 +4,36 @@
|
||||||
|
|
||||||
defmodule Pleroma.Workers.ReceiverWorker do
|
defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
alias Pleroma.Instances
|
alias Pleroma.Instances
|
||||||
alias Pleroma.Signature
|
|
||||||
alias Pleroma.User
|
|
||||||
alias Pleroma.Web.Federator
|
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]
|
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do
|
||||||
def perform(%Job{
|
if signature_retry_job?(args) do
|
||||||
args: %{
|
perform_signature_retry(job)
|
||||||
"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}
|
|
||||||
else
|
else
|
||||||
e -> process_errors(e)
|
perform_incoming(params)
|
||||||
end
|
end
|
||||||
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
|
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
|
||||||
unless Instances.reachable?(params["actor"]) do
|
unless Instances.reachable?(params["actor"]) do
|
||||||
domain = URI.parse(params["actor"]).host
|
domain = URI.parse(params["actor"]).host
|
||||||
|
|
@ -65,21 +46,15 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
end
|
end
|
||||||
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
|
@impl true
|
||||||
def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
|
def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
|
||||||
|
|
||||||
def timeout(_job), do: :timer.seconds(5)
|
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({:error, {:error, _} = error}), do: process_errors(error)
|
||||||
|
|
||||||
defp process_errors(errors) do
|
defp process_errors(errors) do
|
||||||
|
|
@ -103,6 +78,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
|
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
|
||||||
# Unclear if this can be reached
|
# Unclear if this can be reached
|
||||||
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
|
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
|
||||||
|
:missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params}
|
||||||
# Catchall
|
# Catchall
|
||||||
{:error, _} = e -> e
|
{:error, _} = e -> e
|
||||||
e -> {:error, e}
|
e -> {:error, e}
|
||||||
|
|
|
||||||
144
lib/pleroma/workers/signature_retry_worker.ex
Normal file
144
lib/pleroma/workers/signature_retry_worker.ex
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# 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
|
||||||
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Workers.ReceiverWorker
|
alias Pleroma.Workers.ReceiverWorker
|
||||||
|
alias Pleroma.Workers.SignatureRetryWorker
|
||||||
|
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
|
@ -36,6 +37,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
setup do: clear_config([:instance, :federating], true)
|
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
|
describe "/relay" do
|
||||||
setup do: clear_config([:instance, :allow_relay])
|
setup do: clear_config([:instance, :allow_relay])
|
||||||
|
|
||||||
|
|
@ -727,6 +746,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not create a forged post after failed signature retry", %{conn: conn} do
|
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")
|
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
|
||||||
object_id = "https://two.com/objects/inbox-forged-note"
|
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 =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, false)
|
|> assign(:valid_signature, false)
|
||||||
|
|
@ -760,13 +782,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" == json_response(conn, 200)
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
assert [{:cancel, :actor_signature_mismatch}] =
|
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 Activity.get_by_ap_id(data["id"])
|
||||||
refute Object.get_by_ap_id(object_id)
|
refute Object.get_by_ap_id(object_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not create a forged like after failed signature retry", %{conn: conn} do
|
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")
|
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
|
||||||
note = insert(:note)
|
note = insert(:note)
|
||||||
|
|
||||||
|
|
@ -779,6 +802,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
"object" => note.data["id"]
|
"object" => note.data["id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, false)
|
|> assign(:valid_signature, false)
|
||||||
|
|
@ -789,7 +814,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" == json_response(conn, 200)
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
assert [{:cancel, :actor_signature_mismatch}] =
|
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 Activity.get_by_ap_id(data["id"])
|
||||||
end
|
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 =
|
||||||
conn
|
conn
|
||||||
|
|
@ -837,7 +862,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" == json_response(conn, 200)
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
assert [{:cancel, :actor_signature_mismatch}] =
|
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 Activity.get_by_ap_id(data["id"])
|
||||||
refute Object.get_by_ap_id(object_id)
|
refute Object.get_by_ap_id(object_id)
|
||||||
|
|
@ -858,7 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
"object" => note.data["id"]
|
"object" => note.data["id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|
|
@ -875,7 +900,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert "ok" == json_response(conn, 200)
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
assert [{:cancel, :actor_signature_mismatch}] =
|
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 Activity.get_by_ap_id(data["id"])
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.ActivityPub.UserView
|
|
||||||
alias Pleroma.Web.Federator
|
|
||||||
alias Pleroma.Workers.ReceiverWorker
|
alias Pleroma.Workers.ReceiverWorker
|
||||||
|
|
||||||
defp mismatched_signature_headers do
|
defp signature_headers_for(%User{} = signer) do
|
||||||
[
|
[
|
||||||
{"host", "local.test"},
|
{"host", "local.test"},
|
||||||
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
|
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
|
||||||
|
|
@ -23,39 +21,15 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
{"content-type", "application/activity+json"},
|
{"content-type", "application/activity+json"},
|
||||||
{
|
{
|
||||||
"signature",
|
"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
|
end
|
||||||
|
|
||||||
defp expect_signature_from(%User{} = signer) do
|
defp perform_incoming(params) do
|
||||||
signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
|
ReceiverWorker.perform(%Oban.Job{
|
||||||
|
args: %{"op" => "incoming_ap_doc", "params" => params}
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not retry MRF reject" do
|
test "it does not retry MRF reject" do
|
||||||
|
|
@ -125,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
insert(:note_activity).data
|
insert(:note_activity).data
|
||||||
|> Map.put("actor", "https://springfield.social/users/bart")
|
|> Map.put("actor", "https://springfield.social/users/bart")
|
||||||
|
|
||||||
{:ok, oban_job} =
|
assert {:cancel, {:error, :forbidden}} = perform_incoming(params)
|
||||||
Federator.incoming_ap_doc(%{
|
|
||||||
method: "POST",
|
|
||||||
req_headers: [],
|
|
||||||
request_path: "/inbox",
|
|
||||||
params: params,
|
|
||||||
query_string: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "when request returns a 404" do
|
test "when request returns a 404" do
|
||||||
|
|
@ -142,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
insert(:note_activity).data
|
insert(:note_activity).data
|
||||||
|> Map.put("actor", "https://springfield.social/users/troymcclure")
|
|> Map.put("actor", "https://springfield.social/users/troymcclure")
|
||||||
|
|
||||||
{:ok, oban_job} =
|
assert {:cancel, {:error, :not_found}} = perform_incoming(params)
|
||||||
Federator.incoming_ap_doc(%{
|
|
||||||
method: "POST",
|
|
||||||
req_headers: [],
|
|
||||||
request_path: "/inbox",
|
|
||||||
params: params,
|
|
||||||
query_string: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "when request returns a 410" do
|
test "when request returns a 410" do
|
||||||
|
|
@ -159,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
insert(:note_activity).data
|
insert(:note_activity).data
|
||||||
|> Map.put("actor", "https://springfield.social/users/hankscorpio")
|
|> Map.put("actor", "https://springfield.social/users/hankscorpio")
|
||||||
|
|
||||||
{:ok, oban_job} =
|
assert {:cancel, {:error, :not_found}} = perform_incoming(params)
|
||||||
Federator.incoming_ap_doc(%{
|
|
||||||
method: "POST",
|
|
||||||
req_headers: [],
|
|
||||||
request_path: "/inbox",
|
|
||||||
params: params,
|
|
||||||
query_string: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "when user account is disabled" do
|
test "when user account is disabled" do
|
||||||
|
|
@ -182,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
|
|
||||||
{:ok, %User{}} = User.set_activation(user, false)
|
{:ok, %User{}} = User.set_activation(user, false)
|
||||||
|
|
||||||
{:ok, oban_job} =
|
assert {:cancel, {:user_active, false}} = perform_incoming(params)
|
||||||
Federator.incoming_ap_doc(%{
|
|
||||||
method: "POST",
|
|
||||||
req_headers: [],
|
|
||||||
request_path: "/inbox",
|
|
||||||
params: params,
|
|
||||||
query_string: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job)
|
|
||||||
end
|
end
|
||||||
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
|
test "cancels due to origin containment" do
|
||||||
params =
|
params =
|
||||||
insert(:note_activity).data
|
insert(:note_activity).data
|
||||||
|> Map.put("id", "https://notorigindomain.com/activity")
|
|> Map.put("id", "https://notorigindomain.com/activity")
|
||||||
|
|
||||||
{:ok, oban_job} =
|
assert {:cancel, :origin_containment_failed} = perform_incoming(params)
|
||||||
Federator.incoming_ap_doc(%{
|
|
||||||
method: "POST",
|
|
||||||
req_headers: [],
|
|
||||||
request_path: "/inbox",
|
|
||||||
params: params,
|
|
||||||
query_string: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "canceled due to deleted object" do
|
test "canceled due to deleted object" do
|
||||||
|
|
@ -277,16 +154,98 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, oban_job} =
|
assert {:cancel, _} = perform_incoming(params)
|
||||||
Federator.incoming_ap_doc(%{
|
end
|
||||||
method: "POST",
|
|
||||||
req_headers: [],
|
|
||||||
request_path: "/inbox",
|
|
||||||
params: params,
|
|
||||||
query_string: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
describe "Server reachability:" do
|
describe "Server reachability:" do
|
||||||
|
|
@ -346,262 +305,4 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
469
test/pleroma/workers/signature_retry_worker_test.exs
Normal file
469
test/pleroma/workers/signature_retry_worker_test.exs
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue