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 2022-04-14 20:09:43 +02:00
parent d39f803bdd
commit c899af1d6a
8 changed files with 140 additions and 8 deletions

View File

@ -216,6 +216,7 @@
allow_relay: true, allow_relay: true,
public: true, public: true,
quarantined_instances: [], quarantined_instances: [],
rejected_instances: [],
static_dir: "instance/static/", static_dir: "instance/static/",
allowed_post_formats: [ allowed_post_formats: [
"text/plain", "text/plain",

View File

@ -714,6 +714,18 @@
{"*.quarantined.com", "Reason"} {"*.quarantined.com", "Reason"}
] ]
}, },
%{
key: :rejected_instances,
type: {:list, :tuple},
key_placeholder: "instance",
value_placeholder: "reason",
description:
"List of ActivityPub instances to reject requests from if authorized_fetch_mode is enabled",
suggestions: [
{"rejected.com", "Reason"},
{"*.rejected.com", "Reason"}
]
},
%{ %{
key: :static_dir, key: :static_dir,
type: :string, type: :string,

View File

@ -41,6 +41,7 @@ To add configuration to your config file, you can copy it from the base config.
* `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your instance. * `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your instance.
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details. * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details.
* `quarantined_instances`: ActivityPub instances where private (DMs, followers-only) activities will not be send. * `quarantined_instances`: ActivityPub instances where private (DMs, followers-only) activities will not be send.
* `rejected_instances`: ActivityPub instances to reject requests from if authorized_fetch_mode is enabled.
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with * `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
older software for theses nicknames. older software for theses nicknames.

View File

@ -37,8 +37,7 @@ def key_id_to_actor_id(key_id) do
end end
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
@ -48,8 +47,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}
@ -59,6 +57,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{} = user, headers) do def sign(%User{} = user, headers) do
with {:ok, %{keys: keys}} <- User.ensure_keys_present(user), with {:ok, %{keys: keys}} <- User.ensure_keys_present(user),
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do {:ok, private_key, _} <- Keys.keys_from_pem(keys) do

View File

@ -105,6 +105,7 @@ def features do
def federation do def federation do
quarantined = Config.get([:instance, :quarantined_instances], []) quarantined = Config.get([:instance, :quarantined_instances], [])
rejected = Config.get([:instance, :rejected_instances], [])
if Config.get([:mrf, :transparency]) do if Config.get([:mrf, :transparency]) do
{:ok, data} = MRF.describe() {:ok, data} = MRF.describe()
@ -124,6 +125,12 @@ def federation do
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end) |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|> Map.new() |> Map.new()
}) })
|> Map.put(
:rejected_instances,
rejected
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|> Map.new()
)
else else
%{} %{}
end end

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) == "activity+json" do if get_format(conn) == "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
@ -46,6 +52,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
@ -62,4 +78,28 @@ defp maybe_require_signature(conn) do
conn conn
end end
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
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
end end

View File

@ -70,6 +70,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