# Pleroma: A lightweight social networking server # Copyright © 2017-2023 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do import Plug.Conn import Phoenix.Controller, only: [get_format: 1, text: 2] require Logger def init(options) do options end def call(%{assigns: %{valid_signature: true}} = conn, _opts) do conn end def call(conn, _opts) do if get_format(conn) == "activity+json" do conn |> maybe_assign_valid_signature() |> maybe_require_signature() else conn end end defp validate_signature(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) HTTPSignatures.validate_conn(conn) end defp validate_signature(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 = 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 = 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 defp maybe_assign_valid_signature(conn) do if has_signature_header?(conn) do # we replace the digest header with the one we computed in DigestPlug conn = case conn do %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) conn -> conn end assign(conn, :valid_signature, validate_signature(conn)) else Logger.debug("No signature header!") conn end end defp has_signature_header?(conn) do conn |> get_req_header("signature") |> Enum.at(0, false) end defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn defp maybe_require_signature(conn) do if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do conn |> put_status(:unauthorized) |> text("Request not signed") |> halt() else conn end end end