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