Rework HTTPSignatures and fix bridgy interop #874

Merged
Oneric merged 13 commits from Oneric/akkoma:httpsig_rework into develop 2025-06-07 19:00:02 +00:00
25 changed files with 363 additions and 518 deletions

View file

@ -12,6 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- status and user HTML pages now provide ActivityPub alternate links
- the `prune_objects` mix task no longer deletes pinned posts by default
- added `--prune-pinned` and `--keep-followed {posts,full,none}` options to the `prune_objects` mix task
- timestamps of incoming HTTP signatures are now verified.
By default up to two hour old signatures and a maximal clock skew
of 40 min for future timestamps or explicit expiry deadlines are accepted
### Fixed
- Internal actors no longer pretend to have unresolvable follow(er|ing) collections
@ -21,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
this lead e.g. to unlisted replies from Pleroma instances being partially treated as private posts
- fixed our fetch actor advertising bogus follower and following collection ActivityPub IDs
- fix network-path references not being handled by media proxy
- federation with bridgy now works
- remote signing keys are no longer refreshed multiple times per incoming request
### Changed
- Internal and relay actors are now again represented with type "Application"
@ -28,6 +33,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- shared inboxes are now generally preferred over personal inboxes, cutting down on duplicate publishing churn
- instance actors are now really of type `Service`
- ActivityPub delivery attempts are spaced out more giving up after 3h instead of ~20min before
- inboxes now fake a succcess reply on incoming Delete documents whose signing key is unknown but gone;
this prevents older Mastodon from repeatedly trying to deliver Deletes of actors we never knew anyway
## 2025.03

View file

@ -161,7 +161,6 @@ defp cachex_children do
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300),
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("http_backoff", default_ttl: :timer.hours(24 * 30), limit: 10000)

View file

@ -103,7 +103,7 @@ defp load_pack(pack_dir, emoji_groups) do
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
Logger.info("Loading emoji pack from JSON: #{pack_file}")
Logger.debug("Loading emoji pack from JSON: #{pack_file}")
contents = Jason.decode!(File.read!(pack_file))
contents["files"]
@ -116,13 +116,13 @@ defp load_pack(pack_dir, emoji_groups) do
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
Logger.info("Loading emoji pack from emoji.txt: #{emoji_txt}")
Logger.debug("Loading emoji pack from emoji.txt: #{emoji_txt}")
load_from_file(emoji_txt, emoji_groups)
else
extensions = Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"
"No pack.json or emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"
)
make_shortcode_to_file_map(pack_dir, extensions)

View file

@ -77,6 +77,12 @@ defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do
defp next_backoff_timestamp(_), do: DateTime.utc_now() |> Timex.shift(seconds: 5 * 60)
defp log_ratelimit(429, host, time),
do: Logger.error("Rate limited on #{host}! Backing off until #{time}...")
defp log_ratelimit(503, host, time),
do: Logger.warning("#{host} temporarily unavailable! Backing off until #{time}...")
# utility function to check the HTTP response for potential backoff headers
# will check if we get a 429 or 503 response, and if we do, will back off for a bit
@spec check_backoff({:ok | :error, HTTP.Env.t()}, binary()) ::
@ -84,8 +90,8 @@ defp next_backoff_timestamp(_), do: DateTime.utc_now() |> Timex.shift(seconds: 5
defp check_backoff({:ok, env}, host) do
case env.status do
status when status in [429, 503] ->
Logger.error("Rate limited on #{host}! Backing off...")
timestamp = next_backoff_timestamp(env)
log_ratelimit(status, host, timestamp)
ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds)
# we will cache the host for 5 minutes
@cachex.put(@backoff_cache, host, true, ttl: ttl)

View file

@ -43,6 +43,4 @@ def host(url_or_host) when is_binary(url_or_host) do
url_or_host
end
end
defdelegate set_request_signatures(url_or_host), to: Instance
end

View file

@ -26,7 +26,6 @@ defmodule Pleroma.Instances.Instance do
field(:favicon, :string)
field(:metadata_updated_at, :naive_datetime)
field(:nodeinfo, :map, default: %{})
field(:has_request_signatures, :boolean)
timestamps()
end
@ -40,8 +39,7 @@ def changeset(struct, params \\ %{}) do
:unreachable_since,
:favicon,
:nodeinfo,
:metadata_updated_at,
:has_request_signatures
:metadata_updated_at
])
|> validate_required([:host])
|> unique_constraint(:host)
@ -332,24 +330,4 @@ def get_cached_by_url(url_or_host) do
end)
end
end
def set_request_signatures(url_or_host) when is_binary(url_or_host) do
host = host(url_or_host)
existing_record = Repo.get_by(Instance, %{host: host})
changes = %{has_request_signatures: true}
cond do
is_nil(existing_record) ->
%Instance{}
|> changeset(Map.put(changes, :host, host))
|> Repo.insert()
true ->
existing_record
|> changeset(changes)
|> Repo.update()
end
end
def set_request_signatures(_), do: {:error, :invalid_input}
end

View file

@ -233,9 +233,9 @@ defp make_signature(id, date) do
signature =
InternalFetchActor.get_actor()
|> Signature.sign(%{
"(request-target)": "get #{uri.path}",
host: uri.host,
date: date
"(request-target)" => "get #{uri.path}",
"host" => uri.host,
"date" => date
})
{"signature", signature}

