
Previously there were mainly two attack vectors:
- for raw keys the owner <-> key mapping wasn't verified at all
- keys were retrieved with refetching allowed
and only the top-level ID was sanitised while
usually keys are but a subobject
This reintroduces public key checks in the user actor,
previously removed in 9728e2f8f7
but now adapted to account for the new mapping mechanism.
255 lines
7.7 KiB
Elixir
255 lines
7.7 KiB
Elixir
defmodule Pleroma.User.SigningKey do
|
|
use Ecto.Schema
|
|
import Ecto.Query
|
|
import Ecto.Changeset
|
|
require Pleroma.Constants
|
|
alias Pleroma.User
|
|
alias Pleroma.Repo
|
|
|
|
require Logger
|
|
|
|
@primary_key false
|
|
schema "signing_keys" do
|
|
belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType)
|
|
field :public_key, :string
|
|
field :private_key, :string
|
|
# This is an arbitrary field given by the remote instance
|
|
field :key_id, :string, primary_key: true
|
|
timestamps()
|
|
end
|
|
|
|
def load_key(%User{} = user) do
|
|
user
|
|
|> Repo.preload(:signing_key)
|
|
end
|
|
|
|
def key_id_of_local_user(%User{local: true} = user) do
|
|
case Repo.preload(user, :signing_key) do
|
|
%User{signing_key: %__MODULE__{key_id: key_id}} -> key_id
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
@spec remote_changeset(__MODULE__, map) :: Changeset.t()
|
|
def remote_changeset(%__MODULE__{} = signing_key, attrs) do
|
|
signing_key
|
|
|> cast(attrs, [:public_key, :key_id])
|
|
|> validate_required([:public_key, :key_id])
|
|
end
|
|
|
|
@spec key_id_to_user_id(String.t()) :: String.t() | nil
|
|
@doc """
|
|
Given a key ID, return the user ID associated with that key.
|
|
Returns nil if the key ID is not found.
|
|
"""
|
|
def key_id_to_user_id(key_id) do
|
|
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|
|
|> select([sk], sk.user_id)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@spec key_id_to_ap_id(String.t()) :: String.t() | nil
|
|
@doc """
|
|
Given a key ID, return the AP ID associated with that key.
|
|
Returns nil if the key ID is not found.
|
|
"""
|
|
def key_id_to_ap_id(key_id) do
|
|
Logger.debug("Looking up key ID: #{key_id}")
|
|
|
|
result =
|
|
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|
|
|> join(:inner, [sk], u in User, on: sk.user_id == u.id)
|
|
|> select([sk, u], %{user: u})
|
|
|> Repo.one()
|
|
|
|
case result do
|
|
%{user: %User{ap_id: ap_id}} -> ap_id
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
@spec generate_rsa_pem() :: {:ok, binary()}
|
|
@doc """
|
|
Generate a new RSA private key and return it as a PEM-encoded string.
|
|
"""
|
|
def generate_rsa_pem do
|
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
|
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
|
{:ok, pem}
|
|
end
|
|
|
|
@spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()}
|
|
@doc """
|
|
Generate a new RSA key pair and create a changeset for it
|
|
"""
|
|
def generate_local_keys(ap_id) do
|
|
{:ok, private_pem} = generate_rsa_pem()
|
|
{:ok, local_pem} = private_pem_to_public_pem(private_pem)
|
|
|
|
%__MODULE__{}
|
|
|> change()
|
|
|> put_change(:public_key, local_pem)
|
|
|> put_change(:private_key, private_pem)
|
|
|> put_change(:key_id, local_key_id(ap_id))
|
|
end
|
|
|
|
@spec local_key_id(String.t()) :: String.t()
|
|
@doc """
|
|
Given an AP ID, return the key ID for the local user.
|
|
"""
|
|
def local_key_id(ap_id) do
|
|
ap_id <> "#main-key"
|
|
end
|
|
|
|
@spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()}
|
|
@doc """
|
|
Given a private key in PEM format, return the corresponding public key in PEM format.
|
|
"""
|
|
def private_pem_to_public_pem(private_pem) do
|
|
[private_key_code] = :public_key.pem_decode(private_pem)
|
|
private_key = :public_key.pem_entry_decode(private_key_code)
|
|
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
|
|
public_key = {:RSAPublicKey, modulus, exponent}
|
|
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
|
{:ok, :public_key.pem_encode([public_key])}
|
|
end
|
|
|
|
@spec public_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
|
|
@doc """
|
|
Given a user, return the public key for that user in binary format.
|
|
"""
|
|
def public_key(%User{} = user) do
|
|
case Repo.preload(user, :signing_key) do
|
|
%User{signing_key: %__MODULE__{public_key: public_key_pem}} ->
|
|
key =
|
|
public_key_pem
|
|
|> :public_key.pem_decode()
|
|
|> hd()
|
|
|> :public_key.pem_entry_decode()
|
|
|
|
{:ok, key}
|
|
|
|
_ ->
|
|
{:error, "key not found"}
|
|
end
|
|
end
|
|
|
|
def public_key(_), do: {:error, "key not found"}
|
|
|
|
def public_key_pem(%User{} = user) do
|
|
case Repo.preload(user, :signing_key) do
|
|
%User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem}
|
|
_ -> {:error, "key not found"}
|
|
end
|
|
end
|
|
|
|
def public_key_pem(_e) do
|
|
{:error, "key not found"}
|
|
end
|
|
|
|
@spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
|
|
@doc """
|
|
Given a user, return the private key for that user in binary format.
|
|
"""
|
|
def private_key(%User{} = user) do
|
|
case Repo.preload(user, :signing_key) do
|
|
%{signing_key: %__MODULE__{private_key: private_key_pem}} ->
|
|
key =
|
|
private_key_pem
|
|
|> :public_key.pem_decode()
|
|
|> hd()
|
|
|> :public_key.pem_entry_decode()
|
|
|
|
{:ok, key}
|
|
|
|
_ ->
|
|
{:error, "key not found"}
|
|
end
|
|
end
|
|
|
|
@spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
|
|
@doc """
|
|
Given a key ID, return the signing key associated with that key.
|
|
Will either return the key if it exists locally, or fetch it from the remote instance.
|
|
"""
|
|
def get_or_fetch_by_key_id(key_id) do
|
|
case key_id_to_user_id(key_id) do
|
|
nil ->
|
|
fetch_remote_key(key_id)
|
|
|
|
user_id ->
|
|
{:ok, Repo.get_by(__MODULE__, user_id: user_id)}
|
|
end
|
|
end
|
|
|
|
@spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
|
|
@doc """
|
|
Fetch a remote key by key ID.
|
|
Will send a request to the remote instance to get the key ID.
|
|
This request should, at the very least, return a user ID and a public key object.
|
|
Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request.
|
|
This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key...
|
|
So if we're rejected, we should probably just give up.
|
|
"""
|
|
def fetch_remote_key(key_id) do
|
|
Logger.debug("Fetching remote key: #{key_id}")
|
|
|
|
with {:ok, _body} = resp <-
|
|
Pleroma.Object.Fetcher.fetch_and_contain_remote_key(key_id),
|
|
{:ok, ap_id, public_key_pem} <- handle_signature_response(resp) do
|
|
Logger.debug("Fetched remote key: #{ap_id}")
|
|
# fetch the user
|
|
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
|
|
# store the key
|
|
key = %__MODULE__{
|
|
user_id: user.id,
|
|
public_key: public_key_pem,
|
|
key_id: key_id
|
|
}
|
|
|
|
Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id)
|
|
else
|
|
e ->
|
|
Logger.debug("Failed to fetch remote key: #{inspect(e)}")
|
|
{:error, "Could not fetch key"}
|
|
end
|
|
end
|
|
|
|
# Take the response from the remote instance and extract the key details
|
|
# will check if the key ID matches the owner of the key, if not, error
|
|
defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do
|
|
if ap_id !== public_key["owner"] do
|
|
{:error, "Key ID does not match owner"}
|
|
else
|
|
%{"publicKeyPem" => public_key_pem} = public_key
|
|
{:ok, ap_id, public_key_pem}
|
|
end
|
|
end
|
|
|
|
defp handle_signature_response({:ok, body}) do
|
|
case body do
|
|
%{
|
|
"type" => "CryptographicKey",
|
|
"publicKeyPem" => public_key_pem,
|
|
"owner" => ap_id
|
|
} ->
|
|
{:ok, ap_id, public_key_pem}
|
|
|
|
# for when we get a subset of the user object
|
|
%{
|
|
"id" => _user_id,
|
|
"publicKey" => _public_key,
|
|
"type" => actor_type
|
|
}
|
|
when actor_type in Pleroma.Constants.actor_types() ->
|
|
extract_key_details(body)
|
|
|
|
%{"error" => error} ->
|
|
{:error, error}
|
|
end
|
|
end
|
|
|
|
defp handle_signature_response({:error, e}), do: {:error, e}
|
|
defp handle_signature_response(other), do: {:error, "Could not fetch key: #{inspect(other)}"}
|
|
end
|