Browse Source

Add ID and signature validation on incoming events

master
buttercat1791 3 months ago
parent
commit
7d0f80992d
  1. 13
      lib/gc_index_relay/nostr.ex
  2. 55
      lib/gc_index_relay/nostr/validator.ex
  3. 2
      lib/gc_index_relay_web/controllers/event_controller.ex
  4. 3
      mix.exs
  5. 1
      mix.lock

13
lib/gc_index_relay/nostr.ex

@ -10,6 +10,7 @@ defmodule GcIndexRelay.Nostr do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias GcIndexRelay.Nostr.Validator
alias GcIndexRelay.Nostr.PubEvent alias GcIndexRelay.Nostr.PubEvent
alias GcIndexRelay.Repo alias GcIndexRelay.Repo
alias GcIndexRelay.Nostr.Event alias GcIndexRelay.Nostr.Event
@ -30,6 +31,18 @@ defmodule GcIndexRelay.Nostr do
Writes a `GcIndexRelay.Nostr.PubEvent` to the database. Writes a `GcIndexRelay.Nostr.PubEvent` to the database.
""" """
def create_event(event) when is_struct(event, PubEvent) do def create_event(event) when is_struct(event, PubEvent) do
with {:ok, event} <- Validator.validate_id(event),
{:ok, event} <- Validator.validate_signature(event) do
case do_create_event(event) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
end
else
{:error, reason} -> {:error, reason}
end
end
defp do_create_event(event) when is_struct(event, PubEvent) do
db_event = PubEvent.to_db(event) db_event = PubEvent.to_db(event)
%Event{} %Event{}

55
lib/gc_index_relay/nostr/validator.ex

@ -0,0 +1,55 @@
defmodule GcIndexRelay.Nostr.Validator do
@moduledoc """
Nostr key and signature validation.
Uses [Curvy](https://hexdocs.pm/curvy/Curvy.html).
Will migrate to [noscrypt](https://www.vaughnnugent.com/resources/software/modules/noscrypt) (via
NIF integration) at a later date.
"""
alias GcIndexRelay.Nostr.PubEvent
@doc """
Validates a Nostr event ID per [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md).
"""
def validate_id(event) when is_struct(event, PubEvent) do
if valid_id?(event),
do: {:ok, event},
else: {:error, "ID #{event.id} is invalid for event:\n#{event}"}
end
defp valid_id?(event)
when is_struct(event, PubEvent) do
computed_id = compute_id!(event)
event.id == computed_id
end
@doc """
Validates a Nostr event signature per [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md).
"""
def validate_signature(event) when is_struct(event, PubEvent) do
if valid_signature?(event),
do: {:ok, event},
else: {:error, "Signature #{event.sig} is invalid for event:\n#{event}"}
end
defp valid_signature?(event) when is_struct(event, PubEvent) do
data = compute_id!(event)
Curvy.verify(event.sig, data, event.pubkey)
end
defp compute_id!(event) when is_struct(event, PubEvent) do
Jason.encode!([0, event.pubkey, event.created_at, event.kind, event.tags, event.content])
|> sha256(:lowercase_hex)
end
defp sha256(data, :binary = output_type) when is_binary(data) and is_atom(output_type),
do: :crypto.hash(:sha256, data)
defp sha256(data, :lowercase_hex = output_type) when is_binary(data) and is_atom(output_type) do
sha256(data, :binary)
|> Base.encode16(case: :lower)
end
end

2
lib/gc_index_relay_web/controllers/event_controller.ex

@ -7,6 +7,7 @@ defmodule GcIndexRelayWeb.EventController do
action_fallback GcIndexRelayWeb.FallbackController action_fallback GcIndexRelayWeb.FallbackController
def create(conn, %{"event" => event_params}) do def create(conn, %{"event" => event_params}) do
# TODO: Add 400 for invalid event in FallbackController
with {:ok, %Event{} = event} <- Nostr.create_event(event_params) do with {:ok, %Event{} = event} <- Nostr.create_event(event_params) do
conn conn
|> put_status(:created) |> put_status(:created)
@ -16,6 +17,7 @@ defmodule GcIndexRelayWeb.EventController do
end end
def show(conn, %{"id" => id}) do def show(conn, %{"id" => id}) do
# TODO: Add 404 for event not found in FallbackController
event = Nostr.get_event!(id) event = Nostr.get_event!(id)
render(conn, :show, event: event) render(conn, :show, event: event)
end end

3
mix.exs

@ -56,7 +56,8 @@ defmodule GcIndexRelay.MixProject do
{:gettext, "~> 1.0"}, {:gettext, "~> 1.0"},
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:dns_cluster, "~> 0.2.0"}, {:dns_cluster, "~> 0.2.0"},
{:bandit, "~> 1.5"} {:bandit, "~> 1.5"},
{:curvy, "~> 0.3.1"}
] ]
end end

1
mix.lock

@ -1,6 +1,7 @@
%{ %{
"bandit": {:hex, :bandit, "1.10.1", "6b1f8609d947ae2a74da5bba8aee938c94348634e54e5625eef622ca0bbbb062", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b4c35f273030e44268ace53bf3d5991dfc385c77374244e2f960876547671aa"}, "bandit": {:hex, :bandit, "1.10.1", "6b1f8609d947ae2a74da5bba8aee938c94348634e54e5625eef622ca0bbbb062", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b4c35f273030e44268ace53bf3d5991dfc385c77374244e2f960876547671aa"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"curvy": {:hex, :curvy, "0.3.1", "2645a11452743a37de2393da4d2e60700632498b166413b4f73bc34c57a911e1", [:mix], [], "hexpm", "82df293452f7b751becabc29e8aad0f7d88ffdcd790ac7a2ea16ea1544681d8a"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},

Loading…
Cancel
Save