From 38af22fea621ce5d7057100d2f62c4a228a95c68 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Thu, 5 Aug 2021 13:19:38 +0200 Subject: [PATCH] Add function to sign event Add signing server to perform signing Add enacl library to perform ed25519 crypto --- .gitignore | 3 ++ .tool-versions | 2 + README.md | 12 +++-- config/dev.exs | 3 +- keys/.keep | 0 lib/matrix_server.ex | 2 +- lib/matrix_server/application.ex | 3 +- lib/matrix_server/event.ex | 36 +++++++++---- lib/matrix_server/signing_server.ex | 78 +++++++++++++++++++++++++++++ mix.exs | 3 +- mix.lock | 1 + 11 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 .tool-versions create mode 100644 keys/.keep create mode 100644 lib/matrix_server/signing_server.ex diff --git a/.gitignore b/.gitignore index f0e0eed..4e69868 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..c5599c0 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.12.2-otp-24 +erlang 24.0.3 diff --git a/README.md b/README.md index 7cd076b..8f65aaf 100644 --- a/README.md +++ b/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/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 diff --git a/config/dev.exs b/config/dev.exs index dea1427..32f73ce 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -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" diff --git a/keys/.keep b/keys/.keep new file mode 100644 index 0000000..e69de29 diff --git a/lib/matrix_server.ex b/lib/matrix_server.ex index c1896ef..38872c1 100644 --- a/lib/matrix_server.ex +++ b/lib/matrix_server.ex @@ -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("=") diff --git a/lib/matrix_server/application.ex b/lib/matrix_server/application.ex index 7f32531..9281889 100644 --- a/lib/matrix_server/application.ex +++ b/lib/matrix_server/application.ex @@ -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) diff --git a/lib/matrix_server/event.ex b/lib/matrix_server/event.ex index 672103a..68ed749 100644 --- a/lib/matrix_server/event.ex +++ b/lib/matrix_server/event.ex @@ -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", diff --git a/lib/matrix_server/signing_server.ex b/lib/matrix_server/signing_server.ex new file mode 100644 index 0000000..b024793 --- /dev/null +++ b/lib/matrix_server/signing_server.ex @@ -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 diff --git a/mix.exs b/mix.exs index 9ff7d67..8e940de 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index 68135a5..6f01698 100644 --- a/mix.lock +++ b/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_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"},