111 lines
3 KiB
Elixir
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
|