feat(clear_signing): support multi-value (array) fields
What changed, and why it matters
This commit adds support in Trezor's Ethereum 'clear signing' feature for displaying array (multi-value) fields on the device screen. It also fixes a minor display quirk where booleans were being treated as numbers. There is no direct evidence in the commit that this fixes an active security vulnerability; it reads as a feature improvement with some defensive hardening against malformed display definitions.
Review as a normal feature/robustness commit. If auditing, verify that `_format_field_value` correctly handles nested arrays, empty arrays, and `AboveThreshold` values, and that broadening `PathWalker` to return `AnyValue` does not allow malformed EIP-7730 definitions to bypass validation elsewhere. No urgent security response is indicated by the commit alone.
Security signals we found
Broadened path-walker return type and removed 'must not arrive at Array/Tuple' guard, which could change error-handling behavior for malformed field definitions.
Added explicit boolean handling in RawFormatter to avoid incorrect rendering under MicroPython.
New helper raises `InvalidFormatDefinition` if any array element formats to `None`, which is a defensive consistency check.
No explicit security relevance, CVE, or vulnerability description in commit message or changelog.
Evidence from the diff
The change introduces _format_field_value() in core/src/apps/ethereum/clear_signing.py. When a field path resolves to a list, each element is passed through the existing formatter and results are joined with newlines. The PathWalker return type is broadened from Value to AnyValue to permit lists, and the path-walking logic is adjusted so that arriving at a list at the end of a path is no longer an error. RawFormatter now explicitly handles bool before int because MicroPython does not treat bool as an int subclass. The changelog calls this an ‘improved clear signing support’ addition, not a security fix.
Changed components
core/src/apps/ethereum/clear_signing.pyEthereum clear-signing display formattingInspect captured patch +56 / −9
diff --git a/core/.changelog.d/6733.added b/core/.changelog.d/6733.added
new file mode 100644
index 00000000..813fbe4b
--- /dev/null
+++ b/core/.changelog.d/6733.added
@@ -0,0 +1 @@
+Ethereum: improved clear signing support.
diff --git a/core/src/apps/ethereum/clear_signing.py b/core/src/apps/ethereum/clear_signing.py
index b1fa8966..13fbd80c 100644
--- a/core/src/apps/ethereum/clear_signing.py
+++ b/core/src/apps/ethereum/clear_signing.py
@@ -31,13 +31,14 @@ if TYPE_CHECKING:
# Represents values that have been parsed from the calldata
# into our internal representation.
+ # TODO: Revisit simplifying this.
Value = int | bytes | bool | str | None | list["Value"]
TupleValue = tuple[Value, ...]
ListValue = list[TupleValue]
- AnyValue = Value | TupleValue | ListValue | list[Value | TupleValue | ListValue]
+ AnyValue = Value | TupleValue | list["AnyValue"]
Path = tuple[int | tuple[int] | tuple[int, int], ...] | int
- PathWalker = Callable[[Path], Value]
+ PathWalker = Callable[[Path], AnyValue]
# Parses a Value from a slice of the calldata.
# Assumes that the memoryview contains just that value.
@@ -432,8 +433,9 @@ class RawFormatter(FieldFormatter):
from ubinascii import hexlify
return hexlify(value).decode(), None, None
+ elif isinstance(value, bool):
+ return str(value), None, None
elif isinstance(value, int):
- # bool is an int subclass; rendered as "True"/"False".
return str(value), None, None
else:
raise InvalidFormatDefinition
@@ -460,6 +462,51 @@ class DateFormatter(FieldFormatter):
raise InvalidFormatDefinition
+async def _format_field_value(
+ formatter: FieldFormatter,
+ value: AnyValue,
+ msg: MsgInSignTx,
+ defs: Definitions,
+ path_walker: PathWalker,
+) -> tuple[str | AboveThreshold | None, EthereumTokenInfo | None, AnyBytes | None]:
+ """Format a field value.
+
+ When the field's path resolves to an array (a `list`), the formatter is
+ applied to each element and the rendered values are joined with newlines,
+ so the field is shown as one value per line - eg. an `amount.[]` field over
+ `[1, 2]` renders as "1 token\n2 token". This works for any formatter pointed
+ at an array (amount, address, raw, ...). A non-list value is formatted
+ directly.
+
+ Only flat arrays of formattable leaves are handled."""
+ if not isinstance(value, list):
+ return await formatter.format(value, msg, defs, path_walker)
+
+ from trezor.ui.layouts.properties import AboveThreshold
+
+ lines: list[str] = []
+ # The same formatter instance is reused for every element, so a
+ # `tokenAmount`'s single `token_path` resolves to the same token on each
+ # iteration: the token is shared across the array and returned once.
+ token: EthereumTokenInfo | None = None
+ token_address: AnyBytes | None = None
+ for element in value:
+ formatted, element_token, element_address = await formatter.format(
+ element, msg, defs, path_walker
+ )
+ if isinstance(formatted, AboveThreshold):
+ formatted = formatted.message
+ if formatted is None:
+ # Raise if any member returns None.
+ raise InvalidFormatDefinition
+ lines.append(formatted)
+ if element_token is not None:
+ token = element_token
+ if element_address is not None:
+ token_address = element_address
+ return "\n".join(lines), token, token_address
+
+
# https://eips.ethereum.org/EIPS/eip-7730#context-section
@@ -787,7 +834,7 @@ class DisplayFormat:
parameters.append(value)
offset += consumed
- def get_value_for_path(path: Path) -> Value:
+ def get_value_for_path(path: Path) -> AnyValue:
if isinstance(path, int): # ContainerPath
# standard container paths like @.from, @.value...
if path == ContainerPath.From:
@@ -828,9 +875,8 @@ class DisplayFormat:
else:
# can't walk inside basic types
raise InvalidFormatDefinition
- if isinstance(p, (list, tuple)):
- # at the end of the path, we must have arrived somewhere
- # ie. not on an Array or Tuple
+ if isinstance(p, tuple):
+ # Array/list makes sense. Not expecting tuples here.
raise InvalidFormatDefinition
return p
@@ -845,8 +891,8 @@ class DisplayFormat:
try:
value = get_value_for_path(field_definition.path)
formatter = field_definition.get_formatter()
- formatted, token, token_address = await formatter.format(
- value, msg, defs, get_value_for_path
+ formatted, token, token_address = await _format_field_value(
+ formatter, value, msg, defs, get_value_for_path
)
except Exception as e:
if __debug__:
Why this scored 20/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.