diff options
-rw-r--r-- | config/config.exs | 1 | ||||
-rw-r--r-- | config/description.exs | 12 | ||||
-rw-r--r-- | docs/configuration/cheatsheet.md | 1 | ||||
-rw-r--r-- | lib/pleroma/signature.ex | 16 | ||||
-rw-r--r-- | lib/pleroma/web/mastodon_api/views/instance_view.ex | 7 | ||||
-rw-r--r-- | lib/pleroma/web/plugs/http_signature_plug.ex | 40 | ||||
-rw-r--r-- | test/pleroma/signature_test.exs | 8 | ||||
-rw-r--r-- | test/pleroma/web/plugs/http_signature_plug_test.exs | 63 |
8 files changed, 140 insertions, 8 deletions
diff --git a/config/config.exs b/config/config.exs index 3804dd809..c3b8ae0b7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -192,6 +192,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 index a9c285421..12beec842 100644 --- a/config/description.exs +++ b/config/description.exs @@ -775,6 +775,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 index 8f6d6aeea..bb6e5d279 100644 --- 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 index 8fd422a6e..900d40c4b 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -44,8 +44,7 @@ defmodule Pleroma.Signature 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 @@ 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} @@ -66,6 +64,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{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) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 99fc6d0c3..913684928 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -152,6 +152,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() @@ -171,6 +172,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 index 7ec202662..71b2a5f51 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -7,6 +7,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 @@ -21,7 +25,9 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug 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 @@ -85,6 +91,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 @@ -109,4 +125,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 index 8edf67a7b..572d7acc3 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -67,6 +67,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 index deb7e4a23..b871d956e 100644 --- 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( @@ -101,4 +113,47 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do 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 |