feat(python): allow exporting unit serial number
What changed, and why it matters
This commit adds a new Python command and library function that lets a user ask a Trezor device for its hardware serial number. The device already had a way to provide this number; the change only exposes it through the PC-side tools and adds tests. There is no obvious security bug in the code shown, though making serial numbers easier to read could slightly help someone track or fingerprint a specific device.
Treat as a low-risk feature addition. Review the firmware-side `GetSerialNumber` handler to confirm it requires on-device user confirmation and that the serial number is not returned to unauthorized callers. No immediate patch is indicated by this diff alone.
Security signals we found
New host-side exposure of device-identifying information (serial number)
CLI command marked seedless, meaning it does not require a wallet seed
No input validation or access-control logic visible in the added code
Test includes a cancel flow, suggesting the device prompts the user before releasing the serial number
Evidence from the diff
The patch adds device.get_serial_number(session) in python/src/trezorlib/device.py, which sends messages.GetSerialNumber() and returns messages.SerialNumber.serial_number. It also wires a CLI command trezorctl device serial-number in cli/device.py using @with_session(seedless=True). Tests and UI fixtures for the T3W1 model are added. The firmware-side message handling is not in the diff, so the security of the underlying implementation cannot be judged from this commit alone.
Changed components
python/src/trezorlib/device.pypython/src/trezorlib/cli/device.pytests/device_tests/test_serialnumber.pytests/ui_tests/fixtures.jsonInspect captured patch +65 / −0
diff --git a/python/.changelog.d/5922.added b/python/.changelog.d/5922.added
new file mode 100644
index 000000000..3efa9fd58
--- /dev/null
+++ b/python/.changelog.d/5922.added
@@ -0,0 +1 @@
+Allow exporting device serial number.
diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py
index bace95966..6571542f2 100644
--- a/python/src/trezorlib/cli/device.py
+++ b/python/src/trezorlib/cli/device.py
@@ -473,3 +473,10 @@ def authenticate(
except authentication.DeviceNotAuthentic:
click.echo("Device is not authentic.")
sys.exit(5)
+
+
+@cli.command()
+@with_session(seedless=True)
+def serial_number(session: "Session") -> str:
+ """Get serial number."""
+ return device.get_serial_number(session)
diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py
index 0a6c669a5..bdcf5427a 100644
--- a/python/src/trezorlib/device.py
+++ b/python/src/trezorlib/device.py
@@ -650,3 +650,8 @@ def authenticate(session: "Session", challenge: bytes) -> messages.AuthenticityP
def set_brightness(session: "Session", value: Optional[int] = None) -> str | None:
ret = session.call(messages.SetBrightness(value=value), expect=messages.Success)
return _return_success(ret)
+
+
+def get_serial_number(session: "Session") -> str:
+ ret = session.call(messages.GetSerialNumber(), expect=messages.SerialNumber)
+ return ret.serial_number
diff --git a/tests/device_tests/test_serialnumber.py b/tests/device_tests/test_serialnumber.py
new file mode 100644
index 000000000..daa0eb0ab
--- /dev/null
+++ b/tests/device_tests/test_serialnumber.py
@@ -0,0 +1,40 @@
+# This file is part of the Trezor project.
+#
+# Copyright (C) SatoshiLabs and contributors
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the License along with this library.
+# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+
+import pytest
+
+from trezorlib import device
+from trezorlib.debuglink import SessionDebugWrapper as Session
+from trezorlib.exceptions import Cancelled
+
+pytestmark = [
+ pytest.mark.models("t3w1"),
+]
+
+
+def test_same(session: Session):
+ sn = device.get_serial_number(session)
+ assert sn == device.get_serial_number(session)
+
+
+def test_cancel(session: Session):
+ def input_flow():
+ yield
+ session.cancel()
+
+ with pytest.raises(Cancelled), session.client as client:
+ client.set_input_flow(input_flow)
+ device.get_serial_number(session)
diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json
index d80610238..0ec6f043f 100644
--- a/tests/ui_tests/fixtures.json
+++ b/tests/ui_tests/fixtures.json
@@ -30800,6 +30800,8 @@
"T3W1_cs_test_repeated_backup.py::test_repeated_backup_via_host_cancel": "770f1c78587f3740fd04e852a8a27f94ac8649387667d68b7db226aff0022870",
"T3W1_cs_test_repeated_backup.py::test_repeated_backup_via_host_send_disallowed_message": "770f1c78587f3740fd04e852a8a27f94ac8649387667d68b7db226aff0022870",
"T3W1_cs_test_repeated_backup.py::test_repeated_backup_via_host_upgrade_single": "083ff0eeb5bbac77c886f8a2e95b5c33fde6891bd39e1c5b3ec6d5b97f09d960",
+"T3W1_cs_test_serialnumber.py::test_cancel": "a98412b2534f99c6d9461356cab1dddde769b5dde669bf2e904c8c6da559a92f",
+"T3W1_cs_test_serialnumber.py::test_same": "28d51af462320b9a3cf6854fa532884c29db80f1c1c0faa02133f2cdee29e958",
"T3W1_cs_test_session.py::test_clear_session": "5d42be00b26b1479856b46991050af2b5b6d8214bb1e9dbdb2849fed3810fe3a",
"T3W1_cs_test_session.py::test_create_session_with_passphrase_on_device": "31c0809d446caa2dbfcc22f25890eb2889bba465a3eddd9ce2006209dee76e63",
"T3W1_cs_test_session.py::test_end_session": "c24521e569c08e3605b164212c876f8ac57c5eef6cca6f2ca53a635a883ebc4b",
@@ -32289,6 +32291,8 @@
"T3W1_de_test_repeated_backup.py::test_repeated_backup_via_host_cancel": "2d6870478697d59a4d36b44473229624fb42c5769b7dfecc7c0f3c2beffa8ea8",
"T3W1_de_test_repeated_backup.py::test_repeated_backup_via_host_send_disallowed_message": "2d6870478697d59a4d36b44473229624fb42c5769b7dfecc7c0f3c2beffa8ea8",
"T3W1_de_test_repeated_backup.py::test_repeated_backup_via_host_upgrade_single": "304bdf662052edf2df08f83dd16e7249e6780aebcfa6a26b87c70fad34a5d1de",
+"T3W1_de_test_serialnumber.py::test_cancel": "4b2961ea2412c0dcc28a41bd90b3214cb826c5208437263f96aec90da28a6bd6",
+"T3W1_de_test_serialnumber.py::test_same": "4b1c17d0e920c334666a74f7db3248cdd91453928bf76c991552cc096b81d72e",
"T3W1_de_test_session.py::test_clear_session": "256bd01a2ce4abcd1353eaa0ca54775383bfde3c819cf91319fe22ca49ff09d0",
"T3W1_de_test_session.py::test_create_session_with_passphrase_on_device": "70e27c1fdaa9d716161982aa0526b50d02701d83a9ea32e0357bc4af225465c8",
"T3W1_de_test_session.py::test_end_session": "98735f888f827501b25c78c6a737c1983a800c620f709c65b1d95f6c8c9a2b90",
@@ -33778,6 +33782,8 @@
"T3W1_en_test_repeated_backup.py::test_repeated_backup_via_host_cancel": "dbaefe56d981e98e7b7a4d8d5b60bdbb0abb3fff7601d0ded80ff0feb3dc8809",
"T3W1_en_test_repeated_backup.py::test_repeated_backup_via_host_send_disallowed_message": "dbaefe56d981e98e7b7a4d8d5b60bdbb0abb3fff7601d0ded80ff0feb3dc8809",
"T3W1_en_test_repeated_backup.py::test_repeated_backup_via_host_upgrade_single": "f598bb5c6ad397f03c9033821b5ae1d61f0733baada22697035ccd5e0ed7be96",
+"T3W1_en_test_serialnumber.py::test_cancel": "30f6051129ef804aec713329a46496b44731c631bc309010ffce5afbd4c8b970",
+"T3W1_en_test_serialnumber.py::test_same": "350da7dfc3c1c5bdf2c57a8b20860cd157e51ed0deaa89f313f8693c9d34db04",
"T3W1_en_test_session.py::test_clear_session": "79a561b4bd7fade37128427bbdfabb0069e5bc0b49a95834d908893b468bd12d",
"T3W1_en_test_session.py::test_create_session_with_passphrase_on_device": "bdacf4d58793ad7a313407a159c6c10cb2e165514dd86e6e83f95f860d4a81ac",
"T3W1_en_test_session.py::test_end_session": "931d9afceb0ba1e4faae891775819277242d889644d5c0c5863fc8c9fcf859b1",
@@ -35267,6 +35273,8 @@
"T3W1_es_test_repeated_backup.py::test_repeated_backup_via_host_cancel": "9761e5a332c7ab00173cf9aa7176d85e2d5b1c5eb1c7e5b6b216c21eeeebb883",
"T3W1_es_test_repeated_backup.py::test_repeated_backup_via_host_send_disallowed_message": "9761e5a332c7ab00173cf9aa7176d85e2d5b1c5eb1c7e5b6b216c21eeeebb883",
"T3W1_es_test_repeated_backup.py::test_repeated_backup_via_host_upgrade_single": "d4c13d101163b24725a16cdb7df360112c14da87a6371e7ca0647dfa7cc00c15",
+"T3W1_es_test_serialnumber.py::test_cancel": "b038f11c95609ef409bf32c2cdf62a7c86e6a582fe2c76f3ddd342e38388aeb4",
+"T3W1_es_test_serialnumber.py::test_same": "2cc868f3fd1b4355ed46c7e60809ee7cb350458efed04df266d1ace7ce1f67d9",
"T3W1_es_test_session.py::test_clear_session": "15e11f480e92dc4cc5c7abe123b024525f6ba0af5a99101d0d649ba536dfacb8",
"T3W1_es_test_session.py::test_create_session_with_passphrase_on_device": "736eb340253751140a7c472a54f422de87babc360fbc81814047d212ce8373a8",
"T3W1_es_test_session.py::test_end_session": "56536ae9cd7c4ff8022def4ec3350031d3614064d961f53a2850f2be425af201",
@@ -36756,6 +36764,8 @@
"T3W1_fr_test_repeated_backup.py::test_repeated_backup_via_host_cancel": "f86ace368946b68306a2388faa56dd955dbe360f175f8984dc56610b1a70348c",
"T3W1_fr_test_repeated_backup.py::test_repeated_backup_via_host_send_disallowed_message": "f86ace368946b68306a2388faa56dd955dbe360f175f8984dc56610b1a70348c",
"T3W1_fr_test_repeated_backup.py::test_repeated_backup_via_host_upgrade_single": "ba4e1e241e254d69deea37d0ca088ba1f56eb2b9dae1bda840aacab6a052ace2",
+"T3W1_fr_test_serialnumber.py::test_cancel": "9d4b754be33a412703249aad18f6831a0ba0d5dd85a568a6704d68b27f7d4dcf",
+"T3W1_fr_test_serialnumber.py::test_same": "0bc1850a3413beccdb83f4cbd65253cd21cb76992a711348c90cddcc43cf5bee",
"T3W1_fr_test_session.py::test_clear_session": "8c819698d65f3e8a420fa884d863f993e3d06959badc276eb98e1ec56bc5c3aa",
"T3W1_fr_test_session.py::test_create_session_with_passphrase_on_device": "e53f8437e66c5c996a6bd6963b7652f4c3f75b4ecb268ad990b0691840a27b84",
"T3W1_fr_test_session.py::test_end_session": "e8156cf4eda1d29060f05b4a18d50032b0b344230609b7de7cababfe0a86b20b",
@@ -38245,6 +38255,8 @@
"T3W1_pt_test_repeated_backup.py::test_repeated_backup_via_host_cancel": "8d8c376180c84b51104eb05b2e438b5735f689b5d84d92511cb5d2cec11bc2c4",
"T3W1_pt_test_repeated_backup.py::test_repeated_backup_via_host_send_disallowed_message": "8d8c376180c84b51104eb05b2e438b5735f689b5d84d92511cb5d2cec11bc2c4",
"T3W1_pt_test_repeated_backup.py::test_repeated_backup_via_host_upgrade_single": "186f7f995671bace9f430b6caa2e1507e418427496929c378b832beb3f86f758",
+"T3W1_pt_test_serialnumber.py::test_cancel": "effe4181f12d30391ce091468891b4c68312626b83b547f880b351cc59ce1205",
+"T3W1_pt_test_serialnumber.py::test_same": "ee06a6cd004b3a7884ee074cc4c388172cb85c94e37d502ee9d9e6108a983dc9",
"T3W1_pt_test_session.py::test_clear_session": "d6d3881429c90e1e3ff5748b209b8c3c6d29e9f1bd4e046fa7a4e0d699dc104e",
"T3W1_pt_test_session.py::test_create_session_with_passphrase_on_device": "2c9408329e374e58f3f689363be241df8436442b4999d4de84118b90561b289c",
"T3W1_pt_test_session.py::test_end_session": "1c74667c078e25e7e0d37c7b2aa35f7c8ab02cd88e74695ecb355c82a293b0cc",
Why this scored 23/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.