Reject requests from specified instances if authorized_fetch_mode is enabled

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-01-25 12:14:52 +01:00
parent 51edd70319
commit 3a23924ee8
4 changed files with 148 additions and 12 deletions

View file

@ -44,8 +44,7 @@ defp remove_suffix(uri, [test | rest]) do
defp remove_suffix(uri, []), do: uri
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
with {:ok, actor_id} <- get_actor_id(conn),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
@ -55,8 +54,7 @@ def fetch_public_key(conn) do
end
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
with {:ok, actor_id} <- get_actor_id(conn),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
@ -66,6 +64,16 @@ def refetch_public_key(conn) do
end
end
def get_actor_id(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid) do
{:ok, actor_id}
else
e ->
{:error, e}
end
end
def sign(%User{keys: keys} = user, headers) do
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)

View file

@ -5,6 +5,10 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
import Plug.Conn
import Phoenix.Controller, only: [get_format: 1, text: 2]
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger
def init(options) do
@ -19,7 +23,9 @@ def call(conn, _opts) do
if get_format(conn) in ["json", "activity+json"] do
conn
|> maybe_assign_valid_signature()
|> maybe_assign_actor_id()
|> maybe_require_signature()
|> maybe_filter_requests()
else
conn
end
@ -83,6 +89,16 @@ defp maybe_assign_valid_signature(conn) do
end
end
defp maybe_assign_actor_id(%{assigns: %{valid_signature: true}} = conn) do
adapter = Application.get_env(:http_signatures, :adapter)
{:ok, actor_id} = adapter.get_actor_id(conn)
assign(conn, :actor_id, actor_id)
end
defp maybe_assign_actor_id(conn), do: conn
defp has_signature_header?(conn) do
conn |> get_req_header("signature") |> Enum.at(0, false)
end
@ -90,13 +106,62 @@ defp has_signature_header?(conn) do
defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
defp maybe_require_signature(conn) do
cond do
get_ip(conn) in Config.get([:instance, :trusted_unsigned], []) ->
conn
|> assign(:valid_signature, true)
|> assign(:actor_id, Pleroma.Web.ActivityPub.Relay.ap_id)
Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) ->
conn
|> put_status(:unauthorized)
|> text("Request not signed")
|> halt()
true ->
conn
end
end
defp maybe_filter_requests(%{halted: true} = conn), do: conn
defp maybe_filter_requests(conn) do
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
conn
|> put_status(:unauthorized)
|> text("Request not signed")
|> halt()
%{host: host} = URI.parse(conn.assigns.actor_id)
if MRF.subdomain_match?(rejected_domains(), host) do
conn
|> put_status(:unauthorized)
|> halt()
else
conn
end
else
conn
end
end
defp rejected_domains do
Config.get([:instance, :rejected_instances])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
end
defp get_ip(conn) do
forwarded_for =
conn
|> Plug.Conn.get_req_header("x-forwarded-for")
|> List.first()
if forwarded_for do
forwarded_for
|> String.split(",")
|> Enum.map(&String.trim/1)
|> List.first()
else
conn.remote_ip
|> :inet_parse.ntoa()
|> to_string()
end
end
end

View file

@ -67,6 +67,14 @@ test "it returns error when not found user" do
end
end
describe "get_actor_id/1" do
test "it returns actor id" do
ap_id = "https://mastodon.social/users/lambadalambda"
assert Signature.get_actor_id(make_fake_conn(ap_id)) == {:ok, ap_id}
end
end
describe "sign/2" do
test "it returns signature headers" do
user =

View file

@ -10,11 +10,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
import Phoenix.Controller, only: [put_format: 2]
import Mock
test "it call HTTPSignatures to check validity if the actor sighed it" do
test "it call HTTPSignatures to check validity if the actor signed it" do
params = %{"actor" => "http://mastodon.example.org/users/admin"}
conn = build_conn(:get, "/doesntmattter", params)
with_mock HTTPSignatures, validate_conn: fn _ -> true end do
with_mock HTTPSignatures,
validate_conn: fn _ -> true end,
signature_for_conn: fn _ ->
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
end do
conn =
conn
|> put_req_header(
@ -41,7 +45,11 @@ test "it call HTTPSignatures to check validity if the actor sighed it" do
end
test "when signature header is present", %{conn: conn} do
with_mock HTTPSignatures, validate_conn: fn _ -> false end do
with_mock HTTPSignatures,
validate_conn: fn _ -> false end,
signature_for_conn: fn _ ->
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
end do
conn =
conn
|> put_req_header(
@ -58,7 +66,11 @@ test "when signature header is present", %{conn: conn} do
assert called(HTTPSignatures.validate_conn(:_))
end
with_mock HTTPSignatures, validate_conn: fn _ -> true end do
with_mock HTTPSignatures,
validate_conn: fn _ -> true end,
signature_for_conn: fn _ ->
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
end do
conn =
conn
|> put_req_header(
@ -82,4 +94,47 @@ test "halts the connection when `signature` header is not present", %{conn: conn
assert conn.resp_body == "Request not signed"
end
end
test "rejects requests from `rejected_instances` when `authorized_fetch_mode` is enabled" do
clear_config([:activitypub, :authorized_fetch_mode], true)
clear_config([:instance, :rejected_instances], [{"mastodon.example.org", "no reason"}])
with_mock HTTPSignatures,
validate_conn: fn _ -> true end,
signature_for_conn: fn _ ->
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
end do
conn =
build_conn(:get, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|> put_req_header(
"signature",
"keyId=\"http://mastodon.example.org/users/admin#main-key"
)
|> put_format("activity+json")
|> HTTPSignaturePlug.call(%{})
assert conn.assigns.valid_signature == true
assert conn.halted == true
assert called(HTTPSignatures.validate_conn(:_))
end
with_mock HTTPSignatures,
validate_conn: fn _ -> true end,
signature_for_conn: fn _ ->
%{"keyId" => "http://allowed.example.org/users/admin#main-key"}
end do
conn =
build_conn(:get, "/doesntmattter", %{"actor" => "http://allowed.example.org/users/admin"})
|> put_req_header(
"signature",
"keyId=\"http://allowed.example.org/users/admin#main-key"
)
|> put_format("activity+json")
|> HTTPSignaturePlug.call(%{})
assert conn.assigns.valid_signature == true
assert conn.halted == false
assert called(HTTPSignatures.validate_conn(:_))
end
end
end