Add an option to require fetches to be signed
authorEgor Kislitsyn <egor@kislitsyn.com>
Mon, 16 Dec 2019 15:24:03 +0000 (22:24 +0700)
committerEgor Kislitsyn <egor@kislitsyn.com>
Mon, 16 Dec 2019 15:24:03 +0000 (22:24 +0700)
CHANGELOG.md
config/config.exs
docs/configuration/cheatsheet.md
lib/pleroma/plugs/http_signature.ex
test/plugs/http_signature_plug_test.exs

index c133cd9ec4f3335f9132c025f5822bb457ffe7c7..ee9e1091ccdbc4b50327d1e714cd7ff02564d22b 100644 (file)
@@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - User notification settings: Add `privacy_option` option.
 - User settings: Add _This account is a_ option.
 - OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
+- Add an option `authorized_fetch_mode` to requrie HTTP Signature for AP fetches.
 <details>
   <summary>API Changes</summary>
 
index 370ddd855063b801c1ee4c198484a43a0c68f44c..541fcc2d419ee5764e2cc147a966cbbaa2e89f8e 100644 (file)
@@ -343,7 +343,8 @@ config :pleroma, :activitypub,
   unfollow_blocked: true,
   outgoing_blocks: true,
   follow_handshake_timeout: 500,
-  sign_object_fetches: true
+  sign_object_fetches: true,
+  authorized_fetch_mode: false
 
 config :pleroma, :streamer,
   workers: 3,
index ce2a1421069733840044d37c461014b3f813722d..8fa4a1747d71cf65113e999dff5d676b0779d5f9 100644 (file)
@@ -147,10 +147,11 @@ config :pleroma, :mrf_user_allowlist,
   * `:reject` rejects the message entirely
 
 ### :activitypub
-* ``unfollow_blocked``: Whether blocks result in people getting unfollowed
-* ``outgoing_blocks``: Whether to federate blocks to other instances
-* ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question
-* ``sign_object_fetches``: Sign object fetches with HTTP signatures
+* `unfollow_blocked`: Whether blocks result in people getting unfollowed
+* `outgoing_blocks`: Whether to federate blocks to other instances
+* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question
+* `sign_object_fetches`: Sign object fetches with HTTP signatures
+* `authorized_fetch_mode`: Require HTTP Signature for AP fetches
 
 ### :fetch_initial_posts
 * `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
index 23d22a712fcf9839490b9d4cf44de3a370c7cf9b..ecd7a55bf26898d3d4ec0eb4c287bd404d8fb6dd 100644 (file)
@@ -15,25 +15,23 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
   end
 
   def call(conn, _opts) do
-    headers = get_req_header(conn, "signature")
-    signature = Enum.at(headers, 0)
+    conn
+    |> maybe_assign_valid_signature()
+    |> maybe_require_signature()
+  end
 
-    if signature do
+  defp maybe_assign_valid_signature(conn) do
+    if has_signature_header?(conn) do
       # set (request-target) header to the appropriate value
       # we also replace the digest header with the one we computed
-      conn =
-        conn
-        |> put_req_header(
-          "(request-target)",
-          String.downcase("#{conn.method}") <> " #{conn.request_path}"
-        )
+      request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
 
       conn =
-        if conn.assigns[:digest] do
-          conn
-          |> put_req_header("digest", conn.assigns[:digest])
-        else
-          conn
+        conn
+        |> put_req_header("(request-target)", request_target)
+        |> case do
+          %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
+          conn -> conn
         end
 
       assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
@@ -42,4 +40,21 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
       conn
     end
   end
+
+  defp has_signature_header?(conn) do
+    conn |> get_req_header("signature") |> Enum.at(0, false)
+  end
+
+  defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
+
+  defp maybe_require_signature(conn) do
+    if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
+      conn
+      |> put_status(:unauthorized)
+      |> Phoenix.Controller.text("Request not signed")
+      |> halt()
+    else
+      conn
+    end
+  end
 end
index d8ace36da90f50f68ebda7d6980f1d370c510baa..007193dd96c961abbcebb6dc4fb170154407198b 100644 (file)
@@ -23,7 +23,65 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
         |> HTTPSignaturePlug.call(%{})
 
       assert conn.assigns.valid_signature == true
+      assert conn.halted == false
       assert called(HTTPSignatures.validate_conn(:_))
     end
   end
+
+  describe "requries a signature when `authorized_fetch_mode` is enabled" do
+    setup do
+      Pleroma.Config.put([:activitypub, :authorized_fetch_mode], true)
+
+      on_exit(fn ->
+        Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+      end)
+
+      params = %{"actor" => "http://mastodon.example.org/users/admin"}
+      conn = build_conn(:get, "/doesntmattter", params)
+
+      [conn: conn]
+    end
+
+    test "when signature header is present", %{conn: conn} do
+      with_mock HTTPSignatures, validate_conn: fn _ -> false end do
+        conn =
+          conn
+          |> put_req_header(
+            "signature",
+            "keyId=\"http://mastodon.example.org/users/admin#main-key"
+          )
+          |> HTTPSignaturePlug.call(%{})
+
+        assert conn.assigns.valid_signature == false
+        assert conn.halted == true
+        assert conn.status == 401
+        assert conn.state == :sent
+        assert conn.resp_body == "Request not signed"
+        assert called(HTTPSignatures.validate_conn(:_))
+      end
+
+      with_mock HTTPSignatures, validate_conn: fn _ -> true end do
+        conn =
+          conn
+          |> put_req_header(
+            "signature",
+            "keyId=\"http://mastodon.example.org/users/admin#main-key"
+          )
+          |> HTTPSignaturePlug.call(%{})
+
+        assert conn.assigns.valid_signature == true
+        assert conn.halted == false
+        assert called(HTTPSignatures.validate_conn(:_))
+      end
+    end
+
+    test "halts the connection when `signature` header is not present", %{conn: conn} do
+      conn = HTTPSignaturePlug.call(conn, %{})
+      assert conn.assigns[:valid_signature] == nil
+      assert conn.halted == true
+      assert conn.status == 401
+      assert conn.state == :sent
+      assert conn.resp_body == "Request not signed"
+    end
+  end
 end