2024-12-02 11:03:06 +00:00
|
|
|
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
|
2024-12-03 18:36:31 +00:00
|
|
|
cache_key = "KEY:#{did}:#{id}"
|
2024-12-02 11:03:06 +00:00
|
|
|
|
|
|
|
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
|
2024-12-03 18:36:31 +00:00
|
|
|
cache_key = "KEY:#{url}:#{id}"
|
2024-12-02 11:03:06 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-12-02 12:38:14 +00:00
|
|
|
defp extract_key(controller, key = %{}, id)
|
|
|
|
when is_binary(controller) and (is_binary(id) or is_nil(id)),
|
|
|
|
do: extract_key(controller, [key], id)
|
2024-12-02 11:03:06 +00:00
|
|
|
|
2024-12-02 12:38:14 +00:00
|
|
|
defp extract_key(controller, keys, id)
|
|
|
|
when is_list(keys) and is_binary(controller) and (is_binary(id) or is_nil(id)) do
|
2024-12-02 11:03:06 +00:00
|
|
|
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)
|
2024-12-02 12:38:14 +00:00
|
|
|
|
2024-12-02 11:03:06 +00:00
|
|
|
# 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
|