View file

@ -5,52 +5,71 @@
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter
alias HTTPSignatures.HTTPKey
alias Pleroma.User
alias Pleroma.User.SigningKey
require Logger
def fetch_public_key(conn) do
with {_, %{"keyId" => kid}} <- {:keyid, HTTPSignatures.signature_for_conn(conn)},
{_, {:ok, %SigningKey{} = sk}, _} <-
{:fetch, SigningKey.get_or_fetch_by_key_id(kid), kid},
def fetch_public_key(kid, _) do
with {_, {:ok, %SigningKey{} = sk}} <- {:fetch, SigningKey.get_or_fetch_by_key_id(kid)},
{_, {%User{} = key_user, _}} <- {:user, {User.get_by_id(sk.user_id), sk.user_id}},
{_, {:ok, decoded_key}} <- {:decode, SigningKey.public_key_decoded(sk)} do
{:ok, decoded_key}
{:ok, %HTTPKey{key: decoded_key, user_data: %{"key_user" => key_user}}}
else
{:fetch, error, kid} ->
Logger.error("Failed to acquire key from signature: #{kid} #{inspect(error)}")
{:error, {:fetch, error}}
e ->
{:error, e}
handle_common_errors(e, kid, "acquire")
end
end
def refetch_public_key(conn) do
with {_, %{"keyId" => kid}} <- {:keyid, HTTPSignatures.signature_for_conn(conn)},
{_, {:ok, %SigningKey{} = sk}, _} <- {:fetch, SigningKey.refresh_by_key_id(kid), kid},
def refetch_public_key(kid, _) do
with {_, {:ok, %SigningKey{} = sk}} <- {:fetch, SigningKey.refresh_by_key_id(kid)},
{_, {%User{} = key_user, _}} <- {:user, {User.get_by_id(sk.user_id), sk.user_id}},
{_, {:ok, decoded_key}} <- {:decode, SigningKey.public_key_decoded(sk)} do
{:ok, decoded_key}
{:ok, %HTTPKey{key: decoded_key, user_data: %{"key_user" => key_user}}}
else
{:fetch, {:error, :too_young}, kid} ->
{:fetch, {:error, :too_young}} ->
Logger.debug("Refusing to refetch recently updated key: #{kid}")
{:error, {:fetch, :too_young}}
{:error, {:too_young, kid}}
{:fetch, {:error, :unknown}, kid} ->
{:fetch, {:error, :unknown}} ->
Logger.warning("Attempted to refresh unknown key; this should not happen: #{kid}")
{:error, {:fetch, :unknown}}
{:error, {:unknown, kid}}
{:fetch, error, kid} ->
Logger.error("Failed to refresh stale key from signature: #{kid} #{inspect(error)}")
e ->
handle_common_errors(e, kid, "refresh stale")
end
end
defp handle_common_errors(error, kid, action_name) do
case error do
{:fetch, {:error, :not_found}} ->
{:halt, {:error, :gone}}
{:fetch, {:reject, reason}} ->
{:halt, {:error, {:reject, reason}}}
{:fetch, error} ->
Logger.error("Failed to #{action_name} key from signature: #{kid} #{inspect(error)}")
{:error, {:fetch, error}}
{:user, {_, uid}} ->
Logger.warning(
"Failed to resolve user (id=#{uid}) for retrieved signing key. Race condition?"
)
e ->
{:error, e}
end
end
def sign(%User{} = user, headers) do
def sign(%User{} = user, headers, opts \\ []) do
with {:ok, private_key} <- SigningKey.private_key(user) do
HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers)
HTTPSignatures.sign(
%HTTPKey{key: private_key},
SigningKey.local_key_id(user.ap_id),
headers,
opts
)
end
end

View file

@ -208,7 +208,12 @@ def fetch_remote_key(key_id) do
else
e ->
Logger.debug("Failed to fetch remote key: #{inspect(e)}")
{:error, "Could not fetch key"}
case e do
{:error, e} -> {:error, e}
{:reject, reason} -> {:reject, reason}
_ -> {:error, {"Could not fetch key", e}}
end
end
end

View file

@ -304,8 +304,6 @@ def outbox(conn, %{"nickname" => nickname}) do
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")

View file

