architex/lib/matrix_server/key_server.ex

96 lines
2.7 KiB
Elixir
Raw Normal View History

defmodule MatrixServer.KeyServer do
2021-08-22 13:19:48 +00:00
@moduledoc """
A GenServer holding the homeserver's keys, and responsible for signing objects.
Currently, it only supports one key pair that cannot expire.
"""
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
2021-08-22 13:19:48 +00:00
@doc """
Sign the given object using the homeserver's signing keys.
Return the signature and the key ID used.
On error, return `:error`.
"""
@spec sign_object(map()) :: {:ok, String.t(), String.t()} | :error
def sign_object(object) do
GenServer.call(__MODULE__, {:sign_object, object})
end
2021-08-22 13:19:48 +00:00
@doc """
Get the homeserver's signing keys.
Return a list of tuples, each holding the key ID and the key itself.
"""
@spec get_own_signing_keys() :: list({String.t(), binary()})
def get_own_signing_keys() do
GenServer.call(__MODULE__, :get_own_signing_keys)
end
## Implementation
@impl true
def init(_opts) do
{public_key, private_key} = read_keys()
{:ok, %{public_key: public_key, private_key: private_key}}
end
@impl true
2021-08-10 16:02:53 +00:00
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_own_signing_keys, _from, %{public_key: public_key} = state) do
encoded_key = MatrixServer.encode_unpadded_base64(public_key)
{:reply, [{@signing_key_id, encoded_key}], state}
end
2021-08-10 16:02:53 +00:00
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
@spec sign_object(map(), binary()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()}
2021-08-10 16:02:53 +00:00
defp sign_object(object, private_key) do
object = Map.drop(object, [:signatures, :unsigned])
2021-08-10 16:02:53 +00:00
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...
@spec read_keys() :: {binary(), binary()}
defp read_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