Packages

Elixir SDK for the Hallpass agent identity and message proof protocol.

Current section

Files

Jump to
hallpass_sdk lib hallpass_sdk keys.ex
Raw

lib/hallpass_sdk/keys.ex

defmodule HallpassSdk.Keys do
@moduledoc """
Agent key material generation and DID derivation.
Generates EC P-256 keypairs, derives `did:jwk` identifiers, and computes
deterministic key fingerprints compatible with the TypeScript and Python SDKs.
"""
@type key_material :: %{
did: String.t(),
verification_method: String.t(),
public_jwk: map(),
private_jwk: map(),
key_fingerprint: String.t()
}
@spec generate() :: key_material()
def generate do
private_jwk = JOSE.JWK.generate_key({:ec, "P-256"})
{_kty, public_map} = JOSE.JWK.to_public_map(private_jwk)
{_kty, private_map} = JOSE.JWK.to_map(private_jwk)
did = did_jwk(public_map)
%{
did: did,
verification_method: verification_method_for_did(did),
public_jwk: public_map,
private_jwk: private_map,
key_fingerprint: fingerprint_public_jwk(public_map)
}
end
@doc """
Load agent key material from `HALLPASS_AGENT_KEY` (JSON string), `HALLPASS_AGENT_KEY_FILE`
(path), or `./agent-key.json` in the current working directory.
"""
@spec load() :: key_material()
def load do
cond do
json = System.get_env("HALLPASS_AGENT_KEY") ->
json |> Jason.decode!() |> key_material_from_decoded_json!()
path = System.get_env("HALLPASS_AGENT_KEY_FILE") ->
path |> File.read!() |> Jason.decode!() |> key_material_from_decoded_json!()
File.exists?("agent-key.json") ->
"agent-key.json" |> File.read!() |> Jason.decode!() |> key_material_from_decoded_json!()
true ->
raise RuntimeError,
"No agent key material found. Set HALLPASS_AGENT_KEY (JSON), HALLPASS_AGENT_KEY_FILE (path), or place agent-key.json in the working directory."
end
end
defp key_material_from_decoded_json!(data) when is_map(data) do
%{
did: Map.fetch!(data, "did"),
verification_method: Map.fetch!(data, "verificationMethod"),
public_jwk: Map.fetch!(data, "publicJwk"),
private_jwk: Map.fetch!(data, "privateJwk"),
key_fingerprint: Map.fetch!(data, "keyFingerprint")
}
end
@spec did_jwk(map()) :: String.t()
def did_jwk(public_jwk) when is_map(public_jwk) do
encoded =
public_jwk
|> stable_stringify()
|> Base.url_encode64(padding: false)
"did:jwk:" <> encoded
end
@spec verification_method_for_did(String.t()) :: String.t()
def verification_method_for_did(did) when is_binary(did) do
did <> "#0"
end
@spec fingerprint_public_jwk(map()) :: String.t()
def fingerprint_public_jwk(public_jwk) when is_map(public_jwk) do
public_jwk
|> stable_stringify()
|> then(&:crypto.hash(:sha256, &1))
|> Base.encode16(case: :lower)
end
@doc false
def stable_stringify(value) when is_map(value) do
entries =
value
|> Enum.sort_by(fn {key, _} -> to_string(key) end)
|> Enum.map(fn {key, nested} ->
Jason.encode!(to_string(key)) <> ":" <> stable_stringify(nested)
end)
"{" <> Enum.join(entries, ",") <> "}"
end
def stable_stringify(value) when is_list(value) do
"[" <> Enum.map_join(value, ",", &stable_stringify/1) <> "]"
end
def stable_stringify(value), do: Jason.encode!(value)
end