Add function to sign event
Add signing server to perform signing Add enacl library to perform ed25519 crypto
This commit is contained in:
parent
0c40c26bca
commit
38af22fea6
11 changed files with 124 additions and 19 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -26,3 +26,6 @@ matrix_server-*.tar
|
||||||
# we ignore priv/static. You may want to comment
|
# we ignore priv/static. You may want to comment
|
||||||
# this depending on your deployment strategy.
|
# this depending on your deployment strategy.
|
||||||
/priv/static/
|
/priv/static/
|
||||||
|
|
||||||
|
keys/*
|
||||||
|
!keys/.keep
|
||||||
|
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
elixir 1.12.2-otp-24
|
||||||
|
erlang 24.0.3
|
12
README.md
12
README.md
|
@ -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/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.
|
* `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.
|
Dependencies:
|
||||||
* Create the database with name `matrix_server_dev` and credentials `matrix_server:matrix_server`.
|
|
||||||
* Fetch Elixir dependencies with `mix deps.get`.
|
* Elixir 1.12.2 compiled for OTP 24
|
||||||
* Run the server using `mix phx.server`.
|
* Erlang 24.0.3
|
||||||
|
* PostgreSQL
|
||||||
|
* Libsodium
|
||||||
|
|
|
@ -56,4 +56,5 @@ config :phoenix, :stacktrace_depth, 20
|
||||||
# Initialize plugs at runtime for faster development compilation
|
# Initialize plugs at runtime for faster development compilation
|
||||||
config :phoenix, :plug_init_mode, :runtime
|
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
0
keys/.keep
Normal file
|
@ -49,7 +49,7 @@ defmodule MatrixServer do
|
||||||
end
|
end
|
||||||
|
|
||||||
# https://matrix.org/docs/spec/appendices#unpadded-base64
|
# https://matrix.org/docs/spec/appendices#unpadded-base64
|
||||||
def unpadded_base64(data) do
|
def encode_unpadded_base64(data) do
|
||||||
data
|
data
|
||||||
|> Base.encode64()
|
|> Base.encode64()
|
||||||
|> String.trim_trailing("=")
|
|> String.trim_trailing("=")
|
||||||
|
|
|
@ -12,7 +12,8 @@ defmodule MatrixServer.Application do
|
||||||
{Phoenix.PubSub, name: MatrixServer.PubSub},
|
{Phoenix.PubSub, name: MatrixServer.PubSub},
|
||||||
MatrixServerWeb.Endpoint,
|
MatrixServerWeb.Endpoint,
|
||||||
{Registry, keys: :unique, name: MatrixServer.RoomServer.Registry},
|
{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)
|
Supervisor.start_link(children, name: MatrixServer.Supervisor, strategy: :one_for_one)
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule MatrixServer.Event do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap}
|
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, SigningServer}
|
||||||
|
|
||||||
@schema_meta_fields [:__meta__]
|
@schema_meta_fields [:__meta__]
|
||||||
@primary_key {:event_id, :string, []}
|
@primary_key {:event_id, :string, []}
|
||||||
|
@ -15,6 +15,11 @@ defmodule MatrixServer.Event do
|
||||||
field :content, :map
|
field :content, :map
|
||||||
field :prev_events, {:array, :string}
|
field :prev_events, {:array, :string}
|
||||||
field :auth_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
|
belongs_to :room, Room, type: :string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -270,7 +275,16 @@ defmodule MatrixServer.Event do
|
||||||
end)
|
end)
|
||||||
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 =
|
result =
|
||||||
event
|
event
|
||||||
|> to_map()
|
|> to_map()
|
||||||
|
@ -281,14 +295,14 @@ defmodule MatrixServer.Event do
|
||||||
case result do
|
case result do
|
||||||
{:ok, json} ->
|
{:ok, json} ->
|
||||||
:crypto.hash(:sha256, json)
|
:crypto.hash(:sha256, json)
|
||||||
|> MatrixServer.unpadded_base64()
|
|> MatrixServer.encode_unpadded_base64()
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def redact(%Event{type: type, content: content} = event) do
|
defp redact(%Event{type: type, content: content} = event) do
|
||||||
redacted_event =
|
redacted_event =
|
||||||
event
|
event
|
||||||
|> to_map()
|
|> to_map()
|
||||||
|
@ -313,15 +327,17 @@ defmodule MatrixServer.Event do
|
||||||
%{redacted_event | content: redact_content(type, content)}
|
%{redacted_event | content: redact_content(type, content)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp redact_content("m.room.member", content), do: Map.take(["membership"])
|
defp redact_content("m.room.member", content), do: Map.take(content, ["membership"])
|
||||||
defp redact_content("m.room.create", content), do: Map.take(["creator"])
|
defp redact_content("m.room.create", content), do: Map.take(content, ["creator"])
|
||||||
defp redact_content("m.room.join_rules", content), do: Map.take(["join_rule"])
|
defp redact_content("m.room.join_rules", content), do: Map.take(content, ["join_rule"])
|
||||||
defp redact_content("m.room.aliases", content), do: Map.take(["aliases"])
|
defp redact_content("m.room.aliases", content), do: Map.take(content, ["aliases"])
|
||||||
defp redact_content("m.room.history_visibility", content), do: Map.take(["history_visibility"])
|
|
||||||
|
defp redact_content("m.room.history_visibility", content),
|
||||||
|
do: Map.take(content, ["history_visibility"])
|
||||||
|
|
||||||
defp redact_content("m.room.power_levels", content),
|
defp redact_content("m.room.power_levels", content),
|
||||||
do:
|
do:
|
||||||
Map.take([
|
Map.take(content, [
|
||||||
"ban",
|
"ban",
|
||||||
"events",
|
"events",
|
||||||
"events_default",
|
"events_default",
|
||||||
|
|
78
lib/matrix_server/signing_server.ex
Normal file
78
lib/matrix_server/signing_server.ex
Normal 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
|
3
mix.exs
3
mix.exs
|
@ -43,7 +43,8 @@ defmodule MatrixServer.MixProject do
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:plug_cowboy, "~> 2.0"},
|
||||||
{:bcrypt_elixir, "~> 2.3"},
|
{:bcrypt_elixir, "~> 2.3"},
|
||||||
{:cors_plug, "~> 2.0"},
|
{:cors_plug, "~> 2.0"},
|
||||||
{:ex_machina, "~> 2.7", only: :test}
|
{:ex_machina, "~> 2.7", only: :test},
|
||||||
|
{:enacl, "~> 1.2"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -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": {: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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
||||||
|
|
Loading…
Reference in a new issue