This commit is contained in:
Mirai Kumiko 2025-06-29 14:59:33 +02:00
parent 748baff8f3
commit 1faafeee26
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
11 changed files with 126 additions and 48 deletions

View file

@ -7,12 +7,11 @@ defmodule Nulla.HTTPSignature do
date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT")
uri = URI.parse(inbox_url) uri = URI.parse(inbox_url)
signature_string = """ signature_string =
(request-target): post #{uri.path} "(request-target): post #{uri.path}\n" <>
host: #{uri.host} "host: #{uri.host}\n" <>
date: #{date} "date: #{date}\n" <>
digest: #{digest} "digest: #{digest}"
"""
user = User.get_user(id: actor.id) user = User.get_user(id: actor.id)
@ -56,6 +55,7 @@ defmodule Nulla.HTTPSignature do
end end
defp parse_signature_header(header) do defp parse_signature_header(header) do
header =
header header
|> String.split(",") |> String.split(",")
|> Enum.map(fn pair -> |> Enum.map(fn pair ->
@ -63,6 +63,8 @@ defmodule Nulla.HTTPSignature do
{String.trim(k), String.trim(v, ~s("))} {String.trim(k), String.trim(v, ~s("))}
end) end)
|> Enum.into(%{}) |> Enum.into(%{})
header
end end
defp build_signature_string(nil, _conn), do: {:error, :missing_headers} defp build_signature_string(nil, _conn), do: {:error, :missing_headers}
@ -83,6 +85,9 @@ defmodule Nulla.HTTPSignature do
"(request-target): #{method} #{path}" "(request-target): #{method} #{path}"
"host" ->
"host: #{conn.host}"
_ -> _ ->
value = get_req_header(conn, header) |> List.first() value = get_req_header(conn, header) |> List.first()
if value, do: "#{header}: #{value}", else: nil if value, do: "#{header}: #{value}", else: nil

View file

@ -0,0 +1,40 @@
defmodule Nulla.HTTPSignatureTest do
use NullaWeb.ConnCase, async: false
import Plug.Conn
import Plug.Test
import Nulla.Fixtures.Data
alias Nulla.HTTPSignature
alias Nulla.Snowflake
alias Nulla.Models.Actor
setup do
create_data()
:ok
end
test "make_headers/3 creates valid signature headers and verify/2 validates them" do
actor = Actor.get_actor(preferredUsername: "test")
target_actor = Actor.get_actor(preferredUsername: "test2")
follow_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => Snowflake.next_id(),
"type" => "Follow",
"actor" => actor.ap_id,
"object" => target_actor.ap_id
}
body = Jason.encode!(follow_activity)
headers = HTTPSignature.make_headers(body, target_actor.inbox, actor)
conn =
conn(:post, "/users/test2/inbox", body)
|> put_req_header("content-type", Map.new(headers) |> Map.get("Content-Type"))
|> put_req_header("date", Map.new(headers) |> Map.get("Date"))
|> put_req_header("digest", Map.new(headers) |> Map.get("Digest"))
|> put_req_header("signature", Map.new(headers) |> Map.get("Signature"))
|> Map.put(:host, URI.parse(target_actor.inbox).host)
assert :ok = HTTPSignature.verify(conn, actor.publicKey["publicKeyPem"])
end
end

32
test/nulla/keys.exs Normal file
View file

@ -0,0 +1,32 @@
defmodule Nulla.KeysTest do
use NullaWeb.ConnCase, async: false
import Nulla.Fixtures.Data
alias Nulla.Models.User
alias Nulla.Models.Actor
setup do
create()
:ok
end
test "verify user's keys" do
actor = Actor.get_actor(preferredUsername: "test")
user = User.get_user(id: actor.id)
message = "test message"
private_key =
:public_key.pem_decode(user.privateKeyPem)
|> hd()
|> :public_key.pem_entry_decode()
public_key =
:public_key.pem_decode(actor.publicKey["publicKeyPem"])
|> hd()
|> :public_key.pem_entry_decode()
signature = :public_key.sign(message, :sha256, private_key)
assert :public_key.verify(message, :sha256, signature, public_key)
end
end

View file

@ -1,8 +1,9 @@
defmodule NullaWeb.ActorControllerTest do defmodule NullaWeb.ActorControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -1,8 +1,9 @@
defmodule NullaWeb.FollowControllerTest do defmodule NullaWeb.FollowControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -1,11 +1,12 @@
defmodule NullaWeb.InboxControllerTest do defmodule NullaWeb.InboxControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
alias Nulla.Snowflake alias Nulla.Snowflake
alias Nulla.Models.User alias Nulla.Models.User
alias Nulla.Models.Actor alias Nulla.Models.Actor
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -1,8 +1,9 @@
defmodule NullaWeb.NodeinfoControllerTest do defmodule NullaWeb.NodeinfoControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -1,10 +1,11 @@
defmodule NullaWeb.NoteControllerTest do defmodule NullaWeb.NoteControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
alias Nulla.Models.Actor alias Nulla.Models.Actor
alias Nulla.Models.Note alias Nulla.Models.Note
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -1,8 +1,9 @@
defmodule NullaWeb.OutboxControllerTest do defmodule NullaWeb.OutboxControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -1,8 +1,9 @@
defmodule NullaWeb.WebfingerControllerTest do defmodule NullaWeb.WebfingerControllerTest do
use NullaWeb.ConnCase use NullaWeb.ConnCase
import Nulla.Fixtures.Data
setup do setup do
Nulla.Fixtures.Data.create() create_data()
:ok :ok
end end

