Browse Source

Merge branch 'refactor/keys' into 'develop'

move key generation functions into Pleroma.Keys module

See merge request pleroma/pleroma!1186
tags/v1.1.4
lambda 5 years ago
parent
commit
ff363f70b5
14 changed files with 133 additions and 124 deletions
  1. +44
    -0
      lib/pleroma/keys.ex
  2. +3
    -4
      lib/pleroma/signature.ex
  3. +21
    -0
      lib/pleroma/user.ex
  4. +7
    -7
      lib/pleroma/web/activity_pub/activity_pub_controller.ex
  5. +5
    -6
      lib/pleroma/web/activity_pub/views/user_view.ex
  6. +2
    -4
      lib/pleroma/web/federator/federator.ex
  7. +3
    -41
      lib/pleroma/web/salmon/salmon.ex
  8. +2
    -24
      lib/pleroma/web/web_finger/web_finger.ex
  9. +20
    -0
      test/keys_test.exs
  10. +15
    -0
      test/user_test.exs
  11. +1
    -1
      test/web/activity_pub/activity_pub_test.exs
  12. +7
    -6
      test/web/activity_pub/views/user_view_test.exs
  13. +3
    -16
      test/web/salmon/salmon_test.exs
  14. +0
    -15
      test/web/web_finger/web_finger_test.exs

+ 44
- 0
lib/pleroma/keys.ex View File

@@ -0,0 +1,44 @@
# 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

+ 3
- 4
lib/pleroma/signature.ex View File

@@ -5,11 +5,10 @@
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter

alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger

def fetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
@@ -33,8 +32,8 @@ defmodule Pleroma.Signature do
end

def sign(%User{} = user, headers) do
with {:ok, %{info: %{keys: keys}}} <- WebFinger.ensure_keys_present(user),
{:ok, private_key, _} <- Salmon.keys_from_pem(keys) do
with {:ok, %{info: %{keys: keys}}} <- User.ensure_keys_present(user),
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end


+ 21
- 0
lib/pleroma/user.ex View File

@@ -10,6 +10,7 @@ defmodule Pleroma.User do

alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Keys
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
@@ -1422,4 +1423,24 @@ defmodule Pleroma.User do
}
}
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

+ 7
- 7
lib/pleroma/web/activity_pub/activity_pub_controller.ex View File

@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def user(conn, %{"nickname" => nickname}) do
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
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
@@ -106,7 +106,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def following(conn, %{"nickname" => nickname, "page" => page}) do
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)

conn
@@ -117,7 +117,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def following(conn, %{"nickname" => nickname}) do
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
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("following.json", %{user: user}))
@@ -126,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def followers(conn, %{"nickname" => nickname, "page" => page}) do
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)

conn
@@ -137,7 +137,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def followers(conn, %{"nickname" => nickname}) do
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
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("followers.json", %{user: user}))
@@ -146,7 +146,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def outbox(conn, %{"nickname" => nickname} = params) do
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
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
@@ -195,7 +195,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

def relay(conn, _params) do
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
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user}))


+ 5
- 6
lib/pleroma/web/activity_pub/views/user_view.ex View File

@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view

alias Pleroma.Keys
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -12,8 +13,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger

import Ecto.Query

@@ -34,8 +33,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do

# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
{:ok, user} = User.ensure_keys_present(user)
{: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_encode([public_key])

@@ -62,8 +61,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
end

def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
{:ok, user} = User.ensure_keys_present(user)
{: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_encode([public_key])



+ 2
- 4
lib/pleroma/web/federator/federator.ex View File

@@ -11,7 +11,6 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub

require Logger
@@ -77,9 +76,8 @@ defmodule Pleroma.Web.Federator do
def perform(:publish, activity) do
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
{:ok, actor} = WebFinger.ensure_keys_present(actor)

with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]),
{:ok, actor} <- User.ensure_keys_present(actor) do
Publisher.publish(actor, activity)
end
end


+ 3
- 41
lib/pleroma/web/salmon/salmon.ex View File

@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Salmon do

alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator.Publisher
@@ -89,45 +90,6 @@ defmodule Pleroma.Web.Salmon do
"RSA.#{modulus_enc}.#{exponent_enc}"
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
type = "application/atom+xml"
encoding = "base64url"
@@ -227,7 +189,7 @@ defmodule Pleroma.Web.Salmon do
|> :xmerl.export_simple(:xmerl_xml)
|> to_string

{:ok, private, _} = keys_from_pem(keys)
{:ok, private, _} = Keys.keys_from_pem(keys)
{:ok, feed} = encode(private, feed)

