@@ -383,10 +383,33 @@ defmodule Pleroma.User do | |||||
:ok | :ok | ||||
end | end | ||||
def get_or_fetch_by_ap_id(ap_id) do | |||||
if user = get_by_ap_id(ap_id) do | |||||
user | |||||
else | |||||
with {:ok, user} <- ActivityPub.make_user_from_ap_id(ap_id) do | |||||
user | |||||
end | |||||
end | |||||
end | |||||
# AP style | |||||
def public_key_from_info(%{"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do | |||||
key = :public_key.pem_decode(public_key_pem) | |||||
|> hd() | |||||
|> :public_key.pem_entry_decode() | |||||
{:ok, key} | |||||
end | |||||
# OStatus Magic Key | |||||
def public_key_from_info(%{"magic_key" => magic_key}) do | |||||
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)} | |||||
end | |||||
def get_public_key_for_ap_id(ap_id) do | def get_public_key_for_ap_id(ap_id) do | ||||
with %User{} = user <- get_cached_by_ap_id(ap_id), | |||||
%{info: %{"magic_key" => magic_key}} <- user, | |||||
public_key <- Pleroma.Web.Salmon.decode_key(magic_key) do | |||||
with %User{} = user <- get_or_fetch_by_ap_id(ap_id), | |||||
{:ok, public_key} <- public_key_from_info(user.info) do | |||||
{:ok, public_key} | {:ok, public_key} | ||||
else | else | ||||
_ -> :error | _ -> :error | ||||
@@ -223,18 +223,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
Repo.insert(%Object{data: data}) | Repo.insert(%Object{data: data}) | ||||
end | end | ||||
def prepare_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do | |||||
with {:ok, user} <- OStatus.find_or_make_user(data["actor"]) do | |||||
data | |||||
else | |||||
_e -> :error | |||||
end | |||||
end | |||||
def prepare_incoming(_) do | |||||
:error | |||||
end | |||||
def make_user_from_ap_id(ap_id) do | def make_user_from_ap_id(ap_id) do | ||||
with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]), | with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]), | ||||
{:ok, data} <- Poison.decode(body) | {:ok, data} <- Poison.decode(body) | ||||
@@ -252,4 +240,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
User.insert_or_update_user(user_data) | User.insert_or_update_user(user_data) | ||||
end | end | ||||
end | end | ||||
# TODO: Extract to own module, align as close to Mastodon format as possible. | |||||
def sanitize_outgoing_activity_data(data) do | |||||
data | |||||
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") | |||||
end | |||||
def prepare_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do | |||||
with {:ok, user} <- OStatus.find_or_make_user(data["actor"]) do | |||||
{:ok, data} | |||||
else | |||||
_e -> :error | |||||
end | |||||
end | |||||
def prepare_incoming(_) do | |||||
:error | |||||
end | |||||
def publish(actor, activity) do | |||||
remote_users = Pleroma.Web.Salmon.remote_users(activity) | |||||
data = sanitize_outgoing_activity_data(activity.data) | |||||
Enum.each remote_users, fn(user) -> | |||||
if user.info["ap_enabled"] do | |||||
inbox = user.info["source_data"]["inbox"] | |||||
Logger.info("Federating #{activity.data["id"]} to #{inbox}") | |||||
host = URI.parse(inbox).host | |||||
signature = Pleroma.Web.HTTPSignatures.sign(actor, %{host: host}) | |||||
@httpoison.post(inbox, Poison.encode!(data), [{"Content-Type", "application/activity+json"}, {"signature", signature}]) | |||||
end | |||||
end | |||||
end | |||||
end | end |
@@ -23,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||||
with {:ok, data} <- ActivityPub.prepare_incoming(params), | with {:ok, data} <- ActivityPub.prepare_incoming(params), | ||||
{:ok, activity} <- ActivityPub.insert(data, false) do | {:ok, activity} <- ActivityPub.insert(data, false) do | ||||
json(conn, "ok") | json(conn, "ok") | ||||
else | |||||
e -> IO.inspect(e) | |||||
end | end | ||||
end | end | ||||
end | end |
@@ -47,6 +47,9 @@ defmodule Pleroma.Web.Federator do | |||||
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end) | Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end) | ||||
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) | Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) | ||||
Logger.debug(fn -> "Sending #{activity.data["id"]} out via AP" end) | |||||
Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity) | |||||
end | end | ||||
end | end | ||||
@@ -1,6 +1,7 @@ | |||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-08 | # https://tools.ietf.org/html/draft-cavage-http-signatures-08 | ||||
defmodule Pleroma.Web.HTTPSignatures do | defmodule Pleroma.Web.HTTPSignatures do | ||||
alias Pleroma.User | alias Pleroma.User | ||||
alias Pleroma.Web.ActivityPub.ActivityPub | |||||
def split_signature(sig) do | def split_signature(sig) do | ||||
default = %{"headers" => "date"} | default = %{"headers" => "date"} | ||||
@@ -28,7 +29,16 @@ defmodule Pleroma.Web.HTTPSignatures do | |||||
# For now, fetch the key for the actor. | # For now, fetch the key for the actor. | ||||
with actor_id <- conn.params["actor"], | with actor_id <- conn.params["actor"], | ||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do | {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do | ||||
validate_conn(conn, public_key) | |||||
if validate_conn(conn, public_key) do | |||||
true | |||||
else | |||||
# Fetch user anew and try one more time | |||||
with actor_id <- conn.params["actor"], | |||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), | |||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do | |||||
validate_conn(conn, public_key) | |||||
end | |||||
end | |||||
else | else | ||||
_ -> false | _ -> false | ||||
end | end | ||||
@@ -45,4 +55,22 @@ defmodule Pleroma.Web.HTTPSignatures do | |||||
|> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end) | |> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end) | ||||
|> Enum.join("\n") | |> Enum.join("\n") | ||||
end | end | ||||
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 | |||||
sigstring = build_signing_string(headers, Map.keys(headers)) | |||||
signature = :public_key.sign(sigstring, :sha256, private_key) | |||||
|> Base.encode64() | |||||
[ | |||||
keyId: user.ap_id <> "#main-key", | |||||
algorithm: "rsa-sha256", | |||||
headers: Map.keys(headers) |> Enum.join(" "), | |||||
signature: signature | |||||
] | |||||
|> Enum.map(fn({k, v}) -> "#{k}=\"#{v}\"" end) | |||||
|> Enum.join(",") | |||||
end | |||||
end | |||||
end | end |
@@ -370,4 +370,8 @@ defmodule Pleroma.UserTest do | |||||
refute Repo.get(Activity, activity.id) | refute Repo.get(Activity, activity.id) | ||||
end | end | ||||
test "get_public_key_for_ap_id fetches a user that's not in the db" do | |||||
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") | |||||
end | |||||
end | end |
@@ -3,6 +3,7 @@ | |||||
defmodule Pleroma.Web.HTTPSignaturesTest do | defmodule Pleroma.Web.HTTPSignaturesTest do | ||||
use Pleroma.DataCase | use Pleroma.DataCase | ||||
alias Pleroma.Web.HTTPSignatures | alias Pleroma.Web.HTTPSignatures | ||||
import Pleroma.Factory | |||||
@private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key"))) | @private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key"))) | ||||
|> :public_key.pem_entry_decode()) | |> :public_key.pem_entry_decode()) | ||||
@@ -86,4 +87,29 @@ defmodule Pleroma.Web.HTTPSignaturesTest do | |||||
assert HTTPSignatures.validate_conn(conn, public_key) | assert HTTPSignatures.validate_conn(conn, public_key) | ||||
end | end | ||||
test "it validates a conn and fetches the key" do | |||||
conn = %{ | |||||
params: %{"actor" => "http://mastodon.example.org/users/admin"}, | |||||
req_headers: [ | |||||
{"host", "localtesting.pleroma.lol"}, | |||||
{"x-forwarded-for", "127.0.0.1"}, | |||||
{"connection", "close"}, | |||||
{"content-length", "2307"}, | |||||
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"}, | |||||
{"date", "Sun, 11 Feb 2018 17:12:01 GMT"}, | |||||
{"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="}, | |||||
{"content-type", "application/activity+json"}, | |||||
{"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""}, | |||||
{"(request-target)", "post /users/demiurge/inbox"} | |||||
] | |||||
} | |||||
assert HTTPSignatures.validate_conn(conn) | |||||
end | |||||
test "it generates a signature" do | |||||
user = insert(:user) | |||||
assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\"" | |||||
end | |||||
end | end |