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

View file

@ -5,6 +5,10 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
import Plug.Conn import Plug.Conn
import Phoenix.Controller, only: [get_format: 1, text: 2] import Phoenix.Controller, only: [get_format: 1, text: 2]
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger require Logger
def init(options) do def init(options) do
@ -19,7 +23,9 @@ def call(conn, _opts) do
if get_format(conn) in ["json", "activity+json"] do if get_format(conn) in ["json", "activity+json"] do
conn conn
|> maybe_assign_valid_signature() |> maybe_assign_valid_signature()
|> maybe_assign_actor_id()
|> maybe_require_signature() |> maybe_require_signature()
|> maybe_filter_requests()
else else
conn conn
end end
@ -83,6 +89,16 @@ defp maybe_assign_valid_signature(conn) do
end end
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 defp has_signature_header?(conn) do
conn |> get_req_header("signature") |> Enum.at(0, false) conn |> get_req_header("signature") |> Enum.at(0, false)
end 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(%{assigns: %{valid_signature: true}} = conn), do: conn
defp maybe_require_signature(conn) do defp maybe_require_signature(conn) do
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) 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 conn
|> put_status(:unauthorized) |> put_status(:unauthorized)
|> text("Request not signed") |> text("Request not signed")
|> halt() |> 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
%{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 else
conn conn
end end
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 end

View file

@ -67,6 +67,14 @@ test "it returns error when not found user" do
end end
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 describe "sign/2" do
test "it returns signature headers" do test "it returns signature headers" do
user = user =

View file

@ -10,11 +10,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
import Phoenix.Controller, only: [put_format: 2] import Phoenix.Controller, only: [put_format: 2]
import Mock 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"} params = %{"actor" => "http://mastodon.example.org/users/admin"}
conn = build_conn(:get, "/doesntmattter", params) 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 =
conn conn
|> put_req_header( |> put_req_header(
@ -41,7 +45,11 @@ test "it call HTTPSignatures to check validity if the actor sighed it" do
end end
test "when signature header is present", %{conn: conn} do 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 =
conn conn
|> put_req_header( |> put_req_header(
@ -58,7 +66,11 @@ test "when signature header is present", %{conn: conn} do
assert called(HTTPSignatures.validate_conn(:_)) assert called(HTTPSignatures.validate_conn(:_))
end 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 =
conn conn
|> put_req_header( |> 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" assert conn.resp_body == "Request not signed"
end end
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 end