You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
76 lines
2.5 KiB
76 lines
2.5 KiB
defmodule GcIndexRelay.Nostr.Validator do |
|
@moduledoc """ |
|
Nostr key and signature validation. |
|
|
|
Uses [lib_secp256k1](https://hexdocs.pm/lib_secp256k1/) for Schnorr signature verification (BIP-340). |
|
|
|
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 the given 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 the given event"} |
|
end |
|
|
|
defp valid_signature?(event) when is_struct(event, PubEvent) do |
|
with true <- is_binary(event.sig) and is_binary(event.pubkey), |
|
{:ok, binary_signature} <- decode_hex(event.sig), |
|
{:ok, binary_pubkey} <- decode_hex(event.pubkey), |
|
{:ok, binary_event_data} <- decode_hex(compute_id!(event)) do |
|
# Use Schnorr signature verification (BIP-340) |
|
# Wrap in try/rescue since Secp256k1.schnorr_valid? raises on invalid input |
|
try do |
|
Secp256k1.schnorr_valid?(binary_signature, binary_event_data, binary_pubkey) |
|
rescue |
|
_ -> false |
|
end |
|
else |
|
_ -> false |
|
end |
|
end |
|
|
|
defp decode_hex(hex_string) when is_binary(hex_string) do |
|
case Base.decode16(hex_string, case: :lower) do |
|
{:ok, _} = result -> result |
|
:error -> {:error, :invalid_hex} |
|
end |
|
end |
|
|
|
defp decode_hex(_), do: {:error, :invalid_input} |
|
|
|
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
|
|
|