commit: c899af1d6acad1895240a0247e9b91eca5db08df
parent d39f803bddb04a4c0a9e0742a437fd07f461c615
Author: marcin mikołajczak <git@mkljczk.pl>
Date: Thu, 14 Apr 2022 20:09:43 +0200
Reject requests from specified instances if `authorized_fetch_mode` is enabled
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
Diffstat:
8 files changed, 140 insertions(+), 8 deletions(-)
diff --git a/config/config.exs b/config/config.exs
@@ -216,6 +216,7 @@ config :pleroma, :instance,
allow_relay: true,
public: true,
quarantined_instances: [],
+ rejected_instances: [],
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
diff --git a/config/description.exs b/config/description.exs
@@ -715,6 +715,18 @@ config :pleroma, :config_description, [
]
},
%{
+ 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,
type: :string,
description: "Instance static directory",
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
@@ -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.
* `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.
+* `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).
* `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.
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
@@ -37,8 +37,7 @@ defmodule Pleroma.Signature do
end
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
@@ -48,8 +47,7 @@ defmodule Pleroma.Signature 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}
@@ -59,6 +57,16 @@ defmodule Pleroma.Signature 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{} = user, headers) do
with {:ok, %{keys: keys}} <- User.ensure_keys_present(user),
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -105,6 +105,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
def federation do
quarantined = Config.get([:instance, :quarantined_instances], [])
+ rejected = Config.get([:instance, :rejected_instances], [])
if Config.get([:mrf, :transparency]) do
{:ok, data} = MRF.describe()
@@ -124,6 +125,12 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|> Map.new()
})
+ |> Map.put(
+ :rejected_instances,
+ rejected
+ |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
+ |> Map.new()
+ )
else
%{}
end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
if get_format(conn) == "activity+json" do
conn
|> maybe_assign_valid_signature()
+ |> maybe_assign_actor_id()
|> maybe_require_signature()
+ |> maybe_filter_requests()
else
conn
end
@@ -46,6 +52,16 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug 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
@@ -62,4 +78,28 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
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
+ 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
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
@@ -70,6 +70,14 @@ defmodule Pleroma.SignatureTest 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 =
diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs
@@ -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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest 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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest 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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
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