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

3
.gitignore vendored
View file

@ -26,3 +26,6 @@ matrix_server-*.tar
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
keys/*
!keys/.keep

2
.tool-versions Normal file
View file

@ -0,0 +1,2 @@
elixir 1.12.2-otp-24
erlang 24.0.3

View file

@ -9,9 +9,11 @@ Some noteworthy contributions:
* `lib/matrix_server/state_resolution/authorization.ex`: Implementation of authorization rules for the state resolution algorithm.
* `lib/matrix_server/room_server.ex`: A GenServer that holds and manages the state of a room.
To run the server in development mode, run:
Generate the server's ed25510 keys by executing `ssh-keygen -t ed25519 -f keys/id_ed25519 -N ""`
* Install the latest Erlang, Elixir and Postgresql.
* Create the database with name `matrix_server_dev` and credentials `matrix_server:matrix_server`.
* Fetch Elixir dependencies with `mix deps.get`.
* Run the server using `mix phx.server`.
Dependencies:
* Elixir 1.12.2 compiled for OTP 24
* Erlang 24.0.3
* PostgreSQL
* Libsodium

View file

@ -56,4 +56,5 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config :matrix_server, :server_name, "localhost"
config :matrix_server, server_name: "localhost"
config :matrix_server, private_key_file: "keys/id_ed25519"

0
keys/.keep Normal file
View file

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

View file

@ -43,7 +43,8 @@ defmodule MatrixServer.MixProject do
{:plug_cowboy, "~> 2.0"},
{:bcrypt_elixir, "~> 2.3"},
{:cors_plug, "~> 2.0"},
{:ex_machina, "~> 2.7", only: :test}
{:ex_machina, "~> 2.7", only: :test},
{:enacl, "~> 1.2"}
]
end

View file

@ -11,6 +11,7 @@
"ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"},
"ecto_sql": {:hex, :ecto_sql, "3.6.2", "9526b5f691701a5181427634c30655ac33d11e17e4069eff3ae1176c764e0ba3", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.6.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ec9d7e6f742ea39b63aceaea9ac1d1773d574ea40df5a53ef8afbd9242fdb6b"},
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
"enacl": {:hex, :enacl, "1.2.1", "7776480b9b3d42a51d66dbbcbf17fa3d79285b3d2adcb4d5b5bd0b70f0ef1949", [:rebar3], [], "hexpm", "67bbbeddd2564dc899a3dcbc3765cd6ad71629134f1e500a50ec071f0f75e552"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},