-
Notifications
You must be signed in to change notification settings - Fork 2
fix: payment signature format validation and SIWX ETS size cap #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6600692
95212f0
d318aab
fcce72d
894789a
aaeb332
ec469fc
1ea791e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,20 @@ defmodule X402.PaymentSignature do | |
|
|
||
| @required_fields ~w(transactionHash network scheme payerWallet) | ||
|
|
||
| # EIP-55 Ethereum address: 0x followed by exactly 40 hex characters (case-insensitive). | ||
| # We accept mixed-case (checksummed) and lower-case (normalised) addresses. | ||
| @eth_address_regex ~r/^0x[0-9a-fA-F]{40}$/ | ||
|
|
||
| # Transaction hash: 0x followed by exactly 64 hex characters (256-bit hash). | ||
| # Solana tx IDs are base58, 87-88 chars β we detect them by absence of "0x" prefix. | ||
| @eth_tx_hash_regex ~r/^0x[0-9a-fA-F]{64}$/ | ||
|
|
||
| # Solana base-58 transaction signature (87β88 characters of base58 alphabet). | ||
| @solana_tx_sig_regex ~r/^[1-9A-HJ-NP-Za-km-z]{87,88}$/ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solana tx signature regex rejects valid short signaturesLow Severity
|
||
|
|
||
| # Solana wallet address: base58, 32-44 characters. | ||
| @solana_address_regex ~r/^[1-9A-HJ-NP-Za-km-z]{43,44}$/ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wallet address regexes duplicated from
|
||
|
|
||
| # Single source of truth for the 8 KB decode guard β see X402.Header. | ||
| @max_header_bytes X402.Header.max_header_bytes() | ||
|
|
||
|
|
@@ -31,6 +45,7 @@ defmodule X402.PaymentSignature do | |
| :invalid_payload | ||
| | {:missing_fields, [String.t()]} | ||
| | {:invalid_upto_payment, upto_validation_error()} | ||
| | {:invalid_format, [{field :: String.t(), reason :: atom()}]} | ||
|
|
||
| @type decode_and_validate_error :: decode_error() | validate_error() | ||
|
|
||
|
|
@@ -52,7 +67,7 @@ defmodule X402.PaymentSignature do | |
|
|
||
| ## Examples | ||
|
|
||
| iex> payload = %{"transactionHash" => "0xabc", "network" => "eip155:8453", "scheme" => "exact", "payerWallet" => "0x1111111111111111111111111111111111111111"} | ||
| iex> payload = %{"transactionHash" => "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "network" => "eip155:8453", "scheme" => "exact", "payerWallet" => "0x1111111111111111111111111111111111111111"} | ||
| iex> value = payload |> Jason.encode!() |> Base.encode64() | ||
| iex> X402.PaymentSignature.decode(value) | ||
| {:ok, payload} | ||
|
|
@@ -124,7 +139,7 @@ defmodule X402.PaymentSignature do | |
| ## Examples | ||
|
|
||
| iex> payload = %{ | ||
| ...> "transactionHash" => "0xabc", | ||
| ...> "transactionHash" => "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||
| ...> "network" => "eip155:8453", | ||
| ...> "scheme" => "exact", | ||
| ...> "payerWallet" => "0x1111111111111111111111111111111111111111" | ||
|
|
@@ -152,30 +167,35 @@ defmodule X402.PaymentSignature do | |
| """ | ||
| @spec validate(map(), map()) :: {:ok, map()} | {:error, validate_error()} | ||
| def validate(payload, requirements) when is_map(payload) and is_map(requirements) do | ||
| missing = missing_fields(payload) | ||
|
|
||
| case missing do | ||
| [] -> | ||
| case validate_scheme(payload, requirements) do | ||
| :ok -> | ||
| result = {:ok, payload} | ||
|
|
||
| Telemetry.emit(:payment_signature, :validate, :ok, %{ | ||
| required_fields: @required_fields | ||
| }) | ||
|
|
||
| result | ||
| with :ok <- check_missing_fields(payload), | ||
| :ok <- check_field_formats(payload) do | ||
| case validate_scheme(payload, requirements) do | ||
| :ok -> | ||
| Telemetry.emit(:payment_signature, :validate, :ok, %{required_fields: @required_fields}) | ||
| {:ok, payload} | ||
|
|
||
| {:error, {:invalid_upto_payment, reason}} = error -> | ||
| Telemetry.emit(:payment_signature, :validate, :error, %{ | ||
| reason: :invalid_upto_payment, | ||
| detail: reason | ||
| }) | ||
|
|
||
| error | ||
| end | ||
| end | ||
| end | ||
|
|
||
| {:error, {:invalid_upto_payment, reason}} = error -> | ||
| Telemetry.emit(:payment_signature, :validate, :error, %{ | ||
| reason: :invalid_upto_payment, | ||
| detail: reason | ||
| }) | ||
| def validate(_payload, _requirements) do | ||
| Telemetry.emit(:payment_signature, :validate, :error, %{reason: :invalid_payload}) | ||
| {:error, :invalid_payload} | ||
| end | ||
|
|
||
| error | ||
| end | ||
| defp check_missing_fields(payload) do | ||
| case missing_fields(payload) do | ||
| [] -> | ||
| :ok | ||
|
|
||
| _ -> | ||
| missing -> | ||
| Telemetry.emit(:payment_signature, :validate, :error, %{ | ||
| reason: :missing_fields, | ||
| fields: missing | ||
|
|
@@ -185,9 +205,19 @@ defmodule X402.PaymentSignature do | |
| end | ||
| end | ||
|
|
||
| def validate(_payload, _requirements) do | ||
| Telemetry.emit(:payment_signature, :validate, :error, %{reason: :invalid_payload}) | ||
| {:error, :invalid_payload} | ||
| defp check_field_formats(payload) do | ||
| case validate_field_formats(payload) do | ||
| [] -> | ||
| :ok | ||
|
|
||
| format_errors -> | ||
| Telemetry.emit(:payment_signature, :validate, :error, %{ | ||
| reason: :invalid_format, | ||
| fields: Enum.map(format_errors, &elem(&1, 0)) | ||
| }) | ||
|
|
||
| {:error, {:invalid_format, format_errors}} | ||
| end | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing rejection_error clause for new invalid_format errorMedium Severity The new |
||
| end | ||
|
|
||
|
cursor[bot] marked this conversation as resolved.
|
||
| @doc since: "0.1.0", group: :verification | ||
|
|
@@ -196,7 +226,7 @@ defmodule X402.PaymentSignature do | |
|
|
||
| ## Examples | ||
|
|
||
| iex> payload = %{"transactionHash" => "0xabc", "network" => "eip155:8453", "scheme" => "exact", "payerWallet" => "0x1111111111111111111111111111111111111111"} | ||
| iex> payload = %{"transactionHash" => "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "network" => "eip155:8453", "scheme" => "exact", "payerWallet" => "0x1111111111111111111111111111111111111111"} | ||
| iex> value = payload |> Jason.encode!() |> Base.encode64() | ||
| iex> X402.PaymentSignature.decode_and_validate(value) | ||
| {:ok, payload} | ||
|
|
@@ -240,6 +270,50 @@ defmodule X402.PaymentSignature do | |
| |> Enum.sort() | ||
| end | ||
|
|
||
| # Validates the format of fields that have known structural constraints: | ||
| # - transactionHash: EVM 0x+64hex OR Solana base58 87-88 chars | ||
| # - payerWallet: EVM 0x+40hex OR Solana base58 32-44 chars | ||
| # | ||
| # We intentionally do NOT validate `network` and `scheme` here β those fields | ||
| # are validated downstream by the facilitator / scheme validators which have | ||
| # the authoritative list of supported values. | ||
| # | ||
| # Returns a list of {field, reason} tuples for each invalid field; empty list = ok. | ||
| @spec validate_field_formats(map()) :: [{String.t(), atom()}] | ||
| defp validate_field_formats(payload) do | ||
| [ | ||
| validate_transaction_hash(Map.get(payload, "transactionHash")), | ||
| validate_payer_wallet(Map.get(payload, "payerWallet")) | ||
| ] | ||
| |> Enum.reject(&is_nil/1) | ||
| end | ||
|
|
||
| @spec validate_transaction_hash(String.t() | nil) :: {String.t(), atom()} | nil | ||
| defp validate_transaction_hash(nil), do: nil | ||
|
|
||
| defp validate_transaction_hash(hash) when is_binary(hash) do | ||
| cond do | ||
| Regex.match?(@eth_tx_hash_regex, hash) -> nil | ||
| Regex.match?(@solana_tx_sig_regex, hash) -> nil | ||
| true -> {"transactionHash", :invalid_format} | ||
| end | ||
| end | ||
|
|
||
| defp validate_transaction_hash(_), do: {"transactionHash", :invalid_format} | ||
|
|
||
| @spec validate_payer_wallet(String.t() | nil) :: {String.t(), atom()} | nil | ||
| defp validate_payer_wallet(nil), do: nil | ||
|
|
||
| defp validate_payer_wallet(wallet) when is_binary(wallet) do | ||
| cond do | ||
| Regex.match?(@eth_address_regex, wallet) -> nil | ||
| Regex.match?(@solana_address_regex, wallet) -> nil | ||
| true -> {"payerWallet", :invalid_format} | ||
| end | ||
| end | ||
|
|
||
| defp validate_payer_wallet(_), do: {"payerWallet", :invalid_format} | ||
|
|
||
| @spec validate_scheme(map(), map()) :: | ||
| :ok | {:error, {:invalid_upto_payment, upto_validation_error()}} | ||
| defp validate_scheme(payload, requirements) do | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delegate specs missing new
invalid_formaterror typeMedium Severity
The
@specforvalidate_payment_signature/1anddecode_and_validate_payment_signature/1inlib/x402.exdon't include{:invalid_format, [{String.t(), atom()}]}as a possible error return. The underlyingPaymentSignature.validate/1now returns this new error type viacheck_field_formats/1, but the delegate specs still only list:invalid_payload,{:missing_fields, ...}, and{:invalid_upto_payment, ...}. Callers relying on these specs (or Dialyzer) won't know to handle the new error case.Additional Locations (1)
lib/x402.ex#L110-L118