Various HTTP signature code improvements
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
6f6d617eba
commit
f35db6540b
|
@ -10,10 +10,9 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
||||||
|
|
||||||
@behaviour HTTPSignatures.Adapter
|
@behaviour HTTPSignatures.Adapter
|
||||||
|
|
||||||
|
alias Mobilizon.Actors
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
|
||||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||||
alias Mobilizon.Service.ErrorReporting.Sentry
|
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@ -52,36 +51,37 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
||||||
# Gets a public key for a given ActivityPub actor ID (url).
|
# Gets a public key for a given ActivityPub actor ID (url).
|
||||||
@spec get_public_key_for_url(String.t()) ::
|
@spec get_public_key_for_url(String.t()) ::
|
||||||
{:ok, String.t()}
|
{:ok, String.t()}
|
||||||
| {:error, :actor_fetch_error | :pem_decode_error | :actor_not_fetchable}
|
| {:error, :actor_not_found | :pem_decode_error}
|
||||||
defp get_public_key_for_url(url) do
|
defp get_public_key_for_url(url) do
|
||||||
case ActivityPubActor.get_or_fetch_actor_by_url(url) do
|
case Actors.get_actor_by_url(url) do
|
||||||
{:ok, %Actor{keys: keys}} ->
|
{:ok, %Actor{} = actor} ->
|
||||||
case prepare_public_key(keys) do
|
get_actor_public_key(actor)
|
||||||
{:ok, public_key} ->
|
|
||||||
{:ok, public_key}
|
|
||||||
|
|
||||||
{:error, :pem_decode_error} ->
|
{:error, :actor_not_found} ->
|
||||||
Logger.error("Error while decoding PEM")
|
Logger.info(
|
||||||
|
"Unable to get actor from URL from local database, returning empty keys to trigger refreshment"
|
||||||
{:error, :pem_decode_error}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, err} ->
|
|
||||||
Sentry.capture_message("Unable to fetch actor, so no keys for you",
|
|
||||||
extra: %{url: url}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Logger.error("Unable to fetch actor, so no keys for you")
|
{:ok, ""}
|
||||||
Logger.error(inspect(err))
|
end
|
||||||
|
end
|
||||||
|
|
||||||
{:error, :actor_fetch_error}
|
@spec get_actor_public_key(Actor.t()) :: {:ok, String.t()} | {:error, :pem_decode_error}
|
||||||
|
defp get_actor_public_key(%Actor{keys: keys}) do
|
||||||
|
case prepare_public_key(keys) do
|
||||||
|
{:ok, public_key} ->
|
||||||
|
{:ok, public_key}
|
||||||
|
|
||||||
|
{:error, :pem_decode_error} ->
|
||||||
|
Logger.error("Error while decoding PEM")
|
||||||
|
|
||||||
|
{:error, :pem_decode_error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec fetch_public_key(Plug.Conn.t()) ::
|
@spec fetch_public_key(Plug.Conn.t()) ::
|
||||||
{:ok, String.t()}
|
{:ok, String.t()}
|
||||||
| {:error,
|
| {:error, :actor_not_found | :pem_decode_error | :no_signature_in_conn}
|
||||||
:actor_fetch_error | :actor_not_fetchable | :pem_decode_error | :no_signature_in_conn}
|
|
||||||
def fetch_public_key(conn) do
|
def fetch_public_key(conn) do
|
||||||
case HTTPSignatures.signature_for_conn(conn) do
|
case HTTPSignatures.signature_for_conn(conn) do
|
||||||
%{"keyId" => kid} ->
|
%{"keyId" => kid} ->
|
||||||
|
@ -100,8 +100,8 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
||||||
:actor_is_local}
|
:actor_is_local}
|
||||||
def refetch_public_key(conn) do
|
def refetch_public_key(conn) do
|
||||||
%{"keyId" => kid} = HTTPSignatures.signature_for_conn(conn)
|
%{"keyId" => kid} = HTTPSignatures.signature_for_conn(conn)
|
||||||
actor_id = key_id_to_actor_url(kid)
|
actor_url = key_id_to_actor_url(kid)
|
||||||
Logger.debug("Refetching public key for #{actor_id}")
|
Logger.debug("Refetching public key for #{actor_url}")
|
||||||
|
|
||||||
with {:ok, %Actor{} = actor} <-
|
with {:ok, %Actor{} = actor} <-
|
||||||
ActivityPubActor.make_actor_from_url(actor_url, ignore_sign_object_fetches: true) do
|
ActivityPubActor.make_actor_from_url(actor_url, ignore_sign_object_fetches: true) do
|
||||||
|
@ -134,6 +134,8 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
||||||
|
|
||||||
@spec generate_date_header(NaiveDateTime.t()) :: String.t()
|
@spec generate_date_header(NaiveDateTime.t()) :: String.t()
|
||||||
def generate_date_header(%NaiveDateTime{} = date) do
|
def generate_date_header(%NaiveDateTime{} = date) do
|
||||||
|
# We make sure the format is correct
|
||||||
|
# TODO: Remove Timex, as this is the only usage (with parsing)
|
||||||
Timex.lformat!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT", "en")
|
Timex.lformat!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT", "en")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,14 +36,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSignatures do
|
||||||
"(request-target)",
|
"(request-target)",
|
||||||
String.downcase("#{conn.method}") <> " #{conn.request_path}"
|
String.downcase("#{conn.method}") <> " #{conn.request_path}"
|
||||||
)
|
)
|
||||||
|
|> maybe_put_digest_header()
|
||||||
conn =
|
|
||||||
if conn.assigns[:digest] do
|
|
||||||
conn
|
|
||||||
|> put_req_header("digest", conn.assigns[:digest])
|
|
||||||
else
|
|
||||||
conn
|
|
||||||
end
|
|
||||||
|
|
||||||
signature_valid = HTTPSignatures.validate_conn(conn)
|
signature_valid = HTTPSignatures.validate_conn(conn)
|
||||||
Logger.debug("Is signature valid ? #{inspect(signature_valid)}")
|
Logger.debug("Is signature valid ? #{inspect(signature_valid)}")
|
||||||
|
@ -53,6 +46,11 @@ defmodule Mobilizon.Web.Plugs.HTTPSignatures do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_put_digest_header(%Plug.Conn{assigns: %{digest: digest}} = conn),
|
||||||
|
do: put_req_header(conn, "digest", digest)
|
||||||
|
|
||||||
|
defp maybe_put_digest_header(%Plug.Conn{} = conn), do: conn
|
||||||
|
|
||||||
@spec date_valid?(Plug.Conn.t()) :: boolean()
|
@spec date_valid?(Plug.Conn.t()) :: boolean()
|
||||||
defp date_valid?(conn) do
|
defp date_valid?(conn) do
|
||||||
date = conn |> get_req_header("date") |> List.first()
|
date = conn |> get_req_header("date") |> List.first()
|
||||||
|
|
|
@ -11,7 +11,6 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
|
||||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||||
alias Mobilizon.Federation.ActivityPub.Utils
|
alias Mobilizon.Federation.ActivityPub.Utils
|
||||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||||
|
@ -32,16 +31,20 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec actor_from_key_id(Plug.Conn.t()) :: Actor.t() | nil
|
@spec actor_from_key_id(Plug.Conn.t()) ::
|
||||||
|
{:ok, Actor.t()} | {:error, :actor_not_found | :no_key_in_conn}
|
||||||
defp actor_from_key_id(conn) do
|
defp actor_from_key_id(conn) do
|
||||||
Logger.debug("Determining actor from connection signature")
|
Logger.debug("Determining actor from connection signature")
|
||||||
|
|
||||||
with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
|
case key_id_from_conn(conn) do
|
||||||
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(key_actor_id) do
|
key_actor_id when is_binary(key_actor_id) ->
|
||||||
actor
|
# We don't need to call refreshment here since
|
||||||
else
|
# the Mobilizon.Federation.HTTPSignatures.Signature plug
|
||||||
_ ->
|
# should already have refreshed the actor if needed
|
||||||
nil
|
ActivityPubActor.make_actor_from_url(key_actor_id, ignore_sign_object_fetches: true)
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
{:error, :no_key_in_conn}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,7 +54,7 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do
|
||||||
# if this has payload make sure it is signed by the same actor that made it
|
# if this has payload make sure it is signed by the same actor that made it
|
||||||
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
|
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
|
||||||
with actor_id when actor_id != nil <- Utils.get_url(actor),
|
with actor_id when actor_id != nil <- Utils.get_url(actor),
|
||||||
{:actor, %Actor{} = actor} <- {:actor, actor_from_key_id(conn)},
|
{:ok, %Actor{} = actor} <- actor_from_key_id(conn),
|
||||||
{:actor_match, true} <- {:actor_match, actor.url == actor_id} do
|
{:actor_match, true} <- {:actor_match, actor.url == actor_id} do
|
||||||
Logger.debug("Mapped identity to #{actor.url} from actor param")
|
Logger.debug("Mapped identity to #{actor.url} from actor param")
|
||||||
assign(conn, :actor, actor)
|
assign(conn, :actor, actor)
|
||||||
|
@ -61,8 +64,12 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do
|
||||||
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
|
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
|
||||||
assign(conn, :valid_signature, false)
|
assign(conn, :valid_signature, false)
|
||||||
|
|
||||||
|
{:error, :no_key_in_conn} ->
|
||||||
|
Logger.debug("There was no key in conn")
|
||||||
|
conn
|
||||||
|
|
||||||
# TODO: remove me once testsuite uses mapped capabilities instead of what we do now
|
# TODO: remove me once testsuite uses mapped capabilities instead of what we do now
|
||||||
{:actor, nil} ->
|
{:error, :actor_not_found} ->
|
||||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||||
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
|
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
|
||||||
conn
|
conn
|
||||||
|
@ -72,11 +79,15 @@ defmodule Mobilizon.Web.Plugs.MappedSignatureToIdentity do
|
||||||
# no payload, probably a signed fetch
|
# no payload, probably a signed fetch
|
||||||
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
||||||
case actor_from_key_id(conn) do
|
case actor_from_key_id(conn) do
|
||||||
%Actor{} = actor ->
|
{:ok, %Actor{} = actor} ->
|
||||||
Logger.debug("Mapped identity to #{actor.url} from signed fetch")
|
Logger.debug("Mapped identity to #{actor.url} from signed fetch")
|
||||||
assign(conn, :actor, actor)
|
assign(conn, :actor, actor)
|
||||||
|
|
||||||
_ ->
|
{:error, :no_key_in_conn} ->
|
||||||
|
Logger.debug("There was no key in conn")
|
||||||
|
assign(conn, :valid_signature, false)
|
||||||
|
|
||||||
|
{:error, :actor_not_found} ->
|
||||||
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
||||||
Logger.debug("key_id=#{key_id_from_conn(conn)}")
|
Logger.debug("key_id=#{key_id_from_conn(conn)}")
|
||||||
assign(conn, :valid_signature, false)
|
assign(conn, :valid_signature, false)
|
||||||
|
|
Loading…
Reference in a new issue