Merge branch 'update-spoofing' of ssh://git.pleroma.social:22/pleroma-secteam/pleroma into develop
This commit is contained in:
commit
78a41dfdcd
18 changed files with 1524 additions and 239 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -56,6 +56,9 @@ pleroma.iml
|
||||||
# asdf
|
# asdf
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
|
||||||
|
# mise
|
||||||
|
mise.toml
|
||||||
|
|
||||||
# Editor temp files
|
# Editor temp files
|
||||||
*~
|
*~
|
||||||
*#
|
*#
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
when:
|
when:
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
|
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
|
||||||
- event: push
|
- event: push
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
|
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
when:
|
when:
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
|
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
|
||||||
- event: push
|
- event: push
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
|
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
when:
|
when:
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
|
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
|
||||||
- event: push
|
- event: push
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
|
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
|
|
|
||||||
1
changelog.d/activitypub-spoofing.security
Normal file
1
changelog.d/activitypub-spoofing.security
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed
|
||||||
|
|
@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|
|
||||||
show_birthday = !!birthday
|
show_birthday = !!birthday
|
||||||
|
|
||||||
# if WebFinger request was already done, we probably have acct, otherwise
|
with {:ok, nickname} <- nickname_from_actor(data, additional) do
|
||||||
# we request WebFinger here
|
{:ok,
|
||||||
nickname = additional[:nickname_from_acct] || generate_nickname(data)
|
%{
|
||||||
|
ap_id: data["id"],
|
||||||
|
uri: get_actor_url(data["url"]),
|
||||||
|
banner: normalize_image(data["image"]),
|
||||||
|
fields: fields,
|
||||||
|
emoji: emojis,
|
||||||
|
is_locked: is_locked,
|
||||||
|
is_discoverable: is_discoverable,
|
||||||
|
invisible: invisible,
|
||||||
|
avatar: normalize_image(data["icon"]),
|
||||||
|
name: data["name"],
|
||||||
|
follower_address: data["followers"],
|
||||||
|
following_address: data["following"],
|
||||||
|
featured_address: featured_address,
|
||||||
|
bio: data["summary"] || "",
|
||||||
|
actor_type: actor_type,
|
||||||
|
also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
|
||||||
|
public_key: public_key,
|
||||||
|
inbox: data["inbox"],
|
||||||
|
shared_inbox: shared_inbox,
|
||||||
|
accepts_chat_messages: accepts_chat_messages,
|
||||||
|
birthday: birthday,
|
||||||
|
show_birthday: show_birthday,
|
||||||
|
pinned_objects: pinned_objects,
|
||||||
|
nickname: nickname
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
%{
|
defp nickname_from_actor(data, additional) do
|
||||||
ap_id: data["id"],
|
generated = generated_nickname(data)
|
||||||
uri: get_actor_url(data["url"]),
|
|
||||||
banner: normalize_image(data["image"]),
|
case additional[:nickname_from_acct] do
|
||||||
fields: fields,
|
^generated when is_binary(generated) ->
|
||||||
emoji: emojis,
|
{:ok, generated}
|
||||||
is_locked: is_locked,
|
|
||||||
is_discoverable: is_discoverable,
|
acct when is_binary(acct) ->
|
||||||
invisible: invisible,
|
with ^acct <- webfinger_nickname(data) do
|
||||||
avatar: normalize_image(data["icon"]),
|
{:ok, acct}
|
||||||
name: data["name"],
|
else
|
||||||
follower_address: data["followers"],
|
_ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}}
|
||||||
following_address: data["following"],
|
end
|
||||||
featured_address: featured_address,
|
|
||||||
bio: data["summary"] || "",
|
_ ->
|
||||||
actor_type: actor_type,
|
{:ok, generate_nickname(data)}
|
||||||
also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
|
end
|
||||||
public_key: public_key,
|
end
|
||||||
inbox: data["inbox"],
|
|
||||||
shared_inbox: shared_inbox,
|
defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id})
|
||||||
accepts_chat_messages: accepts_chat_messages,
|
when is_binary(username) and is_binary(ap_id) do
|
||||||
birthday: birthday,
|
case URI.parse(ap_id) do
|
||||||
show_birthday: show_birthday,
|
%URI{host: host} when is_binary(host) -> "#{username}@#{host}"
|
||||||
pinned_objects: pinned_objects,
|
_ -> nil
|
||||||
nickname: nickname
|
end
|
||||||
}
|
end
|
||||||
|
|
||||||
|
defp generated_nickname(_), do: nil
|
||||||
|
|
||||||
|
defp webfinger_nickname(data) do
|
||||||
|
with generated when is_binary(generated) <- generated_nickname(data),
|
||||||
|
{:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated),
|
||||||
|
true <- ap_id == data["id"] do
|
||||||
|
acct
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
|
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
|
||||||
generated = "#{username}@#{URI.parse(data["id"]).host}"
|
generated = generated_nickname(data)
|
||||||
|
|
||||||
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
|
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
|
||||||
case WebFinger.finger(generated) do
|
case webfinger_nickname(data) do
|
||||||
{:ok, %{"subject" => "acct:" <> acct}} -> acct
|
acct when is_binary(acct) -> acct
|
||||||
_ -> generated
|
_ -> generated
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
defp collection_private(_data), do: {:ok, true}
|
defp collection_private(_data), do: {:ok, true}
|
||||||
|
|
||||||
def user_data_from_user_object(data, additional \\ []) do
|
def user_data_from_user_object(data, additional \\ []) do
|
||||||
with {:ok, data} <- MRF.filter(data) do
|
with {:ok, data} <- MRF.filter(data),
|
||||||
{:ok, object_to_user_data(data, additional)}
|
{:ok, data} <- object_to_user_data(data, additional) do
|
||||||
|
{:ok, data}
|
||||||
else
|
else
|
||||||
|
{:error, _} = e -> e
|
||||||
e -> {:error, e}
|
e -> {:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -75,15 +75,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# For remote Updates, verify the host is the same.
|
# For remote Updates, verify the Actor is the same
|
||||||
def validate_updating_rights_remote(cng) do
|
def validate_updating_rights_remote(cng) do
|
||||||
with actor = get_field(cng, :actor),
|
with actor = get_field(cng, :actor),
|
||||||
object = get_field(cng, :object),
|
object = get_field(cng, :object),
|
||||||
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
||||||
actor_uri <- URI.parse(actor),
|
entity <-
|
||||||
object_uri <- URI.parse(object_id),
|
Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do
|
||||||
true <- actor_uri.host == object_uri.host do
|
case entity do
|
||||||
cng
|
# Actor must own Object to update it
|
||||||
|
%Object{} ->
|
||||||
|
if actor == entity.data["actor"] do
|
||||||
|
cng
|
||||||
|
else
|
||||||
|
cng
|
||||||
|
|> add_error(:object, "Can't be updated by this actor")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actor must only be allowed to update itself
|
||||||
|
%User{} ->
|
||||||
|
if actor == entity.ap_id do
|
||||||
|
cng
|
||||||
|
else
|
||||||
|
cng
|
||||||
|
|> add_error(:object, "Can't be updated by this actor")
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
cng
|
||||||
|
|> add_error(:object, "Can't be updated by this actor")
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
cng
|
||||||
|
|> add_error(:object, "Update is neither for Object or Actor")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
_e ->
|
_e ->
|
||||||
cng
|
cng
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||||
# remove me once testsuite uses mapped capabilities instead of what we do now
|
# remove me once testsuite uses mapped capabilities instead of what we do now
|
||||||
{:user, nil} ->
|
{:user, nil} ->
|
||||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
|
||||||
conn
|
assign(conn, :valid_signature, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,37 @@
|
||||||
|
|
||||||
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.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
|
||||||
|
if signature_retry_job?(args) do
|
||||||
|
perform_signature_retry(job)
|
||||||
|
else
|
||||||
|
perform_incoming(params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def perform(%Job{
|
def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do
|
||||||
args: %{
|
if signature_retry_job?(args) do
|
||||||
"op" => "incoming_ap_doc",
|
perform_signature_retry(job)
|
||||||
"method" => method,
|
else
|
||||||
"params" => params,
|
process_errors(:missing_incoming_ap_doc_params)
|
||||||
"req_headers" => req_headers,
|
end
|
||||||
"request_path" => request_path,
|
end
|
||||||
"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{
|
defp perform_signature_retry(%Job{args: args} = job) do
|
||||||
method: method,
|
SignatureRetryWorker.perform(%Job{
|
||||||
params: params,
|
job
|
||||||
req_headers: req_headers,
|
| args: Map.put(args, "op", "incoming_failed_signature_ap_doc")
|
||||||
request_path: request_path,
|
})
|
||||||
query_string: query_string
|
end
|
||||||
}
|
|
||||||
|
|
||||||
with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
|
defp perform_incoming(params) do
|
||||||
{:ok, _public_key} <- Signature.refetch_public_key(conn_data),
|
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
|
||||||
{:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
|
|
||||||
{: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
|
||||||
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
|
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
|
||||||
|
|
@ -49,17 +46,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
|
defp signature_retry_job?(args) do
|
||||||
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
|
Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1))
|
||||||
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
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -85,10 +73,12 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
{:error, {:reject, _} = reason} -> {:cancel, reason}
|
{:error, {:reject, _} = reason} -> {:cancel, reason}
|
||||||
# HTTP Sigs
|
# HTTP Sigs
|
||||||
{:signature, false} -> {:cancel, :invalid_signature}
|
{:signature, false} -> {:cancel, :invalid_signature}
|
||||||
|
{:same_actor, false} -> {:cancel, :actor_signature_mismatch}
|
||||||
# Origin / URL validation failed somewhere possibly due to spoofing
|
# Origin / URL validation failed somewhere possibly due to spoofing
|
||||||
{: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}
|
||||||
|
|
|
||||||
254
lib/pleroma/workers/signature_retry_worker.ex
Normal file
254
lib/pleroma/workers/signature_retry_worker.ex
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
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
|
||||||
|
case normalize_req_headers(req_headers) do
|
||||||
|
{:ok, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
signature_actor_result = signature_actor_id(conn_data)
|
||||||
|
|
||||||
|
with actor_id = Utils.get_ap_id(params["actor"]),
|
||||||
|
{:signature_actor, {:ok, signature_actor_id}} <-
|
||||||
|
{:signature_actor, signature_actor_result},
|
||||||
|
{: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, retry_log_context(params, request_path, signature_actor_result))
|
||||||
|
end
|
||||||
|
|
||||||
|
e ->
|
||||||
|
process_errors(e, retry_log_context(params, request_path, nil))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do
|
||||||
|
process_errors(
|
||||||
|
:missing_signature_retry_metadata,
|
||||||
|
retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(%Job{args: args}) when is_map(args) do
|
||||||
|
process_errors(
|
||||||
|
:missing_signature_retry_metadata,
|
||||||
|
retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
|
||||||
|
)
|
||||||
|
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(errors, context \\ %{})
|
||||||
|
|
||||||
|
defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context)
|
||||||
|
|
||||||
|
defp process_errors(errors, context) do
|
||||||
|
result =
|
||||||
|
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
|
||||||
|
|
||||||
|
log_signature_retry_rejection(result, context)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do
|
||||||
|
signature_actor =
|
||||||
|
case signature_actor_result do
|
||||||
|
{:ok, actor} when is_binary(actor) -> actor
|
||||||
|
actor when is_binary(actor) -> actor
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
activity_id: params["id"],
|
||||||
|
payload_actor: Utils.get_ap_id(params["actor"]),
|
||||||
|
request_path: request_path,
|
||||||
|
signature_actor: signature_actor,
|
||||||
|
type: params["type"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp retry_log_context(_params, request_path, signature_actor_result) do
|
||||||
|
signature_actor =
|
||||||
|
case signature_actor_result do
|
||||||
|
{:ok, actor} when is_binary(actor) -> actor
|
||||||
|
actor when is_binary(actor) -> actor
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
activity_id: nil,
|
||||||
|
payload_actor: nil,
|
||||||
|
request_path: request_path,
|
||||||
|
signature_actor: signature_actor,
|
||||||
|
type: nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp log_signature_retry_rejection({:cancel, reason}, context)
|
||||||
|
when reason in [
|
||||||
|
:actor_signature_mismatch,
|
||||||
|
:invalid_signature,
|
||||||
|
:invalid_signature_retry_metadata,
|
||||||
|
:missing_signature_retry_metadata,
|
||||||
|
:origin_containment_failed
|
||||||
|
] do
|
||||||
|
Logger.warning(
|
||||||
|
"Failed-signature inbox retry rejected " <>
|
||||||
|
"reason=#{inspect(reason)} " <>
|
||||||
|
"payload_actor=#{inspect(context[:payload_actor])} " <>
|
||||||
|
"signature_actor=#{inspect(context[:signature_actor])} " <>
|
||||||
|
"activity_id=#{inspect(context[:activity_id])} " <>
|
||||||
|
"type=#{inspect(context[:type])} " <>
|
||||||
|
"request_path=#{inspect(context[:request_path])}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp log_signature_retry_rejection(_result, _context), do: :ok
|
||||||
|
end
|
||||||
|
|
@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do
|
||||||
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
|
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
|
||||||
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
||||||
|
|
||||||
test "for mastodon" do
|
test "fetches a mastodon split-domain nickname" do
|
||||||
ap_id = "a@mastodon.example"
|
nickname = "a@mastodon.example"
|
||||||
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
{:ok, fetched_user} = User.get_or_fetch(nickname)
|
||||||
|
|
||||||
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
|
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
|
||||||
assert fetched_user.nickname == "a@mastodon.example"
|
assert fetched_user.nickname == "a@mastodon.example"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "for pleroma" do
|
test "fetches a pleroma split-domain nickname" do
|
||||||
ap_id = "a@pleroma.example"
|
nickname = "a@pleroma.example"
|
||||||
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
{:ok, fetched_user} = User.get_or_fetch(nickname)
|
||||||
|
|
||||||
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
|
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
|
||||||
assert fetched_user.nickname == "a@pleroma.example"
|
assert fetched_user.nickname == "a@pleroma.example"
|
||||||
|
|
@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do
|
||||||
assert fetched_user == "not found nonexistent"
|
assert fetched_user == "not found nonexistent"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "does not rename an existing remote actor from rogue WebFinger data" do
|
||||||
|
clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
||||||
|
|
||||||
|
actor_id = "https://legit-actor.example/users/alice"
|
||||||
|
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{url: "https://evil-webfinger.example/.well-known/host-meta"} ->
|
||||||
|
{:ok, %Tesla.Env{status: 404}}
|
||||||
|
|
||||||
|
%{
|
||||||
|
url:
|
||||||
|
"https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example"
|
||||||
|
} ->
|
||||||
|
Tesla.Mock.json(%{
|
||||||
|
"subject" => "acct:claimed@evil-webfinger.example",
|
||||||
|
"links" => [
|
||||||
|
%{
|
||||||
|
"rel" => "self",
|
||||||
|
"type" => "application/activity+json",
|
||||||
|
"href" => actor_id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
%{url: ^actor_id} ->
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
headers: [{"content-type", "application/activity+json"}],
|
||||||
|
body:
|
||||||
|
Jason.encode!(%{
|
||||||
|
"id" => actor_id,
|
||||||
|
"type" => "Person",
|
||||||
|
"preferredUsername" => "alice",
|
||||||
|
"name" => "Alice",
|
||||||
|
"summary" => "",
|
||||||
|
"inbox" => "https://legit-actor.example/users/alice/inbox",
|
||||||
|
"outbox" => "https://legit-actor.example/users/alice/outbox",
|
||||||
|
"followers" => "https://legit-actor.example/users/alice/followers",
|
||||||
|
"following" => "https://legit-actor.example/users/alice/following"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
|
||||||
|
%{url: "https://legit-actor.example/.well-known/host-meta"} ->
|
||||||
|
{:ok, %Tesla.Env{status: 404}}
|
||||||
|
|
||||||
|
%{
|
||||||
|
url:
|
||||||
|
"https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example"
|
||||||
|
} ->
|
||||||
|
Tesla.Mock.json(%{
|
||||||
|
"subject" => "acct:alice@legit-actor.example",
|
||||||
|
"links" => [
|
||||||
|
%{
|
||||||
|
"rel" => "self",
|
||||||
|
"type" => "application/activity+json",
|
||||||
|
"href" => actor_id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
|
||||||
|
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
|
||||||
|
|
||||||
|
refute User.get_by_ap_id(actor_id)
|
||||||
|
refute User.get_by_nickname("claimed@evil-webfinger.example")
|
||||||
|
|
||||||
|
orig_user =
|
||||||
|
insert(:user,
|
||||||
|
local: false,
|
||||||
|
nickname: "alice@legit-actor.example",
|
||||||
|
ap_id: actor_id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
|
||||||
|
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
|
||||||
|
|
||||||
|
assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example")
|
||||||
|
assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example"
|
||||||
|
refute User.get_by_nickname("claimed@evil-webfinger.example")
|
||||||
|
end
|
||||||
|
|
||||||
test "updates an existing user, if stale" do
|
test "updates an existing user, if stale" do
|
||||||
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
|
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
setup do: clear_config([:instance, :federating], true)
|
setup do: clear_config([:instance, :federating], true)
|
||||||
|
|
||||||
|
defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do
|
||||||
|
assign_valid_signature_for_actor(conn, actor_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_valid_signature_for_actor(conn, actor) do
|
||||||
|
actor_id = Utils.get_ap_id(actor)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, true)
|
||||||
|
|> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"")
|
||||||
|
end
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
@ -688,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
|
@ -716,7 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
|
@ -726,6 +757,199 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert Activity.get_by_ap_id(data["id"])
|
assert Activity.get_by_ap_id(data["id"])
|
||||||
end
|
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"
|
||||||
|
|
||||||
|
data = %{
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => bob.ap_id,
|
||||||
|
"id" => "https://two.com/activities/inbox-forged-create",
|
||||||
|
"context" => "https://two.com/contexts/inbox-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/inbox-forged-create",
|
||||||
|
"content" => "forged post",
|
||||||
|
"published" => "2024-07-25T13:33:31Z",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|
||||||
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
|
assert [{:cancel, :actor_signature_mismatch}] =
|
||||||
|
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)
|
||||||
|
|
||||||
|
data = %{
|
||||||
|
"type" => "Like",
|
||||||
|
"actor" => bob.ap_id,
|
||||||
|
"id" => "https://two.com/activities/inbox-forged-like",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => note.data["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|
||||||
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
|
assert [{:cancel, :actor_signature_mismatch}] =
|
||||||
|
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
|
||||||
|
|
||||||
|
refute Activity.get_by_ap_id(data["id"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not delete an object 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)
|
||||||
|
object_id = note.data["id"]
|
||||||
|
|
||||||
|
data = %{
|
||||||
|
"type" => "Delete",
|
||||||
|
"actor" => bob.ap_id,
|
||||||
|
"id" => "https://two.com/activities/inbox-forged-delete",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => object_id
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:valid_signature, false)
|
||||||
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|
||||||
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
|
assert [{:cancel, :actor_signature_mismatch}] =
|
||||||
|
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
|
||||||
|
|
||||||
|
refute Activity.get_by_ap_id(data["id"])
|
||||||
|
assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not create a forged post signed by a different actor", %{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-signed-forged-note"
|
||||||
|
|
||||||
|
data = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type" => "Create",
|
||||||
|
"actor" => bob.ap_id,
|
||||||
|
"id" => "https://two.com/activities/inbox-signed-forged-create",
|
||||||
|
"context" => "https://two.com/contexts/inbox-signed-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/inbox-signed-forged-create",
|
||||||
|
"content" => "forged post",
|
||||||
|
"published" => "2024-07-25T13:33:31Z",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|
||||||
|
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||||
|
|> put_req_header(
|
||||||
|
"signature",
|
||||||
|
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||||
|
)
|
||||||
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
assert conn.assigns.valid_signature == false
|
||||||
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
|
assert [{:cancel, :actor_signature_mismatch}] =
|
||||||
|
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 signed by a different actor", %{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)
|
||||||
|
|
||||||
|
data = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type" => "Like",
|
||||||
|
"actor" => bob.ap_id,
|
||||||
|
"id" => "https://two.com/activities/inbox-signed-forged-like",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => note.data["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_signature_retry_from(alice)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|
||||||
|
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||||
|
|> put_req_header(
|
||||||
|
"signature",
|
||||||
|
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||||
|
)
|
||||||
|
|> post("/inbox", data)
|
||||||
|
|
||||||
|
assert conn.assigns.valid_signature == false
|
||||||
|
assert "ok" == json_response(conn, 200)
|
||||||
|
|
||||||
|
assert [{:cancel, :actor_signature_mismatch}] =
|
||||||
|
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
|
||||||
|
|
||||||
|
refute Activity.get_by_ap_id(data["id"])
|
||||||
|
end
|
||||||
|
|
||||||
test "accept follow activity", %{conn: conn} do
|
test "accept follow activity", %{conn: conn} do
|
||||||
clear_config([:instance, :federating], true)
|
clear_config([:instance, :federating], true)
|
||||||
relay = Relay.get_actor()
|
relay = Relay.get_actor()
|
||||||
|
|
@ -742,7 +966,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(followed_relay)
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", accept)
|
|> post("/inbox", accept)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -822,16 +1046,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
test "Unknown activity types are discarded", %{conn: conn} do
|
test "Unknown activity types are discarded", %{conn: conn} do
|
||||||
unknown_types = ["Poke", "Read", "Dazzle"]
|
unknown_types = ["Poke", "Read", "Dazzle"]
|
||||||
|
|
||||||
|
actor =
|
||||||
|
insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody")
|
||||||
|
|
||||||
Enum.each(unknown_types, fn bad_type ->
|
Enum.each(unknown_types, fn bad_type ->
|
||||||
params =
|
params =
|
||||||
%{
|
%{
|
||||||
"type" => bad_type,
|
"type" => bad_type,
|
||||||
"actor" => "https://unknown.mastodon.instance/users/somebody"
|
"actor" => actor.ap_id
|
||||||
}
|
}
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(actor)
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", params)
|
|> post("/inbox", params)
|
||||||
|> json_response(400)
|
|> json_response(400)
|
||||||
|
|
@ -900,7 +1127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -921,7 +1148,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -990,7 +1217,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -1009,7 +1236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
assert "ok" ==
|
assert "ok" ==
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/inbox", data)
|
|> post("/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -1040,7 +1267,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1061,7 +1288,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1082,7 +1309,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1106,7 +1333,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1133,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1163,7 +1390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{recipient.nickname}/inbox", data)
|
|> post("/users/#{recipient.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1228,7 +1455,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{recipient.nickname}/inbox", data)
|
|> post("/users/#{recipient.nickname}/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -1318,7 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{reported_user.nickname}/inbox", data)
|
|> post("/users/#{reported_user.nickname}/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -1372,7 +1599,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{reported_user.nickname}/inbox", data)
|
|> post("/users/#{reported_user.nickname}/inbox", data)
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
@ -1405,7 +1632,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1428,7 +1655,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -1451,7 +1678,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:valid_signature, true)
|
|> assign_valid_signature_for_actor(data["actor"])
|
||||||
|> put_req_header("content-type", "application/activity+json")
|
|> put_req_header("content-type", "application/activity+json")
|
||||||
|> post("/users/#{user.nickname}/inbox", data)
|
|> post("/users/#{user.nickname}/inbox", data)
|
||||||
|
|
||||||
|
|
@ -2571,6 +2798,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
setup do: clear_config([:media_proxy])
|
setup do: clear_config([:media_proxy])
|
||||||
setup do: clear_config([Pleroma.Upload])
|
setup do: clear_config([Pleroma.Upload])
|
||||||
|
|
||||||
|
# majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
|
||||||
|
@tag :skip_darwin
|
||||||
test "POST /api/ap/upload_media", %{conn: conn} do
|
test "POST /api/ap/upload_media", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
||||||
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
|
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the object can't be updated by the actor", %{
|
test "returns an error if the object can't be updated by the actor (different domain)", %{
|
||||||
valid_update: valid_update
|
valid_update: valid_update
|
||||||
} do
|
} do
|
||||||
other_user = insert(:user, local: false)
|
other_user = insert(:user, local: false)
|
||||||
|
|
@ -41,27 +41,72 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
||||||
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates as long as the object is same-origin with the actor", %{
|
test "returns an error if the object can't be updated by the actor (same domain)", %{
|
||||||
|
user: user,
|
||||||
valid_update: valid_update
|
valid_update: valid_update
|
||||||
} do
|
} do
|
||||||
other_user = insert(:user)
|
user_ap_id = user.ap_id
|
||||||
|
user_domain = URI.parse(user_ap_id).host
|
||||||
|
other_user = insert(:user, local: false, domain: user_domain)
|
||||||
|
|
||||||
update =
|
update =
|
||||||
valid_update
|
valid_update
|
||||||
|> Map.put("actor", other_user.ap_id)
|
|> Map.put("actor", other_user.ap_id)
|
||||||
|
|
||||||
assert {:ok, _update, []} = ObjectValidator.validate(update, [])
|
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates if the object is not of an Actor type" do
|
test "validates if the object is not of an Actor type", %{user: user} do
|
||||||
note = insert(:note)
|
note = insert(:note, user: user)
|
||||||
updated_note = note.data |> Map.put("content", "edited content")
|
updated_note = note.data |> Map.put("content", "edited content")
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, update, _} = Builder.update(other_user, updated_note)
|
{:ok, update, _} = Builder.update(user, updated_note)
|
||||||
|
|
||||||
assert {:ok, _update, _} = ObjectValidator.validate(update, [])
|
assert {:ok, _update, _} = ObjectValidator.validate(update, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns an error if the remote update target is unknown" do
|
||||||
|
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
|
||||||
|
|
||||||
|
update = %{
|
||||||
|
"type" => "Update",
|
||||||
|
"actor" => remote_user.ap_id,
|
||||||
|
"id" => "https://example.com/activities/update-unknown-object",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"id" => "https://example.com/objects/unknown",
|
||||||
|
"actor" => remote_user.ap_id,
|
||||||
|
"content" => "edited content",
|
||||||
|
"published" => "2024-07-25T13:33:31Z",
|
||||||
|
"updated" => "2024-07-25T13:34:31Z",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
|
||||||
|
refute cng.valid?
|
||||||
|
assert Keyword.has_key?(cng.errors, :object)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an error if the remote update target IRI is unknown" do
|
||||||
|
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
|
||||||
|
|
||||||
|
update = %{
|
||||||
|
"type" => "Update",
|
||||||
|
"actor" => remote_user.ap_id,
|
||||||
|
"id" => "https://example.com/activities/update-unknown-object-iri",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => "https://example.com/objects/unknown-iri"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
|
||||||
|
refute cng.valid?
|
||||||
|
assert Keyword.has_key?(cng.errors, :object)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "update note" do
|
describe "update note" do
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,27 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
|
||||||
assert %{valid_signature: false} == conn.assigns
|
assert %{valid_signature: false} == conn.assigns
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag skip: "known breakage; the testsuite presently depends on it"
|
|
||||||
test "it considers a mapped identity to be invalid when the identity cannot be found" do
|
test "it considers a mapped identity to be invalid when the identity cannot be found" do
|
||||||
|
actor = "http://niu.moe/users/rye"
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
build_conn(:post, "/doesntmattter", %{"actor" => actor})
|
||||||
|> set_signature("http://niu.moe/users/rye")
|
|> set_signature(actor)
|
||||||
|> MappedSignatureToIdentityPlug.call(%{})
|
|> MappedSignatureToIdentityPlug.call(%{})
|
||||||
|
|
||||||
assert %{valid_signature: false} == conn.assigns
|
assert conn.assigns.valid_signature == false
|
||||||
|
refute Map.has_key?(conn.assigns, :user)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it considers a mapped identity to be invalid when embedded actor identity cannot be found" do
|
||||||
|
actor = "http://niu.moe/users/rye"
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn(:post, "/doesntmattter", %{"actor" => %{"id" => actor}})
|
||||||
|
|> set_signature(actor)
|
||||||
|
|> MappedSignatureToIdentityPlug.call(%{})
|
||||||
|
|
||||||
|
assert conn.assigns.valid_signature == false
|
||||||
|
refute Map.has_key?(conn.assigns, :user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,27 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.Federator
|
|
||||||
alias Pleroma.Workers.ReceiverWorker
|
alias Pleroma.Workers.ReceiverWorker
|
||||||
|
|
||||||
|
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 perform_incoming(params) do
|
||||||
|
ReceiverWorker.perform(%Oban.Job{
|
||||||
|
args: %{"op" => "incoming_ap_doc", "params" => params}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
test "it does not retry MRF reject" do
|
test "it does not retry MRF reject" do
|
||||||
params = insert(:note).data
|
params = insert(:note).data
|
||||||
|
|
||||||
|
|
@ -81,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
|
||||||
|
|
@ -98,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
|
||||||
|
|
@ -115,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
|
||||||
|
|
@ -138,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
|
||||||
|
|
@ -233,16 +154,114 @@ 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 legacy retry jobs missing one metadata field" do
|
||||||
|
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||||
|
params = insert(:note_activity).data
|
||||||
|
|
||||||
|
assert {:cancel, :missing_signature_retry_metadata} =
|
||||||
|
ReceiverWorker.perform(%Oban.Job{
|
||||||
|
args: %{
|
||||||
|
"op" => "incoming_ap_doc",
|
||||||
|
"method" => "POST",
|
||||||
|
"params" => params,
|
||||||
|
"req_headers" => signature_headers_for(alice),
|
||||||
|
"request_path" => "/inbox"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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
|
||||||
|
|
|
||||||
574
test/pleroma/workers/signature_retry_worker_test.exs
Normal file
574
test/pleroma/workers/signature_retry_worker_test.exs
Normal file
|
|
@ -0,0 +1,574 @@
|
||||||
|
# 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 ExUnit.CaptureLog
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
@moduletag capture_log: true
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
capture_log([level: :warning], fn ->
|
||||||
|
assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job)
|
||||||
|
end)
|
||||||
|
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
|
||||||
|
|
||||||
|
log =
|
||||||
|
capture_log([level: :warning], fn ->
|
||||||
|
assert {:cancel, :missing_signature_retry_metadata} =
|
||||||
|
SignatureRetryWorker.perform(%Oban.Job{
|
||||||
|
args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert log =~ "Failed-signature inbox retry rejected"
|
||||||
|
assert log =~ "reason=:missing_signature_retry_metadata"
|
||||||
|
assert log =~ "payload_actor=#{inspect(params["actor"])}"
|
||||||
|
assert log =~ "activity_id=#{inspect(params["id"])}"
|
||||||
|
assert log =~ "type=#{inspect(params["type"])}"
|
||||||
|
assert log =~ "request_path=nil"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cancels retry jobs with malformed serialized request headers" do
|
||||||
|
params = insert(:note_activity).data
|
||||||
|
|
||||||
|
log =
|
||||||
|
capture_log([level: :warning], fn ->
|
||||||
|
assert {:cancel, :invalid_signature_retry_metadata} =
|
||||||
|
SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]]))
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert log =~ "Failed-signature inbox retry rejected"
|
||||||
|
assert log =~ "reason=:invalid_signature_retry_metadata"
|
||||||
|
assert log =~ "signature_actor=nil"
|
||||||
|
assert log =~ "request_path=\"/inbox\""
|
||||||
|
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
|
||||||
|
|
||||||
|
log =
|
||||||
|
capture_log([level: :warning], fn ->
|
||||||
|
assert {:cancel, :invalid_signature} =
|
||||||
|
SignatureRetryWorker.perform(
|
||||||
|
failed_signature_job(params, [{"host", "local.test"}])
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert log =~ "Failed-signature inbox retry rejected"
|
||||||
|
assert log =~ "reason=:invalid_signature"
|
||||||
|
assert log =~ "payload_actor=#{inspect(params["actor"])}"
|
||||||
|
assert log =~ "signature_actor=nil"
|
||||||
|
assert log =~ "request_path=\"/inbox\""
|
||||||
|
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)
|
||||||
|
|
||||||
|
log =
|
||||||
|
capture_log([level: :warning], fn ->
|
||||||
|
assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert log =~ "Failed-signature inbox retry rejected"
|
||||||
|
assert log =~ "reason=:invalid_signature"
|
||||||
|
assert log =~ "payload_actor=\"https://one.com/users/alice\""
|
||||||
|
assert log =~ "signature_actor=\"https://one.com/users/alice\""
|
||||||
|
assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
|
||||||
|
assert log =~ "type=\"Create\""
|
||||||
|
assert log =~ "request_path=\"/inbox\""
|
||||||
|
|
||||||
|
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 when payload actor is embedded" 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" => %{"id" => bob.ap_id},
|
||||||
|
"id" => "https://two.com/activities/embedded-actor-forged-create",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"id" => "https://two.com/objects/embedded-actor-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 "logs signature actor mismatch retry rejections" 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/logged-forged-create",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"id" => "https://two.com/objects/logged-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" => []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log = assert_mismatched_signature_cancelled(create, alice)
|
||||||
|
|
||||||
|
assert log =~ "Failed-signature inbox retry rejected"
|
||||||
|
assert log =~ "reason=:actor_signature_mismatch"
|
||||||
|
assert log =~ "payload_actor=\"https://two.com/users/bob\""
|
||||||
|
assert log =~ "signature_actor=\"https://one.com/users/alice\""
|
||||||
|
assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\""
|
||||||
|
assert log =~ "type=\"Create\""
|
||||||
|
assert log =~ "request_path=\"/inbox\""
|
||||||
|
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