From f41d970a592568956aa97959f28cb89cadf5f2bc Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Mon, 18 Jul 2022 15:21:27 +0100 Subject: [PATCH 1/3] fix resolution of GTS user keys --- lib/pleroma/signature.ex | 23 +++++++++++++++-------- test/pleroma/signature_test.exs | 5 +++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index dbe6fd209..ff0c56856 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -10,17 +10,14 @@ defmodule Pleroma.Signature do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + @known_suffixes ["/publickey", "/main-key"] + def key_id_to_actor_id(key_id) do uri = - URI.parse(key_id) + key_id + |> URI.parse() |> Map.put(:fragment, nil) - - uri = - if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do - Map.put(uri, :path, String.replace(uri.path, "/publickey", "")) - else - uri - end + |> remove_suffix(@known_suffixes) maybe_ap_id = URI.to_string(uri) @@ -36,6 +33,16 @@ def key_id_to_actor_id(key_id) do 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 %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), {:ok, actor_id} <- key_id_to_actor_id(kid), diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 92d05f26c..b849cbee7 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -109,6 +109,11 @@ test "it properly deduces the actor id for mastodon and pleroma" do {:ok, "https://example.com/users/1234"} end + test "it deduces the actor id for gotoSocial" do + assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") == + {:ok, "https://example.com/users/1234"} + end + test "it calls webfinger for 'acct:' accounts" do with_mock(Pleroma.Web.WebFinger, finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end From 61254111e59f02118cad15de49d1e0704c07030e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Wed, 17 Aug 2022 03:30:02 +0200 Subject: [PATCH 2/3] HttpSignaturePlug: accept standard (request-target) The (request-target) used by Pleroma is non-standard, but many HTTP signature implementations do it this way due to a misinterpretation of the draft 06 of HTTP signatures: "path" was interpreted as not having the query, though later examples show that it must be the absolute path with the query part of the URL as well. This behavior is kept to make sure most software (Pleroma itself, Mastodon, and probably others) do not break, but Pleroma now accepts signatures for a (request-target) containing the query, as expected by many HTTP signature libraries, and clarified in the draft 11 of HTTP signatures. Additionally, the new draft renamed (request-target) to @request-target. We now support both for incoming requests' signatures. --- lib/pleroma/web/plugs/http_signature_plug.ex | 53 +++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index d023754a6..4bf325218 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -25,21 +25,58 @@ def call(conn, _opts) 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) + + 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 - # set (request-target) header to the appropriate value - # we also replace the digest header with the one we computed - request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" - + # we replace the digest header with the one we computed in DigestPlug conn = - conn - |> put_req_header("(request-target)", request_target) - |> case do + case conn do %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) conn -> conn end - assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) + assign(conn, :valid_signature, validate_signature(conn)) else Logger.debug("No signature header!") conn From 4661b56720b4f70eb6996bf975c4d88db9828006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Fri, 19 Aug 2022 02:45:49 +0200 Subject: [PATCH 3/3] ArticleNotePageValidator: fix replies fixing Some software, like GoToSocial, expose replies as ActivityPub Collections, but do not expose any item array directly in the object, causing validation to fail via the ObjectID validator. Now, Pleroma will drop that field in this situation too. --- .../article_note_page_validator.ex | 5 ++++- .../article_note_page_validator_test.exs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 57c8d1dc0..4243e0fbf 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -60,7 +60,10 @@ defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data) defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies), do: Map.put(data, "replies", replies) - defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies), + # TODO: Pleroma does not have any support for Collections at the moment. + # If the `replies` field is not something the ObjectID validator can handle, + # the activity/object would be rejected, which is bad behavior. + defp fix_replies(%{"replies" => replies} = data) when not is_list(replies), do: Map.drop(data, ["replies"]) defp fix_replies(data), do: data diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index e59bf6787..2dd1361ea 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -54,4 +54,17 @@ test "a note with an attachment should work", _ do %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + + test "a Note without replies/first/items validates" do + insert(:user, ap_id: "https://mastodon.social/users/emelie") + + note = + "test/fixtures/tesla_mock/status.emelie.json" + |> File.read!() + |> Jason.decode!() + |> pop_in(["replies", "first", "items"]) + |> elem(1) + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end end