Fix server signature verification
This commit is contained in:
parent
33b64d80f5
commit
e6b3c4752d
8 changed files with 138 additions and 110 deletions
|
@ -41,6 +41,16 @@ defmodule MatrixServer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO Eventually move to regex with named captures.
|
||||||
|
def get_localpart(id) do
|
||||||
|
with [part, _] <- String.split(id, ":", parts: 2),
|
||||||
|
{_, localpart} <- String.split_at(part, 1) do
|
||||||
|
localpart
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# https://elixirforum.com/t/22709/9
|
# https://elixirforum.com/t/22709/9
|
||||||
def has_duplicates?(list) do
|
def has_duplicates?(list) do
|
||||||
list
|
list
|
||||||
|
|
|
@ -26,32 +26,11 @@ defmodule MatrixServer.SigningServer do
|
||||||
{:ok, %{public_key: public_key, private_key: private_key}}
|
{:ok, %{public_key: public_key, private_key: private_key}}
|
||||||
end
|
end
|
||||||
|
|
||||||
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call(
|
def handle_call({:sign_object, object}, _from, %{private_key: private_key} = state) do
|
||||||
{:sign_object, object},
|
case sign_object(object, private_key) do
|
||||||
_from,
|
{:ok, signature} -> {:reply, {:ok, signature, @signing_key_id}, state}
|
||||||
%{private_key: private_key} = state
|
{:error, _reason} -> {:reply, :error, state}
|
||||||
) do
|
|
||||||
case MatrixServer.encode_canonical_json(object) do
|
|
||||||
{:ok, json} ->
|
|
||||||
signature =
|
|
||||||
json
|
|
||||||
|> :enacl.sign_detached(private_key)
|
|
||||||
|> MatrixServer.encode_unpadded_base64()
|
|
||||||
|
|
||||||
signature_map = %{@signing_key_id => signature}
|
|
||||||
servername = MatrixServer.server_name()
|
|
||||||
|
|
||||||
signed_object =
|
|
||||||
Map.update(object, :signatures, %{servername => signature_map}, fn signatures ->
|
|
||||||
Map.put(signatures, servername, signature_map)
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:reply, {:ok, signed_object}, state}
|
|
||||||
|
|
||||||
{:error, _msg} ->
|
|
||||||
{:reply, {:error, :json_encode}, state}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,6 +40,18 @@ defmodule MatrixServer.SigningServer do
|
||||||
{:reply, [{@signing_key_id, result}], state}
|
{:reply, [{@signing_key_id, result}], state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
|
||||||
|
defp sign_object(object, private_key) do
|
||||||
|
with {:ok, json} <- MatrixServer.encode_canonical_json(object) do
|
||||||
|
signature =
|
||||||
|
json
|
||||||
|
|> :enacl.sign_detached(private_key)
|
||||||
|
|> MatrixServer.encode_unpadded_base64()
|
||||||
|
|
||||||
|
{:ok, signature}
|
||||||
|
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...
|
||||||
defp get_keys do
|
defp get_keys do
|
||||||
raw_priv_key =
|
raw_priv_key =
|
||||||
|
|
|
@ -1,75 +1,50 @@
|
||||||
defmodule MatrixServerWeb.AuthenticateServer do
|
defmodule MatrixServerWeb.AuthenticateServer do
|
||||||
import Ecto.Changeset
|
import MatrixServerWeb.Plug.Error
|
||||||
|
|
||||||
alias MatrixServer.SigningServer
|
alias MatrixServer.SigningServer
|
||||||
alias Ecto.Changeset
|
|
||||||
|
|
||||||
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
|
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
|
||||||
|
|
||||||
defmodule SignedJSON do
|
def authenticate(
|
||||||
use Ecto.Schema
|
%Plug.Conn{
|
||||||
|
body_params: body_params,
|
||||||
|
req_headers: headers,
|
||||||
|
request_path: path,
|
||||||
|
method: method,
|
||||||
|
query_string: query_string
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
object_to_sign = %{
|
||||||
|
uri: path <> "?" <> URI.decode_www_form(query_string),
|
||||||
|
method: method,
|
||||||
|
destination: MatrixServer.server_name()
|
||||||
|
}
|
||||||
|
|
||||||
import Ecto.Changeset
|
object_to_sign =
|
||||||
|
if method != "GET", do: Map.put(object_to_sign, :content, body_params), else: object_to_sign
|
||||||
|
|
||||||
@primary_key false
|
object_fun = &Map.put(object_to_sign, :origin, &1)
|
||||||
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
|
authenticate_with_headers(headers, object_fun)
|
||||||
%__MODULE__{}
|
|
||||||
|> cast(params, [:method, :uri, :origin, :destination, :content, :signatures])
|
|
||||||
|> validate_required([:method, :uri, :origin, :destination])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticated?(%Plug.Conn{body_params: params}) do
|
defp authenticate_with_headers(headers, object_fun) do
|
||||||
with %Changeset{valid?: true} = cs <- SignedJSON.changeset(params),
|
headers
|
||||||
input <- apply_changes(cs) do
|
|> parse_authorization_headers()
|
||||||
verify_signature(input)
|
|> Enum.find(:error, fn {origin, _, sig} ->
|
||||||
else
|
# TODO: fetch actual signing keys for origin from cache/key store.
|
||||||
_ -> false
|
{_, signing_key} = SigningServer.get_signing_keys() |> hd()
|
||||||
end
|
object = object_fun.(origin)
|
||||||
end
|
|
||||||
|
|
||||||
defp verify_signature(%SignedJSON{signatures: signatures, origin: origin} = input) do
|
with {:ok, raw_sig} <- MatrixServer.decode_base64(sig),
|
||||||
if Map.has_key?(signatures, origin) do
|
{:ok, encoded_object} <- MatrixServer.encode_canonical_json(object) do
|
||||||
# TODO: fetch actual signing keys from cache/key store.
|
:enacl.sign_verify_detached(raw_sig, encoded_object, signing_key)
|
||||||
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
|
else
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
else
|
end)
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Not actually needed?
|
|
||||||
def parse_authorization_headers(headers) do
|
def parse_authorization_headers(headers) do
|
||||||
headers
|
headers
|
||||||
|> Enum.filter(&(elem(&1, 0) == "authorization"))
|
|> Enum.filter(&(elem(&1, 0) == "authorization"))
|
||||||
|
@ -77,9 +52,10 @@ defmodule MatrixServerWeb.AuthenticateServer do
|
||||||
Regex.named_captures(@auth_header_regex, auth_header)
|
Regex.named_captures(@auth_header_regex, auth_header)
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&Kernel.is_nil/1)
|
|> Enum.reject(&Kernel.is_nil/1)
|
||||||
|> Enum.reduce(%{}, fn %{"origin" => origin, "key" => key, "sig" => sig}, acc ->
|
|> Enum.map(fn %{"origin" => origin, "key" => key, "sig" => sig} ->
|
||||||
Map.update(acc, origin, %{key => sig}, &Map.put(&1, key, sig))
|
{origin, key, sig}
|
||||||
end)
|
end)
|
||||||
|
|> Enum.filter(fn {_, key, _} -> String.starts_with?(key, "ed25519:") end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
|
@ -89,10 +65,14 @@ defmodule MatrixServerWeb.AuthenticateServer do
|
||||||
def action(conn, _) do
|
def action(conn, _) do
|
||||||
action = action_name(conn)
|
action = action_name(conn)
|
||||||
|
|
||||||
if action not in unquote(except) and
|
if action not in unquote(except) do
|
||||||
not MatrixServerWeb.AuthenticateServer.authenticated?(conn) do
|
case MatrixServerWeb.AuthenticateServer.authenticate(conn) do
|
||||||
IO.puts("Not authorized!")
|
{origin, _key, _sig} ->
|
||||||
apply(__MODULE__, action, [conn, conn.params])
|
conn = Plug.Conn.assign(conn, :origin, origin)
|
||||||
|
apply(__MODULE__, action, [conn, conn.params])
|
||||||
|
:error ->
|
||||||
|
put_error(conn, :unauthorized, "Signature verification failed.")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
apply(__MODULE__, action, [conn, conn.params])
|
apply(__MODULE__, action, [conn, conn.params])
|
||||||
end
|
end
|
||||||
|
|
55
lib/matrix_server_web/federation/query_controller.ex
Normal file
55
lib/matrix_server_web/federation/query_controller.ex
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule MatrixServerWeb.Federation.QueryController do
|
||||||
|
use MatrixServerWeb, :controller
|
||||||
|
use MatrixServerWeb.AuthenticateServer
|
||||||
|
|
||||||
|
import MatrixServerWeb.Plug.Error
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias MatrixServer.{Repo, Account}
|
||||||
|
|
||||||
|
defmodule ProfileRequest do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
embedded_schema do
|
||||||
|
field :user_id, :string
|
||||||
|
field :field, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate(params) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> cast(params, [:user_id, :field])
|
||||||
|
|> validate_required([:user_id])
|
||||||
|
|> validate_inclusion(:field, ["displayname", "avatar_url"])
|
||||||
|
|> tap(fn
|
||||||
|
%Ecto.Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
|
||||||
|
_ -> :error
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def profile(conn, params) do
|
||||||
|
with %ProfileRequest{user_id: user_id} <- ProfileRequest.validate(params) do
|
||||||
|
if MatrixServer.get_domain(user_id) == MatrixServer.server_name() do
|
||||||
|
localpart = MatrixServer.get_localpart(user_id)
|
||||||
|
|
||||||
|
case Repo.one(from a in Account, where: a.localpart == ^localpart) do
|
||||||
|
%Account{} ->
|
||||||
|
# TODO: Return displayname and avatar_url when we implement them.
|
||||||
|
conn
|
||||||
|
|> put_status(200)
|
||||||
|
|> json(%{})
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
put_error(:not_found, "User does not exist.")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
put_error(:not_found, "Wrong server name.")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_ -> put_error(conn, :bad_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,10 +0,0 @@
|
||||||
defmodule MatrixServerWeb.Federation.TestController do
|
|
||||||
use MatrixServerWeb, :controller
|
|
||||||
use MatrixServerWeb.AuthenticateServer
|
|
||||||
|
|
||||||
def test(conn, _params) do
|
|
||||||
conn
|
|
||||||
|> put_status(200)
|
|
||||||
|> json(%{})
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -20,26 +20,27 @@ defmodule MatrixServerWeb.FederationClient 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
|
# TODO: Create tesla middleware to add signature and headers.
|
||||||
origin = "localhost:4001"
|
def query_profile(client, server_name, user_id, field \\ nil) do
|
||||||
destination = "localhost:4000"
|
origin = MatrixServer.server_name()
|
||||||
path = RouteHelpers.test_path(Endpoint, :test)
|
path = RouteHelpers.query_path(Endpoint, :profile) |> Tesla.build_url(user_id: user_id)
|
||||||
|
path = if field, do: Tesla.build_url(path, field: field), else: path
|
||||||
|
|
||||||
params = %{
|
object_to_sign = %{
|
||||||
method: "POST",
|
method: "GET",
|
||||||
uri: path,
|
uri: path,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
destination: destination,
|
destination: server_name
|
||||||
content: %{"hi" => "hello"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, signed_object} = MatrixServer.SigningServer.sign_object(params)
|
{:ok, signature, key_id} = MatrixServer.SigningServer.sign_object(object_to_sign)
|
||||||
auth_headers = create_signature_authorization_headers(signed_object, origin)
|
signatures = %{origin => %{key_id => signature}}
|
||||||
|
auth_headers = create_signature_authorization_headers(signatures, origin)
|
||||||
|
|
||||||
Tesla.post(client, path, signed_object, headers: auth_headers)
|
Tesla.get(client, path, headers: auth_headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_signature_authorization_headers(%{signatures: signatures}, origin) do
|
defp create_signature_authorization_headers(signatures, origin) do
|
||||||
Enum.map(signatures[origin], fn {key, sig} ->
|
Enum.map(signatures[origin], fn {key, sig} ->
|
||||||
{"Authorization", "X-Matrix origin=#{origin},key=\"#{key}\",sig=\"#{sig}\""}
|
{"Authorization", "X-Matrix origin=#{origin},key=\"#{key}\",sig=\"#{sig}\""}
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -11,10 +11,11 @@ defmodule MatrixServerWeb.Plug.Error do
|
||||||
unknown: {400, "M_UNKNOWN", "An unknown error occurred."},
|
unknown: {400, "M_UNKNOWN", "An unknown error occurred."},
|
||||||
invalid_room_state:
|
invalid_room_state:
|
||||||
{400, "M_INVALID_ROOM_STATE", "The request would leave the room in an invalid state."},
|
{400, "M_INVALID_ROOM_STATE", "The request would leave the room in an invalid state."},
|
||||||
|
unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."},
|
||||||
unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."},
|
unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."},
|
||||||
missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
|
missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
|
||||||
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},
|
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},
|
||||||
room_alias_exists: {409, "M.UNKNOWN", "The given room alias already exists."}
|
room_alias_exists: {409, "M_UNKNOWN", "The given room alias already exists."}
|
||||||
}
|
}
|
||||||
|
|
||||||
def put_error(conn, {error, msg}), do: put_error(conn, error, msg)
|
def put_error(conn, {error, msg}), do: put_error(conn, error, msg)
|
||||||
|
|
|
@ -56,7 +56,7 @@ defmodule MatrixServerWeb.Router do
|
||||||
scope "/_matrix", MatrixServerWeb.Federation do
|
scope "/_matrix", MatrixServerWeb.Federation do
|
||||||
pipe_through :authenticate_server
|
pipe_through :authenticate_server
|
||||||
|
|
||||||
post "/test", TestController, :test
|
get "/federation/v1/query/profile", QueryController, :profile
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", MatrixServerWeb.Client do
|
scope "/", MatrixServerWeb.Client do
|
||||||
|
|
Loading…
Reference in a new issue