66 lines
1.8 KiB
Elixir
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
|