User: generate private keys on user creation

This fixes a race condition bug where keys could be regenerated
post-federation, causing activities and HTTP signatures from an user to
be dropped due to key differences.
This commit is contained in:
Hélène 2022-08-26 18:30:43 +02:00 committed by FloatingGhost
parent b6891fe190
commit 0b14f02ed2
9 changed files with 32 additions and 95 deletions

View file

@ -66,9 +66,8 @@ defmodule Pleroma.Signature do
end end
end end
def sign(%User{} = user, headers) do def sign(%User{keys: keys} = user, headers) do
with {:ok, %{keys: keys}} <- User.ensure_keys_present(user), with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end end
end end

View file

@ -681,9 +681,9 @@ defmodule Pleroma.User do
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames])) |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> put_ap_id() |> put_ap_id()
|> put_keys()
|> unique_constraint(:ap_id) |> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address() |> put_following_and_follower_and_featured_address()
|> put_private_key()
end end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
@ -741,10 +741,10 @@ defmodule Pleroma.User do
|> validate_length(:registration_reason, max: reason_limit) |> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external]) |> maybe_validate_required_email(opts[:external])
|> put_password_hash |> put_password_hash
|> put_keys()
|> put_ap_id() |> put_ap_id()
|> unique_constraint(:ap_id) |> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address() |> put_following_and_follower_and_featured_address()
|> put_private_key()
end end
def maybe_validate_required_email(changeset, true), do: changeset def maybe_validate_required_email(changeset, true), do: changeset
@ -757,11 +757,6 @@ defmodule Pleroma.User do
end end
end end
def put_keys(changeset) do
{:ok, pem} = Keys.generate_rsa_pem()
put_change(changeset, :keys, pem)
end
def put_ap_id(changeset) do def put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)}) ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id) put_change(changeset, :ap_id, ap_id)
@ -779,6 +774,11 @@ defmodule Pleroma.User do
|> put_change(:featured_address, featured) |> put_change(:featured_address, featured)
end end
defp put_private_key(changeset) do
{:ok, pem} = Keys.generate_rsa_pem()
put_change(changeset, :keys, pem)
end
defp autofollow_users(user) do defp autofollow_users(user) do
candidates = Config.get([:instance, :autofollowed_nicknames]) candidates = Config.get([:instance, :autofollowed_nicknames])
@ -1955,6 +1955,7 @@ defmodule Pleroma.User do
follower_address: uri <> "/followers" follower_address: uri <> "/followers"
} }
|> change |> change
|> put_private_key()
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> Repo.insert() |> Repo.insert()
|> set_cache() |> set_cache()
@ -2220,17 +2221,6 @@ defmodule Pleroma.User do
} }
end end
def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
user
|> cast(%{keys: pem}, [:keys])
|> validate_required([:keys])
|> update_and_set_cache()
end
end
def get_ap_ids_by_nicknames(nicknames) do def get_ap_ids_by_nicknames(nicknames) do
from(u in User, from(u in User,
where: u.nickname in ^nicknames, where: u.nickname in ^nicknames,

View file

@ -66,8 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
def user(conn, %{"nickname" => nickname}) do def user(conn, %{"nickname" => nickname}) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname), with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
@ -174,7 +173,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_follows, true} <- {:show_follows, true} <-
{:show_follows, (for_user && for_user == user) || !user.hide_follows} do {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
@ -192,8 +190,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname) do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
@ -213,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_followers, true} <- {:show_followers, true} <-
{:show_followers, (for_user && for_user == user) || !user.hide_followers} do {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
@ -231,8 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname) do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
@ -245,8 +240,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
%{"nickname" => nickname, "page" => page?} = params %{"nickname" => nickname, "page" => page?} = params
) )
when page? in [true, "true"] do when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} <- User.ensure_keys_present(user) do
# "include_poll_votes" is a hack because postgres generates inefficient # "include_poll_votes" is a hack because postgres generates inefficient
# queries when filtering by 'Answer', poll votes will be hidden by the # queries when filtering by 'Answer', poll votes will be hidden by the
# visibility filter in this case anyway # visibility filter in this case anyway
@ -270,8 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
def outbox(conn, %{"nickname" => nickname}) do def outbox(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
@ -328,14 +321,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
defp represent_service_actor(%User{} = user, conn) do defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do conn
conn |> put_resp_content_type("application/activity+json")
|> put_resp_content_type("application/activity+json") |> put_view(UserView)
|> put_view(UserView) |> render("user.json", %{user: user})
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
end
end end
defp represent_service_actor(nil, _), do: {:error, :not_found} defp represent_service_actor(nil, _), do: {:error, :not_found}
@ -388,12 +377,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{ def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
"nickname" => nickname "nickname" => nickname
}) do }) do
with {:ok, user} <- User.ensure_keys_present(user) do conn
conn |> put_resp_content_type("application/activity+json")
|> put_resp_content_type("application/activity+json") |> put_view(UserView)
|> put_view(UserView) |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
end end
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
@ -530,19 +517,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn conn
end end
defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
{:ok, new_user} = User.ensure_keys_present(user)
for_user =
if new_user != user and match?(%User{}, for_user) do
User.get_cached_by_nickname(for_user.nickname)
else
for_user
end
{new_user, for_user}
end
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <- with {:ok, object} <-
ActivityPub.upload( ActivityPub.upload(

View file

@ -34,7 +34,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("endpoints.json", _), do: %{} def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do def render("service.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
@ -71,7 +70,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])

View file

@ -69,10 +69,8 @@ defmodule Pleroma.Web.Federator do
def perform(:publish, activity) do def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]), %User{} = actor = User.get_cached_by_ap_id(activity.data["actor"])
{:ok, actor} <- User.ensure_keys_present(actor) do Publisher.publish(actor, activity)
Publisher.publish(actor, activity)
end
end end
def perform(:incoming_ap_doc, params) do def perform(:incoming_ap_doc, params) do

