commit: 1b9c887dbb8d87814f8d9cc11cfcbc8802348b22
parent 1a482a73c3b99f7fdc512b734dd746e9f9cd396d
Author: Mark Felder <feld@feld.me>
Date: Thu, 25 Jul 2024 12:54:27 -0400
Extract validate_signature/2 from the HTTPSignaturePlug
This logic only exists in the Plug, so attempting to validate the signature by calling the library function HTTPSignature.validate_conn/2 directly will never work because we do not attempt to construct the (request-target) and @request-target headers with both the commonly misinterpreted and correct implementation of this field. Therefore all attempts to validate a signature from an Oban Job will fail.
Diffstat:
4 files changed, 56 insertions(+), 51 deletions(-)
diff --git a/config/test.exs b/config/test.exs
@@ -158,8 +158,7 @@ config :pleroma, Pleroma.Uploaders.IPFS, config_impl: Pleroma.UnstubbedConfigMoc
config :pleroma, Pleroma.Web.Plugs.HTTPSecurityPlug, config_impl: Pleroma.StaticStubbedConfigMock
config :pleroma, Pleroma.Web.Plugs.HTTPSignaturePlug, config_impl: Pleroma.StaticStubbedConfigMock
-config :pleroma, Pleroma.Web.Plugs.HTTPSignaturePlug,
- http_signatures_impl: Pleroma.StubbedHTTPSignaturesMock
+config :pleroma, Pleroma.Signature, http_signatures_impl: Pleroma.StubbedHTTPSignaturesMock
peer_module =
if String.to_integer(System.otp_release()) >= 25 do
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
@@ -10,6 +10,14 @@ defmodule Pleroma.Signature do
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
@@ -85,4 +93,48 @@ defmodule Pleroma.Signature do
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
+
+ @spec validate_signature(map(), String.t()) :: boolean()
+ def 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)
+
+ @http_signatures_impl.validate_conn(conn)
+ end
+
+ @spec validate_signature(map()) :: boolean()
+ def 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
end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -8,16 +8,12 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
import Plug.Conn
import Phoenix.Controller, only: [get_format: 1, text: 2]
+ alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.MRF
require Logger
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
- @http_signatures_impl Application.compile_env(
- :pleroma,
- [__MODULE__, :http_signatures_impl],
- HTTPSignatures
- )
def init(options) do
options
@@ -39,48 +35,6 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
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)
-
- @http_signatures_impl.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
@@ -90,7 +44,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
conn -> conn
end
- assign(conn, :valid_signature, validate_signature(conn))
+ assign(conn, :valid_signature, Signature.validate_signature(conn))
else
Logger.debug("No signature header!")
conn
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
@@ -35,7 +35,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
{:ok, _public_key} <- Signature.refetch_public_key(conn_data),
- {:signature, true} <- {:signature, HTTPSignatures.validate_conn(conn_data)},
+ {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
{:ok, res}
else