Add code for verifying homeservers' signatures on API requests
This commit is contained in:
parent
f50f08061c
commit
33b64d80f5
9 changed files with 181 additions and 33 deletions
|
@ -1,4 +1,6 @@
|
||||||
defmodule MatrixServer do
|
defmodule MatrixServer do
|
||||||
|
alias MatrixServer.OrderedMap
|
||||||
|
|
||||||
def get_mxid(localpart) when is_binary(localpart) do
|
def get_mxid(localpart) when is_binary(localpart) do
|
||||||
"@#{localpart}:#{server_name()}"
|
"@#{localpart}:#{server_name()}"
|
||||||
end
|
end
|
||||||
|
@ -54,4 +56,28 @@ defmodule MatrixServer do
|
||||||
|> Base.encode64()
|
|> Base.encode64()
|
||||||
|> String.trim_trailing("=")
|
|> String.trim_trailing("=")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Decode (possibly unpadded) base64.
|
||||||
|
def decode_base64(data) when is_binary(data) do
|
||||||
|
rem = rem(String.length(data), 4)
|
||||||
|
padded_data = if rem > 0, do: data <> String.duplicate("=", 4 - rem), else: data
|
||||||
|
Base.decode64(padded_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode_canonical_json(object) do
|
||||||
|
object
|
||||||
|
|> Map.drop([:signatures, :unsigned])
|
||||||
|
|> OrderedMap.from_map()
|
||||||
|
|> Jason.encode()
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/41523762/41671211
|
||||||
|
def to_serializable_map(struct) do
|
||||||
|
association_fields = struct.__struct__.__schema__(:associations)
|
||||||
|
waste_fields = association_fields ++ [:__meta__]
|
||||||
|
|
||||||
|
struct
|
||||||
|
|> Map.from_struct()
|
||||||
|
|> Map.drop(waste_fields)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ defmodule MatrixServer.Event do
|
||||||
|
|
||||||
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, SigningServer}
|
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, SigningServer}
|
||||||
|
|
||||||
@schema_meta_fields [:__meta__]
|
|
||||||
@primary_key {:event_id, :string, []}
|
@primary_key {:event_id, :string, []}
|
||||||
schema "events" do
|
schema "events" do
|
||||||
field :type, :string
|
field :type, :string
|
||||||
|
@ -287,7 +286,7 @@ defmodule MatrixServer.Event do
|
||||||
defp calculate_content_hash(event) do
|
defp calculate_content_hash(event) do
|
||||||
result =
|
result =
|
||||||
event
|
event
|
||||||
|> to_map()
|
|> MatrixServer.to_serializable_map()
|
||||||
|> Map.drop([:unsigned, :signature, :hashes])
|
|> Map.drop([:unsigned, :signature, :hashes])
|
||||||
|> OrderedMap.from_map()
|
|> OrderedMap.from_map()
|
||||||
|> Jason.encode()
|
|> Jason.encode()
|
||||||
|
@ -305,7 +304,7 @@ defmodule MatrixServer.Event do
|
||||||
defp 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()
|
|> MatrixServer.to_serializable_map()
|
||||||
|> Map.take([
|
|> Map.take([
|
||||||
:event_id,
|
:event_id,
|
||||||
:type,
|
:type,
|
||||||
|
@ -347,14 +346,4 @@ defmodule MatrixServer.Event do
|
||||||
"users",
|
"users",
|
||||||
"users_default"
|
"users_default"
|
||||||
])
|
])
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/41523762/41671211
|
|
||||||
def to_map(event) do
|
|
||||||
association_fields = event.__struct__.__schema__(:associations)
|
|
||||||
waste_fields = association_fields ++ @schema_meta_fields
|
|
||||||
|
|
||||||
event
|
|
||||||
|> Map.from_struct()
|
|
||||||
|> Map.drop(waste_fields)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
defmodule MatrixServer.SigningServer do
|
defmodule MatrixServer.SigningServer do
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
alias MatrixServer.OrderedMap
|
|
||||||
|
|
||||||
# TODO: only support one signing key for now.
|
# TODO: only support one signing key for now.
|
||||||
@signing_key_id "ed25519:1"
|
@signing_key_id "ed25519:1"
|
||||||
|
|
||||||
|
@ -16,8 +14,8 @@ defmodule MatrixServer.SigningServer do
|
||||||
GenServer.call(__MODULE__, {:sign_object, object})
|
GenServer.call(__MODULE__, {:sign_object, object})
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_signing_keys do
|
def get_signing_keys(encoded \\ false) do
|
||||||
GenServer.call(__MODULE__, :get_signing_keys)
|
GenServer.call(__MODULE__, {:get_signing_keys, encoded})
|
||||||
end
|
end
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
@ -35,12 +33,7 @@ defmodule MatrixServer.SigningServer do
|
||||||
_from,
|
_from,
|
||||||
%{private_key: private_key} = state
|
%{private_key: private_key} = state
|
||||||
) do
|
) do
|
||||||
ordered_map =
|
case MatrixServer.encode_canonical_json(object) do
|
||||||
object
|
|
||||||
|> Map.drop([:signatures, :unsigned])
|
|
||||||
|> OrderedMap.from_map()
|
|
||||||
|
|
||||||
case Jason.encode(ordered_map) do
|
|
||||||
{:ok, json} ->
|
{:ok, json} ->
|
||||||
signature =
|
signature =
|
||||||
json
|
json
|
||||||
|
@ -62,10 +55,10 @@ defmodule MatrixServer.SigningServer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call(:get_signing_keys, _from, %{public_key: public_key} = state) do
|
def handle_call({:get_signing_keys, encoded}, _from, %{public_key: public_key} = state) do
|
||||||
encoded_public_key = MatrixServer.encode_unpadded_base64(public_key)
|
result = if encoded, do: MatrixServer.encode_unpadded_base64(public_key), else: public_key
|
||||||
|
|
||||||
{:reply, [{@signing_key_id, encoded_public_key}], state}
|
{:reply, [{@signing_key_id, result}], state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: not sure if there is a better way to do this...
|
# TODO: not sure if there is a better way to do this...
|
||||||
|
|
102
lib/matrix_server_web/authenticate_server.ex
Normal file
102
lib/matrix_server_web/authenticate_server.ex
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
defmodule MatrixServerWeb.AuthenticateServer do
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias MatrixServer.SigningServer
|
||||||
|
alias Ecto.Changeset
|
||||||
|
|
||||||
|
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
|
||||||
|
|
||||||
|
defmodule SignedJSON do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
embedded_schema do
|
||||||
|
field :method, :string
|
||||||
|
field :uri, :string
|
||||||
|
field :origin, :string
|
||||||
|
field :destination, :string
|
||||||
|
field :content, :map
|
||||||
|
field :signatures, :map
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(params) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> cast(params, [:method, :uri, :origin, :destination, :content, :signatures])
|
||||||
|
|> validate_required([:method, :uri, :origin, :destination])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticated?(%Plug.Conn{body_params: params}) do
|
||||||
|
with %Changeset{valid?: true} = cs <- SignedJSON.changeset(params),
|
||||||
|
input <- apply_changes(cs) do
|
||||||
|
verify_signature(input)
|
||||||
|
else
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp verify_signature(%SignedJSON{signatures: signatures, origin: origin} = input) do
|
||||||
|
if Map.has_key?(signatures, origin) do
|
||||||
|
# TODO: fetch actual signing keys from cache/key store.
|
||||||
|
signing_keys = SigningServer.get_signing_keys() |> Enum.into(%{})
|
||||||
|
|
||||||
|
found_signatures =
|
||||||
|
Enum.filter(signatures[origin], fn {key, _} ->
|
||||||
|
case String.split(key, ":", parts: 2) do
|
||||||
|
[algorithm, _] -> algorithm == "ed25519"
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.map(fn {key_id, sig} ->
|
||||||
|
if Map.has_key?(signing_keys, key_id) do
|
||||||
|
{key_id, sig, signing_keys[key_id]}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.reject(&Kernel.is_nil/1)
|
||||||
|
|
||||||
|
with [{_, sig, signing_key} | _] <- found_signatures,
|
||||||
|
{:ok, raw_sig} <- MatrixServer.decode_base64(sig),
|
||||||
|
serializable_input <- MatrixServer.to_serializable_map(input),
|
||||||
|
{:ok, encoded_input} <- MatrixServer.encode_canonical_json(serializable_input) do
|
||||||
|
:enacl.sign_verify_detached(raw_sig, encoded_input, signing_key)
|
||||||
|
else
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Not actually needed?
|
||||||
|
def parse_authorization_headers(headers) do
|
||||||
|
headers
|
||||||
|
|> Enum.filter(&(elem(&1, 0) == "authorization"))
|
||||||
|
|> Enum.map(fn {_, auth_header} ->
|
||||||
|
Regex.named_captures(@auth_header_regex, auth_header)
|
||||||
|
end)
|
||||||
|
|> Enum.reject(&Kernel.is_nil/1)
|
||||||
|
|> Enum.reduce(%{}, fn %{"origin" => origin, "key" => key, "sig" => sig}, acc ->
|
||||||
|
Map.update(acc, origin, %{key => sig}, &Map.put(&1, key, sig))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defmacro __using__(opts) do
|
||||||
|
except = Keyword.get(opts, :except) || []
|
||||||
|
|
||||||
|
quote do
|
||||||
|
def action(conn, _) do
|
||||||
|
action = action_name(conn)
|
||||||
|
|
||||||
|
if action not in unquote(except) and
|
||||||
|
not MatrixServerWeb.AuthenticateServer.authenticated?(conn) do
|
||||||
|
IO.puts("Not authorized!")
|
||||||
|
apply(__MODULE__, action, [conn, conn.params])
|
||||||
|
else
|
||||||
|
apply(__MODULE__, action, [conn, conn.params])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,7 +9,7 @@ defmodule MatrixServerWeb.Federation.KeyController do
|
||||||
|
|
||||||
def get_signing_keys(conn, _params) do
|
def get_signing_keys(conn, _params) do
|
||||||
keys =
|
keys =
|
||||||
SigningServer.get_signing_keys()
|
SigningServer.get_signing_keys(true)
|
||||||
|> Enum.into(%{}, fn {key_id, key} ->
|
|> Enum.into(%{}, fn {key_id, key} ->
|
||||||
{key_id, %{"key" => key}}
|
{key_id, %{"key" => key}}
|
||||||
end)
|
end)
|
||||||
|
|
10
lib/matrix_server_web/federation/test_controller.ex
Normal file
10
lib/matrix_server_web/federation/test_controller.ex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule MatrixServerWeb.Federation.TestController do
|
||||||
|
use MatrixServerWeb, :controller
|
||||||
|
use MatrixServerWeb.AuthenticateServer
|
||||||
|
|
||||||
|
def test(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> put_status(200)
|
||||||
|
|> json(%{})
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,17 +7,41 @@ defmodule MatrixServerWeb.FederationClient do
|
||||||
# TODO: Maybe create database-backed homeserver struct to pass to client function.
|
# TODO: Maybe create database-backed homeserver struct to pass to client function.
|
||||||
|
|
||||||
@middleware [
|
@middleware [
|
||||||
{Tesla.Middleware.Headers, [{"Content-Type", "application/json"}]},
|
|
||||||
Tesla.Middleware.JSON
|
Tesla.Middleware.JSON
|
||||||
]
|
]
|
||||||
|
|
||||||
@adapter {Tesla.Adapter.Finch, name: MatrixServerWeb.HTTPClient}
|
@adapter {Tesla.Adapter.Finch, name: MatrixServerWeb.HTTPClient}
|
||||||
|
|
||||||
def client(server_name) do
|
def client(server_name) do
|
||||||
Tesla.client([{Tesla.Middleware.BaseUrl, server_name} | @middleware], @adapter)
|
Tesla.client([{Tesla.Middleware.BaseUrl, "http://" <> server_name} | @middleware], @adapter)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_signing_keys(client) do
|
def get_signing_keys(client) do
|
||||||
Tesla.get(client, RouteHelpers.key_path(Endpoint, :get_signing_keys))
|
Tesla.get(client, RouteHelpers.key_path(Endpoint, :get_signing_keys))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_server_auth(client) do
|
||||||
|
origin = "localhost:4001"
|
||||||
|
destination = "localhost:4000"
|
||||||
|
path = RouteHelpers.test_path(Endpoint, :test)
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
method: "POST",
|
||||||
|
uri: path,
|
||||||
|
origin: origin,
|
||||||
|
destination: destination,
|
||||||
|
content: %{"hi" => "hello"}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, signed_object} = MatrixServer.SigningServer.sign_object(params)
|
||||||
|
auth_headers = create_signature_authorization_headers(signed_object, origin)
|
||||||
|
|
||||||
|
Tesla.post(client, path, signed_object, headers: auth_headers)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_signature_authorization_headers(%{signatures: signatures}, origin) do
|
||||||
|
Enum.map(signatures[origin], fn {key, sig} ->
|
||||||
|
{"Authorization", "X-Matrix origin=#{origin},key=\"#{key}\",sig=\"#{sig}\""}
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule MatrixServerWeb.Plug.Authenticate do
|
defmodule MatrixServerWeb.Plug.AuthenticateClient do
|
||||||
import MatrixServerWeb.Plug.Error
|
import MatrixServerWeb.Plug.Error
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
defmodule MatrixServerWeb.Router do
|
defmodule MatrixServerWeb.Router do
|
||||||
use MatrixServerWeb, :router
|
use MatrixServerWeb, :router
|
||||||
|
|
||||||
alias MatrixServerWeb.Plug.Authenticate
|
alias MatrixServerWeb.Plug.AuthenticateClient
|
||||||
|
|
||||||
|
# TODO: might be able to handle malformed JSON with custom body reader:
|
||||||
|
# https://elixirforum.com/t/write-malformed-json-in-the-body-plug/30578/13
|
||||||
|
|
||||||
pipeline :public do
|
pipeline :public do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
|
@ -9,12 +12,11 @@ defmodule MatrixServerWeb.Router do
|
||||||
|
|
||||||
pipeline :authenticate_client do
|
pipeline :authenticate_client do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
plug Authenticate
|
plug AuthenticateClient
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :authenticate_server do
|
pipeline :authenticate_server do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
# TODO: Add plug to verify peer.
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/_matrix", MatrixServerWeb do
|
scope "/_matrix", MatrixServerWeb do
|
||||||
|
@ -52,7 +54,9 @@ defmodule MatrixServerWeb.Router do
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/_matrix", MatrixServerWeb.Federation do
|
scope "/_matrix", MatrixServerWeb.Federation do
|
||||||
|
pipe_through :authenticate_server
|
||||||
|
|
||||||
|
post "/test", TestController, :test
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", MatrixServerWeb.Client do
|
scope "/", MatrixServerWeb.Client do
|
||||||
|
|
Loading…
Reference in a new issue