Add Signed Fetch Statistics #312

Merged
floatingghost merged 4 commits from luna/akkoma:http-signature-statistics into develop 2022-11-26 19:22:56 +00:00
7 changed files with 216 additions and 58 deletions

View file

@ -157,7 +157,8 @@ defmodule Pleroma.Application do
build_cachex("failed_proxy_url", limit: 2500), build_cachex("failed_proxy_url", limit: 2500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000), build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500), build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500) build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000)
] ]
end end

View file

@ -43,4 +43,6 @@ defmodule Pleroma.Instances do
url_or_host url_or_host
end end
end end
defdelegate set_request_signatures(url_or_host), to: Instance
end end

View file

@ -26,6 +26,7 @@ defmodule Pleroma.Instances.Instance do
field(:favicon, :string) field(:favicon, :string)
field(:metadata_updated_at, :naive_datetime) field(:metadata_updated_at, :naive_datetime)
field(:nodeinfo, :map, default: %{}) field(:nodeinfo, :map, default: %{})
field(:has_request_signatures, :boolean)
timestamps() timestamps()
end end
@ -34,7 +35,14 @@ defmodule Pleroma.Instances.Instance do
def changeset(struct, params \\ %{}) do def changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:host, :unreachable_since, :favicon, :nodeinfo, :metadata_updated_at]) |> cast(params, [
:host,
:unreachable_since,
:favicon,
:nodeinfo,
:metadata_updated_at,
:has_request_signatures
])
|> validate_required([:host]) |> validate_required([:host])
|> unique_constraint(:host) |> unique_constraint(:host)
end end
@ -316,4 +324,24 @@ defmodule Pleroma.Instances.Instance do
end) end)
end end
end end
def set_request_signatures(url_or_host) when is_binary(url_or_host) do
host = host(url_or_host)
existing_record = Repo.get_by(Instance, %{host: host})
changes = %{has_request_signatures: true}
cond do
is_nil(existing_record) ->
%Instance{}
|> changeset(Map.put(changes, :host, host))
|> Repo.insert()
true ->
existing_record
|> changeset(changes)
|> Repo.update()
end
end
def set_request_signatures(_), do: {:error, :invalid_input}
end end

View file

