From c1b42e3dae23c456a8283bdecf7406b388cf549b Mon Sep 17 00:00:00 2001 From: lain Date: Sat, 8 Sep 2018 16:31:40 +0200 Subject: [PATCH] Extract Keys, Actor module. --- lib/pleroma/keys.ex | 48 +++++++++++++++++++ lib/pleroma/web/activity_pub/actor.ex | 23 +++++++++ .../web/activity_pub/views/user_view.ex | 6 +-- .../web/http_signatures/http_signatures.ex | 2 +- lib/pleroma/web/salmon/salmon.ex | 42 +--------------- lib/pleroma/web/web_finger/web_finger.ex | 7 +-- test/keys_test.exs | 26 ++++++++++ test/web/activity_pub/actor_test.exs | 16 +++++++ test/web/salmon/salmon_test.exs | 17 +------ 9 files changed, 125 insertions(+), 62 deletions(-) create mode 100644 lib/pleroma/keys.ex create mode 100644 lib/pleroma/web/activity_pub/actor.ex create mode 100644 test/keys_test.exs create mode 100644 test/web/activity_pub/actor_test.exs diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex new file mode 100644 index 000000000..4cf1e8e09 --- /dev/null +++ b/lib/pleroma/keys.ex @@ -0,0 +1,48 @@ +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, 65537}) + + def generate_rsa_pem do + key = :public_key.generate_key({:rsa, 2048, 65537}) + 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 pems_from_pem(pem) do + {:ok, _, public_key} = keys_from_pem(pem) + public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) + pubkey_pem = :public_key.pem_encode([public_key]) + + {:ok, pem, pubkey_pem} + end +end diff --git a/lib/pleroma/web/activity_pub/actor.ex b/lib/pleroma/web/activity_pub/actor.ex new file mode 100644 index 000000000..5303a78ef --- /dev/null +++ b/lib/pleroma/web/activity_pub/actor.ex @@ -0,0 +1,23 @@ +defmodule Pleroma.Web.ActivityPub.Actor do + alias Pleroma.Keys + + def build(base) do + {:ok, pem} = Keys.generate_rsa_pem() + {:ok, privkey, pubkey} = Keys.pems_from_pem(pem) + + {:ok, + %{ + "id" => base, + "inbox" => "#{base}/inbox", + "outbox" => "#{base}/outbox", + "following" => "#{base}/following", + "followers" => "#{base}/followers", + "privateKey" => %{ + "privateKeyPem" => privkey + }, + "publicKey" => %{ + "publicKeyPem" => pubkey + } + }} + end +end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 16419e1b7..f81c2afbe 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do use Pleroma.Web, :view - alias Pleroma.Web.Salmon + alias Pleroma.Keys alias Pleroma.Web.WebFinger alias Pleroma.User alias Pleroma.Repo @@ -12,7 +12,7 @@ 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, _, 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]) @@ -40,7 +40,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do 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, _, 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]) diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex index 5e42a871b..8eb745416 100644 --- a/lib/pleroma/web/http_signatures/http_signatures.ex +++ b/lib/pleroma/web/http_signatures/http_signatures.ex @@ -66,7 +66,7 @@ defmodule Pleroma.Web.HTTPSignatures do def sign(user, headers) do with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user), - {:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do + {:ok, private_key, _} = Pleroma.Keys.keys_from_pem(keys) do sigstring = build_signing_string(headers, Map.keys(headers)) signature = diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 562ec3d9c..83c0a3aea 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.Salmon do alias Pleroma.Web.XML alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.User + alias Pleroma.Keys require Logger def decode(salmon) do @@ -76,45 +77,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, 65537}) - - def generate_rsa_pem do - key = :public_key.generate_key({:rsa, 2048, 65537}) - 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" @@ -195,7 +157,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(activity) diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 9f554d286..88899b7b6 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.WebFinger do alias Pleroma.{User, XmlBuilder} alias Pleroma.Web alias Pleroma.Web.{XML, Salmon, OStatus} + alias Pleroma.Keys require Jason require Logger @@ -45,7 +46,7 @@ defmodule Pleroma.Web.WebFinger do def represent_user(user, "JSON") do {:ok, user} = ensure_keys_present(user) - {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"]) + {:ok, _private, public} = Keys.keys_from_pem(user.info["keys"]) magic_key = Salmon.encode_key(public) %{ @@ -83,7 +84,7 @@ defmodule Pleroma.Web.WebFinger do def represent_user(user, "XML") do {:ok, user} = ensure_keys_present(user) - {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"]) + {:ok, _private, public} = Keys.keys_from_pem(user.info["keys"]) magic_key = Salmon.encode_key(public) { @@ -118,7 +119,7 @@ defmodule Pleroma.Web.WebFinger do if info["keys"] do {:ok, user} else - {:ok, pem} = Salmon.generate_rsa_pem() + {:ok, pem} = Keys.generate_rsa_pem() info = Map.put(info, "keys", pem) Ecto.Changeset.change(user, info: info) diff --git a/test/keys_test.exs b/test/keys_test.exs new file mode 100644 index 000000000..0a3edb2c5 --- /dev/null +++ b/test/keys_test.exs @@ -0,0 +1,26 @@ +defmodule Pleroma.KeysTest do + use Pleroma.DataCase + alias Pleroma.Keys + + test "it generates private keys" do + {:ok, pem} = Keys.generate_rsa_pem() + + assert pem =~ "RSA PRIVATE KEY" + end + + test "it gives private and public key pems for a private key pem" do + {:ok, pem} = Keys.generate_rsa_pem() + {:ok, private, public} = Keys.pems_from_pem(pem) + + assert private =~ "RSA PRIVATE KEY" + assert public =~ "RSA PUBLIC 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 diff --git a/test/web/activity_pub/actor_test.exs b/test/web/activity_pub/actor_test.exs new file mode 100644 index 000000000..5376fe94e --- /dev/null +++ b/test/web/activity_pub/actor_test.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Web.ActivityPub.ActorTest do + use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.Actor + + test "it builds and actor, including keys and inbox, outbox, followers" do + {:ok, user} = Actor.build("http://example.org/users/lain") + + assert user["id"] == "http://example.org/users/lain" + assert user["inbox"] == "http://example.org/users/lain/inbox" + assert user["outbox"] == "http://example.org/users/lain/outbox" + assert user["following"] == "http://example.org/users/lain/following" + assert user["followers"] == "http://example.org/users/lain/followers" + assert user["privateKey"]["privateKeyPem"] =~ "RSA PRIVATE KEY" + assert user["publicKey"]["publicKeyPem"] =~ "RSA PUBLIC KEY" + end +end diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs index 1b39b4b2d..76bd46447 100644 --- a/test/web/salmon/salmon_test.exs +++ b/test/web/salmon/salmon_test.exs @@ -2,6 +2,7 @@ defmodule Pleroma.Web.Salmon.SalmonTest do use Pleroma.DataCase alias Pleroma.Web.Salmon alias Pleroma.{Repo, Activity, User} + alias Pleroma.Keys import Pleroma.Factory @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" @@ -21,12 +22,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) @@ -38,18 +33,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)