feat(core): restrict payment requests to COIN SWAP
What changed, and why it matters
This commit tightens which types of signed payment requests a Trezor hardware wallet will accept. Previously, the code accepted any validly-signed payment request; now it rejects requests that are not specifically 'coin swap' requests (which must contain both a coin-purchase memo and a refund memo). It also adds a check that each memo has exactly one memo type. The change appears to be a defensive hardening measure rather than a fix for an active exploit, because the commit message and diff do not describe a specific vulnerability.
Treat as a hardening change. Review whether non-coin-swap payment requests were ever intended to be supported in production, and confirm the memo sanitization covers all memo fields. If this change is backported, ensure the debug bypass is not accidentally enabled in release builds.
Security signals we found
Input validation added for memo type exclusivity
Business-logic restriction added: only COIN SWAP payment requests accepted
Debug build bypasses supported-type check, preserving test behavior
No changelog entry suggests internal hardening or feature gating
Evidence from the diff
The patch modifies core/src/apps/common/payment_request.py. It introduces _sanitize_payment_request(), which validates that each PaymentRequestMemo has exactly one of text_memo, text_details_memo, refund_memo, or coin_purchase_memo set. It adds _is_coin_swap(), which returns true only if the request contains at least one coin_purchase_memo and one refund_memo. A new method verify_payment_request_is_supported() enforces that the request has memos and is a coin swap. In debug builds, the debug public key is still loaded, but verification of supported request types is bypassed via a lambda. The verifier now calls sanitization and the supported-type check before processing outputs.
Changed components
core/src/apps/common/payment_request.pyPaymentRequestVerifier classPayment request parsing and verification flowInspect captured patch +46 / −4
diff --git a/core/src/apps/common/payment_request.py b/core/src/apps/common/payment_request.py
index e4cc0b4ad..372aeca23 100644
--- a/core/src/apps/common/payment_request.py
+++ b/core/src/apps/common/payment_request.py
@@ -24,12 +24,47 @@ def parse_amount(payment_request: PaymentRequest) -> int:
return int.from_bytes(payment_request.amount, "little")
+def _sanitize_payment_request(payment_request: PaymentRequest) -> PaymentRequest:
+ for memo in payment_request.memos:
+ if (
+ memo.text_memo,
+ memo.text_details_memo,
+ memo.refund_memo,
+ memo.coin_purchase_memo,
+ ).count(None) != 3:
+ raise DataError(
+ "Exactly one memo type must be specified in each PaymentRequestMemo."
+ )
+ return payment_request
+
+
+def _is_coin_swap(payment_request: PaymentRequest) -> bool:
+ has_coin_purchase = any(m.coin_purchase_memo for m in payment_request.memos)
+ has_refund = any(m.refund_memo for m in payment_request.memos)
+
+ return has_coin_purchase and has_refund
+
+
class PaymentRequestVerifier:
+ PUBLIC_KEY = b""
+
+ def verify_payment_request_is_supported(
+ self, payment_request: PaymentRequest
+ ) -> None:
+ if not payment_request.memos:
+ raise DataError("Payment request must contain at least one memo.")
+
+ if not _is_coin_swap(payment_request):
+ raise DataError("Only COIN SWAP payment requests are supported.")
+
if __debug__:
- # nist256p1 public key of m/0h for "all all ... all" seed.
- PUBLIC_KEY = b"\x03\xd9\xd9\x3f\x89\xc6\x96\x3b\x94\xbb\xd7\xa5\x11\x88\x28\xe4\x4c\x1c\x39\x59\x15\xac\xe8\x48\x88\x71\x7f\x56\x8c\xb0\x19\x74\xc3"
- else:
- PUBLIC_KEY = b""
+
+ def _use_debug_key(self) -> None:
+ # nist256p1 public key of m/0h for "all all ... all" seed.
+ self.PUBLIC_KEY = b"\x03\xd9\xd9\x3f\x89\xc6\x96\x3b\x94\xbb\xd7\xa5\x11\x88\x28\xe4\x4c\x1c\x39\x59\x15\xac\xe8\x48\x88\x71\x7f\x56\x8c\xb0\x19\x74\xc3"
+
+ def _use_debug_verification(self) -> None:
+ self.verify_payment_request_is_supported = lambda payment_request: None
def __init__(
self,
@@ -48,6 +83,13 @@ class PaymentRequestVerifier:
from . import writers # pylint: disable=import-outside-toplevel
+ if __debug__:
+ self._use_debug_key()
+ self._use_debug_verification()
+
+ payment_request = _sanitize_payment_request(payment_request)
+ self.verify_payment_request_is_supported(payment_request)
+
self.h_outputs = HashWriter(sha256())
self.amount = 0
self.h_pr = HashWriter(sha256())
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.