@ -7,8 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
import Phoenix.Controller, only: [get_format: 1, text: 2] import Phoenix.Controller, only: [get_format: 1, text: 2]
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Web.Router alias Pleroma.Web.Router
alias Pleroma.Signature
alias Pleroma.Instances
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def init(options) do def init(options) do
options options
end end
@ -57,6 +61,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
conn conn
|> assign(:valid_signature, HTTPSignatures.validate_conn(conn)) |> assign(:valid_signature, HTTPSignatures.validate_conn(conn))
|> assign(:signature_actor_id, signature_host(conn))
|> assign_valid_signature_on_route_aliases(rest) |> assign_valid_signature_on_route_aliases(rest)
end end
@ -78,6 +83,36 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
conn |> get_req_header("signature") |> Enum.at(0, false) conn |> get_req_header("signature") |> Enum.at(0, false)
end end
defp maybe_require_signature(
%{assigns: %{valid_signature: true, signature_actor_id: actor_id}} = conn
) do
# inboxes implicitly need http signatures for authentication
# so we don't really know if the instance will have broken federation after
# we turn on authorized_fetch_mode.
#
# to "check" this is a signed fetch, verify if method is GET
if conn.method == "GET" do
actor_host = URI.parse(actor_id).host
case @cachex.get(:request_signatures_cache, actor_host) do
{:ok, nil} ->
Logger.debug("Successful signature from #{actor_host}")
Instances.set_request_signatures(actor_host)
@cachex.put(:request_signatures_cache, actor_host, true)
{:ok, true} ->
:noop
any ->
Logger.warn(
"expected request signature cache to return a boolean, instead got #{inspect(any)}"
)
end
end
conn
end
defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
defp maybe_require_signature(conn) do defp maybe_require_signature(conn) do
@ -90,4 +125,14 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
conn conn
end end
end end
defp signature_host(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
actor_id
else
e ->
{:error, e}
end
end
end end

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddHasRequestSignatures do
use Ecto.Migration
def change do
alter table(:instances) do
add(:has_request_signatures, :boolean, default: false, null: false)
end
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.InstancesTest do defmodule Pleroma.InstancesTest do
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Instances.Instance
use Pleroma.DataCase use Pleroma.DataCase
@ -121,4 +122,21 @@ defmodule Pleroma.InstancesTest do
refute Instances.reachable?(host) refute Instances.reachable?(host)
end end
end end
describe "set_request_signatures/1" do
test "sets instance has request signatures" do
host = "domain.com"
{:ok, instance} = Instances.set_request_signatures(host)
luna marked this conversation as resolved
Review

maybe you want to check that the expected value is cached here as well

maybe you want to check that the expected value is cached here as well
assert instance.has_request_signatures
{:ok, cached_instance} = Instance.get_cached_by_url(host)
assert cached_instance.has_request_signatures
end
test "returns error status on non-binary input" do
assert {:error, _} = Instances.set_request_signatures(nil)
assert {:error, _} = Instances.set_request_signatures(1)
end
end
end end

View file

@ -1,34 +1,90 @@
# Pleroma: A lightweight social networking server # Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.Plugs.HTTPSignaturePlug alias Pleroma.Web.Plugs.HTTPSignaturePlug
alias Pleroma.Instances.Instance
alias Pleroma.Repo
import Plug.Conn import Plug.Conn
import Phoenix.Controller, only: [put_format: 2] import Phoenix.Controller, only: [put_format: 2]
import Mock import Mock
test "it call HTTPSignatures to check validity if the actor sighed it" do setup_with_mocks([
{HTTPSignatures, [],
[
signature_for_conn: fn _ ->
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
end,
validate_conn: fn conn ->
Map.get(conn.assigns, :valid_signature, true)
end
]}
]) do
:ok
end
defp submit_to_plug(host), do: submit_to_plug(host, :get, "/doesntmattter")
defp submit_to_plug(host, method, path) do
params = %{"actor" => "http://#{host}/users/admin"}
build_conn(method, path, params)
|> put_req_header(
"signature",
"keyId=\"http://#{host}/users/admin#main-key"
)
|> put_format("activity+json")
|> HTTPSignaturePlug.call(%{})
end
test "it call HTTPSignatures to check validity if the actor signed it" do
params = %{"actor" => "http://mastodon.example.org/users/admin"} params = %{"actor" => "http://mastodon.example.org/users/admin"}
conn = build_conn(:get, "/doesntmattter", params) conn = build_conn(:get, "/doesntmattter", params)
with_mock HTTPSignatures, validate_conn: fn _ -> true end do conn =
conn = conn
conn |> put_req_header(
|> put_req_header( "signature",
"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key"
"keyId=\"http://mastodon.example.org/users/admin#main-key" )
) |> put_format("activity+json")
|> 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 conn.assigns.signature_actor_id == params["actor"]
assert called(HTTPSignatures.validate_conn(:_)) assert conn.halted == false
end assert called(HTTPSignatures.validate_conn(:_))
end
test "it sets request signatures property on the instance" do
host = "mastodon.example.org"
conn = submit_to_plug(host)
assert conn.assigns.valid_signature == true
instance = Repo.get_by(Instance, %{host: host})
assert instance.has_request_signatures
end
test "it does not set request signatures property on the instance when using inbox" do
host = "mastodon.example.org"
conn = submit_to_plug(host, :post, "/inbox")
assert conn.assigns.valid_signature == true
# we don't even create the instance entry if its just POST /inbox
refute Repo.get_by(Instance, %{host: host})
end
test "it does not set request signatures property on the instance when its cached" do
host = "mastodon.example.org"
Cachex.put(:request_signatures_cache, host, true)
conn = submit_to_plug(host)
assert conn.assigns.valid_signature == true
# we don't even create the instance entry if it was already done
refute Repo.get_by(Instance, %{host: host})
end end
describe "requires a signature when `authorized_fetch_mode` is enabled" do describe "requires a signature when `authorized_fetch_mode` is enabled" do
@ -41,40 +97,39 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
[conn: conn] [conn: conn]
end end
test "when signature header is present", %{conn: conn} do test "and signature is present and incorrect", %{conn: conn} do
with_mock HTTPSignatures, validate_conn: fn _ -> false end do conn =
conn = conn
conn |> assign(:valid_signature, false)
|> put_req_header( |> put_req_header(
"signature", "signature",
"keyId=\"http://mastodon.example.org/users/admin#main-key" "keyId=\"http://mastodon.example.org/users/admin#main-key"
) )
|> HTTPSignaturePlug.call(%{}) |> HTTPSignaturePlug.call(%{})
assert conn.assigns.valid_signature == false assert conn.assigns.valid_signature == false
assert conn.halted == true assert conn.halted == true
assert conn.status == 401 assert conn.status == 401
assert conn.state == :sent assert conn.state == :sent
assert conn.resp_body == "Request not signed" assert conn.resp_body == "Request not signed"
assert called(HTTPSignatures.validate_conn(:_)) 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 end
test "halts the connection when `signature` header is not present", %{conn: conn} do test "and signature is correct", %{conn: conn} 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
test "and halts the connection when `signature` header is not present", %{conn: conn} do
conn = HTTPSignaturePlug.call(conn, %{}) conn = HTTPSignaturePlug.call(conn, %{})
assert conn.assigns[:valid_signature] == nil assert conn.assigns[:valid_signature] == nil
assert conn.halted == true assert conn.halted == true
@ -82,16 +137,16 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
assert conn.state == :sent assert conn.state == :sent
assert conn.resp_body == "Request not signed" assert conn.resp_body == "Request not signed"
end end
end
test "aliases redirected /object endpoints", _ do test "aliases redirected /object endpoints", _ do
obj = insert(:note) obj = insert(:note)
act = insert(:note_activity, note: obj) act = insert(:note_activity, note: obj)
params = %{"actor" => "someparam"} params = %{"actor" => "someparam"}
path = URI.parse(obj.data["id"]).path path = URI.parse(obj.data["id"]).path
conn = build_conn(:get, path, params) conn = build_conn(:get, path, params)
assert ["/notice/#{act.id}", "/notice/#{act.id}?actor=someparam"] == assert ["/notice/#{act.id}", "/notice/#{act.id}?actor=someparam"] ==
HTTPSignaturePlug.route_aliases(conn) HTTPSignaturePlug.route_aliases(conn)
end
end end
end end