@ -57,13 +57,17 @@ def publish_one(
date = Pleroma.Signature.signed_date()
signature =
Pleroma.Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
date: date
})
Pleroma.Signature.sign(
actor,
%{
"(request-target)" => "post #{path}",
"host" => signature_host(uri),
"content-length" => byte_size(json),
"digest" => digest,
"date" => date
},
has_body: true
)
with {:ok, %{status: code}} = result when code in 200..299 <-
HTTP.post(

View file

@ -76,18 +76,6 @@ def label_in_message?(label, params),
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.any?(&label_in_collection?(label, &1))
@spec unaddressed_message?(map()) :: boolean()
def unaddressed_message?(params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.all?(&is_nil(&1))
@spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
do:
label_in_message?(ap_id, params) || unaddressed_message?(params) ||
User.following?(recipient, actor)
defp extract_list(target) when is_binary(target), do: [target]
defp extract_list(lst) when is_list(lst), do: lst
defp extract_list(_), do: []

View file

@ -1,32 +0,0 @@
defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do
@moduledoc """
This plug will attempt to pull in a user's public key if we do not have it.
We _should_ be able to request the URL from the key URL...
"""
alias Pleroma.User
def init(options), do: options
def call(conn, _opts) do
key_id = key_id_from_conn(conn)
unless is_nil(key_id) do
User.SigningKey.get_or_fetch_by_key_id(key_id)
# now we SHOULD have the user that owns the key locally. maybe.
# if we don't, we'll error out when we try to validate.
end
conn
end
defp key_id_from_conn(conn) do
case HTTPSignatures.signature_for_conn(conn) do
%{"keyId" => key_id} when is_binary(key_id) ->
key_id
_ ->
nil
end
end
end

View file

@ -6,14 +6,12 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
import Plug.Conn
import Phoenix.Controller, only: [get_format: 1]
alias HTTPSignatures.HTTPKey
use Pleroma.Web, :verified_routes
alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.User.SigningKey
require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def init(options) do
options
end
@ -26,70 +24,109 @@ def call(conn, _opts) do
if get_format(conn) in ["json", "activity+json"] do
conn
|> maybe_assign_valid_signature()
|> maybe_require_signature()
else
conn
end
end
def route_aliases(%{path_info: ["objects", id], query_string: query_string}) do
def route_aliases(%{path_info: ["objects", id], query_string: query_string, method: method}) do
ap_id = url(~p[/objects/#{id}])
method = String.downcase(method)
with %Activity{} = activity <- Activity.get_by_object_ap_id_with_object(ap_id) do
[~p"/notice/#{activity.id}", "/notice/#{activity.id}?#{query_string}"]
else
_ -> []
end
[
fn ->
with %Activity{} = activity <- Activity.get_by_object_ap_id_with_object(ap_id) do
["#{method} /notice/#{activity.id}", "#{method} /notice/#{activity.id}?#{query_string}"]
else
_ -> []
end
end
]
end
def route_aliases(_), do: []
def maybe_put_created_psudoheader(conn) do
case HTTPSignatures.signature_for_conn(conn) do
%{"created" => created} ->
put_req_header(conn, "(created)", created)
defp maybe_log_error(conn, verification_error) do
siginfo_str =
"<#{conn.method} #{conn.request_path}> #{inspect(get_req_header(conn, "signature"))}"
_ ->
conn
case verification_error do
:gone ->
# We can't verify the data since the actor was deleted and not previously known.
# Likely we just received the actors Delete activity, so just silently drop.
Logger.debug(
"Unable to verify request signature of deleted actor; dropping (#{siginfo_str})"
)
{:reject, reason} ->
Logger.debug(
"Refusing to validate signature from rejected key due to #{inspect(reason)} #{siginfo_str}"
)
:wrong_signature ->
Logger.warning("Received request with invalid signature!\n#{inspect(conn)}")
{:fetch_key, e} ->
Logger.info(
"Unable to verify request since key cannot be retrieved: #{inspect(e)} #{siginfo_str}"
)
error ->
Logger.error(
"Failed to verify request signature due to fatal error: #{inspect(error)} #{inspect(conn)}"
)
end
end
def maybe_put_expires_psudoheader(conn) do
case HTTPSignatures.signature_for_conn(conn) do
%{"expires" => expires} ->
put_req_header(conn, "(expires)", expires)
_ ->
conn
end
end
defp assign_valid_signature_on_route_aliases(conn, []), do: conn
defp assign_valid_signature_on_route_aliases(%{assigns: %{valid_signature: true}} = conn, _),
do: conn
defp assign_valid_signature_on_route_aliases(conn, [path | rest]) do
request_target = String.downcase("#{conn.method}") <> " #{path}"
conn =
conn
|> put_req_header("(request-target)", request_target)
|> maybe_put_created_psudoheader()
|> maybe_put_expires_psudoheader()
conn
|> assign(:valid_signature, HTTPSignatures.validate_conn(conn))
|> assign(:signature_actor_id, signature_host(conn))
|> assign_valid_signature_on_route_aliases(rest)
end
defp maybe_halt(conn, :gone) do
# If the key was deleted the error is basically unrecoverable.
# Most likely it was the Delete activity for the key actor and we never knew about this actor before.
# Older Mastodon is very insistent about resending those Deletes until it receives a success.
# see: https://github.com/mastodon/mastodon/pull/33617
with "POST" <- conn.method,
%{"type" => "Delete"} <- conn.body_params do
conn
|> resp(202, "Accepted")
|> halt()
else
_ -> conn
end
end
defp maybe_halt(conn, _), do: conn
defp assign_valid_signature(%{assigns: %{valid_signature: true}} = conn, _),
do: conn
defp assign_valid_signature(conn, request_targets) do
case HTTPSignatures.validate_conn(conn, request_targets) do
{:ok, %HTTPKey{user_data: ud}} ->
conn
|> assign(:valid_signature, true)
|> assign(:signature_user, ud["key_user"])
{:error, e} ->
conn
|> assign(:valid_signature, false)
|> assign(:signature_user, nil)
|> maybe_log_error(e)
|> maybe_halt(e)
end
end
defp maybe_assign_valid_signature(conn) do
if has_signature_header?(conn) do
# set (request-target) header to the appropriate value
# we also replace the digest header with the one we computed
possible_paths =
[conn.request_path, conn.request_path <> "?#{conn.query_string}" | route_aliases(conn)]
method = String.downcase(conn.method)
request_targets =
[
"#{method} " <> conn.request_path,
"#{method} " <> conn.request_path <> "?#{conn.query_string}" | route_aliases(conn)
]
conn =
case conn do
@ -97,7 +134,7 @@ defp maybe_assign_valid_signature(conn) do
conn -> conn
end
assign_valid_signature_on_route_aliases(conn, possible_paths)
assign_valid_signature(conn, request_targets)
else
Logger.debug("No signature header!")
conn
@ -107,53 +144,4 @@ defp maybe_assign_valid_signature(conn) do
defp has_signature_header?(conn) do
conn |> get_req_header("signature") |> Enum.at(0, false)
end
defp maybe_require_signature(
%{assigns: %{valid_signature: true, signature_actor_id: actor_id}} = conn
) do
# inboxes implicitly need http signatures for authentication
# so we don't really know if the instance will have broken federation after
# we turn on authorized_fetch_mode.
#
# to "check" this is a signed fetch, verify if method is GET
if conn.method == "GET" do
actor_host = URI.parse(actor_id).host
case @cachex.get(:request_signatures_cache, actor_host) do
{:ok, nil} ->
Logger.debug("Successful signature from #{actor_host}")
Instances.set_request_signatures(actor_host)
@cachex.put(:request_signatures_cache, actor_host, true)
{:ok, true} ->
:noop
any ->
Logger.warning(
"expected request signature cache to return a boolean, instead got #{inspect(any)}"
)
end
end
conn
end
defp maybe_require_signature(conn), do: conn
defp signature_host(conn) do
with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)},
{:actor_id, actor_id, _} when actor_id != nil <-
{:actor_id, SigningKey.key_id_to_ap_id(kid), kid} do
actor_id
else
{:key_id, e} ->
Logger.error("Failed to extract key_id from signature: #{inspect(e)}")
nil
{:actor_id, _, kid} ->
# SigningKeys SHOULD have been fetched before this gets called!
Logger.error("Failed to extract actor_id from signature: signing key #{kid} not known")
nil
end
end
end

View file

@ -15,101 +15,60 @@ def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _opts), do: conn
# if this has payload make sure it is signed by the same actor that made it
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
def call(
%{
assigns: %{valid_signature: true, signature_user: signature_user},
params: %{"actor" => actor}
} = conn,
_opts
) do
with actor_id <- Utils.get_ap_id(actor),
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
{:federate, true} <- {:federate, should_federate?(user)},
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
{:federate, true} <- {:federate, should_federate?(signature_user)},
{:user_match, true} <- {:user_match, signature_user.ap_id == actor_id} do
conn
|> assign(:user, user)
|> assign(:user, signature_user)
|> AuthHelper.skip_oauth()
else
{:user_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
conn
|> assign(:valid_signature, false)
Logger.debug(
"key_user=#{signature_user.id}(#{signature_user.ap_id}), actor=#{inspect(actor)}"
)
# remove me once testsuite uses mapped capabilities instead of what we do now
{:user, _} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
assign(conn, :valid_signature, false)
conn
|> assign(:valid_signature, false)
{:federate, false} ->
Logger.debug("Identity from signature is instance blocked")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
conn
|> assign(:valid_signature, false)
error ->
handle_common_errors(conn, actor, signature_user, error)
end
end
# no payload, probably a signed fetch
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
with %User{} = user <- user_from_key_id(conn),
{:federate, true} <- {:federate, should_federate?(user)} do
def call(%{assigns: %{valid_signature: true, signature_user: signature_user}} = conn, _opts) do
with {:federate, true} <- {:federate, should_federate?(signature_user)} do
conn
|> assign(:user, user)
|> assign(:user, signature_user)
|> AuthHelper.skip_oauth()
else
{:federate, false} ->
Logger.debug("Identity from signature is instance blocked")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
conn
|> assign(:valid_signature, false)
nil ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
conn
|> assign(:valid_signature, false)
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
conn
|> assign(:valid_signature, false)
error -> handle_common_errors(conn, nil, signature_user, error)
end
end
# supposedly valid signature but no user (this isnt supposed to happen)
def call(%{assigns: %{valid_signature: true}} = conn, _opts),
do: assign(conn, :valid_signature, false)
# no signature at all
def call(conn, _opts), do: conn
defp key_id_from_conn(conn) do
case HTTPSignatures.signature_for_conn(conn) do
%{"keyId" => key_id} when is_binary(key_id) ->
key_id
def handle_common_errors(conn, actor, signature_user, error) do
actor_str = if actor == nil, do: "", else: " actor=#{inspect(actor)}"
_ ->
nil
end
end
defp user_from_key_id(conn) do
with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)},
{:mapped_ap_id, ap_id} when is_binary(ap_id) <-
{:mapped_ap_id, User.SigningKey.key_id_to_ap_id(key_id)},
{:user_fetch, {:ok, %User{} = user}} <- {:user_fetch, User.get_or_fetch_by_ap_id(ap_id)} do
user
else
{:key_id, nil} ->
Logger.debug("Failed to map identity from signature (no key ID)")
{:key_id, nil}
{:mapped_ap_id, nil} ->
Logger.debug("Failed to map identity from signature (could not map key ID to AP ID)")
{:mapped_ap_id, nil}
{:user_fetch, {:error, _}} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
{:user_fetch, nil}
case error do
{:federate, false} ->
Logger.debug("Identity from signature is instance blocked")
Logger.debug("key_user=#{signature_user.nickname}(#{signature_user.id})#{actor_str}")
assign(conn, :valid_signature, false)
end
end

