feat(core): allow exporting unit serial number
What changed, and why it matters
This commit adds a new optional feature that lets a connected computer ask a Trezor device for its hardware serial number. The device shows the request on its screen and requires the user to tap 'Allow' before sending the number. It is a deliberate product feature, not a hidden backdoor or bug, and only works on models that explicitly support it (currently the T3W1 / Trezor Safe 7 line).
No immediate action required. Treat this as a normal privacy-related feature addition. If reviewing for a security model, verify that the serial number is not used as an authentication factor anywhere else, that unit_properties_get_sn returns only the intended identifier, and that the GetSerialNumber message type is not handled when USE_SERIAL_NUMBER is disabled.
Security signals we found
New host-visible device property exposed via protobuf message
User confirmation required before data is returned
Feature is compile-time gated by USE_SERIAL_NUMBER flag
Only enabled for T3W1 hardware revisions and emulator
No authentication or pairing check beyond on-device user approval
Serial number is a static hardware identifier, not a secret key
Evidence from the diff
The change introduces a GetSerialNumber message handler (apps/management/get_serial_number.py) that calls a new C helper unit_properties_get_sn() exposed via modtrezorutils when USE_SERIAL_NUMBER is defined. The feature is gated by a build-time flag and only wired into T3W1 board definitions and the emulator. Before returning the serial number, the handler awaits confirm_action(), which displays a screen asking the user to allow the connected host to read the serial number. The serial number is treated as a string and returned in a SerialNumber protobuf message.
Changed components
core/src/apps/management/get_serial_number.pycore/src/apps/workflow_handlers.pycore/embed/upymod/modtrezorutils/modtrezorutils.ccore/site_scons/models/T3W1/*core/SConscript.firmwarecore/SConscript.unixcore/src/trezor/utils.pycore/translations/en.jsonInspect captured patch +136 / −13
diff --git a/core/.changelog.d/5922.added b/core/.changelog.d/5922.added
new file mode 100644
index 00000000..40e8c9de
--- /dev/null
+++ b/core/.changelog.d/5922.added
@@ -0,0 +1 @@
+[T3W1] Allow exporting device serial number.
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index deb9bf0b..9aa3246f 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -61,6 +61,7 @@ FEATURES_WANTED = [
"rgb_led",
"sd_card",
"secmon_layout",
+ "serial_number",
"storage",
"suspend",
"tropic",
@@ -448,10 +449,8 @@ SOURCE_FIRMWARE = [
'embed/projects/firmware/nlrthumb.c',
]
-if 'sd_card' in FEATURES_AVAILABLE:
- SDCARD = True
-else:
- SDCARD = False
+SDCARD = ('sd_card' in FEATURES_AVAILABLE)
+SERIAL_NUMBER = ('serial_number' in FEATURES_AVAILABLE)
env.Tool('micropython')
@@ -713,10 +712,11 @@ if FROZEN:
] if TREZOR_MODEL != "T3W1" else [])
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
- exclude=[
- SOURCE_PY_DIR + 'apps/management/sd_protect.py',
- ] if not SDCARD else [])
- )
+ exclude=(
+ ([SOURCE_PY_DIR + 'apps/management/sd_protect.py'] if not SDCARD else []) +
+ ([SOURCE_PY_DIR + 'apps/management/get_serial_number.py'] if not SERIAL_NUMBER else [])
+ )
+ ))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*/*.py',
exclude=(
([SOURCE_PY_DIR + 'apps/management/ble/*.py'] if "ble" not in FEATURES_AVAILABLE else [])
diff --git a/core/SConscript.unix b/core/SConscript.unix
index f553658b..cfdb04bc 100644
--- a/core/SConscript.unix
+++ b/core/SConscript.unix
@@ -34,6 +34,7 @@ FEATURES_WANTED = [
"rgb_led",
"sd_card",
"secure_mode",
+ "serial_number",
"storage",
"usb",
"usb_iface_wire",
@@ -729,7 +730,9 @@ if FROZEN:
SOURCE_PY_DIR + 'apps/management/sd_protect.py',
] if "sd_card" not in FEATURES_AVAILABLE else [] + [
SOURCE_PY_DIR + 'apps/management/authenticate_device.py',
- ] if "optiga" not in FEATURES_AVAILABLE else [])
+ ] if "optiga" not in FEATURES_AVAILABLE else [] + [
+ SOURCE_PY_DIR + 'apps/management/get_serial_number.py',
+ ] if "serial_number" not in FEATURES_AVAILABLE else [])
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*/*.py',
exclude=(
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 0b730d7a..f8a53bd1 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -837,6 +837,8 @@ static void _librust_qstrs(void) {
MP_QSTR_sign_message__message_size;
MP_QSTR_sign_message__verify_address;
MP_QSTR_skip_first_paint;
+ MP_QSTR_sn__action;
+ MP_QSTR_sn__title;
MP_QSTR_start_advertising;
MP_QSTR_start_comm;
MP_QSTR_storage_msg__processing;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index a8e436ce..cf665ab4 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1552,6 +1552,8 @@ pub enum TranslatedString {
ble__mac_address = 1162, // "MAC address"
ble__waiting_for_host = 1163, // "Waiting for host..."
ble__apps_connected = 1164, // "Apps connected"
+ sn__action = 1165, // {"Bolt": "", "Caesar": "", "Delizia": "", "Eckhart": "Allow connected host to get serial number of your Trezor Safe 7?"}
+ sn__title = 1166, // {"Bolt": "", "Caesar": "", "Delizia": "", "Eckhart": "Serial number"}
}
impl TranslatedString {
@@ -3538,6 +3540,22 @@ impl TranslatedString {
(Self::ble__mac_address, "MAC address"),
(Self::ble__waiting_for_host, "Waiting for host..."),
(Self::ble__apps_connected, "Apps connected"),
+ #[cfg(feature = "layout_bolt")]
+ (Self::sn__action, ""),
+ #[cfg(feature = "layout_caesar")]
+ (Self::sn__action, ""),
+ #[cfg(feature = "layout_delizia")]
+ (Self::sn__action, ""),
+ #[cfg(feature = "layout_eckhart")]
+ (Self::sn__action, "Allow connected host to get serial number of your Trezor Safe 7?"),
+ #[cfg(feature = "layout_bolt")]
+ (Self::sn__title, ""),
+ #[cfg(feature = "layout_caesar")]
+ (Self::sn__title, ""),
+ #[cfg(feature = "layout_delizia")]
+ (Self::sn__title, ""),
+ #[cfg(feature = "layout_eckhart")]
+ (Self::sn__title, "Serial number"),
];
#[cfg(feature = "micropython")]
@@ -4692,6 +4710,8 @@ impl TranslatedString {
(Qstr::MP_QSTR_sign_message__confirm_without_review, Self::sign_message__confirm_without_review),
(Qstr::MP_QSTR_sign_message__message_size, Self::sign_message__message_size),
(Qstr::MP_QSTR_sign_message__verify_address, Self::sign_message__verify_address),
+ (Qstr::MP_QSTR_sn__action, Self::sn__action),
+ (Qstr::MP_QSTR_sn__title, Self::sn__title),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_solana__account_index, Self::solana__account_index),
#[cfg(feature = "universal_fw")]
diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c
index 626ace8d..0e27a25c 100644
--- a/core/embed/upymod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c
@@ -55,6 +55,8 @@
#include <sys/stack_utils.h>
#endif
+/// from trezor import utils
+
/// def consteq(sec: AnyBytes, pub: AnyBytes) -> bool:
/// """
/// Compares the private information in `sec` with public, user-provided
@@ -268,6 +270,27 @@ STATIC mp_obj_t mod_trezorutils_unit_packaging(void) {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_unit_packaging_obj,
mod_trezorutils_unit_packaging);
+#if USE_SERIAL_NUMBER
+
+/// if utils.USE_SERIAL_NUMBER:
+/// def serial_number() -> str:
+/// """
+/// Returns unit serial number.
+/// """
+STATIC mp_obj_t mod_trezorutils_serial_number(void) {
+ uint8_t device_sn[MAX_DEVICE_SN_SIZE] = {0};
+ size_t device_sn_size = 0;
+ if (!unit_properties_get_sn(device_sn, MAX_DEVICE_SN_SIZE, &device_sn_size)) {
+ mp_raise_msg(&mp_type_RuntimeError,
+ MP_ERROR_TEXT("Failed to read serial number."));
+ }
+ return mp_obj_new_str_copy(&mp_type_str, device_sn, device_sn_size);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_serial_number_obj,
+ mod_trezorutils_serial_number);
+
+#endif // USE_SERIAL_NUMBER
+
/// def sd_hotswap_enabled() -> bool:
/// """
/// Returns True if SD card hot swapping is enabled
@@ -641,6 +664,8 @@ STATIC mp_obj_tuple_t mod_trezorutils_version_obj = {
/// """Whether the hardware supports BLE."""
/// USE_SD_CARD: bool
/// """Whether the hardware supports SD card."""
+/// USE_SERIAL_NUMBER: bool
+/// """Whether the hardware support exporting its serial number."""
/// USE_BACKLIGHT: bool
/// """Whether the hardware supports backlight brightness control."""
/// USE_HAPTIC: bool
@@ -746,6 +771,13 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
MP_ROM_PTR(&mod_trezorutils_unit_packaging_obj)},
{MP_ROM_QSTR(MP_QSTR_unit_btconly),
MP_ROM_PTR(&mod_trezorutils_unit_btconly_obj)},
+#if USE_SERIAL_NUMBER
+ {MP_ROM_QSTR(MP_QSTR_serial_number),
+ MP_ROM_PTR(&mod_trezorutils_serial_number_obj)},
+ {MP_ROM_QSTR(MP_QSTR_USE_SERIAL_NUMBER), mp_const_true},
+#else
+ {MP_ROM_QSTR(MP_QSTR_USE_SERIAL_NUMBER), mp_const_false},
+#endif
#if !PYOPT
#if LOG_STACK_USAGE
{MP_ROM_QSTR(MP_QSTR_zero_unused_stack),
diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h
index 073f72ee..80234347 100644
--- a/core/embed/upymod/qstrdefsport.h
+++ b/core/embed/upymod/qstrdefsport.h
@@ -134,6 +134,7 @@ Q(apps.management.change_pin)
Q(apps.management.change_wipe_code)
Q(apps.management.get_next_u2f_counter)
Q(apps.management.get_nonce)
+Q(apps.management.get_serial_number)
Q(apps.management.reboot_to_bootloader)
Q(apps.management.recovery_device)
Q(apps.management.recovery_device.homescreen)
@@ -225,6 +226,7 @@ Q(get_ownership_id)
Q(get_ownership_proof)
Q(get_pubkey)
Q(get_public_key)
+Q(get_serial_number)
Q(hash_benchmark)
Q(hashlib)
Q(helpers)
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index 54c25615..c471dfad 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -7,6 +7,7 @@ def meminfo(filename: str | None) -> None:
"""Dumps map of micropython GC arena to a file.
The JSON file can be decoded by analyze-memory-dump.py
"""
+from trezor import utils
# upymod/modtrezorutils/modtrezorutils.c
@@ -88,6 +89,11 @@ def unit_packaging() -> int | None:
"""
Returns the packaging version of the unit.
"""
+if utils.USE_SERIAL_NUMBER:
+ def serial_number() -> str:
+ """
+ Returns unit serial number.
+ """
# upymod/modtrezorutils/modtrezorutils.c
@@ -202,6 +208,8 @@ USE_BLE: bool
"""Whether the hardware supports BLE."""
USE_SD_CARD: bool
"""Whether the hardware supports SD card."""
+USE_SERIAL_NUMBER: bool
+"""Whether the hardware support exporting its serial number."""
USE_BACKLIGHT: bool
"""Whether the hardware supports backlight brightness control."""
USE_HAPTIC: bool
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index abc0565a..40a2665f 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -845,6 +845,8 @@ class TR:
sign_message__confirm_without_review: str = "Confirm without review"
sign_message__message_size: str = "Message size"
sign_message__verify_address: str = "Verify address"
+ sn__action: str = "Allow connected host to get serial number of your Trezor Safe 7?"
+ sn__title: str = "Serial number"
solana__account_index: str = "Account index"
solana__associated_token_account: str = "Associated token account"
solana__base_fee: str = "Base fee"
diff --git a/core/site_scons/models/T3W1/emulator.py b/core/site_scons/models/T3W1/emulator.py
index 9ed196ef..674f6fe0 100644
--- a/core/site_scons/models/T3W1/emulator.py
+++ b/core/site_scons/models/T3W1/emulator.py
@@ -137,4 +137,7 @@ def configure(
"embed/gfx/jpegdec/unix/jpegdec.c",
]
+ if "serial_number" in features_wanted:
+ defines += [("USE_SERIAL_NUMBER", "1")]
+
return features_available
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revA.py b/core/site_scons/models/T3W1/trezor_t3w1_revA.py
index ccd1ce75..5eaf4d3b 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revA.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revA.py
@@ -307,6 +307,9 @@ def configure(
paths += ["embed/sec/iwdg/inc"]
defines += [("USE_IWDG", "1")]
+ if "serial_number" in features_wanted:
+ defines += [("USE_SERIAL_NUMBER", "1")]
+
env.get("ENV")["LINKER_SCRIPT"] = linker_script
env.get("ENV")["MEMORY_LAYOUT"] = memory_layout
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revB.py b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
index df61ce31..d5ce2c50 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revB.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
@@ -314,6 +314,9 @@ def configure(
paths += ["embed/sec/iwdg/inc"]
defines += [("USE_IWDG", "1")]
+ if "serial_number" in features_wanted:
+ defines += [("USE_SERIAL_NUMBER", "1")]
+
env.get("ENV")["LINKER_SCRIPT"] = linker_script
env.get("ENV")["MEMORY_LAYOUT"] = memory_layout
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revC.py b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
index 958da9b1..56559578 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revC.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
@@ -313,6 +313,9 @@ def configure(
paths += ["embed/sec/iwdg/inc"]
defines += [("USE_IWDG", "1")]
+ if "serial_number" in features_wanted:
+ defines += [("USE_SERIAL_NUMBER", "1")]
+
env.get("ENV")["LINKER_SCRIPT"] = linker_script
env.get("ENV")["MEMORY_LAYOUT"] = memory_layout
diff --git a/core/src/apps/management/get_serial_number.py b/core/src/apps/management/get_serial_number.py
new file mode 100644
index 00000000..98e4e219
--- /dev/null
+++ b/core/src/apps/management/get_serial_number.py
@@ -0,0 +1,21 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from trezor.messages import GetSerialNumber, SerialNumber
+
+
+async def get_serial_number(msg: GetSerialNumber) -> SerialNumber:
+ from trezor import TR
+ from trezor.messages import SerialNumber
+ from trezor.ui.layouts import confirm_action
+ from trezor.utils import serial_number
+
+ serial_number = serial_number()
+ await confirm_action(
+ br_name="get_serial_number",
+ title=TR.sn__title,
+ action=TR.sn__action,
+ description=f"\n{serial_number}",
+ verb=TR.buttons__allow,
+ )
+ return SerialNumber(serial_number=serial_number)
diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py
index 488de35b..3a120bd6 100644
--- a/core/src/apps/workflow_handlers.py
+++ b/core/src/apps/workflow_handlers.py
@@ -56,6 +56,8 @@ def _find_message_handler_module(msg_type: int) -> str:
return "apps.management.change_wipe_code"
if msg_type == MessageType.GetNonce:
return "apps.management.get_nonce"
+ if utils.USE_SERIAL_NUMBER and msg_type == MessageType.GetSerialNumber:
+ return "apps.management.get_serial_number"
if msg_type == MessageType.RebootToBootloader:
return "apps.management.reboot_to_bootloader"
diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py
index 8be3a1b8..7b92b3c8 100644
--- a/core/src/trezor/utils.py
+++ b/core/src/trezor/utils.py
@@ -30,6 +30,7 @@ from trezorutils import ( # noqa: F401
USE_POWER_MANAGER,
USE_RGB_LED,
USE_SD_CARD,
+ USE_SERIAL_NUMBER,
USE_THP,
USE_TOUCH,
USE_TROPIC,
@@ -54,6 +55,9 @@ from trezorutils import ( # noqa: F401
if USE_NRF:
from trezorutils import nrf_get_version # noqa: F401
+if USE_SERIAL_NUMBER:
+ from trezorutils import serial_number # noqa: F401
+
from typing import TYPE_CHECKING
if __debug__:
diff --git a/core/translations/en.json b/core/translations/en.json
index 4d15f603..5dd7811a 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -1057,6 +1057,18 @@
"sign_message__confirm_without_review": "Confirm without review",
"sign_message__message_size": "Message size",
"sign_message__verify_address": "Verify address",
+ "sn__action": {
+ "Bolt": "",
+ "Caesar": "",
+ "Delizia": "",
+ "Eckhart": "Allow connected host to get serial number of your Trezor Safe 7?"
+ },
+ "sn__title": {
+ "Bolt": "",
+ "Caesar": "",
+ "Delizia": "",
+ "Eckhart": "Serial number"
+ },
"solana__account_index": "Account index",
"solana__associated_token_account": "Associated token account",
"solana__base_fee": "Base fee",
diff --git a/core/translations/order.json b/core/translations/order.json
index 79496257..d8381bda 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1163,5 +1163,7 @@
"1161": "ble__host_info",
"1162": "ble__mac_address",
"1163": "ble__waiting_for_host",
- "1164": "ble__apps_connected"
+ "1164": "ble__apps_connected",
+ "1165": "sn__action",
+ "1166": "sn__title"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index 607a70db..64ff1a6a 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "ef2162032887b6505407669faa6ca66200abfc19e611b0a76fed4bff3896cfbb",
- "datetime": "2025-10-02T07:28:22.652263+00:00",
- "commit": "bb0c9a22e27c38e445188283e7ac8d23f3f3e903"
+ "merkle_root": "50b3a9dfb3ec3cd23ae8168f9cbbc16f89d6158b5cf7bf9a531f8aa701d25ee6",
+ "datetime": "2025-10-02T17:43:35.279801+00:00",
+ "commit": "e71482d2b4e535701f949b441fbab03439d85743"
},
"history": [
{
Why this scored 19/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.