nulla/lib/nulla/httpsignature.ex
2025-06-24 07:01:23 +00:00

66 lines
1.8 KiB
Elixir

defmodule Nulla.HTTPSignature do
import Plug.Conn
def verify(conn, public_key_pem) do
with [sig_header] <- get_req_header(conn, "signature"),
signature_map <- parse_signature_header(sig_header),
{:ok, signed_string} <- build_signature_string(signature_map["headers"], conn),
true <- verify_signature(public_key_pem, signed_string, signature_map["signature"]) do
:ok
else
_ -> {:error, :invalid_signature}
end
end
defp parse_signature_header(header) do
header
|> String.split(",")
|> Enum.map(fn pair ->
[k, v] = String.split(pair, "=", parts: 2)
{String.trim(k), String.trim(v, ~s("))}
end)
|> Enum.into(%{})
end
defp build_signature_string(nil, _conn), do: {:error, :missing_headers}
defp build_signature_string(headers_str, conn) do
headers = String.split(headers_str, " ")
result =
Enum.map(headers, fn header ->
line =
case header do
"(request-target)" ->
method = String.downcase(conn.method)
path =
conn.request_path <>
if conn.query_string != "", do: "?" <> conn.query_string, else: ""
"(request-target): #{method} #{path}"
_ ->
value = get_req_header(conn, header) |> List.first()
if value, do: "#{header}: #{value}", else: nil
end
line
end)
|> Enum.reject(&is_nil/1)
|> Enum.join("\n")
{:ok, result}
end
defp verify_signature(public_key_pem, signed_string, signature_base64) do
public_key =
:public_key.pem_decode(public_key_pem)
|> hd()
|> :public_key.pem_entry_decode()
signature = Base.decode64!(signature_base64)
:public_key.verify(signed_string, :sha256, signature, public_key)
end
end