chore(ethereum): clear sign only first data chunk
What changed, and why it matters
This commit changes how Trezor hardware wallets display and confirm Ethereum transactions that use 'clear signing' (human-readable descriptions of smart-contract calls). Previously, clear signing could be attempted for transactions whose data is split across multiple chunks. Now, clear signing is used only when the entire transaction data fits in the first chunk. If the clear-signing parser fails on that first chunk, the device falls back to the standard raw-data confirmation. The change is described as a feature limitation, not a security fix, but it removes a code path where multi-chunk data might be parsed before all chunks are available or confirmed.
Review whether the prior multi-chunk clear-signing path could have led to incomplete or misleading transaction summaries before all data chunks were received and confirmed. If a security issue existed, request a CVE and a changelog/security advisory from the vendor. Otherwise, treat this as a defensive hardening change and ensure tests cover multi-chunk transactions and the fallback path.
Security signals we found
Clear-signing parser now restricted to first data chunk only
Multi-chunk calldata no longer eligible for clear signing
Fallback to raw data confirmation on clear-signing parse failure
Digest computation begins before data chunk confirmation
No changelog entry; commit framed as feature limitation
Evidence from the diff
The patch modifies core/src/apps/ethereum/clear_signing.py, sign_tx.py, and sign_tx_eip1559.py. It adds an early return in get_approver() when msg.data_length exceeds len(msg.data_initial_chunk), disabling clear signing for any transaction whose calldata spans more than one message chunk. It also restructures sign_tx and sign_tx_eip1559 so that the initial data chunk is confirmed after the digest writer is initialized, and so that a clear-signing summary is awaited immediately for single-chunk transactions. If the clear-signing summary raises InvalidFunctionCall or InvalidFormatDefinition, the code re-runs confirm_tx_data with try_clear_signing=False and confirms the raw initial chunk again. The commit message is a chore with [no changelog] and does not claim security relevance.
Changed components
core/src/apps/ethereum/clear_signing.pycore/src/apps/ethereum/sign_tx.pycore/src/apps/ethereum/sign_tx_eip1559.pyTrezor Ethereum transaction signing flowTrezor clear-signing / human-readable transaction confirmationInspect captured patch +98 / −36
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index 1b5d2f8e..ee6eb87a 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -1,6 +1,5 @@
from micropython import const
from typing import TYPE_CHECKING
-from ubinascii import hexlify
from trezor import TR
from trezor.utils import BufferReader
@@ -639,6 +638,10 @@ def get_approver(
if not address_bytes:
return None
+ if msg.data_length > len(msg.data_initial_chunk):
+ # we only support clear signing one chunk for now
+ return None
+
if msg.data_length > MAX_CALLDATA_STORED:
# skip clear signing if the calldata is longer than what we can process
return None
diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py
index 6cf57b3b..819f1e2c 100644
--- a/core/src/apps/ethereum/sign_tx.py
+++ b/core/src/apps/ethereum/sign_tx.py
@@ -61,6 +61,7 @@ async def sign_tx(
from apps.common import paths, safety_checks
+ from .clear_signing import InvalidFormatDefinition, InvalidFunctionCall
from .helpers import format_ethereum_amount, get_fee_items_regular
# local_cache_attribute
@@ -106,18 +107,6 @@ async def sign_tx(
amount_size_bytes=32,
)
- # data chunks will be confirmed during digest (see below)
- # tx summary will confirmed before signing the digest (see below)
- confirm_data_chunk, confirm_summary = await confirm_tx_data(
- msg,
- defs,
- tx_type,
- address_bytes,
- maximum_fee,
- fee_items,
- payment_req_verifier,
- )
-
# digest
total_length = _get_total_length(msg, data_total)
@@ -130,8 +119,45 @@ async def sign_tx(
for field in (msg.nonce, msg.gas_price, msg.gas_limit, address_bytes, msg.value):
rlp.write(sha, field)
+ confirm_data_chunk, confirm_summary = await confirm_tx_data(
+ msg,
+ defs,
+ tx_type,
+ address_bytes,
+ maximum_fee,
+ fee_items,
+ payment_req_verifier,
+ try_clear_signing=True,
+ )
+
await confirm_data_chunk(msg.data_initial_chunk)
+
data_left = data_total - len(msg.data_initial_chunk)
+
+ summary_confirmed = False
+ if data_left == 0: # Note: clear signing only works with the 1st chunk for now
+ try:
+ await confirm_summary
+ summary_confirmed = True
+ except (InvalidFunctionCall, InvalidFormatDefinition):
+ # parsing calldata by the clear signer failed,
+ # retry without clear signing
+ confirm_data_chunk, confirm_summary = await confirm_tx_data(
+ msg,
+ defs,
+ tx_type,
+ address_bytes,
+ maximum_fee,
+ fee_items,
+ payment_req_verifier,
+ try_clear_signing=False,
+ )
+
+ # we can safely assume that the initial data chunk was not confirmed
+ # because we are currently handling clear signer's exception
+ # so let's finally confirm the initial data chunk!
+ await confirm_data_chunk(msg.data_initial_chunk)
+
rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, msg.data_initial_chunk)
sha.extend(msg.data_initial_chunk)
@@ -148,8 +174,8 @@ async def sign_tx(
digest = sha.get_digest()
- # show tx summary and confirm
- await confirm_summary
+ if not summary_confirmed:
+ await confirm_summary
# transaction data confirmed, proceed with signing
result = _sign_digest(msg, keychain, digest)
@@ -166,8 +192,10 @@ async def confirm_tx_data(
maximum_fee: str,
fee_items: Iterable[StrPropertyType],
payment_request_verifier: PaymentRequestVerifier | None,
+ try_clear_signing: bool,
) -> tuple[ConfirmDataFn, Coroutine[Any, Any, None]]:
"""Returns data chunk callback and transaction summary layout to be awaited."""
+
from trezor.ui.layouts import confirm_value
from . import clear_signing, staking
@@ -198,17 +226,18 @@ async def confirm_tx_data(
value = int.from_bytes(msg.value, "big")
- clear_signing_approver = clear_signing.get_approver(
- msg,
- defs,
- address_bytes,
- value,
- maximum_fee,
- fee_items,
- payment_request_verifier,
- )
- if clear_signing_approver is not None:
- return clear_signing_approver
+ if try_clear_signing:
+ clear_signing_approver = clear_signing.get_approver(
+ msg,
+ defs,
+ address_bytes,
+ value,
+ maximum_fee,
+ fee_items,
+ payment_request_verifier,
+ )
+ if clear_signing_approver is not None:
+ return clear_signing_approver
recipient_str = (
address_from_bytes(address_bytes, network) if address_bytes else None
diff --git a/core/src/apps/ethereum/sign_tx_eip1559.py b/core/src/apps/ethereum/sign_tx_eip1559.py
index 5484c0a5..e6485eb7 100644
--- a/core/src/apps/ethereum/sign_tx_eip1559.py
+++ b/core/src/apps/ethereum/sign_tx_eip1559.py
@@ -43,6 +43,7 @@ async def sign_tx_eip1559(
from apps.common import paths
+ from .clear_signing import InvalidFormatDefinition, InvalidFunctionCall
from .helpers import format_ethereum_amount, get_fee_items_eip1559
from .sign_tx import check_common_fields, confirm_tx_data, send_request_chunk
@@ -80,9 +81,17 @@ async def sign_tx_eip1559(
msg.payment_req, slip44_id, keychain, amount_size_bytes=32
)
+ # digest
+ total_length = _get_total_length(msg, data_total)
+
+ sha = HashWriter(sha3_256(keccak=True))
+
+ rlp.write(sha, _TX_TYPE)
+ rlp.write_header(sha, total_length, rlp.LIST_HEADER_BYTE)
+
# data chunks will be confirmed during digest (see below)
# tx summary will approved before signing the digest (see below)
- confirm_data_chunk, approve_summary = await confirm_tx_data(
+ confirm_data_chunk, confirm_summary = await confirm_tx_data(
msg,
defs,
None,
@@ -90,16 +99,36 @@ async def sign_tx_eip1559(
maximum_fee,
fee_items,
payment_req_verifier,
+ try_clear_signing=True,
)
- # digest
- total_length = _get_total_length(msg, data_total)
-
- sha = HashWriter(sha3_256(keccak=True))
+ await confirm_data_chunk(msg.data_initial_chunk)
- rlp.write(sha, _TX_TYPE)
+ data_left = data_total - len(msg.data_initial_chunk)
- rlp.write_header(sha, total_length, rlp.LIST_HEADER_BYTE)
+ summary_confirmed = False
+ if data_left == 0:
+ try:
+ await confirm_summary
+ summary_confirmed = True
+ except (InvalidFunctionCall, InvalidFormatDefinition):
+ # parsing calldata by the clear signer failed,
+ # retry without clear signing
+ confirm_data_chunk, confirm_summary = await confirm_tx_data(
+ msg,
+ defs,
+ None,
+ address_bytes,
+ maximum_fee,
+ fee_items,
+ payment_req_verifier,
+ try_clear_signing=False,
+ )
+
+ # we can safely assume that the initial data chunk was not confirmed
+ # because we are currently handling clear signer's exception
+ # so let's finally confirm the initial data chunk!
+ await confirm_data_chunk(msg.data_initial_chunk)
fields: tuple[rlp.RLPItem, ...] = (
msg.chain_id,
@@ -113,8 +142,6 @@ async def sign_tx_eip1559(
for field in fields:
rlp.write(sha, field)
- await confirm_data_chunk(msg.data_initial_chunk)
- data_left = data_total - len(msg.data_initial_chunk)
rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, msg.data_initial_chunk)
sha.extend(msg.data_initial_chunk)
@@ -136,7 +163,10 @@ async def sign_tx_eip1559(
rlp.write(sha, item.storage_keys)
digest = sha.get_digest()
- await approve_summary
+
+ if not summary_confirmed:
+ await confirm_summary
+
# transaction data confirmed, proceed with signing
result = _sign_digest(msg, keychain, digest)
Why this scored 40/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.