What changed, and why it matters
This commit adds a new on-screen prompt that asks users to confirm they really want to cancel a transaction-signing flow. It is a user-experience and safety feature, not a security fix. There is no evidence of a vulnerability being patched.
No security action required; treat as normal feature commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces a confirm_cancel() UI API across Trezor firmware layouts. For Delizia and Eckhart device models it now renders a dedicated cancellation confirmation screen (tap-to-cancel / cancel-and-go-back) before aborting a signing flow. Bolt and Caesar return NotImplementedError. Python layout code for Delizia’s confirm_value and Eckhart’s confirm_output now wires the menu’s Cancel entry to this new confirmation layout. A test input flow is updated to select the new Cancel menu index and synchronize at the new PromptScreen. The commit message and diff contain no security-relevant language or bug fix.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/src/trezor/ui/layouts/delizia/__init__.pycore/src/trezor/ui/layouts/eckhart/__init__.pytests/input_flows.pyInspect captured patch +91 / −13
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 487a1580..30304476 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -279,6 +279,7 @@ static void _librust_qstrs(void) {
MP_QSTR_coinjoin_authorized;
MP_QSTR_confirm_action;
MP_QSTR_confirm_address;
+ MP_QSTR_confirm_cancel;
MP_QSTR_confirm_coinjoin;
MP_QSTR_confirm_emphasized;
MP_QSTR_confirm_fido;
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 3c130481..0ee02479 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -1310,6 +1310,14 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
+extern "C" fn new_confirm_cancel(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
+ let block = |_args: &[Obj], _kwargs: &Map| {
+ let layout = ModelUI::confirm_cancel()?;
+ Ok(LayoutObj::new_root(layout)?.into())
+ };
+ unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
+}
+
extern "C" fn new_tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], _kwargs: &Map| {
let layout = ModelUI::tutorial()?;
@@ -2167,6 +2175,11 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// """Warning modal. Bolt: No buttons shown when `button` is empty string. Caesar: middle button and centered text."""
Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(),
+ /// def confirm_cancel() -> LayoutObj[UiResult]:
+ /// """Ask the user to confirm the cancellation (or cancel the cancellation and go back to
+ /// the previous flow)"""
+ Qstr::MP_QSTR_confirm_cancel => obj_fn_kw!(0, new_confirm_cancel).as_obj(),
+
/// def tutorial() -> LayoutObj[UiResult]:
/// """Show user how to interact with the device."""
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, new_tutorial).as_obj(),
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index 3522dc4a..291819bd 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -1305,6 +1305,10 @@ impl FirmwareUI for UIBolt {
)
}
+ fn confirm_cancel() -> Result<impl LayoutMaybeTrace, Error> {
+ Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
+ }
+
fn tutorial() -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
}
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index e35b7402..ca6e64b4 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1420,6 +1420,10 @@ impl FirmwareUI for UICaesar {
Ok(obj)
}
+ fn confirm_cancel() -> Result<impl LayoutMaybeTrace, Error> {
+ Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
+ }
+
fn tutorial() -> Result<impl LayoutMaybeTrace, Error> {
const PAGE_COUNT: usize = 7;
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index 9427ac6d..564be955 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -37,9 +37,10 @@ use crate::{
use super::{
component::{
check_homescreen_format, Bip39Input, CoinJoinProgress, Frame, FrameMsg, Homescreen,
- Lockscreen, MnemonicKeyboard, PinKeyboard, Progress, ScrolledVerticalMenu, SelectWordCount,
- SelectWordCountLayout, Slip39Input, StatusScreen, SwipeContent, SwipeUpScreen, TradeScreen,
- VerticalMenu, VerticalMenuChoiceMsg, VerticalMenuItem, VerticalMenuItems,
+ Lockscreen, MnemonicKeyboard, PinKeyboard, Progress, PromptScreen, ScrolledVerticalMenu,
+ SelectWordCount, SelectWordCountLayout, Slip39Input, StatusScreen, SwipeContent,
+ SwipeUpScreen, TradeScreen, VerticalMenu, VerticalMenuChoiceMsg, VerticalMenuItem,
+ VerticalMenuItems,
},
flow::{
self, new_confirm_action_simple, ConfirmActionExtra, ConfirmActionMenuStrings,
@@ -1316,6 +1317,17 @@ impl FirmwareUI for UIDelizia {
Ok(layout)
}
+ fn confirm_cancel() -> Result<impl LayoutMaybeTrace, Error> {
+ Ok(RootComponent::new(
+ Frame::left_aligned(
+ TR::send__cancel_sign.into(),
+ SwipeContent::new(PromptScreen::new_tap_to_cancel()),
+ )
+ .with_cancel_button()
+ .with_footer(TR::instructions__tap_to_confirm.into(), None),
+ ))
+ }
+
fn tutorial() -> Result<impl LayoutMaybeTrace, Error> {
let flow = flow::show_tutorial::new_show_tutorial()?;
Ok(flow)
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index 03189f8d..abcaecc4 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -1710,6 +1710,28 @@ impl FirmwareUI for UIEckhart {
Ok(layout)
}
+ fn confirm_cancel() -> Result<impl LayoutMaybeTrace, Error> {
+ flow::util::single_page(
+ TextScreen::new(
+ Paragraph::new(&theme::TEXT_REGULAR, TR::send__cancel_sign)
+ .into_paragraphs()
+ .with_placement(LinearPlacement::vertical()),
+ )
+ .with_header(Header::new(TR::words__send.into()))
+ .with_action_bar(ActionBar::new_double(
+ Button::with_icon(theme::ICON_CHEVRON_LEFT),
+ Button::with_text(TR::buttons__cancel.into())
+ .styled(theme::button_actionbar_danger())
+ .with_gradient(Gradient::Alert),
+ ))
+ .map(|msg| match msg {
+ TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
+ TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
+ _ => None,
+ }),
+ )
+ }
+
fn tutorial() -> Result<impl LayoutMaybeTrace, Error> {
let flow = flow::show_tutorial::new_show_tutorial()?;
Ok(flow)
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 471eb116..28a5506b 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -493,5 +493,7 @@ pub trait FirmwareUI {
danger: bool,
) -> Result<Gc<LayoutObj>, Error>; // TODO: return LayoutMaybeTrace
+ fn confirm_cancel() -> Result<impl LayoutMaybeTrace, Error>;
+
fn tutorial() -> Result<impl LayoutMaybeTrace, Error>;
}
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 967a405e..7dd9e25e 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -839,6 +839,12 @@ def show_warning(
"""Warning modal. Bolt: No buttons shown when `button` is empty string. Caesar: middle button and centered text."""
+# rust/src/ui/api/firmware_micropython.rs
+def confirm_cancel() -> LayoutObj[UiResult]:
+ """Ask the user to confirm the cancellation (or cancel the cancellation and go back to
+ the previous flow)"""
+
+
# rust/src/ui/api/firmware_micropython.rs
def tutorial() -> LayoutObj[UiResult]:
"""Show user how to interact with the device."""
diff --git a/core/src/trezor/ui/layouts/common.py b/core/src/trezor/ui/layouts/common.py
index e194db94..5ba2d2e2 100644
--- a/core/src/trezor/ui/layouts/common.py
+++ b/core/src/trezor/ui/layouts/common.py
@@ -135,7 +135,7 @@ async def with_info(
async def confirm_linear_flow(
- *confirm_factories: Callable[[], Awaitable[ui.UiResult]]
+ *confirm_factories: Callable[[], Awaitable[ui.UiResult]],
) -> None:
i = 0
while i < len(confirm_factories):
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 2135fa2c..d7c4e5c6 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -854,7 +854,7 @@ def confirm_value(
) -> Awaitable[ui.UiResult]:
"""General confirmation dialog, used by many other confirm_* functions."""
- from trezor.ui.layouts.menu import Menu, interact_with_menu
+ from trezor.ui.layouts.menu import Cancel, Menu, interact_with_menu
main = trezorui_api.confirm_value(
title=title,
@@ -876,7 +876,10 @@ def confirm_value(
menu_items.append(create_details(name, p, page_title))
menu = Menu.root(
menu_items,
- cancel=(cancel_text or TR.buttons__cancel),
+ cancel=Cancel.from_layout(
+ name=(cancel_text or TR.buttons__cancel),
+ layout_factory=trezorui_api.confirm_cancel,
+ ),
)
return interact_with_menu(main, menu, br_name, br_code)
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index 84e16e4f..deaf604a 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -582,7 +582,7 @@ async def confirm_output(
cancel_text: str | None = None,
description: str | None = None,
) -> None:
- from trezor.ui.layouts.menu import Menu, interact_with_menu
+ from trezor.ui.layouts.menu import Cancel, Menu, interact_with_menu
if address_label is not None:
title = address_label
@@ -619,7 +619,10 @@ async def confirm_output(
menu = Menu.root(
menu_items,
- cancel=TR.buttons__cancel,
+ cancel=Cancel.from_layout(
+ name=TR.buttons__cancel,
+ layout_factory=trezorui_api.confirm_cancel,
+ ),
)
address_layout = trezorui_api.confirm_value(
@@ -643,10 +646,18 @@ async def confirm_output(
back_button=True,
)
- await confirm_linear_flow(
- lambda: interact_with_menu(address_layout, menu, "confirm_output", br_code),
- lambda: interact_with_menu(amount_layout, menu, "confirm_output", br_code),
- )
+ try:
+ await confirm_linear_flow(
+ lambda: interact_with_menu(
+ address_layout, menu, "confirm_output", br_code
+ ),
+ lambda: interact_with_menu(
+ amount_layout, menu, "confirm_output", br_code
+ ),
+ )
+ except ActionCancelled:
+ show_continue_in_app(TR.send__sign_cancelled)
+ raise
else:
await raise_if_not_confirmed(
trezorui_api.flow_confirm_output(
diff --git a/tests/input_flows.py b/tests/input_flows.py
index 3f2bb880..b9b57b04 100644
--- a/tests/input_flows.py
+++ b/tests/input_flows.py
@@ -1280,7 +1280,7 @@ class InputFlowSignTxCancelFromAmount(InputFlowBase):
assert TR.words__recipient + " #1" in layout.title()
self.debug.click(self.debug.screen_buttons.menu())
- self.debug.button_actions.navigate_to_menu_item(1) # click Cancel
+ self.debug.button_actions.navigate_to_menu_item(0) # click Cancel
self.debug.synchronize_at("PromptScreen")
self.debug.click(self.debug.screen_buttons.tap_to_confirm())
Why this scored 15/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.