remote_users = remote_users(activity)
@@ -253,7 +215,7 @@ defmodule Pleroma.Web.Salmon do
def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)

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)

[


+ 2
- 24
lib/pleroma/web/web_finger/web_finger.ex View File

@@ -8,7 +8,6 @@ defmodule Pleroma.Web.WebFinger do
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
@@ -61,7 +60,7 @@ defmodule Pleroma.Web.WebFinger do
end

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()}",
@@ -71,7 +70,7 @@ defmodule Pleroma.Web.WebFinger do
end

def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user)
{:ok, user} = User.ensure_keys_present(user)

links =
gather_links(user)
@@ -88,27 +87,6 @@ defmodule Pleroma.Web.WebFinger do
|> XmlBuilder.to_doc()
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
"data:application/magic-public-key," <> magic_key = magic_key
{:ok, magic_key}


+ 20
- 0
test/keys_test.exs View File

@@ -0,0 +1,20 @@
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

+ 15
- 0
test/user_test.exs View File

@@ -1251,4 +1251,19 @@ defmodule Pleroma.UserTest do
refute user.info.confirmation_token
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

+ 1
- 1
test/web/activity_pub/activity_pub_test.exs View File

@@ -1005,7 +1005,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
describe "update" do
test "it creates an update activity with the new user data" do
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})

{:ok, update} =


+ 7
- 6
test/web/activity_pub/views/user_view_test.exs View File

@@ -2,11 +2,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
use Pleroma.DataCase
import Pleroma.Factory

alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView

test "Renders a user, including the public key" do
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})

@@ -18,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do

test "Does not add an avatar image if the user hasn't set one" do
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})
refute result["icon"]
@@ -32,7 +33,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
}
)

{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
{:ok, user} = User.ensure_keys_present(user)

result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl"
@@ -42,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
describe "endpoints" do
test "local users have a usable endpoints structure" do
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})

@@ -58,7 +59,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do

test "remote users have an empty endpoints structure" do
user = insert(:user, local: false)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
{:ok, user} = User.ensure_keys_present(user)

result = UserView.render("user.json", %{user: user})

@@ -68,7 +69,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do

test "instance users do not expose oAuth endpoints" do
user = insert(:user, nickname: nil, local: true)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
{:ok, user} = User.ensure_keys_present(user)

result = UserView.render("user.json", %{user: user})



+ 3
- 16
test/web/salmon/salmon_test.exs View File

@@ -5,6 +5,7 @@
defmodule Pleroma.Web.Salmon.SalmonTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Keys
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Federator.Publisher
@@ -34,12 +35,6 @@ defmodule Pleroma.Web.Salmon.SalmonTest do
assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error
end

test "generates an RSA private key pem" do
{:ok, key} = Salmon.generate_rsa_pem()
assert is_binary(key)
assert Regex.match?(~r/RSA/, key)
end

test "it encodes a magic key from a public key" do
key = Salmon.decode_key(@magickey)
magic_key = Salmon.encode_key(key)
@@ -51,18 +46,10 @@ defmodule Pleroma.Web.Salmon.SalmonTest do
_key = Salmon.decode_key(@magickey_friendica)
end

test "returns a public and private key from a pem" do
pem = File.read!("test/fixtures/private_key.pem")
{:ok, private, public} = Salmon.keys_from_pem(pem)

assert elem(private, 0) == :RSAPrivateKey
assert elem(public, 0) == :RSAPublicKey
end

test "encodes an xml payload with a private key" do
doc = File.read!("test/fixtures/incoming_note_activity.xml")
pem = File.read!("test/fixtures/private_key.pem")
{:ok, private, public} = Salmon.keys_from_pem(pem)
{:ok, private, public} = Keys.keys_from_pem(pem)

# Let's try a roundtrip.
{:ok, salmon} = Salmon.encode(private, doc)
@@ -105,7 +92,7 @@ defmodule Pleroma.Web.Salmon.SalmonTest do

{:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]})
user = User.get_cached_by_ap_id(activity.data["actor"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
{:ok, user} = User.ensure_keys_present(user)

Salmon.publish(user, activity)



+ 0
- 15
test/web/web_finger/web_finger_test.exs View File

@@ -105,19 +105,4 @@ defmodule Pleroma.Web.WebFingerTest do
assert template == "http://status.alpicola.com/main/xrd?uri={uri}"
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} = WebFinger.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} = WebFinger.ensure_keys_present(user)
assert user.info.keys == "xxx"
end
end
end

Loading…
Cancel
Save