Commit 3eddd9ca authored by lain's avatar lain

Merge branch 'require-signature' into 'develop'

Add an option to require fetches to be signed

Closes #1444

See merge request !2071
parents c69b04c4 77521212
Pipeline #23038 passed with stages
in 71 minutes and 21 seconds
...@@ -74,6 +74,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ...@@ -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 @@ ...@@ -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, ...@@ -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 @@ ...@@ -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,25 +16,27 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do ...@@ -15,25 +16,27 @@ 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) conn
|> maybe_assign_valid_signature()
|> maybe_require_signature()
else
conn
end
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 # set (request-target) header to the appropriate value
# we also replace the digest header with the one we computed # we also replace the digest header with the one we computed
conn = request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
conn
|> put_req_header(
"(request-target)",
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
conn = conn =
if conn.assigns[:digest] do conn
conn |> put_req_header("(request-target)", request_target)
|> put_req_header("digest", conn.assigns[:digest]) |> case do
else %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
conn conn -> conn
end end
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
...@@ -42,4 +45,21 @@ def call(conn, _opts) do ...@@ -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 ...@@ -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 ...@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment