http_signatures.ex 2.7 KB
Newer Older
lain's avatar
lain committed
1
2
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Pleroma.Web.HTTPSignatures do
3
  alias Pleroma.User
4
  alias Pleroma.Web.ActivityPub.ActivityPub
lain's avatar
lain committed
5
  require Logger
6

lain's avatar
lain committed
7
  def split_signature(sig) do
8
    default = %{"headers" => "date"}
lain's avatar
lain committed
9

lain's avatar
lain committed
10
11
12
13
14
15
16
17
18
    sig =
      sig
      |> String.trim()
      |> String.split(",")
      |> Enum.reduce(default, fn part, acc ->
        [key | rest] = String.split(part, "=")
        value = Enum.join(rest, "=")
        Map.put(acc, key, String.trim(value, "\""))
      end)
19
20

    Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
lain's avatar
lain committed
21
22
23
24
  end

  def validate(headers, signature, public_key) do
    sigstring = build_signing_string(headers, signature["headers"])
lain's avatar
lain committed
25
26
    Logger.debug("Signature: #{signature["signature"]}")
    Logger.debug("Sigstring: #{sigstring}")
lain's avatar
lain committed
27
    {:ok, sig} = Base.decode64(signature["signature"])
28
29
30
31
32
33
34
35
    :public_key.verify(sigstring, :sha256, sig, public_key)
  end

  def validate_conn(conn) do
    # TODO: How to get the right key and see if it is actually valid for that request.
    # For now, fetch the key for the actor.
    with actor_id <- conn.params["actor"],
         {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
36
37
38
      if validate_conn(conn, public_key) do
        true
      else
feld's avatar
feld committed
39
        Logger.debug("Could not validate, re-fetching user and trying one more time")
40
41
42
43
44
45
46
        # Fetch user anew and try one more time
        with actor_id <- conn.params["actor"],
             {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
             {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
          validate_conn(conn, public_key)
        end
      end
47
    else
lain's avatar
lain committed
48
49
      e ->
        Logger.debug("Could not public key!")
lain's avatar
lain committed
50
        false
51
    end
lain's avatar
lain committed
52
53
  end

lain's avatar
lain committed
54
55
56
57
58
59
  def validate_conn(conn, public_key) do
    headers = Enum.into(conn.req_headers, %{})
    signature = split_signature(headers["signature"])
    validate(headers, signature, public_key)
  end

lain's avatar
lain committed
60
61
  def build_signing_string(headers, used_headers) do
    used_headers
lain's avatar
lain committed
62
    |> Enum.map(fn header -> "#{header}: #{headers[header]}" end)
lain's avatar
lain committed
63
64
    |> Enum.join("\n")
  end
65
66
67
68
69

  def sign(user, headers) do
    with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
         {:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
      sigstring = build_signing_string(headers, Map.keys(headers))
lain's avatar
lain committed
70
71
72
73

      signature =
        :public_key.sign(sigstring, :sha256, private_key)
        |> Base.encode64()
74
75
76
77
78
79
80

      [
        keyId: user.ap_id <> "#main-key",
        algorithm: "rsa-sha256",
        headers: Map.keys(headers) |> Enum.join(" "),
        signature: signature
      ]
lain's avatar
lain committed
81
      |> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
82
83
84
      |> Enum.join(",")
    end
  end
lain's avatar
lain committed
85
end