View file

@ -69,8 +69,6 @@ defmodule Pleroma.Web.WebFinger do
end end
def represent_user(user, "JSON") do def represent_user(user, "JSON") do
{:ok, user} = User.ensure_keys_present(user)
%{ %{
"subject" => "acct:#{user.nickname}@#{domain()}", "subject" => "acct:#{user.nickname}@#{domain()}",
"aliases" => gather_aliases(user), "aliases" => gather_aliases(user),
@ -79,8 +77,6 @@ defmodule Pleroma.Web.WebFinger do
end end
def represent_user(user, "XML") do def represent_user(user, "XML") do
{:ok, user} = User.ensure_keys_present(user)
aliases = aliases =
user user
|> gather_aliases() |> gather_aliases()

View file

@ -620,15 +620,15 @@ defmodule Pleroma.UserTest do
assert changeset.valid? assert changeset.valid?
end end
test "it sets the password_hash, ap_id and PEM key" do test "it sets the password_hash, ap_id, private key and followers collection address" do
changeset = User.register_changeset(%User{}, @full_user_data) changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid? assert changeset.valid?
assert is_binary(changeset.changes[:password_hash]) assert is_binary(changeset.changes[:password_hash])
assert is_binary(changeset.changes[:keys])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert is_binary(changeset.changes[:keys]) assert is_binary(changeset.changes[:keys])
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
@ -2130,21 +2130,6 @@ defmodule Pleroma.UserTest do
end end
end end
describe "ensure_keys_present" do
test "it creates keys for a user and stores them in info" do
user = insert(:user)
refute is_binary(user.keys)
{:ok, user} = User.ensure_keys_present(user)
assert is_binary(user.keys)
end
test "it doesn't create keys if there already are some" do
user = insert(:user, keys: "xxx")
{:ok, user} = User.ensure_keys_present(user)
assert user.keys == "xxx"
end
end
describe "get_ap_ids_by_nicknames" do describe "get_ap_ids_by_nicknames" do
test "it returns a list of AP ids for a given set of nicknames" do test "it returns a list of AP ids for a given set of nicknames" do
user = insert(:user) user = insert(:user)

View file

@ -12,7 +12,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
test "Renders a user, including the public key" do test "Renders a user, including the public key" do
user = insert(:user) user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
@ -55,7 +54,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
test "Does not add an avatar image if the user hasn't set one" do test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user) user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
refute result["icon"] refute result["icon"]
@ -67,8 +65,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
banner: %{"url" => [%{"href" => "https://somebanner"}]} banner: %{"url" => [%{"href" => "https://somebanner"}]}
) )
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl" assert result["icon"]["url"] == "https://someurl"
assert result["image"]["url"] == "https://somebanner" assert result["image"]["url"] == "https://somebanner"
@ -89,7 +85,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
describe "endpoints" do describe "endpoints" do
test "local users have a usable endpoints structure" do test "local users have a usable endpoints structure" do
user = insert(:user) user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
@ -105,7 +100,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
test "remote users have an empty endpoints structure" do test "remote users have an empty endpoints structure" do
user = insert(:user, local: false) user = insert(:user, local: false)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
@ -115,7 +109,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
test "instance users do not expose oAuth endpoints" do test "instance users do not expose oAuth endpoints" do
user = insert(:user, nickname: nil, local: true) user = insert(:user, nickname: nil, local: true)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Factory do
require Pleroma.Constants require Pleroma.Constants
alias Pleroma.Keys
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@ -28,6 +29,8 @@ defmodule Pleroma.Factory do
end end
def user_factory(attrs \\ %{}) do def user_factory(attrs \\ %{}) do
{:ok, pem} = Keys.generate_rsa_pem()
user = %User{ user = %User{
name: sequence(:name, &"Test テスト User #{&1}"), name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"), email: sequence(:email, &"user#{&1}@example.com"),
@ -39,7 +42,8 @@ defmodule Pleroma.Factory do
last_refreshed_at: NaiveDateTime.utc_now(), last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{}, notification_settings: %Pleroma.User.NotificationSetting{},
multi_factor_authentication_settings: %Pleroma.MFA.Settings{}, multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
ap_enabled: true ap_enabled: true,
keys: pem
} }
urls = urls =