Files
parrhesia/lib/parrhesia/protocol/event_validator.ex

928 lines
30 KiB
Elixir

defmodule Parrhesia.Protocol.EventValidator do
@moduledoc """
Strict NIP-01 event validation helpers.
"""
@required_fields ~w[id pubkey created_at kind tags content sig]
@max_kind 65_535
@default_max_event_future_skew_seconds 900
@default_max_tags_per_event 256
@default_nip43_request_max_age_seconds 300
@supported_mls_ciphersuites MapSet.new(~w[0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007])
@required_mls_extensions MapSet.new(["0xf2ee", "0x000a"])
@supported_keypackage_ref_sizes [32, 48, 64]
@type error_reason ::
:invalid_shape
| :invalid_id
| :invalid_pubkey
| :invalid_created_at
| :created_at_too_far_in_future
| :invalid_kind
| :too_many_tags
| :invalid_tags
| :invalid_content
| :invalid_sig
| :invalid_id_hash
| :invalid_signature
| :invalid_marmot_keypackage_content
| :missing_marmot_encoding_tag
| :invalid_marmot_encoding_tag
| :missing_marmot_protocol_version_tag
| :invalid_marmot_protocol_version_tag
| :missing_marmot_ciphersuite_tag
| :invalid_marmot_ciphersuite_tag
| :missing_marmot_extensions_tag
| :invalid_marmot_extensions_tag
| :missing_marmot_relays_tag
| :invalid_marmot_relays_tag
| :missing_marmot_keypackage_ref_tag
| :invalid_marmot_keypackage_ref_tag
| :missing_marmot_relay_tag
| :invalid_marmot_relay_tag
| :invalid_marmot_direct_welcome_event
| :invalid_giftwrap_content
| :missing_giftwrap_recipient_tag
| :invalid_giftwrap_recipient_tag
| :missing_marmot_group_tag
| :invalid_marmot_group_tag
| :invalid_marmot_group_content
| :missing_nip66_d_tag
| :invalid_nip66_d_tag
| :invalid_nip66_discovery_tag
| :missing_nip66_frequency_tag
| :invalid_nip66_frequency_tag
| :invalid_nip66_timeout_tag
| :invalid_nip66_check_tag
| :missing_nip43_protected_tag
| :missing_nip43_claim_tag
| :invalid_nip43_claim_tag
| :missing_nip43_member_tag
| :invalid_nip43_member_tag
| :missing_nip43_pubkey_tag
| :invalid_nip43_pubkey_tag
| :stale_nip43_join_request
| :stale_nip43_leave_request
@spec validate(map()) :: :ok | {:error, error_reason()}
def validate(event) when is_map(event) do
with :ok <- validate_required_fields(event),
:ok <- validate_id_shape(event["id"]),
:ok <- validate_pubkey(event["pubkey"]),
:ok <- validate_created_at(event["created_at"]),
:ok <- validate_kind(event["kind"]),
:ok <- validate_tags(event["tags"]),
:ok <- validate_content(event["content"]),
:ok <- validate_sig(event["sig"]),
:ok <- validate_id_hash(event),
:ok <- validate_signature(event) do
validate_kind_specific(event)
end
end
def validate(_event), do: {:error, :invalid_shape}
@spec compute_id(map()) :: String.t()
def compute_id(event) do
[
0,
event["pubkey"],
event["created_at"],
event["kind"],
event["tags"],
event["content"]
]
|> JSON.encode!()
|> then(&:crypto.hash(:sha256, &1))
|> Base.encode16(case: :lower)
end
@error_messages %{
invalid_shape:
"invalid: event must include id, pubkey, created_at, kind, tags, content, and sig",
invalid_id: "invalid: id must be 32-byte lowercase hex",
invalid_pubkey: "invalid: pubkey must be 32-byte lowercase hex",
invalid_created_at: "invalid: created_at must be a non-negative integer unix timestamp",
created_at_too_far_in_future:
"invalid: event creation date is too far off from the current time",
invalid_kind: "invalid: kind must be an integer between 0 and 65535",
too_many_tags: "invalid: event tags exceed configured limit",
invalid_tags: "invalid: tags must be an array of non-empty string arrays",
invalid_content: "invalid: content must be a string",
invalid_sig: "invalid: sig must be 64-byte lowercase hex",
invalid_id_hash: "invalid: event id does not match serialized event",
invalid_signature: "invalid: event signature is invalid",
invalid_marmot_keypackage_content: "invalid: kind 443 content must be non-empty base64",
missing_marmot_encoding_tag: "invalid: kind 443 must include [\"encoding\", \"base64\"]",
invalid_marmot_encoding_tag: "invalid: kind 443 must include [\"encoding\", \"base64\"]",
missing_marmot_protocol_version_tag:
"invalid: kind 443 must include [\"mls_protocol_version\", \"1.0\"]",
invalid_marmot_protocol_version_tag:
"invalid: kind 443 must include [\"mls_protocol_version\", \"1.0\"]",
missing_marmot_ciphersuite_tag:
"invalid: kind 443 must include a supported [\"mls_ciphersuite\", \"0x....\"]",
invalid_marmot_ciphersuite_tag:
"invalid: kind 443 must include a supported [\"mls_ciphersuite\", \"0x....\"]",
missing_marmot_extensions_tag:
"invalid: kind 443 must include [\"mls_extensions\", ...] with 0xf2ee and 0x000a",
invalid_marmot_extensions_tag:
"invalid: kind 443 must include [\"mls_extensions\", ...] with 0xf2ee and 0x000a",
missing_marmot_relays_tag: "invalid: kind 443 must include [\"relays\", <ws/wss url>, ...]",
invalid_marmot_relays_tag: "invalid: kind 443 must include [\"relays\", <ws/wss url>, ...]",
missing_marmot_keypackage_ref_tag:
"invalid: kind 443 must include [\"i\", <hex keypackage ref>]",
invalid_marmot_keypackage_ref_tag:
"invalid: kind 443 must include [\"i\", <hex keypackage ref>]",
missing_marmot_relay_tag:
"invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] tag",
invalid_marmot_relay_tag:
"invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] tag",
invalid_marmot_direct_welcome_event:
"invalid: kind 444 welcome events must be wrapped in kind 1059 and remain unsigned",
invalid_giftwrap_content: "invalid: kind 1059 content must be a non-empty encrypted payload",
missing_giftwrap_recipient_tag:
"invalid: kind 1059 must include at least one recipient p tag",
invalid_giftwrap_recipient_tag:
"invalid: kind 1059 recipient p tags must contain lowercase hex pubkeys",
missing_marmot_group_tag: "invalid: kind 445 must include at least one h tag with a group id",
invalid_marmot_group_tag:
"invalid: kind 445 h tags must contain 32-byte lowercase hex group ids",
invalid_marmot_group_content: "invalid: kind 445 content must be non-empty base64",
missing_nip66_d_tag:
"invalid: kind 30166 must include a single [\"d\", <normalized ws/wss url or relay pubkey>] tag",
invalid_nip66_d_tag:
"invalid: kind 30166 must include a single [\"d\", <normalized ws/wss url or relay pubkey>] tag",
invalid_nip66_discovery_tag: "invalid: kind 30166 includes malformed NIP-66 discovery tags",
missing_nip66_frequency_tag:
"invalid: kind 10166 must include a single [\"frequency\", <seconds>] tag",
invalid_nip66_frequency_tag:
"invalid: kind 10166 must include a single [\"frequency\", <seconds>] tag",
invalid_nip66_timeout_tag:
"invalid: kind 10166 timeout tags must be [\"timeout\", <check>, <ms>]",
invalid_nip66_check_tag: "invalid: kind 10166 c tags must contain lowercase check names",
missing_nip43_protected_tag:
"invalid: NIP-43 events must include a NIP-70 protected [\"-\"] tag",
missing_nip43_claim_tag:
"invalid: kinds 28934 and 28935 must include a single [\"claim\", <invite code>] tag",
invalid_nip43_claim_tag:
"invalid: kinds 28934 and 28935 must include a single [\"claim\", <invite code>] tag",
missing_nip43_member_tag:
"invalid: kind 13534 must include at least one [\"member\", <hex pubkey>] tag",
invalid_nip43_member_tag:
"invalid: kind 13534 member tags must contain lowercase hex pubkeys",
missing_nip43_pubkey_tag:
"invalid: kinds 8000 and 8001 must include a single [\"p\", <hex pubkey>] tag",
invalid_nip43_pubkey_tag:
"invalid: kinds 8000 and 8001 must include a single [\"p\", <hex pubkey>] tag",
stale_nip43_join_request: "invalid: kind 28934 created_at must be recent",
stale_nip43_leave_request: "invalid: kind 28936 created_at must be recent"
}
@spec error_message(error_reason()) :: String.t()
def error_message(reason), do: Map.fetch!(@error_messages, reason)
defp validate_required_fields(event) do
if Enum.all?(@required_fields, &Map.has_key?(event, &1)) do
:ok
else
{:error, :invalid_shape}
end
end
defp validate_id_shape(id) when is_binary(id) do
if lowercase_hex?(id, 32), do: :ok, else: {:error, :invalid_id}
end
defp validate_id_shape(_id), do: {:error, :invalid_id}
defp validate_pubkey(pubkey) when is_binary(pubkey) do
if lowercase_hex?(pubkey, 32), do: :ok, else: {:error, :invalid_pubkey}
end
defp validate_pubkey(_pubkey), do: {:error, :invalid_pubkey}
defp validate_created_at(created_at) when is_integer(created_at) and created_at >= 0 do
now = System.system_time(:second)
max_future_skew = max_event_future_skew_seconds()
if created_at <= now + max_future_skew do
:ok
else
{:error, :created_at_too_far_in_future}
end
end
defp validate_created_at(_created_at), do: {:error, :invalid_created_at}
defp validate_kind(kind) when is_integer(kind) and kind >= 0 and kind <= @max_kind, do: :ok
defp validate_kind(_kind), do: {:error, :invalid_kind}
defp validate_tags(tags) when is_list(tags), do: validate_tags(tags, max_tags_per_event(), 0)
defp validate_tags(_tags), do: {:error, :invalid_tags}
defp validate_tags([], _max_tags, _count), do: :ok
defp validate_tags([tag | rest], max_tags, count) do
cond do
count + 1 > max_tags ->
{:error, :too_many_tags}
valid_tag?(tag) ->
validate_tags(rest, max_tags, count + 1)
true ->
{:error, :invalid_tags}
end
end
defp validate_content(content) when is_binary(content), do: :ok
defp validate_content(_content), do: {:error, :invalid_content}
defp validate_sig(sig) when is_binary(sig) do
if lowercase_hex?(sig, 64), do: :ok, else: {:error, :invalid_sig}
end
defp validate_sig(_sig), do: {:error, :invalid_sig}
defp validate_id_hash(event) do
if event["id"] == compute_id(event) do
:ok
else
{:error, :invalid_id_hash}
end
end
defp validate_signature(event) do
if verify_event_signatures?() do
verify_signature(event)
else
:ok
end
end
defp verify_signature(%{"id" => id, "pubkey" => pubkey, "sig" => sig}) do
with {:ok, id_bin} <- Base.decode16(id, case: :lower),
{:ok, pubkey_bin} <- Base.decode16(pubkey, case: :lower),
{:ok, sig_bin} <- Base.decode16(sig, case: :lower),
true <- Secp256k1.schnorr_valid?(sig_bin, id_bin, pubkey_bin) do
:ok
else
_other -> {:error, :invalid_signature}
end
rescue
_error -> {:error, :invalid_signature}
end
defp verify_signature(_event), do: {:error, :invalid_signature}
defp valid_tag?(tag) when is_list(tag) do
tag != [] and Enum.all?(tag, &is_binary/1)
end
defp valid_tag?(_tag), do: false
defp validate_kind_specific(%{"kind" => 443} = event),
do: validate_marmot_keypackage_event(event)
defp validate_kind_specific(%{"kind" => 10_051} = event),
do: validate_marmot_keypackage_relay_list(event)
defp validate_kind_specific(%{"kind" => 445} = event),
do: validate_marmot_group_event(event)
defp validate_kind_specific(%{"kind" => 444}),
do: {:error, :invalid_marmot_direct_welcome_event}
defp validate_kind_specific(%{"kind" => 1059} = event),
do: validate_giftwrap_event(event)
defp validate_kind_specific(%{"kind" => 30_166} = event),
do: validate_nip66_discovery_event(event)
defp validate_kind_specific(%{"kind" => 10_166} = event),
do: validate_nip66_monitor_announcement(event)
defp validate_kind_specific(%{"kind" => 13_534} = event),
do: validate_nip43_membership_list(event)
defp validate_kind_specific(%{"kind" => kind} = event) when kind in [8_000, 8_001],
do: validate_nip43_membership_delta(event)
defp validate_kind_specific(%{"kind" => 28_934} = event),
do: validate_nip43_join_request(event)
defp validate_kind_specific(%{"kind" => 28_935} = event),
do: validate_nip43_invite_response(event)
defp validate_kind_specific(%{"kind" => 28_936} = event),
do: validate_nip43_leave_request(event)
defp validate_kind_specific(_event), do: :ok
defp validate_marmot_keypackage_event(event) do
tags = Map.get(event, "tags", [])
with :ok <- validate_non_empty_base64_content(event),
{:ok, ["encoding", "base64"]} <-
fetch_single_tag(tags, "encoding", :missing_marmot_encoding_tag),
:ok <-
validate_single_string_tag(
tags,
"mls_protocol_version",
"1.0",
:missing_marmot_protocol_version_tag,
:invalid_marmot_protocol_version_tag
),
:ok <-
validate_single_string_tag_with_predicate(
tags,
"mls_ciphersuite",
:missing_marmot_ciphersuite_tag,
:invalid_marmot_ciphersuite_tag,
&supported_mls_ciphersuite?/1
),
:ok <- validate_mls_extensions_tag(tags),
:ok <-
validate_relays_tag(
tags,
"relays",
:missing_marmot_relays_tag,
:invalid_marmot_relays_tag
),
:ok <- validate_keypackage_ref_tag(tags) do
:ok
else
{:ok, _other_tag} -> {:error, :invalid_marmot_encoding_tag}
{:error, _reason} = error -> error
end
end
defp validate_marmot_keypackage_relay_list(event) do
tags = Map.get(event, "tags", [])
relay_tags = Enum.filter(tags, &match_tag_name?(&1, "relay"))
cond do
relay_tags == [] ->
{:error, :missing_marmot_relay_tag}
Enum.all?(relay_tags, &valid_single_relay_tag?/1) ->
:ok
true ->
{:error, :invalid_marmot_relay_tag}
end
end
defp validate_marmot_group_event(event) do
tags = Map.get(event, "tags", [])
with :ok <- validate_non_empty_base64_content(event, :invalid_marmot_group_content) do
validate_marmot_group_tags(tags)
end
end
defp validate_giftwrap_event(event) do
tags = Map.get(event, "tags", [])
with :ok <- validate_non_empty_content(event, :invalid_giftwrap_content) do
validate_giftwrap_recipient_tags(tags)
end
end
defp validate_nip66_discovery_event(event) do
tags = Map.get(event, "tags", [])
with :ok <- validate_nip66_d_tag(tags),
:ok <-
validate_optional_single_string_tag_with_predicate(
tags,
"n",
:invalid_nip66_discovery_tag,
&(&1 in ["clearnet", "tor", "i2p", "loki"])
),
:ok <-
validate_optional_single_string_tag_with_predicate(
tags,
"T",
:invalid_nip66_discovery_tag,
&valid_pascal_case?/1
),
:ok <-
validate_optional_single_string_tag_with_predicate(
tags,
"g",
:invalid_nip66_discovery_tag,
&non_empty_string?/1
),
:ok <-
validate_optional_repeated_tag(
tags,
"N",
&positive_integer_string?/1,
:invalid_nip66_discovery_tag
),
:ok <-
validate_optional_repeated_tag(
tags,
"R",
&valid_nip66_requirement_value?/1,
:invalid_nip66_discovery_tag
),
:ok <-
validate_optional_repeated_tag(
tags,
"k",
&valid_nip66_kind_value?/1,
:invalid_nip66_discovery_tag
),
:ok <-
validate_optional_repeated_tag(
tags,
"t",
&non_empty_string?/1,
:invalid_nip66_discovery_tag
),
:ok <-
validate_optional_single_string_tag_with_predicate(
tags,
"rtt-open",
:invalid_nip66_discovery_tag,
&positive_integer_string?/1
),
:ok <-
validate_optional_single_string_tag_with_predicate(
tags,
"rtt-read",
:invalid_nip66_discovery_tag,
&positive_integer_string?/1
) do
validate_optional_single_string_tag_with_predicate(
tags,
"rtt-write",
:invalid_nip66_discovery_tag,
&positive_integer_string?/1
)
end
end
defp validate_nip66_monitor_announcement(event) do
tags = Map.get(event, "tags", [])
with :ok <-
validate_single_string_tag_with_predicate(
tags,
"frequency",
:missing_nip66_frequency_tag,
:invalid_nip66_frequency_tag,
&positive_integer_string?/1
),
:ok <- validate_optional_repeated_timeout_tags(tags),
:ok <-
validate_optional_repeated_tag(
tags,
"c",
&valid_nip66_check_name?/1,
:invalid_nip66_check_tag
) do
validate_optional_single_string_tag_with_predicate(
tags,
"g",
:invalid_nip66_discovery_tag,
&non_empty_string?/1
)
end
end
defp validate_nip43_membership_list(event) do
tags = Map.get(event, "tags", [])
case validate_protected_tag(tags) do
:ok -> validate_optional_repeated_pubkey_tag(tags, "member", :invalid_nip43_member_tag)
{:error, _reason} = error -> error
end
end
defp validate_nip43_membership_delta(event) do
tags = Map.get(event, "tags", [])
case validate_protected_tag(tags) do
:ok ->
validate_single_pubkey_tag(
tags,
"p",
:missing_nip43_pubkey_tag,
:invalid_nip43_pubkey_tag
)
{:error, _reason} = error ->
error
end
end
defp validate_nip43_join_request(event) do
tags = Map.get(event, "tags", [])
case validate_protected_tag(tags) do
:ok ->
with :ok <-
validate_single_string_tag_with_predicate(
tags,
"claim",
:missing_nip43_claim_tag,
:invalid_nip43_claim_tag,
&non_empty_string?/1
) do
validate_recent_created_at(event, :stale_nip43_join_request)
end
{:error, _reason} = error ->
error
end
end
defp validate_nip43_invite_response(event) do
tags = Map.get(event, "tags", [])
case validate_protected_tag(tags) do
:ok ->
validate_single_string_tag_with_predicate(
tags,
"claim",
:missing_nip43_claim_tag,
:invalid_nip43_claim_tag,
&non_empty_string?/1
)
{:error, _reason} = error ->
error
end
end
defp validate_nip43_leave_request(event) do
tags = Map.get(event, "tags", [])
case validate_protected_tag(tags) do
:ok -> validate_recent_created_at(event, :stale_nip43_leave_request)
{:error, _reason} = error -> error
end
end
defp validate_non_empty_base64_content(event),
do: validate_non_empty_base64_content(event, :invalid_marmot_keypackage_content)
defp validate_non_empty_base64_content(event, error_reason) do
case Base.decode64(Map.get(event, "content", "")) do
{:ok, decoded} when byte_size(decoded) > 0 -> :ok
_other -> {:error, error_reason}
end
end
defp validate_non_empty_content(event, error_reason) do
case Map.get(event, "content", "") do
content when is_binary(content) and content != "" -> :ok
_other -> {:error, error_reason}
end
end
defp validate_marmot_group_tags(tags) do
group_tags = Enum.filter(tags, &match_tag_name?(&1, "h"))
cond do
group_tags == [] ->
{:error, :missing_marmot_group_tag}
Enum.all?(group_tags, &valid_marmot_group_tag?/1) ->
:ok
true ->
{:error, :invalid_marmot_group_tag}
end
end
defp validate_giftwrap_recipient_tags(tags) do
recipient_tags = Enum.filter(tags, &match_tag_name?(&1, "p"))
cond do
recipient_tags == [] ->
{:error, :missing_giftwrap_recipient_tag}
Enum.all?(recipient_tags, &valid_giftwrap_recipient_tag?/1) ->
:ok
true ->
{:error, :invalid_giftwrap_recipient_tag}
end
end
defp validate_single_string_tag(tags, tag_name, expected_value, missing_error, invalid_error) do
validate_single_string_tag_with_predicate(
tags,
tag_name,
missing_error,
invalid_error,
&(&1 == expected_value)
)
end
defp validate_single_string_tag_with_predicate(
tags,
tag_name,
missing_error,
invalid_error,
predicate
)
when is_function(predicate, 1) do
case fetch_single_tag(tags, tag_name, missing_error) do
{:ok, [^tag_name, value]} ->
if predicate.(value) do
:ok
else
{:error, invalid_error}
end
{:ok, _invalid_tag_shape} ->
{:error, invalid_error}
{:error, _reason} = error ->
error
end
end
defp validate_optional_single_string_tag_with_predicate(
tags,
tag_name,
invalid_error,
predicate
)
when is_function(predicate, 1) do
case Enum.filter(tags, &match_tag_name?(&1, tag_name)) do
[] ->
:ok
[[^tag_name, value]] ->
if predicate.(value), do: :ok, else: {:error, invalid_error}
_other ->
{:error, invalid_error}
end
end
defp validate_mls_extensions_tag(tags) do
with {:ok, ["mls_extensions" | extensions]} <-
fetch_single_tag(tags, "mls_extensions", :missing_marmot_extensions_tag),
true <- extensions != [],
true <- Enum.all?(extensions, &valid_mls_extension_id?/1),
true <- required_mls_extensions_present?(extensions) do
:ok
else
{:ok, _invalid_tag_shape} -> {:error, :invalid_marmot_extensions_tag}
false -> {:error, :invalid_marmot_extensions_tag}
{:error, _reason} = error -> error
end
end
defp validate_relays_tag(tags, tag_name, missing_error, invalid_error) do
with {:ok, [^tag_name | relay_urls]} <- fetch_single_tag(tags, tag_name, missing_error),
true <- relay_urls != [],
true <- Enum.all?(relay_urls, &valid_websocket_url?/1) do
:ok
else
{:ok, _invalid_tag_shape} -> {:error, invalid_error}
false -> {:error, invalid_error}
{:error, _reason} = error -> error
end
end
defp validate_keypackage_ref_tag(tags) do
with {:ok, ["i", keypackage_ref]} <-
fetch_single_tag(tags, "i", :missing_marmot_keypackage_ref_tag),
true <- valid_keypackage_ref?(keypackage_ref) do
:ok
else
{:ok, _invalid_tag_shape} -> {:error, :invalid_marmot_keypackage_ref_tag}
false -> {:error, :invalid_marmot_keypackage_ref_tag}
{:error, _reason} = error -> error
end
end
defp validate_nip66_d_tag(tags) do
with {:ok, ["d", value]} <- fetch_single_tag(tags, "d", :missing_nip66_d_tag),
true <- valid_websocket_url?(value) or lowercase_hex?(value, 32) do
:ok
else
{:ok, _invalid_tag_shape} -> {:error, :invalid_nip66_d_tag}
false -> {:error, :invalid_nip66_d_tag}
{:error, _reason} = error -> error
end
end
defp validate_optional_repeated_timeout_tags(tags) do
timeout_tags = Enum.filter(tags, &match_tag_name?(&1, "timeout"))
if Enum.all?(timeout_tags, &valid_nip66_timeout_tag?/1) do
:ok
else
{:error, :invalid_nip66_timeout_tag}
end
end
defp validate_optional_repeated_tag(tags, tag_name, predicate, invalid_error)
when is_function(predicate, 1) do
tags
|> Enum.filter(&match_tag_name?(&1, tag_name))
|> Enum.reduce_while(:ok, fn
[^tag_name, value], :ok ->
if predicate.(value), do: {:cont, :ok}, else: {:halt, {:error, invalid_error}}
_other, :ok ->
{:halt, {:error, invalid_error}}
end)
end
defp validate_protected_tag(tags) do
if Enum.any?(tags, &match?(["-"], &1)) do
:ok
else
{:error, :missing_nip43_protected_tag}
end
end
defp validate_single_pubkey_tag(tags, tag_name, missing_error, invalid_error) do
case fetch_single_tag(tags, tag_name, missing_error) do
{:ok, [^tag_name, value]} ->
if lowercase_hex?(value, 32) do
:ok
else
{:error, invalid_error}
end
{:ok, _invalid_tag_shape} ->
{:error, invalid_error}
{:error, _reason} = error ->
error
end
end
defp validate_optional_repeated_pubkey_tag(tags, tag_name, invalid_error) do
matching_tags = Enum.filter(tags, &match_tag_name?(&1, tag_name))
if Enum.all?(matching_tags, fn
[^tag_name, pubkey | _rest] -> lowercase_hex?(pubkey, 32)
_other -> false
end) do
:ok
else
{:error, invalid_error}
end
end
defp validate_recent_created_at(%{"created_at" => created_at}, error_reason)
when is_integer(created_at) do
if created_at >= System.system_time(:second) - nip43_request_max_age_seconds() do
:ok
else
{:error, error_reason}
end
end
defp validate_recent_created_at(_event, error_reason), do: {:error, error_reason}
defp fetch_single_tag(tags, tag_name, missing_error) do
case Enum.filter(tags, &match_tag_name?(&1, tag_name)) do
[tag] -> {:ok, tag}
[] -> {:error, missing_error}
_duplicates -> {:error, missing_error}
end
end
defp match_tag_name?([tag_name | _rest], expected_tag_name) when is_binary(tag_name),
do: tag_name == expected_tag_name
defp match_tag_name?(_tag, _expected_tag_name), do: false
defp supported_mls_ciphersuite?(value) when is_binary(value) do
value
|> String.downcase()
|> then(&MapSet.member?(@supported_mls_ciphersuites, &1))
end
defp supported_mls_ciphersuite?(_value), do: false
defp valid_mls_extension_id?(value) when is_binary(value) do
String.match?(value, ~r/^0x[0-9a-fA-F]{4}$/)
end
defp valid_mls_extension_id?(_value), do: false
defp required_mls_extensions_present?(extensions) do
normalized = MapSet.new(Enum.map(extensions, &String.downcase/1))
MapSet.subset?(@required_mls_extensions, normalized)
end
defp valid_single_relay_tag?(["relay", relay_url]), do: valid_websocket_url?(relay_url)
defp valid_single_relay_tag?(_tag), do: false
defp valid_marmot_group_tag?(["h", group_id | _rest]), do: lowercase_hex?(group_id, 32)
defp valid_marmot_group_tag?(_tag), do: false
defp valid_giftwrap_recipient_tag?(["p", recipient_pubkey | _rest]),
do: lowercase_hex?(recipient_pubkey, 32)
defp valid_giftwrap_recipient_tag?(_tag), do: false
defp valid_websocket_url?(url) when is_binary(url) do
case URI.parse(url) do
%URI{scheme: scheme, host: host}
when scheme in ["ws", "wss"] and is_binary(host) and host != "" ->
true
_other ->
false
end
end
defp valid_websocket_url?(_url), do: false
defp valid_nip66_timeout_tag?(["timeout", milliseconds]),
do: positive_integer_string?(milliseconds)
defp valid_nip66_timeout_tag?(["timeout", check, milliseconds]) do
valid_nip66_check_name?(check) and positive_integer_string?(milliseconds)
end
defp valid_nip66_timeout_tag?(_tag), do: false
defp valid_nip66_requirement_value?(value) when is_binary(value) do
normalized = String.trim_leading(value, "!")
normalized in ["auth", "writes", "pow", "payment"]
end
defp valid_nip66_requirement_value?(_value), do: false
defp valid_nip66_kind_value?(<<"!", rest::binary>>), do: positive_integer_string?(rest)
defp valid_nip66_kind_value?(value), do: positive_integer_string?(value)
defp valid_nip66_check_name?(value) when is_binary(value) do
String.match?(value, ~r/^[a-z0-9-]+$/)
end
defp valid_nip66_check_name?(_value), do: false
defp valid_pascal_case?(value) when is_binary(value) do
String.match?(value, ~r/^[A-Z][A-Za-z0-9]*$/)
end
defp valid_pascal_case?(_value), do: false
defp positive_integer_string?(value) when is_binary(value) do
case Integer.parse(value) do
{integer, ""} when integer >= 0 -> true
_other -> false
end
end
defp positive_integer_string?(_value), do: false
defp non_empty_string?(value) when is_binary(value), do: value != ""
defp non_empty_string?(_value), do: false
defp valid_keypackage_ref?(value) when is_binary(value) do
Enum.any?(@supported_keypackage_ref_sizes, &lowercase_hex?(value, &1))
end
defp valid_keypackage_ref?(_value), do: false
defp lowercase_hex?(value, bytes) do
byte_size(value) == bytes * 2 and
match?({:ok, _decoded}, Base.decode16(value, case: :lower))
end
defp verify_event_signatures? do
:parrhesia
|> Application.get_env(:features, [])
|> Keyword.get(:verify_event_signatures, true)
end
defp max_event_future_skew_seconds do
:parrhesia
|> Application.get_env(:limits, [])
|> Keyword.get(:max_event_future_skew_seconds, @default_max_event_future_skew_seconds)
end
defp max_tags_per_event do
case Application.get_env(:parrhesia, :limits, []) |> Keyword.get(:max_tags_per_event) do
value when is_integer(value) and value > 0 -> value
_other -> @default_max_tags_per_event
end
end
defp nip43_request_max_age_seconds do
:parrhesia
|> Application.get_env(:nip43, [])
|> Keyword.get(:request_max_age_seconds, @default_nip43_request_max_age_seconds)
end
end