Merge branch 'require-signature' into 'develop'
Add an option to require fetches to be signed Closes #1444 See merge request pleroma/pleroma!2071
This commit is contained in:
commit
3eddd9caa6
|
@ -74,6 +74,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- User settings: Add _This account is a_ option.
|
- User settings: Add _This account is a_ option.
|
||||||
- A new users admin digest email
|
- A new users admin digest email
|
||||||
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
|
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
|
||||||
|
- Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches.
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
|
||||||
|
|
|
@ -326,7 +326,8 @@
|
||||||
unfollow_blocked: true,
|
unfollow_blocked: true,
|
||||||
outgoing_blocks: true,
|
outgoing_blocks: true,
|
||||||
follow_handshake_timeout: 500,
|
follow_handshake_timeout: 500,
|
||||||
sign_object_fetches: true
|
sign_object_fetches: true,
|
||||||
|
authorized_fetch_mode: false
|
||||||
|
|
||||||
config :pleroma, :streamer,
|
config :pleroma, :streamer,
|
||||||
workers: 3,
|
workers: 3,
|
||||||
|
|
|
@ -143,10 +143,11 @@ config :pleroma, :mrf_user_allowlist,
|
||||||
* `:reject` rejects the message entirely
|
* `:reject` rejects the message entirely
|
||||||
|
|
||||||
### :activitypub
|
### :activitypub
|
||||||
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed
|
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||||
* ``outgoing_blocks``: Whether to federate blocks to other instances
|
* `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
|
* `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
|
* `sign_object_fetches`: Sign object fetches with HTTP signatures
|
||||||
|
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches
|
||||||
|
|
||||||
### :fetch_initial_posts
|
### :fetch_initial_posts
|
||||||
* `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
|
* `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
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]
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def init(options) do
|
def init(options) do
|
||||||
|
@ -15,26 +16,28 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(conn, _opts) do
|
def call(conn, _opts) do
|
||||||
headers = get_req_header(conn, "signature")
|
if get_format(conn) == "activity+json" do
|
||||||
signature = Enum.at(headers, 0)
|
|
||||||
|
|
||||||
if signature do
|
|
||||||
# set (request-target) header to the appropriate value
|
|
||||||
# we also replace the digest header with the one we computed
|
|
||||||
conn =
|
|
||||||
conn
|
conn
|
||||||
|> put_req_header(
|
|> maybe_assign_valid_signature()
|
||||||
"(request-target)",
|
|> maybe_require_signature()
|
||||||
String.downcase("#{conn.method}") <> " #{conn.request_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
if conn.assigns[:digest] do
|
|
||||||
conn
|
|
||||||
|> put_req_header("digest", conn.assigns[:digest])
|
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
|
||||||
|
|
||||||
|
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))
|
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
|
||||||
else
|
else
|
||||||
|
@ -42,4 +45,21 @@ def call(conn, _opts) do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
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)
|
||||||
|
|> text("Request not signed")
|
||||||
|
|> halt()
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
||||||
alias Pleroma.Web.Plugs.HTTPSignaturePlug
|
alias Pleroma.Web.Plugs.HTTPSignaturePlug
|
||||||
|
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
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 sighed it" do
|
||||||
|
@ -20,10 +21,69 @@ test "it call HTTPSignatures to check validity if the actor sighed it" do
|
||||||
"signature",
|
"signature",
|
||||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||||
)
|
)
|
||||||
|
|> put_format("activity+json")
|
||||||
|> HTTPSignaturePlug.call(%{})
|
|> HTTPSignaturePlug.call(%{})
|
||||||
|
|
||||||
assert conn.assigns.valid_signature == true
|
assert conn.assigns.valid_signature == true
|
||||||
|
assert conn.halted == false
|
||||||
assert called(HTTPSignatures.validate_conn(:_))
|
assert called(HTTPSignatures.validate_conn(:_))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "requires 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) |> put_format("activity+json")
|
||||||
|
|
||||||
|
[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
|
end
|
||||||
|
|
Loading…
Reference in New Issue