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

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