Add function to sign event

Add signing server to perform signing
Add enacl library to perform ed25519 crypto
This commit is contained in:
Pim Kunis 2021-08-05 13:19:38 +02:00
parent 0c40c26bca
commit 38af22fea6
11 changed files with 124 additions and 19 deletions

View file

@ -49,7 +49,7 @@ defmodule MatrixServer do
end
# https://matrix.org/docs/spec/appendices#unpadded-base64
def unpadded_base64(data) do
def encode_unpadded_base64(data) do
data
|> Base.encode64()
|> String.trim_trailing("=")

View file

@ -12,7 +12,8 @@ defmodule MatrixServer.Application do
{Phoenix.PubSub, name: MatrixServer.PubSub},
MatrixServerWeb.Endpoint,
{Registry, keys: :unique, name: MatrixServer.RoomServer.Registry},
{DynamicSupervisor, name: MatrixServer.RoomServer.Supervisor, strategy: :one_for_one}
{DynamicSupervisor, name: MatrixServer.RoomServer.Supervisor, strategy: :one_for_one},
MatrixServer.SigningServer
]
Supervisor.start_link(children, name: MatrixServer.Supervisor, strategy: :one_for_one)

View file

@ -3,7 +3,7 @@ defmodule MatrixServer.Event do
import Ecto.Query
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap}
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, SigningServer}
@schema_meta_fields [:__meta__]
@primary_key {:event_id, :string, []}
@ -15,6 +15,11 @@ defmodule MatrixServer.Event do
field :content, :map
field :prev_events, {:array, :string}
field :auth_events, {:array, :string}
# TODO: make these database fields eventually?
field :signing_keys, :map, virtual: true, default: %{}
field :unsigned, :map, virtual: true, default: %{}
field :signatures, :map, virtual: true, default: %{}
field :hashes, :map, virtual: true, default: %{}
belongs_to :room, Room, type: :string
end
@ -270,7 +275,16 @@ defmodule MatrixServer.Event do
end)
end
def calculate_content_hash(event) do
def sign(event) do
content_hash = calculate_content_hash(event)
event
|> Map.put(:hashes, %{"sha256" => content_hash})
|> redact()
|> SigningServer.sign_event()
end
defp calculate_content_hash(event) do
result =
event
|> to_map()
@ -281,14 +295,14 @@ defmodule MatrixServer.Event do
case result do
{:ok, json} ->
:crypto.hash(:sha256, json)
|> MatrixServer.unpadded_base64()
|> MatrixServer.encode_unpadded_base64()
error ->
error
end
end
def redact(%Event{type: type, content: content} = event) do
defp redact(%Event{type: type, content: content} = event) do
redacted_event =
event
|> to_map()
@ -313,15 +327,17 @@ defmodule MatrixServer.Event do
%{redacted_event | content: redact_content(type, content)}
end
defp redact_content("m.room.member", content), do: Map.take(["membership"])
defp redact_content("m.room.create", content), do: Map.take(["creator"])
defp redact_content("m.room.join_rules", content), do: Map.take(["join_rule"])
defp redact_content("m.room.aliases", content), do: Map.take(["aliases"])
defp redact_content("m.room.history_visibility", content), do: Map.take(["history_visibility"])
defp redact_content("m.room.member", content), do: Map.take(content, ["membership"])
defp redact_content("m.room.create", content), do: Map.take(content, ["creator"])
defp redact_content("m.room.join_rules", content), do: Map.take(content, ["join_rule"])
defp redact_content("m.room.aliases", content), do: Map.take(content, ["aliases"])
defp redact_content("m.room.history_visibility", content),
do: Map.take(content, ["history_visibility"])
defp redact_content("m.room.power_levels", content),
do:
Map.take([
Map.take(content, [
"ban",
"events",
"events_default",

View file

@ -0,0 +1,78 @@
defmodule MatrixServer.SigningServer do
use GenServer
alias MatrixServer.OrderedMap
# 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_event(event) do
GenServer.call(__MODULE__, {:sign_event, event})
end
## Implementation
@impl true
def init(_opts) do
{public_key, private_key} = get_keys()
{:ok, %{public_key: public_key, private_key: private_key}}
end
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
@impl true
def handle_call(
{:sign_event, event},
_from,
%{private_key: private_key} = state
) do
ordered_map =
event
|> Map.drop([:signatures, :unsigned])
|> OrderedMap.from_map()
case Jason.encode(ordered_map) do
{:ok, json} ->
signature =
json
|> :enacl.sign_detached(private_key)
|> MatrixServer.encode_unpadded_base64()
signature_map = %{@signing_key_id => signature}
servername = MatrixServer.server_name()
event =
Map.update(event, :signatures, %{servername => signature_map}, fn signatures ->
Map.put(signatures, servername, signature_map)
end)
{:reply, event, state}
{:error, _msg} ->
{:reply, {:error, :json_encode}, state}
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