balls/lib/balls_pds/wac.ex

174 lines
5.1 KiB
Elixir

defmodule BallsPDS.WAC do
alias BallsPDS.Util.ACL
alias BallsPDS.Util.Base58
# TODO: use key id in JWT to resolve the right key
defp get_did_document_url(domain, port, path) when is_binary(domain) do
port =
if port == nil do
443
else
{parsed, _} = Integer.parse(port)
parsed
end
path =
if path == nil || path == "/" do
"/.well-known/did.json"
else
path <> "/did.json"
end
URI.parse("https://#{domain}:#{port}#{path}")
end
def cached_query_public_key("did:web:" <> _ = did, id) do
cache_key = "KEY:#{did}:#{id}"
case Cachex.get(:balls_cache, cache_key) do
key when is_binary(key) ->
{:ok, key}
nil ->
case query_public_key(did, id) do
{:ok, raw_key} ->
Cachex.put(:balls_cache, cache_key, expire: 60_000)
{:ok, raw_key}
error ->
error
end
end
end
def cached_query_public_key(url, id) when is_binary(url) do
cache_key = "KEY:#{url}:#{id}"
case Cachex.get(:balls_cache, cache_key) do
key when is_binary(key) ->
{:ok, key}
nil ->
case query_public_key(url, id) do
{:ok, raw_key} ->
Cachex.put(:balls_cache, cache_key, expire: 60_000)
{:ok, raw_key}
error ->
error
end
end
end
def cached_query_public_key("did:key:" <> _ = did, _id), do: query_public_key(did, nil)
def query_public_key("did:web:" <> _ = did, id) do
with {:parse, {:ok, %{:domain => domain, :port => port, :path => path}}} <-
{:parse, ACL.parse_web_did(did)},
{:url, url} <- {:url, get_did_document_url(domain, port, path)},
{:query, {:ok, %{"verificationMethod" => keys}}} <- {:query, query_did_document(url)},
{:extract, {:ok, raw_public_key}} <- {:extract, extract_key(did, keys, id)} do
{:ok, raw_public_key}
else
{:parse, {:error, _} = error} -> error
{:query, {:error, _} = error} -> error
end
end
def query_public_key("did:key:" <> multikey, _id) do
case Base58.decode(multikey) do
{:ok, <<0xED, raw_key::binary-size(32)>>} -> {:ok, raw_key}
{:ok, _} -> {:error, :invalid_key}
{:error, _} = error -> error
end
end
def query_public_key(url, id) when is_binary(url) do
with {:valid_url, true} <- {:valid_url, ACL.is_valid_url?(url)},
{:query, {:ok, %{"assertionMethod" => keys}}} <- query_activitypub_actor(url),
{:key, {:ok, raw_key}} <- {:key, extract_key(url, keys, id)} do
{:ok, raw_key}
else
{:valid_url, false} -> {:error, :invalid_acl}
{:query, {:error, _} = error} -> error
end
end
# This can fail to match if the actor doesn't have an EC key, which is super common case.
defp query_activitypub_actor(url = %URI{}) do
with {:ok,
%{
:status => 200,
:body => %{
"@context" => _,
"id" => "https://" <> _,
"outbox" => _,
"inbox" => _,
"assertionMethod" => _keys
}
} = actor} <- Req.get(url) do
{:ok, actor}
else
{:ok, %{:status => http_error}} when http_error != 200 ->
{:error, http_error}
{:error, _} = error ->
error
end
end
defp query_did_document(url = %URI{}) do
with {:ok, %{:status => 200, :body => %{"verificationMethod" => _}} = did_document} <-
Req.get(url) do
{:ok, did_document}
else
{:ok, %{:status => http_error}} ->
{:error, http_error}
{:ok, %{:body => body}} when is_binary(body) ->
case Jason.decode(body) do
{:ok, did_document = %{"verificationMethod" => _}} -> {:ok, did_document}
{:ok, %{}} -> {:error, :invalid_document}
{:error, _} = error -> error
end
{:ok, %{:body => _body}} ->
{:error, :invalid_document}
end
end
defp extract_key(controller, key = %{}, id)
when is_binary(controller) and (is_binary(id) or is_nil(id)),
do: extract_key(controller, [key], id)
defp extract_key(controller, keys, id)
when is_list(keys) and is_binary(controller) and (is_binary(id) or is_nil(id)) do
Enum.reduce_while(keys, nil, fn
%{"controller" => ^controller, "publicKeyMultibase" => multikey, "id" => key_id}, _
when is_binary(multikey) and is_binary(key_id) ->
if match_id?(key_id, id) do
status =
case Base58.decode(multikey) do
{:ok, <<0xED, raw_key::binary-size(32)>>} -> {:ok, raw_key}
_ -> {:error, :invalid_key}
end
{:halt, status}
else
{:cont, nil}
end
_, _ ->
{:cont, nil}
end)
end
# Match the entire key id or else check if it's just a fragment.
defp match_id?(key_id, test_id) when is_binary(key_id) and is_binary(test_id),
do: key_id == test_id || String.ends_with?(key_id, "#" <> test_id)
# If no test id is passed then match whatever was passed.
defp match_id?(key_id, nil) when is_binary(key_id), do: true
end