defmodule MatrixServer.SigningServer do use GenServer # TODO: only support one signing key for now. @signing_key_id "ed25519:1" ## Interface def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def sign_object(object) do GenServer.call(__MODULE__, {:sign_object, object}) end def get_signing_keys(encoded \\ false) do GenServer.call(__MODULE__, {:get_signing_keys, encoded}) end ## Implementation @impl true def init(_opts) do {public_key, private_key} = get_keys() {:ok, %{public_key: public_key, private_key: private_key}} end @impl true def handle_call({:sign_object, object}, _from, %{private_key: private_key} = state) do case sign_object(object, private_key) do {:ok, signature} -> {:reply, {:ok, signature, @signing_key_id}, state} {:error, _reason} -> {:reply, :error, state} end end def handle_call({:get_signing_keys, encoded}, _from, %{public_key: public_key} = state) do result = if encoded, do: MatrixServer.encode_unpadded_base64(public_key), else: public_key {:reply, [{@signing_key_id, result}], state} end # https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html defp sign_object(object, private_key) do with {:ok, json} <- MatrixServer.encode_canonical_json(object) do signature = json |> :enacl.sign_detached(private_key) |> MatrixServer.encode_unpadded_base64() {:ok, signature} end end # TODO: not sure if there is a better way to do this... defp get_keys do raw_priv_key = Application.get_env(:matrix_server, :private_key_file) |> File.read!() "-----BEGIN OPENSSH PRIVATE KEY-----\n" <> rest = raw_priv_key %{public: public, secret: private} = String.split(rest, "\n") |> Enum.take_while(&(&1 != "-----END OPENSSH PRIVATE KEY-----")) |> Enum.join() |> Base.decode64!() |> :enacl.sign_seed_keypair() {public, private} end end