Handle enacl exceptions during signature checks

Fix usage of undecoded signing key during server authentication
Fix several bugs in profile query endpoint
This commit is contained in:
Pim Kunis 2021-08-13 17:36:34 +02:00
parent ff3dd38369
commit c5de486dba
5 changed files with 52 additions and 40 deletions

View file

@ -116,4 +116,14 @@ defmodule MatrixServer do
Ecto.Changeset.validate_change(changeset, field, augmented_func)
end
# Returns a Boolean whether the signature is valid.
# Also returns false on ArgumentError.
def sign_verify(sig, text, key) do
try do
:enacl.sign_verify_detached(sig, text, key)
rescue
ArgumentError -> false
end
end
end

View file

@ -33,10 +33,16 @@ defmodule MatrixServer.ServerKeyInfo do
end
defp refresh_signing_keys(server_name) do
# TODO: Handle expired keys.
in_a_week = System.os_time(:millisecond) + 1000 * 60 * 60 * 24 * 7
client = FederationClient.client(server_name)
with {:ok, %GetSigningKeys{verify_keys: verify_keys, valid_until_ts: valid_until}} <-
with {:ok,
%GetSigningKeys{
server_name: server_name,
verify_keys: verify_keys,
valid_until_ts: valid_until
}} <-
FederationClient.get_signing_keys(client) do
signing_keys =
Enum.map(verify_keys, fn {key_id, %{"key" => key}} ->
@ -47,7 +53,7 @@ defmodule MatrixServer.ServerKeyInfo do
ski = %ServerKeyInfo{server_name: server_name, valid_until: min(valid_until, in_a_week)}
case upsert_multi(server_name, ski, signing_keys) |> Repo.transaction() do
{:ok, %{ski: ski}} -> {:ok, ski}
{:ok, %{new_ski: ski}} -> {:ok, ski}
{:error, _} -> :error
end
else
@ -63,7 +69,7 @@ defmodule MatrixServer.ServerKeyInfo do
conflict_target: [:server_name]
)
|> Multi.insert_all(:insert_keys, SigningKey, signing_keys, on_conflict: :nothing)
|> Multi.run(:ski, fn _, _ ->
|> Multi.run(:new_ski, fn _, _ ->
case with_signing_keys(server_name) do
nil -> {:error, :ski}
ski -> {:ok, ski}

View file

@ -1,7 +1,7 @@
defmodule MatrixServerWeb.AuthenticateServer do
import MatrixServerWeb.Plug.Error
alias MatrixServer.SigningKey
alias MatrixServer.{SigningKey, ServerKeyInfo}
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
@ -27,18 +27,22 @@ defmodule MatrixServerWeb.AuthenticateServer do
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) do
# TODO: Only query once per origin.
# TODO: Handle expired keys.
SigningKey.for_server(origin)
|> Enum.find_value(false, fn %SigningKey{signing_key: signing_key} ->
:enacl.sign_verify_detached(raw_sig, encoded_object, signing_key)
{: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

View file

@ -23,7 +23,7 @@ defmodule MatrixServerWeb.Federation.QueryController do
|> cast(params, [:user_id, :field])
|> validate_required([:user_id])
|> validate_inclusion(:field, ["displayname", "avatar_url"])
|> tap(fn
|> then(fn
%Ecto.Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
_ -> :error
end)
@ -31,7 +31,7 @@ defmodule MatrixServerWeb.Federation.QueryController do
end
def profile(conn, params) do
with %ProfileRequest{user_id: user_id} <- ProfileRequest.validate(params) do
with {:ok, %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)
@ -43,10 +43,10 @@ defmodule MatrixServerWeb.Federation.QueryController do
|> json(%{})
nil ->
put_error(:not_found, "User does not exist.")
put_error(conn, :not_found, "User does not exist.")
end
else
put_error(:not_found, "Wrong server name.")
put_error(conn, :not_found, "Wrong server name.")
end
else
_ -> put_error(conn, :bad_json)

View file

@ -17,39 +17,31 @@ defmodule MatrixServerWeb.FederationClient do
Tesla.client([{Tesla.Middleware.BaseUrl, "http://" <> server_name} | @middleware], @adapter)
end
@path RouteHelpers.key_path(Endpoint, :get_signing_keys)
def get_signing_keys(client) do
# TODO: Which server_name should we take?
# TODO: Should probably catch enacl exceptions and just return error atom,
# create seperate function for this.
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) do
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.
if Map.has_key?(sigs, server_name) do
server_sigs = sigs[server_name]
all_keys_signed? =
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
:enacl.sign_verify_detached(decoded_sig, encoded_body, decoded_key)
else
_ -> false
end
end)
if all_keys_signed? do
{:ok, 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
:error
_ -> false
end
else
:error
end
end)
|> then(fn
true -> {:ok, response}
false -> :error
end)
else
_ -> :error
end
end