Harden MIP-02 welcome and wrapped payload validation
This commit is contained in:
@@ -29,9 +29,9 @@ Spec source: `~/marmot/README.md` + MIP-00..05.
|
|||||||
## M3 — MIP-02 (welcome events)
|
## M3 — MIP-02 (welcome events)
|
||||||
|
|
||||||
- [ ] Support wrapped Welcome delivery via NIP-59 (`1059`) recipient-gated reads
|
- [ ] Support wrapped Welcome delivery via NIP-59 (`1059`) recipient-gated reads
|
||||||
- [ ] Validate relay behavior for unsigned inner Welcome semantics (kind `444` envelope expectations)
|
- [x] Validate relay behavior for unsigned inner Welcome semantics (kind `444` envelope expectations)
|
||||||
- [ ] Ensure durability/ack semantics support Commit-then-Welcome sequencing requirements
|
- [ ] Ensure durability/ack semantics support Commit-then-Welcome sequencing requirements
|
||||||
- [ ] Add negative tests for malformed wrapped Welcome payloads
|
- [x] Add negative tests for malformed wrapped Welcome payloads
|
||||||
|
|
||||||
## M4 — MIP-03 (group events)
|
## M4 — MIP-03 (group events)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
| :invalid_marmot_keypackage_ref_tag
|
| :invalid_marmot_keypackage_ref_tag
|
||||||
| :missing_marmot_relay_tag
|
| :missing_marmot_relay_tag
|
||||||
| :invalid_marmot_relay_tag
|
| :invalid_marmot_relay_tag
|
||||||
|
| :invalid_marmot_direct_welcome_event
|
||||||
|
| :invalid_giftwrap_content
|
||||||
|
| :missing_giftwrap_recipient_tag
|
||||||
|
| :invalid_giftwrap_recipient_tag
|
||||||
|
|
||||||
@spec validate(map()) :: :ok | {:error, error_reason()}
|
@spec validate(map()) :: :ok | {:error, error_reason()}
|
||||||
def validate(event) when is_map(event) do
|
def validate(event) when is_map(event) do
|
||||||
@@ -106,7 +110,14 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
missing_marmot_relay_tag:
|
missing_marmot_relay_tag:
|
||||||
"invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] tag",
|
"invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] tag",
|
||||||
invalid_marmot_relay_tag:
|
invalid_marmot_relay_tag:
|
||||||
"invalid: kind 10051 must include at least one [\"relay\", <ws/wss url>] 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
@spec error_message(error_reason()) :: String.t()
|
@spec error_message(error_reason()) :: String.t()
|
||||||
@@ -187,6 +198,12 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
defp validate_kind_specific(%{"kind" => 10_051} = event),
|
defp validate_kind_specific(%{"kind" => 10_051} = event),
|
||||||
do: validate_marmot_keypackage_relay_list(event)
|
do: validate_marmot_keypackage_relay_list(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(_event), do: :ok
|
defp validate_kind_specific(_event), do: :ok
|
||||||
|
|
||||||
defp validate_marmot_keypackage_event(event) do
|
defp validate_marmot_keypackage_event(event) do
|
||||||
@@ -244,6 +261,14 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
end
|
end
|
||||||
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_non_empty_base64_content(event) do
|
defp validate_non_empty_base64_content(event) do
|
||||||
case Base.decode64(Map.get(event, "content", "")) do
|
case Base.decode64(Map.get(event, "content", "")) do
|
||||||
{:ok, decoded} when byte_size(decoded) > 0 -> :ok
|
{:ok, decoded} when byte_size(decoded) > 0 -> :ok
|
||||||
@@ -251,6 +276,28 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
end
|
end
|
||||||
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_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
|
defp validate_single_string_tag(tags, tag_name, expected_value, missing_error, invalid_error) do
|
||||||
validate_single_string_tag_with_predicate(
|
validate_single_string_tag_with_predicate(
|
||||||
tags,
|
tags,
|
||||||
@@ -358,6 +405,11 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
defp valid_single_relay_tag?(["relay", relay_url]), do: valid_websocket_url?(relay_url)
|
defp valid_single_relay_tag?(["relay", relay_url]), do: valid_websocket_url?(relay_url)
|
||||||
defp valid_single_relay_tag?(_tag), do: false
|
defp valid_single_relay_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
|
defp valid_websocket_url?(url) when is_binary(url) do
|
||||||
case URI.parse(url) do
|
case URI.parse(url) do
|
||||||
%URI{scheme: scheme, host: host}
|
%URI{scheme: scheme, host: host}
|
||||||
|
|||||||
@@ -42,6 +42,53 @@ defmodule Parrhesia.Protocol.EventValidatorMarmotTest do
|
|||||||
assert {:error, :missing_marmot_relay_tag} = EventValidator.validate(event)
|
assert {:error, :missing_marmot_relay_tag} = EventValidator.validate(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rejects direct kind 444 welcome events" do
|
||||||
|
event =
|
||||||
|
valid_keypackage_event(%{
|
||||||
|
"kind" => 444,
|
||||||
|
"tags" => [["e", String.duplicate("a", 64)], ["encoding", "base64"]],
|
||||||
|
"content" => Base.encode64("welcome")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:error, :invalid_marmot_direct_welcome_event} = EventValidator.validate(event)
|
||||||
|
|
||||||
|
assert {:error,
|
||||||
|
"invalid: kind 444 welcome events must be wrapped in kind 1059 and remain unsigned"} =
|
||||||
|
Protocol.validate_event(event)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects malformed kind 1059 wrapped welcome envelopes" do
|
||||||
|
invalid_missing_recipient =
|
||||||
|
valid_keypackage_event(%{
|
||||||
|
"kind" => 1059,
|
||||||
|
"tags" => [["e", String.duplicate("a", 64)]],
|
||||||
|
"content" => "ciphertext"
|
||||||
|
})
|
||||||
|
|
||||||
|
invalid_recipient_pubkey =
|
||||||
|
valid_keypackage_event(%{
|
||||||
|
"kind" => 1059,
|
||||||
|
"tags" => [["p", "not-hex"]],
|
||||||
|
"content" => "ciphertext"
|
||||||
|
})
|
||||||
|
|
||||||
|
invalid_empty_content =
|
||||||
|
valid_keypackage_event(%{
|
||||||
|
"kind" => 1059,
|
||||||
|
"tags" => [["p", String.duplicate("b", 64)]],
|
||||||
|
"content" => ""
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:error, :missing_giftwrap_recipient_tag} =
|
||||||
|
EventValidator.validate(invalid_missing_recipient)
|
||||||
|
|
||||||
|
assert {:error, :invalid_giftwrap_recipient_tag} =
|
||||||
|
EventValidator.validate(invalid_recipient_pubkey)
|
||||||
|
|
||||||
|
assert {:error, :invalid_giftwrap_content} =
|
||||||
|
EventValidator.validate(invalid_empty_content)
|
||||||
|
end
|
||||||
|
|
||||||
defp valid_keypackage_event(overrides \\ %{}) do
|
defp valid_keypackage_event(overrides \\ %{}) do
|
||||||
base_event = %{
|
base_event = %{
|
||||||
"pubkey" => String.duplicate("1", 64),
|
"pubkey" => String.duplicate("1", 64),
|
||||||
|
|||||||
@@ -135,6 +135,29 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "malformed wrapped welcome EVENT is rejected" do
|
||||||
|
state = connection_state()
|
||||||
|
|
||||||
|
event =
|
||||||
|
valid_event()
|
||||||
|
|> Map.put("kind", 1059)
|
||||||
|
|> Map.put("tags", [["e", String.duplicate("a", 64)]])
|
||||||
|
|> Map.put("content", "ciphertext")
|
||||||
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
|
payload = Jason.encode!(["EVENT", event])
|
||||||
|
|
||||||
|
assert {:push, {:text, response}, ^state} =
|
||||||
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
|
assert Jason.decode!(response) == [
|
||||||
|
"OK",
|
||||||
|
event["id"],
|
||||||
|
false,
|
||||||
|
"invalid: kind 1059 must include at least one recipient p tag"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
test "NEG sessions open and close" do
|
test "NEG sessions open and close" do
|
||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user