logo

pleroma

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

http_signature_plug.ex (3295B)


  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.Web.Plugs.HTTPSignaturePlug do
  5. import Plug.Conn
  6. import Phoenix.Controller, only: [get_format: 1, text: 2]
  7. require Logger
  8. def init(options) do
  9. options
  10. end
  11. def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
  12. conn
  13. end
  14. def call(conn, _opts) do
  15. if get_format(conn) in ["json", "activity+json"] do
  16. conn
  17. |> maybe_assign_valid_signature()
  18. |> maybe_require_signature()
  19. else
  20. conn
  21. end
  22. end
  23. defp validate_signature(conn, request_target) do
  24. # Newer drafts for HTTP signatures now use @request-target instead of the
  25. # old (request-target). We'll now support both for incoming signatures.
  26. conn =
  27. conn
  28. |> put_req_header("(request-target)", request_target)
  29. |> put_req_header("@request-target", request_target)
  30. HTTPSignatures.validate_conn(conn)
  31. end
  32. defp validate_signature(conn) do
  33. # This (request-target) is non-standard, but many implementations do it
  34. # this way due to a misinterpretation of
  35. # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
  36. # "path" was interpreted as not having the query, though later examples
  37. # show that it must be the absolute path + query. This behavior is kept to
  38. # make sure most software (Pleroma itself, Mastodon, and probably others)
  39. # do not break.
  40. request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
  41. # This is the proper way to build the @request-target, as expected by
  42. # many HTTP signature libraries, clarified in the following draft:
  43. # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
  44. # It is the same as before, but containing the query part as well.
  45. proper_target = request_target <> "?#{conn.query_string}"
  46. cond do
  47. # Normal, non-standard behavior but expected by Pleroma and more.
  48. validate_signature(conn, request_target) ->
  49. true
  50. # Has query string and the previous one failed: let's try the standard.
  51. conn.query_string != "" ->
  52. validate_signature(conn, proper_target)
  53. # If there's no query string and signature fails, it's rotten.
  54. true ->
  55. false
  56. end
  57. end
  58. defp maybe_assign_valid_signature(conn) do
  59. if has_signature_header?(conn) do
  60. # we replace the digest header with the one we computed in DigestPlug
  61. conn =
  62. case conn do
  63. %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
  64. conn -> conn
  65. end
  66. assign(conn, :valid_signature, validate_signature(conn))
  67. else
  68. Logger.debug("No signature header!")
  69. conn
  70. end
  71. end
  72. defp has_signature_header?(conn) do
  73. conn |> get_req_header("signature") |> Enum.at(0, false)
  74. end
  75. defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
  76. defp maybe_require_signature(conn) do
  77. if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
  78. conn
  79. |> put_status(:unauthorized)
  80. |> text("Request not signed")
  81. |> halt()
  82. else
  83. conn
  84. end
  85. end
  86. end