@@ -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> | |||
@@ -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, | |||
@@ -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 | |||
@@ -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 |
@@ -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 |