View file

@ -146,13 +146,11 @@ defmodule Pleroma.Web.Router do
end
pipeline :optional_http_signature do
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)

View file

@ -162,7 +162,7 @@ defp deps do
{:linkify, "~> 0.5.3"},
{:http_signatures,
git: "https://akkoma.dev/AkkomaGang/http_signatures.git",
ref: "d44c43d66758c6a73eaa4da9cffdbee0c5da44ae"},
ref: "c98a4df78b0dfde15ff1c8b9ce434440be619bf7"},
{:telemetry, "~> 1.2"},
{:telemetry_poller, "~> 1.0"},
{:telemetry_metrics, "~> 0.6"},

View file

@ -58,7 +58,7 @@
"hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:git, "https://akkoma.dev/AkkomaGang/http_signatures.git", "d44c43d66758c6a73eaa4da9cffdbee0c5da44ae", [ref: "d44c43d66758c6a73eaa4da9cffdbee0c5da44ae"]},
"http_signatures": {:git, "https://akkoma.dev/AkkomaGang/http_signatures.git", "c98a4df78b0dfde15ff1c8b9ce434440be619bf7", [ref: "c98a4df78b0dfde15ff1c8b9ce434440be619bf7"]},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"igniter": {:hex, :igniter, "0.5.29", "6bf7ddaf15e88ae75f6dad514329530ec8f4721ba14782f6386a7345c1be99fd", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "beb6e0f69fc6d4e3975ffa26c5459fc63fd96f85cfaeba984c2dfd3d7333b6ad"},

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.DropInstanceHasRequestSignatures do
use Ecto.Migration
def change do
alter table(:instances) do
remove(:has_request_signatures, :boolean, default: false, null: false)
end
end
end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.InstancesTest do
alias Pleroma.Instances
alias Pleroma.Instances.Instance
use Pleroma.DataCase
@ -122,21 +121,4 @@ test "keeps unreachable url or host unreachable" do
refute Instances.reachable?(host)
end
end
describe "set_request_signatures/1" do
test "sets instance has request signatures" do
host = "domain.com"
{:ok, instance} = Instances.set_request_signatures(host)
assert instance.has_request_signatures
{:ok, cached_instance} = Instance.get_cached_by_url(host)
assert cached_instance.has_request_signatures
end
test "returns error status on non-binary input" do
assert {:error, _} = Instances.set_request_signatures(nil)
assert {:error, _} = Instances.set_request_signatures(1)
end
end
end

