postland/lib/activity_pub/headers.ex
2024-09-25 20:45:10 -05:00

111 lines
3 KiB
Elixir

defmodule ActivityPub.Headers do
alias ActivityPub.Headers.SignatureSplitter
@http_date_format "%a, %d %b %Y %H:%M:%S GMT"
@signing_headers [
"(request-target)",
"host",
"date",
"digest"
]
def signing_headers(method, url, body, actor_url, private_key) do
method = String.downcase("#{method}")
date = DateTime.utc_now() |> Calendar.strftime(@http_date_format)
host = url.host
digest = :crypto.hash(:sha256, body)
digest_header = "SHA-256=#{Base.encode64(digest)}"
headers =
[
{"host", host},
{"date", date},
{"digest", digest_header}
]
to_sign =
signing_text(@signing_headers, [request_target_pseudoheader(method, url.path) | headers])
signature = to_sign |> :public_key.sign(:sha256, private_key) |> Base.encode64()
signature_header =
~s|keyId="#{actor_url}#main-key",headers="(request-target) host date digest",signature="#{signature}"|
[{"signature", signature_header} | headers]
end
def verify(method, path, headers, actor_fetcher \\ &fetch_actor_key/1) do
{_key, signature_header} = Enum.find(headers, fn {key, _} -> key == "signature" end)
signature_kv = SignatureSplitter.split(signature_header)
key_id = find_value(signature_kv, "keyId")
signature = signature_kv |> find_value("signature") |> Base.decode64!()
signing_text_headers = signature_kv |> find_value("headers") |> String.split(" ")
to_verify =
signing_text(signing_text_headers, [request_target_pseudoheader(method, path) | headers])
case actor_fetcher.(key_id) do
{:ok, public_key} ->
:public_key.verify(to_verify, :sha256, signature, public_key)
error ->
error
end
end
def request_target_pseudoheader(method, path) do
formatted_method = String.downcase("#{method}")
{"(request-target)", "#{formatted_method} #{path}"}
end
def signing_text(signing_text_headers, header_pairs) do
signing_text_headers
|> Enum.map_join("", fn header_name ->
value = find_value(header_pairs, header_name)
"#{header_name}: #{value}\n"
end)
end
def split_signature_header(signature_header) do
signature_header
|> String.split(",")
|> Enum.map(fn pair_string ->
[key, value] = String.split(pair_string, "=")
value = String.trim(value, "\"")
{key, value}
end)
end
defp find_value(headers, key) do
Enum.find_value(headers, fn {key_candidate, value} ->
String.downcase(key_candidate) == key && value
end)
end
def fetch_actor_key(key_id) do
request =
Req.new(url: key_id)
|> Req.Request.put_header("accept", "application/json")
case Req.get(request) do
{:ok, result} ->
key_map = result.body["publicKey"]
if key_map["id"] == key_id do
[public_key | _] =
:public_key.pem_decode(key_map["publicKeyPem"])
|> hd()
|> :public_key.pem_entry_decode()
{:ok, public_key}
else
{:error, :id_mismatch}
end
error ->
error
end
end
end