View file

@ -4,30 +4,24 @@ defmodule Nulla.Fixtures.Data do
alias Nulla.Models.Actor alias Nulla.Models.Actor
alias Nulla.Models.Note alias Nulla.Models.Note
def create do def create_data do
endpoint_config = Application.fetch_env!(:nulla, NullaWeb.Endpoint)
ip = endpoint_config[:http][:ip]
host = :inet_parse.ntoa(ip) |> to_string()
port = endpoint_config[:http][:port]
base_url = "http://#{host}:#{port}"
{publicKeyPem, privateKeyPem} = KeyGen.gen() {publicKeyPem, privateKeyPem} = KeyGen.gen()
{:ok, actor} = {:ok, actor} =
Actor.create_actor(%{ Actor.create_actor(%{
domain: "localhost", domain: "localhost",
ap_id: "#{base_url}/users/test", ap_id: "http://localhost/users/test",
type: "Person", type: "Person",
following: "#{base_url}/users/test/following", following: "http://localhost/users/test/following",
followers: "#{base_url}/users/test/followers", followers: "http://localhost/users/test/followers",
inbox: "#{base_url}/users/test/inbox", inbox: "http://localhost/users/test/inbox",
outbox: "#{base_url}/users/test/outbox", outbox: "http://localhost/users/test/outbox",
featured: "#{base_url}/users/test/collections/featured", featured: "http://localhost/users/test/collections/featured",
featuredTags: "#{base_url}/users/test/collections/tags", featuredTags: "http://localhost/users/test/collections/tags",
preferredUsername: "test", preferredUsername: "test",
name: "Test", name: "Test",
summary: "Test User", summary: "Test User",
url: "#{base_url}/@test", url: "http://localhost/@test",
manuallyApprovesFollowers: false, manuallyApprovesFollowers: false,
discoverable: true, discoverable: true,
indexable: true, indexable: true,
@ -35,11 +29,11 @@ defmodule Nulla.Fixtures.Data do
memorial: false, memorial: false,
publicKey: publicKey:
Jason.OrderedObject.new( Jason.OrderedObject.new(
id: "#{base_url}/users/test#main-key", id: "http://localhost/users/test#main-key",
owner: "#{base_url}/users/test", owner: "http://localhost/users/test",
publicKeyPem: publicKeyPem publicKeyPem: publicKeyPem
), ),
endpoints: Jason.OrderedObject.new(sharedInbox: "#{base_url}/inbox") endpoints: Jason.OrderedObject.new(sharedInbox: "http://localhost/inbox")
}) })
User.create_user(%{ User.create_user(%{
@ -62,18 +56,18 @@ defmodule Nulla.Fixtures.Data do
{:ok, actor} = {:ok, actor} =
Actor.create_actor(%{ Actor.create_actor(%{
domain: "localhost", domain: "localhost",
ap_id: "#{base_url}/users/test2", ap_id: "http://localhost/users/test2",
type: "Person", type: "Person",
following: "#{base_url}/users/test2/following", following: "http://localhost/users/test2/following",
followers: "#{base_url}/users/test2/followers", followers: "http://localhost/users/test2/followers",
inbox: "#{base_url}/users/test2/inbox", inbox: "http://localhost/users/test2/inbox",
outbox: "#{base_url}/users/test2/outbox", outbox: "http://localhost/users/test2/outbox",
featured: "#{base_url}/users/test2/collections/featured", featured: "http://localhost/users/test2/collections/featured",
featuredTags: "#{base_url}/users/test2/collections/tags", featuredTags: "http://localhost/users/test2/collections/tags",
preferredUsername: "test2", preferredUsername: "test2",
name: "Test", name: "Test",
summary: "Test User", summary: "Test User",
url: "#{base_url}/@test2", url: "http://localhost/@test2",
manuallyApprovesFollowers: false, manuallyApprovesFollowers: false,
discoverable: true, discoverable: true,
indexable: true, indexable: true,
@ -81,11 +75,11 @@ defmodule Nulla.Fixtures.Data do
memorial: false, memorial: false,
publicKey: publicKey:
Jason.OrderedObject.new( Jason.OrderedObject.new(
id: "#{base_url}/users/test2#main-key", id: "http://localhost/users/test2#main-key",
owner: "#{base_url}/users/test2", owner: "http://localhost/users/test2",
publicKeyPem: publicKeyPem publicKeyPem: publicKeyPem
), ),
endpoints: Jason.OrderedObject.new(sharedInbox: "#{base_url}/inbox") endpoints: Jason.OrderedObject.new(sharedInbox: "http://localhost/inbox")
}) })
User.create_user(%{ User.create_user(%{