View file

@ -10,7 +10,9 @@ defmodule Pleroma.SignatureTest do
import Tesla.Mock
import Mock
alias HTTPSignatures.HTTPKey
alias Pleroma.Signature
alias Pleroma.User.SigningKey
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@ -28,20 +30,27 @@ defmodule Pleroma.SignatureTest do
65_537
}
defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\""
defp keyid(user = %Pleroma.User{}), do: keyid(user.ap_id)
defp keyid(user_ap_id), do: user_ap_id <> "#main-key"
defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
defp assert_key(retval, refkey, refuser) do
assert match?(
{:ok, %HTTPKey{key: ^refkey, user_data: %{"key_user" => %Pleroma.User{}}}},
retval
)
{:ok, key} = retval
# Avoid comparison failures from (not) loaded Ecto associations etc
assert refuser.id == key.user_data["key_user"].id
end
describe "fetch_public_key/1" do
test "it returns the key" do
expected_result = {:ok, @rsa_public_key}
user =
insert(:user)
|> with_signing_key(public_key: @public_key)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
assert_key(Signature.fetch_public_key(keyid(user), nil), @rsa_public_key, user)
end
test "it returns error if public key is nil" do
@ -50,7 +59,7 @@ test "it returns error if public key is nil" do
key_id = user.ap_id <> "#main-key"
Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end)
assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id))
assert {:error, _} = Signature.fetch_public_key(keyid(user), nil)
end
end
@ -60,16 +69,17 @@ test "it returns key" do
ap_id = "https://mastodon.social/users/lambadalambda"
%Pleroma.User{signing_key: sk} =
user =
Pleroma.User.get_or_fetch_by_ap_id(ap_id)
|> then(fn {:ok, u} -> u end)
|> Pleroma.User.SigningKey.load_key()
|> SigningKey.load_key()
{:ok, _} =
%{sk | public_key: "-----BEGIN PUBLIC KEY-----\nasdfghjkl"}
|> Ecto.Changeset.change()
|> Pleroma.Repo.update()
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
assert_key(Signature.refetch_public_key(keyid(ap_id), nil), @rsa_public_key, user)
end
end
@ -111,8 +121,11 @@ test "it returns signature headers" do
|> with_signing_key(private_key: @private_key)
headers = %{
host: "test.test",
"content-length": "100"
"host" => "test.test",
"content-length" => "100",
"date" => "Fri, 23 Aug 2019 18:11:24 GMT",
"digest" => "SHA-256=a29cdd711788c5118a2256c00d31519e0a5a0d4b144214e012f81e67b80b0ec1",
"(request-target)" => "post https://example.com/inbox"
}
assert_signature_equal(
@ -120,7 +133,7 @@ test "it returns signature headers" do
user,
headers
),
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
~s|keyId="https://mastodon.social/users/lambadalambda#main-key",algorithm="rsa-sha256",headers="(request-target) content-length date digest host",signature="fhOT6IBThnCo6rv2Tv8BRXLV7LvVf/7wTX/bbPLtdq5A4GUqrmXUcY5p77jQ6NU9IRIVczeeStxQV6TrHqk/qPdqQOzDcB6cWsSfrB1gsTinBbAWdPzQYqUOTl+Minqn2RERAfPebKYr9QGa0sTODDHvze/UFPuL8a1lDO2VQE0lRCdg49Igr8pGl/CupUx8Fb874omqP0ba3M+siuKEwo02m9hHcbZUeLSN0ZVdvyTMttyqPM1BfwnFXkaQRAblLTyzt4Fv2+fTN+zPipSxJl1YIo1TsmwNq9klqImpjh8NHM3MJ5eZxTZ109S6Q910n1Lm46V/SqByDaYeg9g7Jw=="|
)
end
end

