Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85630000
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
5 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/http_signatures/http_signatures.ex b/lib/http_signatures/http_signatures.ex
index 4d7f026..82e1fd5 100644
--- a/lib/http_signatures/http_signatures.ex
+++ b/lib/http_signatures/http_signatures.ex
@@ -1,166 +1,170 @@
# Pleroma: A lightweight social networking server
# SPDX-FileCopyrightText: 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule HTTPSignatures do
@moduledoc """
HTTP Signatures library.
"""
require Logger
@doc "Split raw signature string into a Map of individual components."
@spec split_signature(binary()) :: map()
def split_signature(sig) do
default = %{"headers" => "date"}
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)
Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
end
@spec validate(map(), map(), any()) :: boolean()
def validate(headers, signature, public_key) do
# Request-target isn't supposed to be in request headers coming from the outside,
# it is supposed to be added by the validate_conn/1 caller prior to calling us.
with :ok <- validate_required_headers(headers, signature["headers"]) do
sigstring = build_signing_string(headers, signature["headers"])
Logger.debug("Signature: #{signature["signature"]}")
Logger.debug("Sigstring: #{sigstring}")
{:ok, sig} = Base.decode64(signature["signature"])
:public_key.verify(sigstring, :sha256, sig, public_key)
else
{:error, :host_header} ->
Logger.error("#{__MODULE__}: required header Host not found in signature headers")
false
{:error, :request_target_header} ->
Logger.error(
"#{__MODULE__}: required header (request-target) or @request-target not found in signature headers"
)
false
end
end
@doc "Fetch public key and validate signature."
@spec validate_conn(Plug.Conn.t()) :: boolean()
def validate_conn(conn) do
adapter = Application.get_env(:http_signatures, :adapter)
with {:ok, public_key} <- adapter.fetch_public_key(conn) do
case validate_conn(conn, public_key) do
true ->
true
false ->
- if retryable_signature_failure?(conn) do
- refetch_key_and_retry(conn, adapter)
- else
- false
- end
+ maybe_refetch_key_and_retry(conn, adapter)
end
else
e ->
Logger.debug("Could not validate against known public keys: #{inspect(e)}")
false
end
end
@doc "Validate signature using specified public key."
@spec validate_conn(Plug.Conn.t(), any()) :: boolean()
def validate_conn(conn, public_key) do
headers = Enum.into(conn.req_headers, %{})
signature = split_signature(headers["signature"])
validate(headers, signature, public_key)
end
@doc "Get signature for conn in split form."
@spec signature_for_conn(Plug.Conn.t()) :: map()
def signature_for_conn(conn) do
with headers <- Enum.into(conn.req_headers, %{}),
signature when is_binary(signature) <- headers["signature"] do
split_signature(signature)
else
_ ->
%{}
end
end
@spec build_signing_string(map(), list()) :: String.t()
def build_signing_string(headers, used_headers) do
used_headers
|> Enum.map_join("\n", fn header -> "#{header}: #{headers[header]}" end)
end
@doc "Create rsa-sha256 signature."
@spec sign(any(), String.t(), map()) :: String.t()
def sign(private_key, key_id, headers) do
sigstring = build_signing_string(headers, Enum.sort(Map.keys(headers)))
signature =
:public_key.sign(sigstring, :sha256, private_key)
|> Base.encode64()
[
keyId: key_id,
algorithm: "rsa-sha256",
headers: Map.keys(headers) |> Enum.sort() |> Enum.join(" "),
signature: signature
]
|> Enum.map_join(",", fn {k, v} -> "#{k}=\"#{v}\"" end)
end
defp empty?(nil), do: true
defp empty?(""), do: true
defp empty?(_), do: false
defp has_header?(header_name, req_headers, sig_headers),
do: header_name in sig_headers && !empty?(req_headers[header_name])
defp has_request_target?(req_headers, sig_headers) do
has_header?("(request-target)", req_headers, sig_headers) ||
has_header?("@request-target", req_headers, sig_headers)
end
defp validate_required_headers(req_headers, sig_headers) do
sig_headers = sig_headers || []
cond do
not has_header?("host", req_headers, sig_headers) -> {:error, :host_header}
not has_request_target?(req_headers, sig_headers) -> {:error, :request_target_header}
true -> :ok
end
end
defp retryable_signature_failure?(conn) do
headers = Enum.into(conn.req_headers, %{})
signature = signature_for_conn(conn)
validate_required_headers(headers, signature["headers"]) == :ok
end
+ defp maybe_refetch_key_and_retry(conn, adapter) do
+ if retryable_signature_failure?(conn) do
+ refetch_key_and_retry(conn, adapter)
+ else
+ false
+ end
+ end
+
@spec refetch_key_and_retry(Plug.Conn.t(), any()) :: boolean()
defp refetch_key_and_retry(conn, adapter) do
Logger.debug("Could not validate, trying to refetch any relevant keys")
with {:ok, public_key} <- adapter.refetch_public_key(conn) do
validate_conn(conn, public_key)
else
_ ->
Logger.error("Could not validate after refetching relevant keys")
false
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Aug 9, 8:58 AM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1722848
Default Alt Text
(5 KB)
Attached To
Mode
R18 http_signatures
Attached
Detach File
Event Timeline
Log In to Comment