From 8f322456a0eb3416fb8f5cf9c720c618c2c16f72 Mon Sep 17 00:00:00 2001 From: Floatingghost Date: Fri, 28 Jun 2024 03:00:10 +0100 Subject: [PATCH] Allow unsigned fetches of a user's public key --- lib/pleroma/user/signing_key.ex | 22 +++++++++--- .../activity_pub/activity_pub_controller.ex | 34 ++++++++++++++++++- .../web/activity_pub/views/user_view.ex | 6 ++-- .../web/plugs/ensure_user_public_key_plug.ex | 33 ++++++++++++++++++ lib/pleroma/web/router.ex | 9 ++++- 5 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 lib/pleroma/web/plugs/ensure_user_public_key_plug.ex diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 3b5f4c35d..1568b5487 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -23,8 +23,12 @@ defmodule Pleroma.User.SigningKey do |> Repo.preload(:signing_key) end - def key_id_of_local_user(%User{local: true, signing_key: %__MODULE__{key_id: key_id}}), - do: key_id + def key_id_of_local_user(%User{local: true} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{key_id: key_id}} -> key_id + _ -> nil + end + end @spec remote_changeset(__MODULE__, map) :: Changeset.t() def remote_changeset(%__MODULE__{} = signing_key, attrs) do @@ -119,9 +123,16 @@ defmodule Pleroma.User.SigningKey do def public_key(_), do: {:error, "key not found"} - def public_key_pem(%User{signing_key: %__MODULE__{public_key: public_key_pem}}), - do: public_key_pem + def public_key_pem(%User{} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem} + _ -> {:error, "key not found"} + end + end + def public_key_pem(e) do + {:error, "key not found"} + end @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()} @doc """ Given a user, return the private key for that user in binary format. @@ -146,7 +157,8 @@ defmodule Pleroma.User.SigningKey do So if we're rejected, we should probably just give up. """ def fetch_remote_key(key_id) do - resp = HTTP.Backoff.get(key_id) + # we should probably sign this, just in case + resp = Pleroma.Object.Fetcher.get_object(key_id) case handle_signature_response(resp) do {:ok, ap_id, public_key_pem} -> diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 6642f7771..dff3e1f8e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -60,7 +60,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def user(conn, %{"nickname" => nickname}) do + + @doc """ + Render the user's AP data + WARNING: we cannot actually check if the request has a fragment! so let's play defensively + - IF we have a valid signature, serve full user + - IF we do not, and authorized_fetch_mode is enabled, serve the key only + - OTHERWISE, serve the full actor (since we don't need to worry about the signature) + """ + def user(%{assigns: %{valid_signature: true}} = conn, params) do + render_full_user(conn, params) + end + + def user(conn, params) do + if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do + render_key_only_user(conn, params) + else + render_full_user(conn, params) + end + end + + defp render_full_user(conn, %{"nickname" => nickname}) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do conn |> put_resp_content_type("application/activity+json") @@ -72,6 +92,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end + def render_key_only_user(conn, %{"nickname" => nickname}) do + with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do + conn + |> put_resp_content_type("application/activity+json") + |> put_view(UserView) + |> render("keys.json", %{user: user}) + else + nil -> {:error, :not_found} + %{local: false} -> {:error, :not_found} + end + end + def object(%{assigns: assigns} = conn, _) do with ap_id <- Endpoint.url() <> conn.request_path, %Object{} = object <- Object.get_cached_by_ap_id(ap_id), diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index d855e1e40..f91810fb8 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do - public_key = User.SigningKey.public_key_pem(user) + {:ok, public_key} = User.SigningKey.public_key_pem(user) endpoints = render("endpoints.json", %{user: user}) @@ -67,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) def render("user.json", %{user: user}) do - public_key = User.SigningKey.public_key_pem(user) + {:ok, public_key} = User.SigningKey.public_key_pem(user) user = User.sanitize_html(user) endpoints = render("endpoints.json", %{user: user}) @@ -112,7 +112,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do end def render("keys.json", %{user: user}) do - public_key = User.SigningKey.public_key_pem(user) + {:ok, public_key} = User.SigningKey.public_key_pem(user) %{ "id" => user.ap_id, diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex new file mode 100644 index 000000000..baebcec29 --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -0,0 +1,33 @@ +defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do + @moduledoc """ + This plug will attempt to pull in a user's public key if we do not have it. + We _should_ be able to request the URL from the key URL... + """ + + import Plug.Conn + + alias Pleroma.User + + def init(options), do: options + + def call(conn, _opts) do + key_id = key_id_from_conn(conn) + unless is_nil(key_id) do + SigningKey.fetch_remote_key(key_id) + # now we SHOULD have the user that owns the key locally. maybe. + # if we don't, we'll error out when we try to validate. + end + + conn + end + + defp key_id_from_conn(conn) do + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} when is_binary(key_id) -> + key_id + + _ -> + nil + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 49ab3540b..c227d0d4a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -144,7 +144,14 @@ defmodule Pleroma.Web.Router do }) end + pipeline :optional_http_signature do + plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug) + plug(Pleroma.Web.Plugs.HTTPSignaturePlug) + plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) + end + pipeline :http_signature do + plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug) @@ -745,7 +752,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled # Note: http signature is only considered for json requests (no auth for non-json requests) - pipe_through([:accepts_html_xml_json, :http_signature, :static_fe]) + pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe]) # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)