View file

@ -555,14 +555,12 @@ test "cached purged after activity deletion", %{conn: conn} do
describe "/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
{:ok, actor} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header(
"signature",
"keyId=\"http://mastodon.example.org/users/admin#main-key\""
)
|> assign(:signature_user, actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@ -592,7 +590,7 @@ test "it inserts an incoming activity into the database" <>
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"")
|> assign(:signature_user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@ -617,7 +615,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@ -642,7 +640,7 @@ test "accept follow activity", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}#main-key\"")
|> assign(:signature_user, followed_relay)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept)
|> json_response(200)
@ -681,10 +679,11 @@ test "accepts Add/Remove activities", %{conn: conn} do
actor = "https://example.com/users/lain"
key_id = "#{actor}#main-key"
insert(:user,
ap_id: actor,
featured_address: "https://example.com/users/lain/collections/featured"
)
sender =
insert(:user,
ap_id: actor,
featured_address: "https://example.com/users/lain/collections/featured"
)
Tesla.Mock.mock(fn
%{
@ -741,7 +740,7 @@ test "accepts Add/Remove activities", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor}#main-key\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@ -764,7 +763,7 @@ test "accepts Add/Remove activities", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor}#main-key\"")
|> assign(:signature_user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@ -863,7 +862,7 @@ test "mastodon pin/unpin", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@ -883,7 +882,7 @@ test "mastodon pin/unpin", %{conn: conn} do
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor}#main-key\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@ -912,10 +911,12 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da
|> Map.put("bcc", [user.ap_id])
|> Kernel.put_in(["object", "bcc"], [user.ap_id])
{:ok, sender} = User.get_or_fetch_by_ap_id(data["actor"])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{data["actor"]}#main-key\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@ -936,10 +937,12 @@ test "it accepts messages with to as string instead of array", %{conn: conn, dat
|> Kernel.put_in(["object", "to"], user.ap_id)
|> Kernel.put_in(["object", "cc"], [])
{:ok, sender} = User.get_or_fetch_by_ap_id(data["actor"])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{data["actor"]}#main-key\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@ -958,10 +961,12 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat
|> Kernel.put_in(["object", "to"], [])
|> Kernel.put_in(["object", "cc"], user.ap_id)
{:ok, sender} = User.get_or_fetch_by_ap_id(data["actor"])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{data["actor"]}#main-key\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@ -985,10 +990,12 @@ test "it accepts messages with bcc as string instead of array", %{conn: conn, da
|> Kernel.put_in(["object", "cc"], [])
|> Kernel.put_in(["object", "bcc"], user.ap_id)
{:ok, sender} = User.get_or_fetch_by_ap_id(data["actor"])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{data["actor"]}#main-key\"")
|> assign(:signature_user, sender)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@ -1019,7 +1026,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"")
|> assign(:signature_user, announcer)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@ -1053,7 +1060,7 @@ test "it accepts messages from actors that are followed by the user", %{
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|> assign(:signature_user, actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
@ -1103,7 +1110,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"")
|> assign(:signature_user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@ -1143,7 +1150,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|> assign(:signature_user, actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
@ -1239,7 +1246,7 @@ test "forwarded report", %{conn: conn} do
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"")
|> assign(:signature_user, actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
@ -1260,39 +1267,15 @@ test "forwarded report from mastodon", %{conn: conn} do
admin = insert(:user, is_admin: true)
actor = insert(:user, local: false)
remote_domain = URI.parse(actor.ap_id).host
remote_actor = "https://#{remote_domain}/actor"
[reported_user, another] = insert_list(2, :user)
note = insert(:note_activity, user: reported_user)
Pleroma.Web.CommonAPI.favorite(another, note.id)
mock_json_body =
"test/fixtures/mastodon/application_actor.json"
|> File.read!()
|> String.replace("{{DOMAIN}}", remote_domain)
key_url = "#{remote_actor}#main-key"
Tesla.Mock.mock(fn
%{url: ^remote_actor} ->
%Tesla.Env{
status: 200,
body: mock_json_body,
headers: [{"content-type", "application/activity+json"}]
}
%{url: ^key_url} ->
%Tesla.Env{
status: 200,
body: mock_json_body,
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => remote_actor,
"actor" => actor.ap_id,
"content" => "test report",
"id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
"object" => [
@ -1304,7 +1287,7 @@ test "forwarded report from mastodon", %{conn: conn} do
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{remote_actor}#main-key\"")
|> assign(:signature_user, actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)

View file

@ -332,74 +332,6 @@ test "fetches last block activities" do
end
end
describe "recipient_in_message/3" do
test "returns true when recipient in `to`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"to" => [recipient.ap_id], "cc" => ""}
)
end
test "returns true when recipient in `cc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"cc" => [recipient.ap_id], "to" => ""}
)
end
test "returns true when recipient in `bto`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bcc" => "", "bto" => [recipient.ap_id]}
)
end
test "returns true when recipient in `bcc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bto" => "", "bcc" => [recipient.ap_id]}
)
end
test "returns true when message without addresses fields" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"btod" => "", "bccc" => [recipient.ap_id]}
)
end
test "returns false" do
recipient = insert(:user)
actor = insert(:user)
refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
end
end
describe "lazy_put_activity_defaults/2" do
test "returns map with id and published data" do
note_activity = insert(:note_activity)

