From 30d62845186c29779b18c95c208d94c1d6291508 Mon Sep 17 00:00:00 2001 From: Ro Date: Tue, 24 Sep 2024 08:16:54 -0500 Subject: [PATCH] feat: Handle HTTP signatures --- README.md | 22 +--- lib/activity_pub/headers.ex | 112 ++++++++++++++++++ .../headers/signature_splitter.ex | 30 +++++ lib/postland.ex | 1 + .../controllers/webfinger_controller.ex | 2 - lib/postland_web/live/user_login_live.ex | 7 -- mix.exs | 4 +- mix.lock | 2 + test/activity_pub/headers_test.exs | 36 ++++++ .../live/user_login_live_test.exs | 14 --- .../live/user_registration_live_test.exs | 87 -------------- test/support/fixtures/accounts_fixtures.ex | 4 +- test/support/fixtures/key_fixtures.ex | 9 ++ test/support/fixtures/private.pem | 52 ++++++++ test/support/fixtures/public.pem | 15 +++ 15 files changed, 269 insertions(+), 128 deletions(-) create mode 100644 lib/activity_pub/headers.ex create mode 100644 lib/activity_pub/headers/signature_splitter.ex create mode 100644 test/activity_pub/headers_test.exs delete mode 100644 test/postland_web/live/user_registration_live_test.exs create mode 100644 test/support/fixtures/key_fixtures.ex create mode 100644 test/support/fixtures/private.pem create mode 100644 test/support/fixtures/public.pem diff --git a/README.md b/README.md index c0c1830..c11688d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,8 @@ # Postland -To start your Phoenix server: - - * Run `mix setup` to install and setup dependencies - * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` - -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. - -Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). - -## Learn more - - * Official website: https://www.phoenixframework.org/ - * Guides: https://hexdocs.pm/phoenix/overview.html - * Docs: https://hexdocs.pm/phoenix - * Forum: https://elixirforum.com/c/phoenix-forum - * Source: https://github.com/phoenixframework/phoenix +- Posting +- Deleting posts +- Following +- Unfollowing +- Liking +- Unliking diff --git a/lib/activity_pub/headers.ex b/lib/activity_pub/headers.ex new file mode 100644 index 0000000..de6f3c3 --- /dev/null +++ b/lib/activity_pub/headers.ex @@ -0,0 +1,112 @@ +defmodule ActivityPub.Headers do + alias ActivityPub.Headers.SignatureSplitter + + @http_date_format "%a, %d %b %Y %H:%M:%S GMT" + @signing_headers [ + "(request-target)", + "host", + "date", + "digest" + ] + + def signing_headers(method, url, body, actor_url, private_key) do + method = String.downcase("#{method}") + date = DateTime.utc_now() |> Calendar.strftime(@http_date_format) + host = url.host + digest = :crypto.hash(:sha256, body) + digest_header = "SHA-256=#{Base.encode64(digest)}" + + headers = + [ + {"host", host}, + {"date", date}, + {"digest", digest_header} + ] + + to_sign = + signing_text(@signing_headers, [request_target_pseudoheader(method, url.path) | headers]) + + signature = to_sign |> :public_key.sign(:sha256, private_key) |> Base.encode64() + + signature_header = + ~s|keyId="#{actor_url}#main-key",headers="(request-target) host date digest",signature="#{signature}"| + + [{"signature", signature_header} | headers] + end + + def verify(method, path, headers, actor_fetcher \\ &fetch_actor_key/1) do + {_key, signature_header} = Enum.find(headers, fn {key, _} -> key == "signature" end) + + signature_kv = SignatureSplitter.split(signature_header) + key_id = find_value(signature_kv, "keyId") + signature = signature_kv |> find_value("signature") |> Base.decode64!() + signing_text_headers = signature_kv |> find_value("headers") |> String.split(" ") + + to_verify = + signing_text(signing_text_headers, [request_target_pseudoheader(method, path) | headers]) + + case actor_fetcher.(key_id) do + {:ok, public_key} -> + :public_key.verify(to_verify, :sha256, signature, public_key) + + error -> + error + end + end + + def request_target_pseudoheader(method, path) do + formatted_method = String.downcase("#{method}") + {"(request-target)", "#{formatted_method} #{path}"} + end + + def signing_text(signing_text_headers, header_pairs) do + signing_text_headers + |> Enum.map_join("", fn header_name -> + value = find_value(header_pairs, header_name) + "#{header_name}: #{value}\n" + end) + end + + def split_signature_header(signature_header) do + signature_header + |> String.split(",") + |> Enum.map(fn pair_string -> + [key, value] = String.split(pair_string, "=") + value = String.trim(value, "\"") + + {key, value} + end) + end + + defp find_value(headers, key) do + Enum.find_value(headers, fn {key_candidate, value} -> + String.downcase(key_candidate) == key && value + end) + end + + def fetch_actor_key(key_id) do + request = + Req.new(url: key_id) + |> Req.Request.put_header("accept", "application/json") + + case Req.get(request) do + {:ok, result} -> + dbg(result) + key_map = result.body["publicKey"] + + if key_map["id"] == key_id do + [public_key | _] = + :public_key.pem_decode(key_map["publicKeyPem"]) + |> hd() + |> :public_key.pem_entry_decode() + + {:ok, public_key} + else + {:error, :id_mismatch} + end + + error -> + error + end + end +end diff --git a/lib/activity_pub/headers/signature_splitter.ex b/lib/activity_pub/headers/signature_splitter.ex new file mode 100644 index 0000000..4a6108d --- /dev/null +++ b/lib/activity_pub/headers/signature_splitter.ex @@ -0,0 +1,30 @@ +defmodule ActivityPub.Headers.SignatureSplitter do + def split(""), do: [] + + def split(value) do + {key_value, rest} = scan_until(value, ",") + {key, value} = split_kv(key_value) + + [{key, value} | split(rest)] + end + + defp split_kv(kv_string) do + {key, wrapped_value} = scan_until(kv_string, "=") + value = String.trim(wrapped_value, ~s(")) + {key, value} + end + + def scan_until(buffer, terminator, acc \\ "") + + def scan_until("", _terminator, acc), do: {acc, ""} + + def scan_until(<>, terminator, acc) do + ch = <> + + if ch == terminator do + {acc, buffer} + else + scan_until(buffer, terminator, acc <> ch) + end + end +end diff --git a/lib/postland.ex b/lib/postland.ex index 101b980..e5571fe 100644 --- a/lib/postland.ex +++ b/lib/postland.ex @@ -30,6 +30,7 @@ defmodule Postland do def on_boot_setup() do if !setup?() do + # TODO: Require the DB have restrictive permissions %{public: public_key, private: private_key} = generate_keys() attrs = %{ diff --git a/lib/postland_web/controllers/webfinger_controller.ex b/lib/postland_web/controllers/webfinger_controller.ex index e78f240..2434192 100644 --- a/lib/postland_web/controllers/webfinger_controller.ex +++ b/lib/postland_web/controllers/webfinger_controller.ex @@ -1,8 +1,6 @@ defmodule PostlandWeb.WebfingerController do use PostlandWeb, :controller - alias Postland.Accounts - def get(conn, _params) do render(conn, :webfinger, %{}) end diff --git a/lib/postland_web/live/user_login_live.ex b/lib/postland_web/live/user_login_live.ex index a478eab..d2791c9 100644 --- a/lib/postland_web/live/user_login_live.ex +++ b/lib/postland_web/live/user_login_live.ex @@ -6,13 +6,6 @@ defmodule PostlandWeb.UserLoginLive do
<.header class="text-center"> Log in to account - <:subtitle> - Don't have an account? - <.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline"> - Sign up - - for an account now. - <.simple_form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore"> diff --git a/mix.exs b/mix.exs index d7119f1..0d08d14 100644 --- a/mix.exs +++ b/mix.exs @@ -57,7 +57,9 @@ defmodule Postland.MixProject do {:gettext, "~> 0.20"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, - {:bandit, "~> 1.2"} + {:bandit, "~> 1.2"}, + {:req, "~> 0.5.6"}, + {:stream_data, "~> 1.1.1", only: [:test]} ] end diff --git a/mix.lock b/mix.lock index 941ee96..6753c6c 100644 --- a/mix.lock +++ b/mix.lock @@ -35,6 +35,8 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, + "stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"}, "swoosh": {:hex, :swoosh, "1.17.1", "01295a82bddd2c6cac1e65856e29444d7c23c4501e0ebc69cea8a82018227e25", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b20d25e580cb79af631335a1bdcfbffd835c08ebcdc16e98577223a241a18a1"}, "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, diff --git a/test/activity_pub/headers_test.exs b/test/activity_pub/headers_test.exs new file mode 100644 index 0000000..b276be9 --- /dev/null +++ b/test/activity_pub/headers_test.exs @@ -0,0 +1,36 @@ +defmodule ActivityPub.HeadersTest do + use Postland.DataCase, async: true + + use ExUnitProperties + + import ActivityPub.Headers + + property "can verify what it signs" do + private = + Postland.KeyFixtures.private() + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + + actor_fetcher = fn _ -> + Postland.KeyFixtures.public() + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + end + + check all( + method <- member_of([:get, :post]), + body <- StreamData.binary(), + host1 <- StreamData.string([?a..?z, ?0..?9, ?-]), + host2 <- StreamData.string([?a..?z, ?0..?9, ?-]) + ) do + url = URI.parse("#{host1}/inbox") + actor_url = "#{host2}/actor" + + headers = signing_headers(method, url, body, actor_url, private) + + verify(method, "/inbox", headers, actor_fetcher) + end + end +end diff --git a/test/postland_web/live/user_login_live_test.exs b/test/postland_web/live/user_login_live_test.exs index fdbc7d0..34f6ec2 100644 --- a/test/postland_web/live/user_login_live_test.exs +++ b/test/postland_web/live/user_login_live_test.exs @@ -57,18 +57,4 @@ defmodule PostlandWeb.UserLoginLiveTest do assert redirected_to(conn) == "/users/log_in" end end - - describe "login navigation" do - test "redirects to registration page when the Register button is clicked", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/log_in") - - {:ok, _login_live, login_html} = - lv - |> element(~s|main a:fl-contains("Sign up")|) - |> render_click() - |> follow_redirect(conn, ~p"/users/register") - - assert login_html =~ "Register" - end - end end diff --git a/test/postland_web/live/user_registration_live_test.exs b/test/postland_web/live/user_registration_live_test.exs deleted file mode 100644 index 1ac64c9..0000000 --- a/test/postland_web/live/user_registration_live_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -defmodule PostlandWeb.UserRegistrationLiveTest do - use PostlandWeb.ConnCase - - import Phoenix.LiveViewTest - import Postland.AccountsFixtures - - describe "Registration page" do - test "renders registration page", %{conn: conn} do - {:ok, _lv, html} = live(conn, ~p"/users/register") - - assert html =~ "Register" - assert html =~ "Log in" - end - - test "redirects if already logged in", %{conn: conn} do - result = - conn - |> log_in_user(user_fixture()) - |> live(~p"/users/register") - |> follow_redirect(conn, "/") - - assert {:ok, _conn} = result - end - - test "renders errors for invalid data", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/register") - - result = - lv - |> element("#registration_form") - |> render_change(user: %{"username" => "with spaces", "password" => "too short"}) - - assert result =~ "Register" - assert result =~ "must contain only letters, numbers, and underscores" - assert result =~ "should be at least 12 character" - end - end - - describe "register user" do - test "creates account and logs the user in", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/register") - - username = unique_user_username() - form = form(lv, "#registration_form", user: valid_user_attributes(username: username)) - render_submit(form) - conn = follow_trigger_action(form, conn) - - assert redirected_to(conn) == ~p"/" - - # Now do a logged in request and assert on the menu - conn = get(conn, "/") - response = html_response(conn, 200) - assert response =~ username - assert response =~ "Settings" - assert response =~ "Log out" - end - - test "renders errors for duplicated username", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/register") - - user = user_fixture(%{username: "test"}) - - result = - lv - |> form("#registration_form", - user: %{"username" => user.username, "password" => "valid_password"} - ) - |> render_submit() - - assert result =~ "has already been taken" - end - end - - describe "registration navigation" do - test "redirects to login page when the Log in button is clicked", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/register") - - {:ok, _login_live, login_html} = - lv - |> element(~s|main a:fl-contains("Log in")|) - |> render_click() - |> follow_redirect(conn, ~p"/users/log_in") - - assert login_html =~ "Log in" - end - end -end diff --git a/test/support/fixtures/accounts_fixtures.ex b/test/support/fixtures/accounts_fixtures.ex index 08e4083..a5b5e1c 100644 --- a/test/support/fixtures/accounts_fixtures.ex +++ b/test/support/fixtures/accounts_fixtures.ex @@ -10,7 +10,9 @@ defmodule Postland.AccountsFixtures do def valid_user_attributes(attrs \\ %{}) do Enum.into(attrs, %{ username: unique_user_username(), - password: valid_user_password() + password: valid_user_password(), + private_key: "test", + public_key: "test" }) end diff --git a/test/support/fixtures/key_fixtures.ex b/test/support/fixtures/key_fixtures.ex new file mode 100644 index 0000000..515c941 --- /dev/null +++ b/test/support/fixtures/key_fixtures.ex @@ -0,0 +1,9 @@ +defmodule Postland.KeyFixtures do + def public do + File.read!("test/support/fixtures/public.pem") + end + + def private do + File.read!("test/support/fixtures/private.pem") + end +end diff --git a/test/support/fixtures/private.pem b/test/support/fixtures/private.pem new file mode 100644 index 0000000..fafcc30 --- /dev/null +++ b/test/support/fixtures/private.pem @@ -0,0 +1,52 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- + diff --git a/test/support/fixtures/public.pem b/test/support/fixtures/public.pem new file mode 100644 index 0000000..c01f0d8 --- /dev/null +++ b/test/support/fixtures/public.pem @@ -0,0 +1,15 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5wphw7hrp6UrKZm6Ops3 +JZmxoAelTH+gWLXGt4+W8j9WzpRU5BuadHqqevgiGEo599bk3ecFlzNY7i7UtnkW +jxtykOq6miMbQObW2WhwE/+wj8S9wcH9AjAa34Pz+6YedP6XZ3RbsAj3ON7xo/a8 +XnDeSwm6hfiYJbkXW83UGeMyzYC4vilM0Ex1zZCGTbJY/FJJyLCikZWxEBtUHuTI +J4vwdnfZ6GraQN3l9fLkDAAdyzpG/0EjjhPkEcniHFB8CUVuWXCfaSnRAt3YU2BH +yMsqV/mWqHUM5LMvE0q2brN1azT+0sKHBiOAc7VOCDd92603h3MyjmqC3tu82jHQ +Wc8wtmEMzwBM6hU+ytKdl/PSud/hiNDATKsogcZLu0NRrrVyvmx4ykmT1DBhyoZy +UcEv46iRMY3LturxsBSA65OCr/1ftGbpuaZ3Ill/mWUedQsm1sqPGPX1voNyaC+J +5XLhwkqw5McHpmAY7YSx9bXiE06cj/KCDJcE5ZsIjIxu9hGxkWb0OInlWivMXjXu +qpFfspEdjl34RFyzb+aX4tnHVz1f4HkeDj94tH1v9njnJsPElJYhwHX8VlGp13rh +Z8lpzhqCD5zADby8es2V0RiXgCPKCWt+hyddiJt9hiBQjSFDVYYfeeBrrJu3UV0O +KTzdT4N7FjGF4AtYEpJiXCsCAwEAAQ== +-----END PUBLIC KEY----- +