What changed, and why it matters
This commit adds a new 'Regulatory certification' screen to the Trezor hardware wallet's user interface. It is purely a UI feature that displays certification marks (FCC, CE, RCM, etc.) and regional compliance information. There is no security-relevant code change, no handling of secrets, and no network or cryptographic logic.
No security action required. This is a normal product/UI feature addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces a new RegulatoryScreen component in the Eckhart layout, wired into the device menu. It adds a new translation key, new icon assets, and integrates the screen into the existing menu navigation. The code only renders static text and images and does not process user input beyond standard navigation/back events.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/regulatory_screen.rscore/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/embed/rust/src/ui/layout_eckhart/firmware/mod.rscore/embed/rust/src/ui/layout_eckhart/theme/mod.rscore/translations/en.jsoncore/translations/order.jsoncore/translations/signatures.jsonInspect captured patch +299 / −7
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 973c7be2..20ec0bec 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -562,6 +562,7 @@ static void _librust_qstrs(void) {
MP_QSTR_recovery__x_of_y_entered_template;
MP_QSTR_recovery__you_have_entered;
MP_QSTR_recovery_type;
+ MP_QSTR_regulatory_certification__title;
MP_QSTR_reject_pairing;
MP_QSTR_remaining_shares;
MP_QSTR_request_bip39;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index 0cbe3029..5eca7f7a 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1466,6 +1466,7 @@ pub enum TranslatedString {
device_name__enter = 1071, // "Enter device name"
words__name = 1072, // "Name"
device_name__continue_with_empty_label = 1073, // "Continue with empty device name?"
+ regulatory_certification__title = 1074, // "Regulatory certification"
}
impl TranslatedString {
@@ -3251,6 +3252,7 @@ impl TranslatedString {
(Self::device_name__enter, "Enter device name"),
(Self::words__name, "Name"),
(Self::device_name__continue_with_empty_label, "Continue with empty device name?"),
+ (Self::regulatory_certification__title, "Regulatory certification"),
];
#[cfg(feature = "micropython")]
@@ -4191,6 +4193,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_recovery__x_more_shares_needed_template_plural, Self::recovery__x_more_shares_needed_template_plural),
(Qstr::MP_QSTR_recovery__x_of_y_entered_template, Self::recovery__x_of_y_entered_template),
(Qstr::MP_QSTR_recovery__you_have_entered, Self::recovery__you_have_entered),
+ (Qstr::MP_QSTR_regulatory_certification__title, Self::regulatory_certification__title),
(Qstr::MP_QSTR_reset__advanced_group_threshold_info, Self::reset__advanced_group_threshold_info),
(Qstr::MP_QSTR_reset__all_x_of_y_template, Self::reset__all_x_of_y_template),
(Qstr::MP_QSTR_reset__any_x_of_y_template, Self::reset__any_x_of_y_template),
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
index bca5e6ac..09ff624f 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
@@ -24,8 +24,8 @@ use super::{
component::{Button, ButtonStyleSheet, FuelGauge},
constant::SCREEN,
firmware::{
- Header, HeaderMsg, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
- VerticalMenuScreenMsg, SHORT_MENU_ITEMS,
+ Header, HeaderMsg, RegulatoryMsg, RegulatoryScreen, TextScreen, TextScreenMsg,
+ VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, SHORT_MENU_ITEMS,
},
},
theme, ShortMenuVec,
@@ -134,6 +134,8 @@ enum Subscreen {
// The about screen
AboutScreen,
+ // A screen showing the regulatory information
+ RegulatoryScreen,
}
// Used to preallocate memory for the largest enum variant
@@ -141,6 +143,7 @@ enum Subscreen {
enum ActiveScreen<'a> {
Menu(VerticalMenuScreen<ShortMenuVec>),
About(TextScreen<Paragraphs<[Paragraph<'a>; 2]>>),
+ Regulatory(RegulatoryScreen),
// used only during `DeviceMenuScreen::new`
Empty,
@@ -183,8 +186,9 @@ impl<'a> DeviceMenuScreen<'a> {
};
let about = screen.add_subscreen(Subscreen::AboutScreen);
+ let regulatory = screen.add_subscreen(Subscreen::RegulatoryScreen);
let security = screen.add_security_menu();
- let device = screen.add_device_menu(device_name, about, auto_lock_delay);
+ let device = screen.add_device_menu(device_name, regulatory, about, auto_lock_delay);
let settings = screen.add_settings_menu(security, device);
let is_connected = !paired_devices.is_empty(); // FIXME after BLE API has this
@@ -284,6 +288,7 @@ impl<'a> DeviceMenuScreen<'a> {
fn add_device_menu(
&mut self,
device_name: Option<TString<'static>>,
+ regulatory_index: usize,
about_index: usize,
auto_lock_delay: TString<'static>,
) -> usize {
@@ -311,6 +316,11 @@ impl<'a> DeviceMenuScreen<'a> {
unwrap!(items.push(autolock_delay_item));
}
+ unwrap!(items.push(MenuItem::new(
+ TR::regulatory_certification__title.into(),
+ Some(Action::GoTo(regulatory_index))
+ )));
+
unwrap!(items.push(MenuItem::new(
"About".into(),
Some(Action::GoTo(about_index))
@@ -431,6 +441,9 @@ impl<'a> DeviceMenuScreen<'a> {
.with_header(Header::new("About".into()).with_close_button()),
);
}
+ Subscreen::RegulatoryScreen => {
+ *self.active_screen.deref_mut() = ActiveScreen::Regulatory(RegulatoryScreen::new());
+ }
}
}
@@ -490,6 +503,9 @@ impl<'a> Component for DeviceMenuScreen<'a> {
ActiveScreen::About(about) => {
about.place(bounds);
}
+ ActiveScreen::Regulatory(regulatory) => {
+ regulatory.place(bounds);
+ }
ActiveScreen::Empty => {}
};
@@ -525,6 +541,11 @@ impl<'a> Component for DeviceMenuScreen<'a> {
return self.go_back(ctx);
}
}
+ (Subscreen::RegulatoryScreen, ActiveScreen::Regulatory(regulatory)) => {
+ if let Some(RegulatoryMsg::Cancelled) = regulatory.event(ctx, event) {
+ return self.go_back(ctx);
+ }
+ }
_ => {}
}
@@ -535,6 +556,7 @@ impl<'a> Component for DeviceMenuScreen<'a> {
match self.active_screen.deref() {
ActiveScreen::Menu(menu) => menu.render(target),
ActiveScreen::About(about) => about.render(target),
+ ActiveScreen::Regulatory(regulatory) => regulatory.render(target),
ActiveScreen::Empty => {}
};
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs
index e56ee4c1..78972cf1 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs
@@ -12,6 +12,7 @@ mod homescreen;
mod keyboard;
mod progress_screen;
mod qr_screen;
+mod regulatory_screen;
mod select_word_screen;
mod share_words;
mod text_screen;
@@ -41,6 +42,7 @@ pub use keyboard::{
};
pub use progress_screen::ProgressScreen;
pub use qr_screen::{QrMsg, QrScreen};
+pub use regulatory_screen::{RegulatoryMsg, RegulatoryScreen};
pub use select_word_screen::{SelectWordMsg, SelectWordScreen};
pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg};
pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg};
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/regulatory_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/regulatory_screen.rs
new file mode 100644
index 00000000..938ef2da
--- /dev/null
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/regulatory_screen.rs
@@ -0,0 +1,254 @@
+use crate::{
+ translations::TR,
+ ui::{
+ component::{
+ swipe_detect::SwipeConfig,
+ text::{
+ layout::{LayoutFit, TextLayout},
+ TextStyle,
+ },
+ Component, Event, EventCtx, Never, Paginate,
+ },
+ display::Icon,
+ flow::Swipable,
+ geometry::{Alignment, Alignment2D, Direction, Rect},
+ shape::{Renderer, ToifImage},
+ util::Pager,
+ },
+};
+
+use super::super::{
+ constant::SCREEN,
+ firmware::{ActionBar, ActionBarMsg, Header, HeaderMsg},
+ theme,
+};
+
+/// Full-screen component for rendering Regulatory certification.
+pub struct RegulatoryScreen {
+ header: Header,
+ content: RegulatoryContent,
+ action_bar: ActionBar,
+}
+
+pub enum RegulatoryMsg {
+ Cancelled,
+}
+
+impl RegulatoryScreen {
+ pub fn new() -> Self {
+ let content = RegulatoryContent::new();
+ let mut action_bar = ActionBar::new_paginate_only();
+ // Set action bar page counter
+ action_bar.update(content.pager());
+
+ Self {
+ header: Header::new(TR::regulatory_certification__title.into()).with_close_button(),
+ content,
+ action_bar,
+ }
+ }
+
+ fn on_page_change(&mut self, direction: Direction) {
+ // Update page based on the direction
+
+ match direction {
+ Direction::Up => {
+ self.content.change_page(self.content.pager().next());
+ }
+ Direction::Down => {
+ self.content.change_page(self.content.pager().prev());
+ }
+ _ => {}
+ }
+
+ // Update action bar content based on the current page
+ self.action_bar.update(self.content.pager());
+ }
+}
+
+impl Swipable for RegulatoryScreen {
+ fn get_pager(&self) -> Pager {
+ self.content.pager()
+ }
+ fn get_swipe_config(&self) -> SwipeConfig {
+ SwipeConfig::default()
+ }
+}
+
+impl Component for RegulatoryScreen {
+ type Msg = RegulatoryMsg;
+
+ fn place(&mut self, bounds: Rect) -> Rect {
+ // assert full screen
+ debug_assert_eq!(bounds.height(), SCREEN.height());
+ debug_assert_eq!(bounds.width(), SCREEN.width());
+
+ let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT);
+ let (mut content_area, action_bar_area) = rest.split_bottom(ActionBar::ACTION_BAR_HEIGHT);
+
+ content_area = content_area.inset(theme::SIDE_INSETS);
+
+ self.header.place(header_area);
+ self.content.place(content_area);
+ self.action_bar.place(action_bar_area);
+
+ bounds
+ }
+
+ fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ if let Some(HeaderMsg::Cancelled) = self.header.event(ctx, event) {
+ return Some(RegulatoryMsg::Cancelled);
+ }
+
+ if let Some(msg) = self.action_bar.event(ctx, event) {
+ match msg {
+ ActionBarMsg::Prev => {
+ self.on_page_change(Direction::Down);
+ }
+ ActionBarMsg::Next => {
+ self.on_page_change(Direction::Up);
+ }
+ _ => {}
+ }
+ }
+
+ None
+ }
+
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
+ self.header.render(target);
+ self.content.render(target);
+ self.action_bar.render(target);
+ }
+}
+
+#[cfg(feature = "ui_debug")]
+impl crate::trace::Trace for RegulatoryScreen {
+ fn trace(&self, t: &mut dyn crate::trace::Tracer) {
+ t.component("RegulatoryScreen");
+ t.child("Header", &self.header);
+ t.child("Content", &self.content);
+ t.child("ActionBar", &self.action_bar);
+ }
+}
+
+/// Component showing regulatory certification information for several
+/// geographical zones.
+struct RegulatoryContent {
+ area: Rect,
+ pager: Pager,
+}
+
+impl RegulatoryContent {
+ const ZONES_NUM: usize = 6;
+ const ZONES: [(&'static str, &'static str, Option<Icon>); RegulatoryContent::ZONES_NUM] = [
+ ("United States", "FCC ID: VUI-TS7A01", Some(theme::ICON_FCC)),
+ ("Canada", "IC: 7582A-TS7A01", None),
+ (
+ "Europe / UK",
+ "Trezor Company s.r.o.\nKundratka 2359/17a\nPrague 8, Czech Republic",
+ Some(theme::ICON_EUROPE),
+ ),
+ ("Australia /\nNew Zealand", "", Some(theme::ICON_RCM)),
+ ("Ukraine", "", Some(theme::ICON_UKRAINE)),
+ ("South Korea", "", Some(theme::ICON_KOREA)),
+ ];
+
+ pub fn new() -> Self {
+ let pager = Pager::new(Self::ZONES_NUM as u16);
+ Self {
+ area: Rect::zero(),
+ pager,
+ }
+ }
+
+ fn render_from_top<'s>(
+ &self,
+ area: Rect,
+ style: TextStyle,
+ align: Alignment,
+ text: &str,
+ spacing_after: i16,
+ target: &mut impl Renderer<'s>,
+ ) -> Rect {
+ // Do not render empty text
+ if text.is_empty() {
+ return area;
+ }
+ // Render if it fits in the area and return the rest of the area
+ let fitting = TextLayout::new(style)
+ .with_align(align)
+ .with_bounds(area)
+ .fit_text(text);
+ match fitting {
+ LayoutFit::Fitting { height, .. } => {
+ let (top, rest) = area.split_top(height + spacing_after);
+ TextLayout::new(style)
+ .with_align(align)
+ .with_bounds(top)
+ .render_text(text, target, true);
+ rest
+ }
+ // Text that does not fit in the area is not rendered
+ _ => area,
+ }
+ }
+}
+
+impl Paginate for RegulatoryContent {
+ fn pager(&self) -> Pager {
+ self.pager
+ }
+
+ fn change_page(&mut self, to_page: u16) {
+ self.pager.set_current(to_page);
+ }
+}
+
+impl Component for RegulatoryContent {
+ type Msg = Never;
+
+ fn place(&mut self, bounds: Rect) -> Rect {
+ self.area = bounds;
+ bounds
+ }
+
+ fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
+ None
+ }
+
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
+ let (title, content, icon) = Self::ZONES[self.pager.current() as usize];
+ let rest = self.render_from_top(
+ self.area,
+ theme::TEXT_REGULAR,
+ Alignment::Start,
+ title,
+ theme::TEXT_VERTICAL_SPACING,
+ target,
+ );
+
+ let rest = self.render_from_top(
+ rest,
+ theme::TEXT_MEDIUM,
+ Alignment::Start,
+ content,
+ theme::TEXT_VERTICAL_SPACING,
+ target,
+ );
+
+ if let Some(icon) = icon {
+ ToifImage::new(rest.top_left(), icon.toif)
+ .with_align(Alignment2D::TOP_LEFT)
+ .with_fg(theme::GREY_LIGHT)
+ .render(target);
+ }
+ }
+}
+
+#[cfg(feature = "ui_debug")]
+impl crate::trace::Trace for RegulatoryContent {
+ fn trace(&self, t: &mut dyn crate::trace::Tracer) {
+ t.component("RegulatoryContent");
+ }
+}
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/europe.png b/core/embed/rust/src/ui/layout_eckhart/res/europe.png
new file mode 100644
index 00000000..183c7be8
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/europe.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/europe.toif b/core/embed/rust/src/ui/layout_eckhart/res/europe.toif
new file mode 100644
index 00000000..a0390ace
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/europe.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/fcc.png b/core/embed/rust/src/ui/layout_eckhart/res/fcc.png
new file mode 100644
index 00000000..ebecacbc
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/fcc.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/fcc.toif b/core/embed/rust/src/ui/layout_eckhart/res/fcc.toif
new file mode 100644
index 00000000..dc1fdde6
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/fcc.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/korea.png b/core/embed/rust/src/ui/layout_eckhart/res/korea.png
new file mode 100644
index 00000000..f269ad24
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/korea.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/korea.toif b/core/embed/rust/src/ui/layout_eckhart/res/korea.toif
new file mode 100644
index 00000000..a19ef732
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/korea.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/korea_full.png b/core/embed/rust/src/ui/layout_eckhart/res/korea_full.png
new file mode 100644
index 00000000..c56f7195
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/korea_full.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/korea_full.toif b/core/embed/rust/src/ui/layout_eckhart/res/korea_full.toif
new file mode 100644
index 00000000..4960e562
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/korea_full.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/rcm.png b/core/embed/rust/src/ui/layout_eckhart/res/rcm.png
new file mode 100644
index 00000000..2bbdc298
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/rcm.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/rcm.toif b/core/embed/rust/src/ui/layout_eckhart/res/rcm.toif
new file mode 100644
index 00000000..158ad92f
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/rcm.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/ukraine.png b/core/embed/rust/src/ui/layout_eckhart/res/ukraine.png
new file mode 100644
index 00000000..fe7025b5
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/ukraine.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/ukraine.toif b/core/embed/rust/src/ui/layout_eckhart/res/ukraine.toif
new file mode 100644
index 00000000..fddf3f33
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/ukraine.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
index 8752a9cb..d34e7fd3 100644
--- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
@@ -161,6 +161,13 @@ include_icon!(ICON_SEVEN, "layout_eckhart/res/bootloader/7.toif");
include_icon!(ICON_TROPIC, "layout_eckhart/res/tropic.toif");
include_icon!(ICON_SECURED, "layout_eckhart/res/secured.toif");
+// Regulatory screen icons
+include_icon!(ICON_UKRAINE, "layout_eckhart/res/ukraine.toif");
+include_icon!(ICON_KOREA, "layout_eckhart/res/korea_full.toif");
+include_icon!(ICON_EUROPE, "layout_eckhart/res/europe.toif");
+include_icon!(ICON_RCM, "layout_eckhart/res/rcm.toif");
+include_icon!(ICON_FCC, "layout_eckhart/res/fcc.toif");
+
// Common text styles and button styles must use fonts accessible from both
// bootloader and firmware
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index 3bb4c571..17a814a0 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -639,6 +639,7 @@ class TR:
recovery__x_more_shares_needed_template_plural: str = "{count} more {plural} needed"
recovery__x_of_y_entered_template: str = "{0} of {1} shares entered"
recovery__you_have_entered: str = "You have entered"
+ regulatory_certification__title: str = "Regulatory certification"
reset__advanced_group_threshold_info: str = "The group threshold specifies the number of groups required to recover your wallet."
reset__all_x_of_y_template: str = "all {0} of {1} shares"
reset__any_x_of_y_template: str = "any {0} of {1} shares"
diff --git a/core/translations/en.json b/core/translations/en.json
index 91d33029..d3b1b0ef 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -811,6 +811,7 @@
"Eckhart": "{0} of {1} shares entered."
},
"recovery__you_have_entered": "You have entered",
+ "regulatory_certification__title": "Regulatory certification",
"reset__advanced_group_threshold_info": "The group threshold specifies the number of groups required to recover your wallet.",
"reset__all_x_of_y_template": "all {0} of {1} shares",
"reset__any_x_of_y_template": "any {0} of {1} shares",
diff --git a/core/translations/order.json b/core/translations/order.json
index 012925ee..2190280b 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1072,5 +1072,6 @@
"1070": "tutorial__tropic_info",
"1071": "device_name__enter",
"1072": "words__name",
- "1073": "device_name__continue_with_empty_label"
+ "1073": "device_name__continue_with_empty_label",
+ "1074": "regulatory_certification__title"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index f8c75a81..f6a09362 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "e332650b4a7a414886d6b719f0e810691cba9ba3dd3ebea3b36651b428e6ae55",
- "datetime": "2025-08-18T14:37:50.578121+00:00",
- "commit": "dcd2735e2a8f1d21e4202495007ccfdddcab70d6"
+ "merkle_root": "5bf5b852e6b2b05fc4178d59543fef50b371164e0cf697030c42295ed1c16284",
+ "datetime": "2025-08-19T18:37:45.931524+00:00",
+ "commit": "e3fcde178a4588696bcf90243e10855e9625ac30"
},
"history": [
{
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.