Commit 04580334 authored by kaniini's avatar kaniini
Browse files

move key generation functions into Pleroma.Keys module

parent be6b5719
Pipeline #12160 failed with stages
in 9 minutes and 50 seconds
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Keys do
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do
_ = :public_key.generate_key({:rsa, 2048, 65_537})
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
rescue
_ ->
def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} =
receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
:error
end
end
end
def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
{:ok, private_key, public_key}
end
end
...@@ -5,11 +5,10 @@ ...@@ -5,11 +5,10 @@
defmodule Pleroma.Signature do defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter @behaviour HTTPSignatures.Adapter
alias Pleroma.Keys
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
def fetch_public_key(conn) do def fetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]), with actor_id <- Utils.get_ap_id(conn.params["actor"]),
...@@ -33,8 +32,8 @@ def refetch_public_key(conn) do ...@@ -33,8 +32,8 @@ def refetch_public_key(conn) do
end end
def sign(%User{} = user, headers) do def sign(%User{} = user, headers) do
with {:ok, %{info: %{keys: keys}}} <- WebFinger.ensure_keys_present(user), with {:ok, %{info: %{keys: keys}}} <- User.ensure_keys_present(user),
{:ok, private_key, _} <- Salmon.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
......
...@@ -10,6 +10,7 @@ defmodule Pleroma.User do ...@@ -10,6 +10,7 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Keys
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Registration alias Pleroma.Registration
...@@ -1422,4 +1423,24 @@ def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do ...@@ -1422,4 +1423,24 @@ def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
} }
} }
end end
def ensure_keys_present(user) do
info = user.info
if info.keys do
{:ok, user}
else
{:ok, pem} = Keys.generate_rsa_pem()
info_cng =
info
|> User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
update_and_set_cache(cng)
end
end
end end
...@@ -39,7 +39,7 @@ def relay_active?(conn, _) do ...@@ -39,7 +39,7 @@ def relay_active?(conn, _) do
def user(conn, %{"nickname" => nickname}) do def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
...@@ -106,7 +106,7 @@ def activity(conn, %{"uuid" => uuid}) do ...@@ -106,7 +106,7 @@ def activity(conn, %{"uuid" => uuid}) do
def following(conn, %{"nickname" => nickname, "page" => page}) do def following(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
...@@ -117,7 +117,7 @@ def following(conn, %{"nickname" => nickname, "page" => page}) do ...@@ -117,7 +117,7 @@ def following(conn, %{"nickname" => nickname, "page" => page}) do
def following(conn, %{"nickname" => nickname}) do def following(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("following.json", %{user: user})) |> json(UserView.render("following.json", %{user: user}))
...@@ -126,7 +126,7 @@ def following(conn, %{"nickname" => nickname}) do ...@@ -126,7 +126,7 @@ def following(conn, %{"nickname" => nickname}) do
def followers(conn, %{"nickname" => nickname, "page" => page}) do def followers(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
...@@ -137,7 +137,7 @@ def followers(conn, %{"nickname" => nickname, "page" => page}) do ...@@ -137,7 +137,7 @@ def followers(conn, %{"nickname" => nickname, "page" => page}) do
def followers(conn, %{"nickname" => nickname}) do def followers(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("followers.json", %{user: user})) |> json(UserView.render("followers.json", %{user: user}))
...@@ -146,7 +146,7 @@ def followers(conn, %{"nickname" => nickname}) do ...@@ -146,7 +146,7 @@ def followers(conn, %{"nickname" => nickname}) do
def outbox(conn, %{"nickname" => nickname} = params) do def outbox(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
...@@ -195,7 +195,7 @@ def inbox(conn, params) do ...@@ -195,7 +195,7 @@ def inbox(conn, params) do
def relay(conn, _params) do def relay(conn, _params) do
with %User{} = user <- Relay.get_actor(), with %User{} = user <- Relay.get_actor(),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.UserView do defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Keys
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
...@@ -12,8 +13,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do ...@@ -12,8 +13,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query
...@@ -34,8 +33,8 @@ def render("endpoints.json", _), do: %{} ...@@ -34,8 +33,8 @@ def render("endpoints.json", _), do: %{}
# the instance itself is not a Person, but instead an Application # the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.info.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])
...@@ -62,8 +61,8 @@ def render("user.json", %{user: %{nickname: nil} = user}) do ...@@ -62,8 +61,8 @@ def render("user.json", %{user: %{nickname: nil} = user}) do
end end
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.info.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])
......
...@@ -11,7 +11,6 @@ defmodule Pleroma.Web.Federator do ...@@ -11,7 +11,6 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub alias Pleroma.Web.Websub
require Logger require Logger
...@@ -77,9 +76,8 @@ def perform(:request_subscription, websub) do ...@@ -77,9 +76,8 @@ def perform(:request_subscription, websub) 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 actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]),
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} <- User.ensure_keys_present(actor) do
Publisher.publish(actor, activity) Publisher.publish(actor, activity)
end end
end end
......
...@@ -10,6 +10,7 @@ defmodule Pleroma.Web.Salmon do ...@@ -10,6 +10,7 @@ defmodule Pleroma.Web.Salmon do
use Bitwise use Bitwise
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Keys
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
...@@ -89,45 +90,6 @@ def encode_key({:RSAPublicKey, modulus, exponent}) do ...@@ -89,45 +90,6 @@ def encode_key({:RSAPublicKey, modulus, exponent}) do
"RSA.#{modulus_enc}.#{exponent_enc}" "RSA.#{modulus_enc}.#{exponent_enc}"
end end
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do
_ = :public_key.generate_key({:rsa, 2048, 65_537})
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
rescue
_ ->
def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} =
receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
:error
end
end
end
def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
{:ok, private_key, public_key}
end
def encode(private_key, doc) do def encode(private_key, doc) do
type = "application/atom+xml" type = "application/atom+xml"
encoding = "base64url" encoding = "base64url"
...@@ -227,7 +189,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity ...@@ -227,7 +189,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity
|> :xmerl.export_simple(:xmerl_xml) |> :xmerl.export_simple(:xmerl_xml)
|> to_string |> to_string
{:ok, private, _} = keys_from_pem(keys) {:ok, private, _} = Keys.keys_from_pem(keys)
{:ok, feed} = encode(private, feed) {:ok, feed} = encode(private, feed)
remote_users = remote_users(activity) remote_users = remote_users(activity)
...@@ -253,7 +215,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity ...@@ -253,7 +215,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity
def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
def gather_webfinger_links(%User{} = user) do def gather_webfinger_links(%User{} = user) do
{:ok, _private, public} = keys_from_pem(user.info.keys) {:ok, _private, public} = Keys.keys_from_pem(user.info.keys)
magic_key = encode_key(public) magic_key = encode_key(public)
[ [
......
...@@ -8,7 +8,6 @@ defmodule Pleroma.Web.WebFinger do ...@@ -8,7 +8,6 @@ defmodule Pleroma.Web.WebFinger do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon
alias Pleroma.Web.XML alias Pleroma.Web.XML
alias Pleroma.XmlBuilder alias Pleroma.XmlBuilder
require Jason require Jason
...@@ -61,7 +60,7 @@ defp gather_links(%User{} = user) do ...@@ -61,7 +60,7 @@ defp gather_links(%User{} = user) do
end end
def represent_user(user, "JSON") do def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
%{ %{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
...@@ -71,7 +70,7 @@ def represent_user(user, "JSON") do ...@@ -71,7 +70,7 @@ def represent_user(user, "JSON") do
end end
def represent_user(user, "XML") do def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
links = links =
gather_links(user) gather_links(user)
...@@ -88,27 +87,6 @@ def represent_user(user, "XML") do ...@@ -88,27 +87,6 @@ def represent_user(user, "XML") do
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end
# This seems a better fit in Salmon
def ensure_keys_present(user) do
info = user.info
if info.keys do
{:ok, user}
else
{:ok, pem} = Salmon.generate_rsa_pem()
info_cng =
info
|> User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
User.update_and_set_cache(cng)
end
end
defp get_magic_key(magic_key) do defp get_magic_key(magic_key) do
"data:application/magic-public-key," <> magic_key = magic_key "data:application/magic-public-key," <> magic_key = magic_key
{:ok, magic_key} {:ok, magic_key}
......
defmodule Pleroma.KeysTest do
use Pleroma.DataCase
alias Pleroma.Keys
test "generates an RSA private key pem" do
{:ok, key} = Keys.generate_rsa_pem()
assert is_binary(key)
assert Regex.match?(~r/RSA/, key)
end
test "returns a public and private key from a pem" do
pem = File.read!("test/fixtures/private_key.pem")
{:ok, private, public} = Keys.keys_from_pem(pem)
assert elem(private, 0) == :RSAPrivateKey
assert elem(public, 0) == :RSAPublicKey
end
end
...@@ -1251,4 +1251,19 @@ test "if user is unconfirmed" do ...@@ -1251,4 +1251,19 @@ test "if user is unconfirmed" do
refute user.info.confirmation_token refute user.info.confirmation_token
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.info.keys)
{:ok, user} = User.ensure_keys_present(user)
assert is_binary(user.info.keys)
end
test "it doesn't create keys if there already are some" do
user = insert(:user, %{info: %{keys: "xxx"}})
{:ok, user} = User.ensure_keys_present(user)
assert user.info.keys == "xxx"
end
end
end end
...@@ -1005,7 +1005,7 @@ test "it filters broken threads" do ...@@ -1005,7 +1005,7 @@ test "it filters broken threads" do
describe "update" do describe "update" do
test "it creates an update activity with the new user data" do test "it creates an update activity with the new user data" do
user = insert(:user) user = insert(:user)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
{:ok, update} = {:ok, update} =
......
...@@ -2,11 +2,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do ...@@ -2,11 +2,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
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} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
...@@ -18,7 +19,7 @@ test "Renders a user, including the public key" do ...@@ -18,7 +19,7 @@ test "Renders a user, including the public key" 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} = Pleroma.Web.WebFinger.ensure_keys_present(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"]
...@@ -32,7 +33,7 @@ test "Does not add an avatar image if the user hasn't set one" do ...@@ -32,7 +33,7 @@ test "Does not add an avatar image if the user hasn't set one" do
} }
) )
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) {: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"
...@@ -42,7 +43,7 @@ test "Does not add an avatar image if the user hasn't set one" do ...@@ -42,7 +43,7 @@ test "Does not add an avatar image if the user hasn't set one" 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} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
...@@ -58,7 +59,7 @@ test "local users have a usable endpoints structure" do ...@@ -58,7 +59,7 @@ test "local users have a usable endpoints structure" do
test "remote users have an empty endpoints structure" do test "remote users have an empty endpoints structure" do