logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://anongit.hacktivis.me/git/pleroma.git/

signature.ex (4565B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Signature do
  5. @behaviour Pleroma.Signature.API
  6. @behaviour HTTPSignatures.Adapter
  7. alias Pleroma.EctoType.ActivityPub.ObjectValidators
  8. alias Pleroma.Keys
  9. alias Pleroma.User
  10. alias Pleroma.Web.ActivityPub.ActivityPub
  11. import Plug.Conn, only: [put_req_header: 3]
  12. @http_signatures_impl Application.compile_env(
  13. :pleroma,
  14. [__MODULE__, :http_signatures_impl],
  15. HTTPSignatures
  16. )
  17. @known_suffixes ["/publickey", "/main-key"]
  18. def key_id_to_actor_id(key_id) do
  19. uri =
  20. key_id
  21. |> URI.parse()
  22. |> Map.put(:fragment, nil)
  23. |> remove_suffix(@known_suffixes)
  24. maybe_ap_id = URI.to_string(uri)
  25. case ObjectValidators.ObjectID.cast(maybe_ap_id) do
  26. {:ok, ap_id} ->
  27. {:ok, ap_id}
  28. _ ->
  29. case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
  30. {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
  31. _ -> {:error, maybe_ap_id}
  32. end
  33. end
  34. end
  35. defp remove_suffix(uri, [test | rest]) do
  36. if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
  37. Map.put(uri, :path, String.replace(uri.path, test, ""))
  38. else
  39. remove_suffix(uri, rest)
  40. end
  41. end
  42. defp remove_suffix(uri, []), do: uri
  43. def fetch_public_key(conn) do
  44. with {:ok, actor_id} <- get_actor_id(conn),
  45. {:ok, public_key} <- User.get_or_fetch_public_key_for_ap_id(actor_id) do
  46. {:ok, public_key}
  47. else
  48. e ->
  49. {:error, e}
  50. end
  51. end
  52. def refetch_public_key(conn) do
  53. with {:ok, actor_id} <- get_actor_id(conn),
  54. {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
  55. {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
  56. {:ok, public_key}
  57. else
  58. e ->
  59. {:error, e}
  60. end
  61. end
  62. def get_actor_id(conn) do
  63. with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
  64. {:ok, actor_id} <- key_id_to_actor_id(kid) do
  65. {:ok, actor_id}
  66. else
  67. e ->
  68. {:error, e}
  69. end
  70. end
  71. def sign(%User{keys: keys} = user, headers) do
  72. with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
  73. HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
  74. end
  75. end
  76. def signed_date, do: signed_date(NaiveDateTime.utc_now())
  77. def signed_date(%NaiveDateTime{} = date) do
  78. Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
  79. end
  80. @spec validate_signature(Plug.Conn.t(), String.t()) :: boolean()
  81. def validate_signature(%Plug.Conn{} = conn, request_target) do
  82. # Newer drafts for HTTP signatures now use @request-target instead of the
  83. # old (request-target). We'll now support both for incoming signatures.
  84. conn =
  85. conn
  86. |> put_req_header("(request-target)", request_target)
  87. |> put_req_header("@request-target", request_target)
  88. @http_signatures_impl.validate_conn(conn)
  89. end
  90. @spec validate_signature(Plug.Conn.t()) :: boolean()
  91. def validate_signature(%Plug.Conn{} = conn) do
  92. # This (request-target) is non-standard, but many implementations do it
  93. # this way due to a misinterpretation of
  94. # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
  95. # "path" was interpreted as not having the query, though later examples
  96. # show that it must be the absolute path + query. This behavior is kept to
  97. # make sure most software (Pleroma itself, Mastodon, and probably others)
  98. # do not break.
  99. request_target = Enum.join([String.downcase(conn.method), conn.request_path], " ")
  100. # This is the proper way to build the @request-target, as expected by
  101. # many HTTP signature libraries, clarified in the following draft:
  102. # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
  103. # It is the same as before, but containing the query part as well.
  104. proper_target = Enum.join([request_target, "?", conn.query_string], "")
  105. cond do
  106. # Normal, non-standard behavior but expected by Pleroma and more.
  107. validate_signature(conn, request_target) ->
  108. true
  109. # Has query string and the previous one failed: let's try the standard.
  110. conn.query_string != "" ->
  111. validate_signature(conn, proper_target)
  112. # If there's no query string and signature fails, it's rotten.
  113. true ->
  114. false
  115. end
  116. end
  117. end