Add HTTP signature verification
This commit is contained in:
parent
f3995911d7
commit
1b8f3e212c
|
@ -29,6 +29,8 @@ config :mime, :types, %{
|
||||||
"application/activity+json" => ["activity+json"]
|
"application/activity+json" => ["activity+json"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config :http_signatures, adapter: Clacks.SignatureAdapter
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{Mix.env()}.exs"
|
import_config "#{Mix.env()}.exs"
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule Clacks.Keys do
|
||||||
{:ok, pem}
|
{:ok, pem}
|
||||||
end
|
end
|
||||||
|
|
||||||
def keys_from_pem(pem) do
|
def keys_from_private_key_pem(pem) do
|
||||||
with [private_key_code] <- :public_key.pem_decode(pem),
|
with [private_key_code] <- :public_key.pem_decode(pem),
|
||||||
private_key <- :public_key.pem_entry_decode(private_key_code),
|
private_key <- :public_key.pem_entry_decode(private_key_code),
|
||||||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
|
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
|
||||||
|
@ -17,6 +17,16 @@ defmodule Clacks.Keys do
|
||||||
end
|
end
|
||||||
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
|
def public_key_pem({:RSAPublicKey, _, _} = key) do
|
||||||
entry = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, key)
|
entry = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, key)
|
||||||
pem = :public_key.pem_encode([entry])
|
pem = :public_key.pem_encode([entry])
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -29,7 +29,8 @@ defmodule ClacksWeb.Endpoint do
|
||||||
plug Plug.Parsers,
|
plug Plug.Parsers,
|
||||||
parsers: [:urlencoded, :multipart, :json],
|
parsers: [:urlencoded, :multipart, :json],
|
||||||
pass: ["*/*"],
|
pass: ["*/*"],
|
||||||
json_decoder: Phoenix.json_library()
|
json_decoder: Jason,
|
||||||
|
body_reader: {ClacksWeb.Plug.Digest, :read_body, []}
|
||||||
|
|
||||||
plug Plug.MethodOverride
|
plug Plug.MethodOverride
|
||||||
plug Plug.Head
|
plug Plug.Head
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -23,6 +23,9 @@ defmodule ClacksWeb.Router do
|
||||||
get "/objects/:id", ObjectsController, :get
|
get "/objects/:id", ObjectsController, :get
|
||||||
get "/users/:nickname", ActorController, :get
|
get "/users/:nickname", ActorController, :get
|
||||||
|
|
||||||
|
post "/inbox", InboxController, :shared
|
||||||
|
post "/users/:nickname/inbox", InboxController, :user_specific
|
||||||
|
|
||||||
get "/.well-known/webfinger", WebFingerController, :get
|
get "/.well-known/webfinger", WebFingerController, :get
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Mix.Tasks.Clacks.User do
|
||||||
# password = IO.gets("Password: ") |> String.trim()
|
# password = IO.gets("Password: ") |> String.trim()
|
||||||
|
|
||||||
{:ok, pem} = Keys.generate_rsa_pem()
|
{: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)
|
{:ok, public_key_pem} = Keys.public_key_pem(public)
|
||||||
|
|
||||||
changeset = User.changeset(%User{}, %{username: username, private_key: pem})
|
changeset = User.changeset(%User{}, %{username: username, private_key: pem})
|
||||||
|
|
Loading…
Reference in New Issue