Add HTTP signature verification

This commit is contained in:
Shadowfacts 2019-10-01 18:39:17 -04:00
parent f3995911d7
commit 1b8f3e212c
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
9 changed files with 132 additions and 3 deletions

View File

@ -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"

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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})