Allow unsigned fetches of a user's public key
This commit is contained in:
parent
4f9f16587b
commit
4d3f52dcc6
5 changed files with 94 additions and 10 deletions
|
@ -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} ->
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
33
lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
Normal file
33
lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
Normal 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
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue