Create tesla middleware for adding signature to federation requests
Change repo structure
This commit is contained in:
parent
214293323c
commit
be9860f7d0
23 changed files with 83 additions and 58 deletions
88
lib/matrix_server_web/federation/authenticate_server.ex
Normal file
88
lib/matrix_server_web/federation/authenticate_server.ex
Normal file
|
@ -0,0 +1,88 @@
|
|||
defmodule MatrixServerWeb.Federation.AuthenticateServer do
|
||||
import MatrixServerWeb.Error
|
||||
|
||||
alias MatrixServer.{SigningKey, ServerKeyInfo}
|
||||
|
||||
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
|
||||
|
||||
def authenticate(%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()
|
||||
}
|
||||
|
||||
object_to_sign =
|
||||
if method != "GET", do: Map.put(object_to_sign, :content, body_params), else: object_to_sign
|
||||
|
||||
object_fun = &Map.put(object_to_sign, :origin, &1)
|
||||
|
||||
authenticate_with_headers(headers, object_fun)
|
||||
end
|
||||
|
||||
defp authenticate_with_headers(headers, object_fun) do
|
||||
# TODO: Only query once per origin.
|
||||
headers
|
||||
|> parse_authorization_headers()
|
||||
|> Enum.find(:error, fn {origin, _, sig} ->
|
||||
object = object_fun.(origin)
|
||||
|
||||
with {:ok, raw_sig} <- MatrixServer.decode_base64(sig),
|
||||
{:ok, encoded_object} <- MatrixServer.encode_canonical_json(object),
|
||||
{:ok, %ServerKeyInfo{signing_keys: keys}} <-
|
||||
ServerKeyInfo.with_fresh_signing_keys(origin) do
|
||||
Enum.find_value(keys, false, fn %SigningKey{signing_key: signing_key} ->
|
||||
with {:ok, decoded_key} <- MatrixServer.decode_base64(signing_key) do
|
||||
MatrixServer.sign_verify(raw_sig, encoded_object, decoded_key)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
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.map(fn %{"origin" => origin, "key" => key, "sig" => sig} ->
|
||||
{origin, key, sig}
|
||||
end)
|
||||
|> Enum.filter(fn {_, key, _} -> String.starts_with?(key, "ed25519:") 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) do
|
||||
case MatrixServerWeb.Federation.AuthenticateServer.authenticate(conn) do
|
||||
{origin, _key, _sig} ->
|
||||
conn = Plug.Conn.assign(conn, :origin, origin)
|
||||
apply(__MODULE__, action, [conn, conn.params])
|
||||
|
||||
:error ->
|
||||
put_error(conn, :unauthorized, "Signature verification failed.")
|
||||
end
|
||||
else
|
||||
apply(__MODULE__, action, [conn, conn.params])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
defmodule MatrixServerWeb.Federation.KeyController do
|
||||
use MatrixServerWeb, :controller
|
||||
|
||||
import MatrixServerWeb.Plug.Error
|
||||
import MatrixServerWeb.Error
|
||||
|
||||
alias MatrixServer.KeyServer
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule MatrixServerWeb.Federation.QueryController do
|
||||
use MatrixServerWeb, :controller
|
||||
use MatrixServerWeb.AuthenticateServer
|
||||
use MatrixServerWeb.Federation.AuthenticateServer
|
||||
|
||||
import MatrixServerWeb.Plug.Error
|
||||
import MatrixServerWeb.Error
|
||||
import Ecto.Query
|
||||
|
||||
alias MatrixServer.{Repo, Account}
|
69
lib/matrix_server_web/federation/http_client.ex
Normal file
69
lib/matrix_server_web/federation/http_client.ex
Normal file
|
@ -0,0 +1,69 @@
|
|||
defmodule MatrixServerWeb.Federation.HTTPClient do
|
||||
use Tesla
|
||||
|
||||
alias MatrixServerWeb.Endpoint
|
||||
alias MatrixServerWeb.Federation.Request.GetSigningKeys
|
||||
alias MatrixServerWeb.Federation.Middleware.SignRequest
|
||||
alias MatrixServerWeb.Router.Helpers, as: RouteHelpers
|
||||
|
||||
# TODO: Maybe create database-backed homeserver struct to pass to client function.
|
||||
# TODO: Fix error propagation.
|
||||
|
||||
@adapter {Tesla.Adapter.Finch, name: MatrixServerWeb.HTTPClient}
|
||||
|
||||
def client(server_name) do
|
||||
Tesla.client(
|
||||
[
|
||||
{Tesla.Middleware.Opts, [server_name: server_name]},
|
||||
SignRequest,
|
||||
{Tesla.Middleware.BaseUrl, "http://" <> server_name},
|
||||
Tesla.Middleware.JSON
|
||||
],
|
||||
@adapter
|
||||
)
|
||||
end
|
||||
|
||||
def get_signing_keys(client) do
|
||||
path = RouteHelpers.key_path(Endpoint, :get_signing_keys)
|
||||
|
||||
with {:ok,
|
||||
%GetSigningKeys{server_name: server_name, verify_keys: verify_keys, signatures: sigs} =
|
||||
response} <- tesla_request(:get, client, path, GetSigningKeys),
|
||||
{:ok, encoded_body} <- MatrixServer.serialize_and_encode(response),
|
||||
server_sigs when not is_nil(server_sigs) <- sigs[server_name] do
|
||||
# For each verify key, check if there is a matching signature.
|
||||
# If not, invalidate the whole response.
|
||||
Enum.all?(verify_keys, fn {key_id, %{"key" => key}} ->
|
||||
with true <- Map.has_key?(server_sigs, key_id),
|
||||
{:ok, decoded_key} <- MatrixServer.decode_base64(key),
|
||||
{:ok, decoded_sig} <- MatrixServer.decode_base64(server_sigs[key_id]) do
|
||||
MatrixServer.sign_verify(decoded_sig, encoded_body, decoded_key)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
|> then(fn
|
||||
true -> {:ok, response}
|
||||
false -> :error
|
||||
end)
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def query_profile(client, user_id, field \\ nil) do
|
||||
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
|
||||
|
||||
Tesla.get(client, path)
|
||||
end
|
||||
|
||||
defp tesla_request(method, client, path, request_schema) do
|
||||
with {:ok, %Tesla.Env{body: body}} <- Tesla.request(client, url: path, method: method),
|
||||
%Ecto.Changeset{valid?: true} = cs <- apply(request_schema, :changeset, [body]) do
|
||||
{:ok, Ecto.Changeset.apply_changes(cs)}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
end
|
38
lib/matrix_server_web/federation/sign_request_middleware.ex
Normal file
38
lib/matrix_server_web/federation/sign_request_middleware.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
defmodule MatrixServerWeb.Federation.Middleware.SignRequest do
|
||||
@behaviour Tesla.Middleware
|
||||
|
||||
def call(%Tesla.Env{opts: opts} = env, next, _opts) do
|
||||
sign = Keyword.get(opts, :sign, true)
|
||||
|
||||
case sign_request(env, sign) do
|
||||
%Tesla.Env{} = env -> Tesla.run(env, next)
|
||||
:error -> {:error, :sign_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp sign_request(env, false), do: env
|
||||
|
||||
defp sign_request(%Tesla.Env{method: method, url: path, opts: opts} = env, true) do
|
||||
origin = MatrixServer.server_name()
|
||||
|
||||
object_to_sign = %{
|
||||
method: Atom.to_string(method) |> String.upcase(),
|
||||
origin: origin,
|
||||
uri: URI.decode_www_form(path),
|
||||
destination: Keyword.fetch!(opts, :server_name)
|
||||
}
|
||||
|
||||
with {:ok, sig, key_id} <- MatrixServer.KeyServer.sign_object(object_to_sign) do
|
||||
sigs = %{origin => %{key_id => sig}}
|
||||
auth_headers = create_signature_authorization_headers(sigs, origin)
|
||||
|
||||
Tesla.put_headers(env, auth_headers)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_signature_authorization_headers(signatures, origin) do
|
||||
Enum.map(signatures[origin], fn {key, sig} ->
|
||||
{"Authorization", "X-Matrix origin=#{origin},key=\"#{key}\",sig=\"#{sig}\""}
|
||||
end)
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue