diff --git a/config/config.exs b/config/config.exs index 478f543..9921a17 100644 --- a/config/config.exs +++ b/config/config.exs @@ -29,6 +29,8 @@ config :mime, :types, %{ "application/activity+json" => ["activity+json"] } +config :http_signatures, adapter: Clacks.SignatureAdapter + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/clacks/keys.ex b/lib/clacks/keys.ex index 3732c7b..9de13f6 100644 --- a/lib/clacks/keys.ex +++ b/lib/clacks/keys.ex @@ -6,7 +6,7 @@ defmodule Clacks.Keys do {:ok, pem} end - def keys_from_pem(pem) do + def keys_from_private_key_pem(pem) do with [private_key_code] <- :public_key.pem_decode(pem), private_key <- :public_key.pem_entry_decode(private_key_code), {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do @@ -17,6 +17,16 @@ defmodule Clacks.Keys do end end + def key_from_pem(pem) do + with [entry] <- :public_key.pem_decode(pem), + key <- :public_key.pem_entry_decode(entry) do + {:ok, key} + else + error -> + {:error, error} + end + end + def public_key_pem({:RSAPublicKey, _, _} = key) do entry = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, key) pem = :public_key.pem_encode([entry]) diff --git a/lib/clacks/signature_adapter.ex b/lib/clacks/signature_adapter.ex new file mode 100644 index 0000000..c9bb81c --- /dev/null +++ b/lib/clacks/signature_adapter.ex @@ -0,0 +1,48 @@ +defmodule Clacks.SignatureAdapter do + alias Clacks.{Actor, Keys} + @behaviour HTTPSignatures.Adapter + + def fetch_public_key(conn) do + get_key(conn, false) + end + + def refetch_public_key(conn) do + get_key(conn, true) + end + + defp get_key(conn, force_refetch) do + actor_id = get_actor_id(conn) + + case actor_id do + nil -> + {:error, "couldn't get actor id"} + + _ -> + case Actor.get_by_ap_id(actor_id, force_refetch) do + %Actor{data: %{"publicKey" => %{"publicKeyPem" => pem}}} -> + Keys.key_from_pem(pem) + + _ -> + {:error, "couldn't get pem from actor #{actor_id}"} + end + end + end + + defp get_actor_id(conn) do + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} -> + key_id_to_actor_id(key_id) + + _ -> + case conn.body_params do + %{"actor" => actor} when is_binary(actor) -> actor + _ -> nil + end + end + end + + defp key_id_to_actor_id(key_id) do + %URI{URI.parse(key_id) | fragment: nil} + |> URI.to_string() + end +end diff --git a/lib/clacks_web/controllers/inbox_controller.ex b/lib/clacks_web/controllers/inbox_controller.ex new file mode 100644 index 0000000..7383293 --- /dev/null +++ b/lib/clacks_web/controllers/inbox_controller.ex @@ -0,0 +1,14 @@ +defmodule ClacksWeb.InboxController do + use ClacksWeb, :controller + + plug Plug.Parsers, parsers: [:urlencoded, :json], json_decoder: Jason + plug ClacksWeb.Plug.HTTPSignature + + def shared(conn, params) do + IO.inspect(params) + end + + def user_specific(conn, params) do + IO.inspect(params) + end +end diff --git a/lib/clacks_web/endpoint.ex b/lib/clacks_web/endpoint.ex index d439067..b90d2dc 100644 --- a/lib/clacks_web/endpoint.ex +++ b/lib/clacks_web/endpoint.ex @@ -29,7 +29,8 @@ defmodule ClacksWeb.Endpoint do plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], - json_decoder: Phoenix.json_library() + json_decoder: Jason, + body_reader: {ClacksWeb.Plug.Digest, :read_body, []} plug Plug.MethodOverride plug Plug.Head diff --git a/lib/clacks_web/plug/digest.ex b/lib/clacks_web/plug/digest.ex new file mode 100644 index 0000000..78c6750 --- /dev/null +++ b/lib/clacks_web/plug/digest.ex @@ -0,0 +1,9 @@ +defmodule ClacksWeb.Plug.Digest do + alias Plug.Conn + + def read_body(conn, opts) do + {:ok, body, conn} = Conn.read_body(conn, opts) + digest = "SHA-256=" <> Base.encode64(:crypto.hash(:sha256, body)) + {:ok, body, Conn.assign(conn, :digest, digest)} + end +end diff --git a/lib/clacks_web/plug/http_signature.ex b/lib/clacks_web/plug/http_signature.ex new file mode 100644 index 0000000..e7e2303 --- /dev/null +++ b/lib/clacks_web/plug/http_signature.ex @@ -0,0 +1,42 @@ +defmodule ClacksWeb.Plug.HTTPSignature do + require Logger + import Plug.Conn + + def init(opts), do: opts + + def call(conn, _opts) do + case get_req_header(conn, "signature") do + [_signature | _] -> + conn = + conn + |> put_req_header( + "(request-target)", + String.downcase(conn.method) <> " " <> conn.request_path + ) + |> case do + %Plug.Conn{assigns: %{digest: digest}} = conn when is_binary(digest) -> + put_req_header(conn, "digest", digest) + + _ -> + conn + end + + if HTTPSignatures.validate_conn(conn) do + conn + else + Logger.debug("Could not validate signature for #{inspect(conn)}") + + conn + |> put_status(401) + |> halt() + end + + _ -> + Logger.debug("No signature header for #{inspect(conn)}") + + conn + |> put_status(401) + |> halt() + end + end +end diff --git a/lib/clacks_web/router.ex b/lib/clacks_web/router.ex index 8a8846c..f45547d 100644 --- a/lib/clacks_web/router.ex +++ b/lib/clacks_web/router.ex @@ -23,6 +23,9 @@ defmodule ClacksWeb.Router do get "/objects/:id", ObjectsController, :get get "/users/:nickname", ActorController, :get + post "/inbox", InboxController, :shared + post "/users/:nickname/inbox", InboxController, :user_specific + get "/.well-known/webfinger", WebFingerController, :get end diff --git a/lib/mix/tasks/clacks/user.ex b/lib/mix/tasks/clacks/user.ex index d57726b..23a4ccd 100644 --- a/lib/mix/tasks/clacks/user.ex +++ b/lib/mix/tasks/clacks/user.ex @@ -8,7 +8,7 @@ defmodule Mix.Tasks.Clacks.User do # password = IO.gets("Password: ") |> String.trim() {:ok, pem} = Keys.generate_rsa_pem() - {:ok, _private, public} = Keys.keys_from_pem(pem) + {:ok, _private, public} = Keys.keys_from_private_key_pem(pem) {:ok, public_key_pem} = Keys.public_key_pem(public) changeset = User.changeset(%User{}, %{username: username, private_key: pem})