View file

@ -7,13 +7,13 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
@moduletag :mocked
import Pleroma.Factory
alias Pleroma.Web.Plugs.HTTPSignaturePlug
alias Pleroma.Instances.Instance
alias Pleroma.Repo
import Plug.Conn
import Phoenix.Controller, only: [put_format: 2]
import Mock
@user_ap_id "http://mastodon.example.org/users/admin"
setup do
user =
:user
@ -26,35 +26,27 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
setup_with_mocks([
{HTTPSignatures, [],
[
signature_for_conn: fn _ ->
%{
"keyId" => "http://mastodon.example.org/users/admin#main-key",
"created" => "1234567890",
"expires" => "1234567890"
}
end,
validate_conn: fn conn ->
Map.get(conn.assigns, :valid_signature, true)
validate_conn: fn conn, _ ->
cond do
Map.get(conn.assigns, :gone_signature_key, false) ->
{:error, :gone}
Map.get(conn.assigns, :rejected_key_id, false) ->
{:error, {:reject, :mrf}}
Map.get(conn.assigns, :valid_signature, true) ->
{:ok, user} = Pleroma.User.get_or_fetch_by_ap_id(@user_ap_id)
{:ok, %HTTPSignatures.HTTPKey{key: "aaa", user_data: %{"key_user" => user}}}
true ->
{:error, :wrong_signature}
end
end
]}
]) do
:ok
end
defp submit_to_plug(host), do: submit_to_plug(host, :get, "/doesntmattter")
defp submit_to_plug(host, method, path) do
params = %{"actor" => "http://#{host}/users/admin"}
build_conn(method, path, params)
|> put_req_header(
"signature",
"keyId=\"http://#{host}/users/admin#main-key"
)
|> put_format("activity+json")
|> HTTPSignaturePlug.call(%{})
end
test "it call HTTPSignatures to check validity if the actor signed it", %{user: user} do
params = %{"actor" => user.ap_id}
conn = build_conn(:get, "/doesntmattter", params)
@ -69,36 +61,9 @@ test "it call HTTPSignatures to check validity if the actor signed it", %{user:
|> HTTPSignaturePlug.call(%{})
assert conn.assigns.valid_signature == true
assert conn.assigns.signature_actor_id == params["actor"]
assert conn.assigns.signature_user.ap_id == params["actor"]
assert conn.halted == false
assert called(HTTPSignatures.validate_conn(:_))
end
test "it sets request signatures property on the instance" do
host = "mastodon.example.org"
conn = submit_to_plug(host)
assert conn.assigns.valid_signature == true
instance = Repo.get_by(Instance, %{host: host})
assert instance.has_request_signatures
end
test "it does not set request signatures property on the instance when using inbox" do
host = "mastodon.example.org"
conn = submit_to_plug(host, :post, "/inbox")
assert conn.assigns.valid_signature == true
# we don't even create the instance entry if its just POST /inbox
refute Repo.get_by(Instance, %{host: host})
end
test "it does not set request signatures property on the instance when its cached" do
host = "mastodon.example.org"
Cachex.put(:request_signatures_cache, host, true)
conn = submit_to_plug(host)
assert conn.assigns.valid_signature == true
# we don't even create the instance entry if it was already done
refute Repo.get_by(Instance, %{host: host})
assert called(HTTPSignatures.validate_conn(:_, :_))
end
describe "requires a signature when `authorized_fetch_mode` is enabled" do
@ -122,7 +87,7 @@ test "and signature is present and incorrect", %{conn: conn} do
|> HTTPSignaturePlug.call(%{})
assert conn.assigns.valid_signature == false
assert called(HTTPSignatures.validate_conn(:_))
assert called(HTTPSignatures.validate_conn(:_, :_))
end
test "and signature is correct", %{conn: conn} do
@ -135,7 +100,7 @@ test "and signature is correct", %{conn: conn} do
|> HTTPSignaturePlug.call(%{})
assert conn.assigns.valid_signature == true
assert called(HTTPSignatures.validate_conn(:_))
assert called(HTTPSignatures.validate_conn(:_, :_))
end
test "and halts the connection when `signature` header is not present", %{conn: conn} do
@ -151,21 +116,66 @@ test "aliases redirected /object endpoints", _ do
path = URI.parse(obj.data["id"]).path
conn = build_conn(:get, path, params)
assert ["/notice/#{act.id}", "/notice/#{act.id}?actor=someparam"] ==
HTTPSignaturePlug.route_aliases(conn)
aliases =
HTTPSignaturePlug.route_aliases(conn)
|> Enum.reduce([], fn
x, acc when is_binary(x) ->
acc ++ [x]
f, acc when is_function(f) ->
add =
case f.() do
a when is_binary(a) -> [a]
a -> a
end
acc ++ add
end)
assert ["get /notice/#{act.id}", "get /notice/#{act.id}?actor=someparam"] == aliases
end
test "(created) psudoheader", _ do
conn = build_conn(:get, "/doesntmattter")
conn = HTTPSignaturePlug.maybe_put_created_psudoheader(conn)
created_header = List.keyfind(conn.req_headers, "(created)", 0)
assert {_, "1234567890"} = created_header
test "fakes success on gone key when receiving Delete" do
build_conn(:post, "/inbox", %{"type" => "Delete"})
|> put_format("activity+json")
|> assign(:gone_signature_key, true)
|> put_req_header(
"signature",
"keyId=\"http://somewhere.example.org/users/deleted#main-key\""
)
|> HTTPSignaturePlug.call(%{})
|> response(202)
end
test "(expires) psudoheader", _ do
conn = build_conn(:get, "/doesntmattter")
conn = HTTPSignaturePlug.maybe_put_expires_psudoheader(conn)
expires_header = List.keyfind(conn.req_headers, "(expires)", 0)
assert {_, "1234567890"} = expires_header
test "fails on gone key for non-Delete" do
conn =
build_conn(:post, "/inbox", %{"type" => "Note"})
|> put_format("activity+json")
|> assign(:gone_signature_key, true)
|> put_req_header(
"signature",
"keyId=\"http://somewhere.example.org/users/deleted#main-key\""
)
|> HTTPSignaturePlug.call(%{})
refute conn.halted
assert conn.assigns.valid_signature == false
assert conn.assigns.signature_user == nil
end
test "fails on rejected keys", %{user: user} do
conn =
build_conn(:post, "/inbox", %{"type" => "Note"})
|> put_format("activity+json")
|> assign(:rejected_key_id, true)
|> put_req_header(
"signature",
"keyId=\"#{user.signing_key.key_id}\""
)
|> HTTPSignaturePlug.call(%{})
refute conn.halted
assert conn.assigns.valid_signature == false
assert conn.assigns.signature_user == nil
end
end

View file

@ -22,16 +22,21 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
{:ok, %{user: user}}
end
defp set_signature(conn, ap_id) do
defp set_signature(conn, %Pleroma.User{} = user) do
conn
|> put_req_header("signature", "keyId=\"#{ap_id}#main-key\"")
|> assign(:valid_signature, true)
|> assign(:signature_user, user)
end
defp set_signature(conn, ap_id) when is_binary(ap_id) do
{:ok, user} = Pleroma.User.get_or_fetch_by_ap_id(ap_id)
set_signature(conn, user)
end
test "it successfully maps a valid identity with a valid signature", %{user: user} do
conn =
build_conn(:get, "/doesntmattter")
|> set_signature(user.ap_id)
|> set_signature(user)
|> MappedSignatureToIdentityPlug.call(%{})
refute is_nil(conn.assigns.user)
@ -40,7 +45,7 @@ test "it successfully maps a valid identity with a valid signature", %{user: use
test "it successfully maps a valid identity with a valid signature with payload", %{user: user} do
conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|> set_signature(user.ap_id)
|> set_signature(user)
|> MappedSignatureToIdentityPlug.call(%{})
refute is_nil(conn.assigns.user)
@ -52,7 +57,9 @@ test "it considers a mapped identity to be invalid when it mismatches a payload"
|> set_signature("https://niu.moe/users/rye")
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
assert conn.assigns.valid_signature == false
refute is_nil(conn.assigns.signature_user)
refute match?(%{user: _}, conn.assigns)
end
test "it considers a mapped identity to be invalid when the associated instance is blocked", %{
@ -74,10 +81,12 @@ test "it considers a mapped identity to be invalid when the associated instance
conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|> set_signature(user.ap_id)
|> set_signature(user)
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
assert conn.assigns.valid_signature == false
refute is_nil(conn.assigns.signature_user)
refute match?(%{user: _}, conn.assigns)
end
test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed",
@ -97,7 +106,7 @@ test "allowlist federation: it considers a mapped identity to be valid when the
conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|> set_signature(user.ap_id)
|> set_signature(user)
|> MappedSignatureToIdentityPlug.call(%{})
assert conn.assigns[:valid_signature]
@ -119,19 +128,11 @@ test "allowlist federation: it considers a mapped identity to be invalid when th
conn =
build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id})
|> set_signature(user.ap_id)
|> set_signature(user)
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
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
conn =
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> set_signature("http://niu.moe/users/rye")
|> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns
assert conn.assigns.valid_signature == false
refute is_nil(conn.assigns.signature_user)
refute match?(%{user: _}, conn.assigns)
end
end