Allow unsigned fetches of a user's public key

This commit is contained in:
Floatingghost 2024-06-28 03:00:10 +01:00
parent 4f9f16587b
commit 4d3f52dcc6
5 changed files with 94 additions and 10 deletions

View file

@ -23,8 +23,12 @@ def load_key(%User{} = user) do
|> Repo.preload(:signing_key) |> Repo.preload(:signing_key)
end end
def key_id_of_local_user(%User{local: true, signing_key: %__MODULE__{key_id: key_id}}), def key_id_of_local_user(%User{local: true} = user) do
do: key_id 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() @spec remote_changeset(__MODULE__, map) :: Changeset.t()
def remote_changeset(%__MODULE__{} = signing_key, attrs) do def remote_changeset(%__MODULE__{} = signing_key, attrs) do
@ -119,9 +123,16 @@ def public_key(%User{signing_key: %__MODULE__{public_key: public_key_pem}}) do
def public_key(_), do: {:error, "key not found"} def public_key(_), do: {:error, "key not found"}
def public_key_pem(%User{signing_key: %__MODULE__{public_key: public_key_pem}}), def public_key_pem(%User{} = user) do
do: public_key_pem 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()} @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
@doc """ @doc """
Given a user, return the private key for that user in binary format. Given a user, return the private key for that user in binary format.
@ -146,7 +157,8 @@ def private_key(%User{signing_key: %__MODULE__{private_key: private_key_pem}}) d
So if we're rejected, we should probably just give up. So if we're rejected, we should probably just give up.
""" """
def fetch_remote_key(key_id) do 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 case handle_signature_response(resp) do
{:ok, ap_id, public_key_pem} -> {:ok, ap_id, public_key_pem} ->

View file

@ -60,7 +60,27 @@ defp relay_active?(conn, _) do
end end
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 with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
@ -72,6 +92,18 @@ def user(conn, %{"nickname" => nickname}) do
end end
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 def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path, with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), %Object{} = object <- Object.get_cached_by_ap_id(ap_id),

View file

@ -32,7 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do
def render("endpoints.json", _), do: %{} def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) 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}) endpoints = render("endpoints.json", %{user: user})
@ -67,7 +67,7 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do 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) user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
@ -112,7 +112,7 @@ def render("user.json", %{user: user}) do
end end
def render("keys.json", %{user: user}) do 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, "id" => user.ap_id,

View file

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

View file

@ -144,7 +144,14 @@ defmodule Pleroma.Web.Router do
}) })
end 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 pipeline :http_signature do
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug) plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)
@ -745,7 +752,7 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled # 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) # 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 # 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) get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)