# Pleroma: A lightweight social networking server # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Signature do @behaviour Pleroma.Signature.API @behaviour HTTPSignatures.Adapter alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Keys alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub import Plug.Conn, only: [put_req_header: 3] @http_signatures_impl Application.compile_env( :pleroma, [__MODULE__, :http_signatures_impl], HTTPSignatures ) @known_suffixes ["/publickey", "/main-key"] def key_id_to_actor_id(key_id) do uri = key_id |> URI.parse() |> Map.put(:fragment, nil) |> remove_suffix(@known_suffixes) maybe_ap_id = URI.to_string(uri) case ObjectValidators.ObjectID.cast(maybe_ap_id) do {:ok, ap_id} -> {:ok, ap_id} _ -> case Pleroma.Web.WebFinger.finger(maybe_ap_id) do {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id} _ -> {:error, maybe_ap_id} end end end defp remove_suffix(uri, [test | rest]) do if not is_nil(uri.path) and String.ends_with?(uri.path, test) do Map.put(uri, :path, String.replace(uri.path, test, "")) else remove_suffix(uri, rest) end end defp remove_suffix(uri, []), do: uri def fetch_public_key(conn) do with {:ok, actor_id} <- get_actor_id(conn), {:ok, public_key} <- User.get_or_fetch_public_key_for_ap_id(actor_id) do {:ok, public_key} else e -> {:error, e} end end def refetch_public_key(conn) do with {:ok, actor_id} <- get_actor_id(conn), {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} else e -> {:error, e} end end def get_actor_id(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), {:ok, actor_id} <- key_id_to_actor_id(kid) do {:ok, actor_id} else e -> {:error, e} end end def sign(%User{keys: keys} = user, headers) do with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) end end def signed_date, do: signed_date(NaiveDateTime.utc_now()) def signed_date(%NaiveDateTime{} = date) do Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") end @spec validate_signature(Plug.Conn.t(), String.t()) :: boolean() def validate_signature(%Plug.Conn{} = conn, request_target) do # Newer drafts for HTTP signatures now use @request-target instead of the # old (request-target). We'll now support both for incoming signatures. conn = conn |> put_req_header("(request-target)", request_target) |> put_req_header("@request-target", request_target) @http_signatures_impl.validate_conn(conn) end @spec validate_signature(Plug.Conn.t()) :: boolean() def validate_signature(%Plug.Conn{} = conn) do # This (request-target) is non-standard, but many implementations do it # this way due to a misinterpretation of # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06 # "path" was interpreted as not having the query, though later examples # show that it must be the absolute path + query. This behavior is kept to # make sure most software (Pleroma itself, Mastodon, and probably others) # do not break. request_target = Enum.join([String.downcase(conn.method), conn.request_path], " ") # This is the proper way to build the @request-target, as expected by # many HTTP signature libraries, clarified in the following draft: # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6 # It is the same as before, but containing the query part as well. proper_target = Enum.join([request_target, "?", conn.query_string], "") cond do # Normal, non-standard behavior but expected by Pleroma and more. validate_signature(conn, request_target) -> true # Has query string and the previous one failed: let's try the standard. conn.query_string != "" -> validate_signature(conn, proper_target) # If there's no query string and signature fails, it's rotten. true -> false end end end