feat(cardano): Show path, address params and longer payload chunk
What changed, and why it matters
This commit improves the Cardano message-signing feature on Trezor hardware wallets by showing users more details on the device screen before signing. It displays the signing path, the address parameters (if provided), and a larger chunk of the message payload when appropriate. It also tightens path validation by checking the path against the keychain and allowing minting paths in addition to public-key paths. These are defensive UI and validation hardening changes rather than fixes for an active vulnerability.
Treat as a routine hardening/feature refinement. Review the new confirm_message_path and show_message_header_credentials flows for consistency with other Cardano signing confirmations, and ensure the relaxed path policy (SCHEMA_MINT) is intentional and documented. No urgent security response is indicated.
Security signals we found
UI hardening: more data shown to user before signing
Path validation hardening: validate_path against keychain and allow mint paths
Address parameter confirmation added for message signing
No explicit vulnerability or CVE referenced in commit
Changelog references issue 3509
Evidence from the diff
The patch modifies core/src/apps/cardano/layout.py and core/src/apps/cardano/sign_message.py. In layout.py it adds a new max_displayed_size parameter to _get_data_chunk_props, raises the displayed payload limit to MAX_CHUNK_SIZE when not signing a hash, and adds helper functions to show message header credentials and the signing path. In sign_message.py it makes path validation asynchronous, calls paths.validate_path against the keychain, permits SCHEMA_MINT in addition to SCHEMA_PUBKEY, and reorders the flow so the payload is fetched and the address/headers are confirmed before signing. The changelog entries are moved from noissue.added to 3509.added.
Changed components
core/src/apps/cardano/layout.pycore/src/apps/cardano/sign_message.pycore/.changelog.d/3509.addedpython/.changelog.d/3509.addedInspect captured patch +91 / −32
diff --git a/core/.changelog.d/3509.added b/core/.changelog.d/3509.added
new file mode 100644
index 00000000..32c2f72d
--- /dev/null
+++ b/core/.changelog.d/3509.added
@@ -0,0 +1 @@
+Cardano: Add support for signing arbitrary messages
diff --git a/core/.changelog.d/noissue.added b/core/.changelog.d/noissue.added
deleted file mode 100644
index 32c2f72d..00000000
--- a/core/.changelog.d/noissue.added
+++ /dev/null
@@ -1 +0,0 @@
-Cardano: Add support for signing arbitrary messages
diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py
index 632245fc..a5ea5240 100644
--- a/core/src/apps/cardano/layout.py
+++ b/core/src/apps/cardano/layout.py
@@ -13,6 +13,7 @@ from trezor.ui import layouts
from trezor.ui.layouts import confirm_metadata, confirm_properties
from trezor.wire import ProcessError
+from apps.cardano.helpers.chunks import MAX_CHUNK_SIZE
from apps.common.paths import address_n_to_str
from . import addresses
@@ -26,7 +27,7 @@ from .helpers.utils import (
)
if TYPE_CHECKING:
- from typing import Literal
+ from typing import Callable, Literal
from trezor import messages
from trezor.enums import CardanoNativeScriptHashDisplayFormat
@@ -76,6 +77,8 @@ BRT_Other = ButtonRequestType.Other # global_import_cache
CVOTE_REWARD_ELIGIBILITY_WARNING = TR.cardano__reward_eligibility_warning
+_DEFAULT_MAX_DISPLAYED_CHUNK_SIZE = 56
+
def format_coin_amount(amount: int, network_id: int) -> str:
from .helpers import network_ids
@@ -329,6 +332,10 @@ async def confirm_message_payload(
) -> None:
props: list[PropertyType]
+ max_displayed_bytes = (
+ _DEFAULT_MAX_DISPLAYED_CHUNK_SIZE if is_signing_hash else MAX_CHUNK_SIZE
+ )
+
if not payload_first_chunk:
assert payload_size == 0
props = _get_data_chunk_props(
@@ -343,40 +350,46 @@ async def confirm_message_payload(
)
props = _get_data_chunk_props(
title="Message text",
- first_chunk=payload_first_chunk.decode("ascii"),
+ first_chunk=payload_first_chunk,
data_size=payload_size,
+ max_displayed_size=max_displayed_bytes,
+ decoder=lambda chunk: chunk.decode("ascii"),
)
else:
props = _get_data_chunk_props(
title="Message hex",
first_chunk=payload_first_chunk,
data_size=payload_size,
+ max_displayed_size=max_displayed_bytes,
)
props.append(("Message hash:", payload_hash))
await confirm_properties(
"confirm_message_payload",
- title="Confirm message hash" if is_signing_hash else "Confirm message",
+ title="Confirm message",
props=props,
br_code=BRT_Other,
)
def _get_data_chunk_props(
- title: str, first_chunk: bytes | str, data_size: int
+ title: str,
+ first_chunk: bytes,
+ data_size: int,
+ max_displayed_size: int = _DEFAULT_MAX_DISPLAYED_CHUNK_SIZE,
+ decoder: Callable[[bytes], bytes | str] | None = None,
) -> list[PropertyType]:
- MAX_DISPLAYED_SIZE = 56
- displayed_bytes = first_chunk[:MAX_DISPLAYED_SIZE]
+ displayed_bytes = first_chunk[:max_displayed_size]
bytes_optional_plural = "byte" if data_size == 1 else "bytes"
props: list[PropertyType] = [
(
f"{title} ({data_size} {bytes_optional_plural}):",
- displayed_bytes,
+ decoder(displayed_bytes) if decoder else displayed_bytes,
True,
)
]
- if data_size > MAX_DISPLAYED_SIZE:
+ if data_size > max_displayed_size:
props.append(("...", None, None))
return props
@@ -402,6 +415,12 @@ async def show_credentials(
await _show_credential(stake_credential, intro_text, purpose="address")
+async def show_message_header_credentials(credentials: list[Credential]) -> None:
+ intro_text = "Address"
+ for credential in credentials:
+ await _show_credential(credential, intro_text, purpose="message")
+
+
async def show_change_output_credentials(
payment_credential: Credential,
stake_credential: Credential,
@@ -446,13 +465,14 @@ async def show_cvote_registration_payment_credentials(
async def _show_credential(
credential: Credential,
intro_text: str,
- purpose: Literal["address", "output", "cvote_reg_payment_address"],
+ purpose: Literal["address", "output", "cvote_reg_payment_address", "message"],
extra_text: str | None = None,
) -> None:
title = {
"address": f"{ADDRESS_TYPE_NAMES[credential.address_type]} address",
"output": TR.cardano__confirm_transaction,
"cvote_reg_payment_address": TR.cardano__confirm_transaction,
+ "message": "Confirm message",
}[purpose]
props: list[PropertyType] = []
@@ -564,23 +584,35 @@ async def warn_unknown_total_collateral() -> None:
)
-async def confirm_witness_request(
- witness_path: list[int],
-) -> None:
+def _get_path_title(path: list[int]) -> str:
from . import seed
- if seed.is_multisig_path(witness_path):
- path_title = TR.cardano__multisig_path
- elif seed.is_minting_path(witness_path):
- path_title = TR.cardano__token_minting_path
+ if seed.is_multisig_path(path):
+ return TR.cardano__multisig_path
+ elif seed.is_minting_path(path):
+ return TR.cardano__token_minting_path
else:
- path_title = TR.cardano__path
+ return TR.cardano__path
+
+async def confirm_witness_request(
+ witness_path: list[int],
+) -> None:
await layouts.confirm_text(
"confirm_total",
TR.cardano__confirm_transaction,
address_n_to_str(witness_path),
- TR.cardano__sign_tx_path_template.format(path_title),
+ TR.cardano__sign_tx_path_template.format(_get_path_title(witness_path)),
+ BRT_Other,
+ )
+
+
+async def confirm_message_path(path: list[int], is_signing_hash: bool) -> None:
+ await layouts.confirm_text(
+ "confirm_message_signing_path",
+ "Confirm message",
+ address_n_to_str(path),
+ f"Sign message{' hash' if is_signing_hash else ''} with {_get_path_title(path)}:",
BRT_Other,
)
diff --git a/core/src/apps/cardano/sign_message.py b/core/src/apps/cardano/sign_message.py
index 91c829b8..edd00014 100644
--- a/core/src/apps/cardano/sign_message.py
+++ b/core/src/apps/cardano/sign_message.py
@@ -5,7 +5,8 @@ from trezor.wire import ProcessError
from trezor.wire.context import call as ctx_call
from apps.cardano.helpers.chunks import MAX_CHUNK_SIZE, ChunkIterator
-from apps.cardano.helpers.paths import SCHEMA_PUBKEY
+from apps.cardano.helpers.credential import Credential
+from apps.cardano.helpers.paths import SCHEMA_MINT, SCHEMA_PUBKEY
from apps.common import cbor
from . import addresses, seed
@@ -24,12 +25,20 @@ _COSE_HEADER_ALGORITHM_KEY = const(1)
_COSE_EDDSA_ALGORITHM_ID = const(-8)
-def _validate_message_signing_path(path: list[int]) -> None:
- if not SCHEMA_PUBKEY.match(path):
+async def _validate_message_signing_path(
+ path: list[int], keychain: seed.Keychain
+) -> None:
+ from apps.common import paths
+
+ await paths.validate_path(keychain, path)
+
+ if not SCHEMA_PUBKEY.match(path) and not SCHEMA_MINT.match(path):
raise ProcessError("Invalid signing path")
-def _validate_message_init(msg: CardanoSignMessageInit) -> None:
+async def _validate_message_init(
+ msg: CardanoSignMessageInit, keychain: seed.Keychain
+) -> None:
if msg.address_parameters:
if msg.network_id is None or msg.protocol_magic is None:
raise ProcessError(
@@ -40,14 +49,26 @@ def _validate_message_init(msg: CardanoSignMessageInit) -> None:
if msg.payload_size > MAX_CHUNK_SIZE and not msg.hash_payload:
raise ProcessError("Payload too long to sign without hashing")
- _validate_message_signing_path(msg.signing_path)
+ await _validate_message_signing_path(msg.signing_path, keychain)
-def _get_header_address(msg: CardanoSignMessageInit, keychain: seed.Keychain) -> bytes:
+async def _get_confirmed_header_address(
+ msg: CardanoSignMessageInit, keychain: seed.Keychain
+) -> bytes:
+ from . import layout
+
if msg.address_parameters:
assert (
msg.protocol_magic is not None and msg.network_id is not None
) # _validate_message_init
+
+ await layout.show_message_header_credentials(
+ [
+ Credential.payment_credential(msg.address_parameters),
+ Credential.stake_credential(msg.address_parameters),
+ ]
+ )
+
return addresses.derive_bytes(
keychain, msg.address_parameters, msg.protocol_magic, msg.network_id
)
@@ -134,19 +155,25 @@ async def sign_message(
) -> CardanoSignMessageFinished:
from trezor.messages import CardanoSignMessageFinished
- _validate_message_init(msg)
+ from . import layout
+
+ await _validate_message_init(msg, keychain)
- address = _get_header_address(msg, keychain)
+ payload = await _get_confirmed_payload(
+ size=msg.payload_size,
+ is_signing_hash=msg.hash_payload,
+ display_ascii=msg.display_ascii,
+ )
+
+ address = await _get_confirmed_header_address(msg, keychain)
headers: Headers = {
_COSE_HEADER_ALGORITHM_KEY: _COSE_EDDSA_ALGORITHM_ID,
_COSE_HEADER_ADDRESS_KEY: address,
}
- payload = await _get_confirmed_payload(
- size=msg.payload_size,
- is_signing_hash=msg.hash_payload,
- display_ascii=msg.display_ascii,
+ await layout.confirm_message_path(
+ path=msg.signing_path, is_signing_hash=msg.hash_payload
)
signature = _sign_sig_structure(
diff --git a/python/.changelog.d/3509.added b/python/.changelog.d/3509.added
new file mode 100644
index 00000000..32c2f72d
--- /dev/null
+++ b/python/.changelog.d/3509.added
@@ -0,0 +1 @@
+Cardano: Add support for signing arbitrary messages
diff --git a/python/.changelog.d/noissue.added b/python/.changelog.d/noissue.added
deleted file mode 100644
index 32c2f72d..00000000
--- a/python/.changelog.d/noissue.added
+++ /dev/null
@@ -1 +0,0 @@
-Cardano: Add support for signing arbitrary messages
Why this scored 28/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.