1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
7 import Phoenix.Controller, only: [get_format: 1, text: 2]
14 def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
18 def call(conn, _opts) do
19 if get_format(conn) in ["json", "activity+json"] do
21 |> maybe_assign_valid_signature()
22 |> maybe_require_signature()
28 defp validate_signature(conn, request_target) do
29 # Newer drafts for HTTP signatures now use @request-target instead of the
30 # old (request-target). We'll now support both for incoming signatures.
33 |> put_req_header("(request-target)", request_target)
34 |> put_req_header("@request-target", request_target)
36 HTTPSignatures.validate_conn(conn)
39 defp validate_signature(conn) do
40 # This (request-target) is non-standard, but many implementations do it
41 # this way due to a misinterpretation of
42 # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
43 # "path" was interpreted as not having the query, though later examples
44 # show that it must be the absolute path + query. This behavior is kept to
45 # make sure most software (Pleroma itself, Mastodon, and probably others)
47 request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
49 # This is the proper way to build the @request-target, as expected by
50 # many HTTP signature libraries, clarified in the following draft:
51 # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
52 # It is the same as before, but containing the query part as well.
53 proper_target = request_target <> "?#{conn.query_string}"
56 # Normal, non-standard behavior but expected by Pleroma and more.
57 validate_signature(conn, request_target) ->
60 # Has query string and the previous one failed: let's try the standard.
61 conn.query_string != "" ->
62 validate_signature(conn, proper_target)
64 # If there's no query string and signature fails, it's rotten.
70 defp maybe_assign_valid_signature(conn) do
71 if has_signature_header?(conn) do
72 # we replace the digest header with the one we computed in DigestPlug
75 %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
79 assign(conn, :valid_signature, validate_signature(conn))
81 Logger.debug("No signature header!")
86 defp has_signature_header?(conn) do
87 conn |> get_req_header("signature") |> Enum.at(0, false)
90 defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
92 defp maybe_require_signature(conn) do
93 if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
95 |> put_status(:unauthorized)
96 |> text("Request not signed")