refactor(core): change algorithm for scm_revision obfuscation
What changed, and why it matters
This commit changes how the Trezor firmware exposes its internal Git commit hash (the 'revision ID'). Previously the full hash was directly readable by software. Now only a shortened version is stored plainly, while the full hash is lightly scrambled (XORed) and only returned through a function that requires a specific key byte. The goal is to make it harder for a malicious fake Trezor to copy the revision ID from an official firmware update and pretend it is running that official firmware. It is a defensive hardening change, not a fix for an active bug or vulnerability in the traditional sense.
Treat as a hardening improvement rather than an urgent vulnerability patch. Review whether the XOR constants are intended to remain at 0 for all builds or should be randomized per release; if left at 0, the obfuscation provides no effective protection. Verify that downstream tools (Trezor Suite, firmware update verification) correctly call get_scm_revision with the expected XOR2 value and that no code still relies on the removed SCM_REVISION constant. Consider adding a changelog entry because the change is security-relevant.
Security signals we found
Defensive obfuscation of firmware revision identifier to mitigate spoofing by malicious bootloaders/fake devices
Removal of deterministic build artifact ordering based on SCM_REVISION
Introduction of compile-time XOR constants for reversible obfuscation
Comment explicitly describes threat model: fake device extracting revision ID from update image
No changelog entry despite security-relevant behavior change
Evidence from the diff
The patch refactors SCM_REVISION handling. The Makefile now splits the 20-byte git hash into an 8-byte SCM_REVISION_SHORT and a 12-byte SCM_REVISION_LONG, defines two XOR bytes (currently both 0), and removes the deterministic shuffling of object files based on SCM_REVISION in SConscript.firmware. A new C function get_scm_revision(xor2) reconstructs the full hash by concatenating the short and long parts, then XORs the long part with SCM_REVISION_XOR1 (even indices) and the caller-supplied xor2 (odd indices). Python code now calls utils.get_scm_revision(utils.SCM_REVISION_XOR2) instead of reading utils.SCM_REVISION directly. The obfuscation is reversible and the XOR values are compile-time constants, so it raises the bar for trivial extraction but does not cryptographically hide the revision.
Changed components
core/Makefilecore/SConscript.firmwarecore/SConscript.kernelcore/SConscript.secmoncore/embed/io/gfx/rsod.ccore/embed/rtl/inc/rtl/scm_revision.hcore/embed/rtl/scm_revision.ccore/embed/upymod/modtrezorutils/modtrezorutils.ccore/mocks/generated/trezorutils.pyicore/src/apps/base.pycore/src/trezor/utils.pyInspect captured patch +86 / −36
diff --git a/core/Makefile b/core/Makefile
index c730a93a..71d8f624 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -111,8 +111,11 @@ STORAGE_2_OFFSET := $(shell expr $(STORAGE_2_START) - $(FLASH_START))
OPENOCD = openocd -f interface/$(OPENOCD_INTERFACE).cfg -c "transport select $(OPENOCD_TRANSPORT)" -f $(OPENOCD_TARGET)
-SCM_REVISION = '$(shell git rev-parse HEAD)'
-CFLAGS += -DSCM_REVISION_INIT='{$(shell echo ${SCM_REVISION} | sed 's:\(..\):0x\1,:g')}'
+SCM_REVISION := $(shell git rev-parse HEAD)
+CFLAGS += -DSCM_REVISION_SHORT_INIT='{$(shell echo $(SCM_REVISION) | cut -c1-8 | sed 's:\(..\):0x\1,:g')}'
+CFLAGS += -DSCM_REVISION_LONG_INIT='{$(shell echo $(SCM_REVISION) | cut -c9-40 | sed 's:\(..\):0x\1,:g')}'
+CFLAGS += -DSCM_REVISION_XOR1=0
+CFLAGS += -DSCM_REVISION_XOR2=0
TESTPATH = $(CURDIR)/../tests
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index 50e35c71..bb0ba872 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -18,7 +18,6 @@ DEBUGLINK = ARGUMENTS.get('DEBUGLINK', '0') == '1'
DISABLE_OPTIGA = ARGUMENTS.get('DISABLE_OPTIGA', '0') == '1'
DISABLE_TROPIC = ARGUMENTS.get('DISABLE_TROPIC', '0') == '1'
HW_REVISION = ARGUMENTS.get('HW_REVISION', None)
-SCM_REVISION = ARGUMENTS.get('SCM_REVISION', None)
THP = ARGUMENTS.get('THP', '0') == '1' # Trezor-Host Protocol
BENCHMARK = ARGUMENTS.get('BENCHMARK', '0') == '1'
LOG_STACK_USAGE = ARGUMENTS.get('LOG_STACK_USAGE', '0') == '1'
@@ -897,8 +896,6 @@ obj_program.extend(env.Object(source=SOURCE_HAL))
if FROZEN:
obj_program.extend(env.Object(source=source_mpyc))
-random.Random(SCM_REVISION).shuffle(obj_program)
-
env.Replace(
ALLSOURCES=source_files,
ALLDEFS=tools.get_defs_for_cmake(env['CPPDEFINES'] + env['CPPDEFINES_IMPLICIT'] + [f"PRODUCTION={int(PRODUCTION)}", f"FORCE_BOOTLOADER_UPGRADE={int(FORCE_BOOTLOADER_UPGRADE)}", f"PYOPT={PYOPT}", f"BITCOIN_ONLY={BITCOIN_ONLY}"]))
diff --git a/core/SConscript.kernel b/core/SConscript.kernel
index 51d00d8f..f9623c1d 100644
--- a/core/SConscript.kernel
+++ b/core/SConscript.kernel
@@ -224,6 +224,7 @@ SOURCE_MOD += [
'embed/io/gfx/terminal/terminal.c',
'embed/io/translations/translations.c',
'embed/rtl/error_handling.c',
+ 'embed/rtl/scm_revision.c',
'embed/rtl/strutils.c',
'embed/sec/image/boot_image.c',
'embed/sec/image/image.c',
@@ -269,7 +270,7 @@ else:
env = Environment(
ENV=os.environ,
- CFLAGS=f"{ARGUMENTS.get('CFLAGS', '')} -DPRODUCTION={int(PRODUCTION)} -DPYOPT={PYOPT} -DBOOTLOADER_DEVEL={int(BOOTLOADER_DEVEL)} -DBITCOIN_ONLY={BITCOIN_ONLY} -USCM_REVISION_INIT {DEBUG_FLAGS}",
+ CFLAGS=f"{ARGUMENTS.get('CFLAGS', '')} -DPRODUCTION={int(PRODUCTION)} -DPYOPT={PYOPT} -DBOOTLOADER_DEVEL={int(BOOTLOADER_DEVEL)} -DBITCOIN_ONLY={BITCOIN_ONLY} {DEBUG_FLAGS}",
CPPDEFINES_IMPLICIT=[],
CPPDEFPREFIX="-D'",
CPPDEFSUFFIX="'",
diff --git a/core/SConscript.secmon b/core/SConscript.secmon
index 5d1a1be8..990b0a93 100644
--- a/core/SConscript.secmon
+++ b/core/SConscript.secmon
@@ -80,7 +80,7 @@ FROZEN = True
env = Environment(
ENV=os.environ,
- CFLAGS=f"{ARGUMENTS.get('CFLAGS', '')} -DPRODUCTION={int(PRODUCTION)} -DPYOPT={PYOPT} -DBOOTLOADER_DEVEL={int(BOOTLOADER_DEVEL)} -DBITCOIN_ONLY={BITCOIN_ONLY} -USCM_REVISION_INIT {DEBUG_FLAGS}",
+ CFLAGS=f"{ARGUMENTS.get('CFLAGS', '')} -DPRODUCTION={int(PRODUCTION)} -DPYOPT={PYOPT} -DBOOTLOADER_DEVEL={int(BOOTLOADER_DEVEL)} -DBITCOIN_ONLY={BITCOIN_ONLY} {DEBUG_FLAGS}",
CPPDEFINES_IMPLICIT=[],
CPPDEFPREFIX="-D'",
CPPDEFSUFFIX="'",
diff --git a/core/embed/io/gfx/rsod.c b/core/embed/io/gfx/rsod.c
index 3d4a4939..0d353a31 100644
--- a/core/embed/io/gfx/rsod.c
+++ b/core/embed/io/gfx/rsod.c
@@ -22,16 +22,12 @@
#include <io/display.h>
#include <io/rsod.h>
#include <io/terminal.h>
+#include <rtl/scm_revision.h>
+#include <rtl/strutils.h>
#include <sec/rsod_special.h>
#include <sys/bootutils.h>
#include <sys/system.h>
-#include <rtl/strutils.h>
-
-#ifdef SCM_REVISION_INIT
-#include <rtl/scm_revision.h>
-#endif
-
#define RSOD_DEFAULT_TITLE "Internal error";
#define RSOD_DEFAULT_MESSAGE "Unspecified";
#define RSOD_DEFAULT_FOOTER "Please visit trezor.io/rsod";
@@ -104,12 +100,10 @@ void rsod_terminal(const systask_postmortem_t* pminfo) {
term_print("\n");
}
-#ifdef SCM_REVISION_INIT
- char rev[10 + 1];
- cstr_encode_hex(rev, sizeof(rev), SCM_REVISION, (sizeof(rev) - 1) / 2);
+ char rev[8 + 1];
+ cstr_encode_hex(rev, sizeof(rev), SCM_REVISION_SHORT, (sizeof(rev) - 1) / 2);
term_print("rev : ");
term_print(rev);
-#endif
if (footer != NULL) {
term_print("\n");
diff --git a/core/embed/rtl/inc/rtl/scm_revision.h b/core/embed/rtl/inc/rtl/scm_revision.h
index d7cf37b6..772c9854 100644
--- a/core/embed/rtl/inc/rtl/scm_revision.h
+++ b/core/embed/rtl/inc/rtl/scm_revision.h
@@ -17,11 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef SCM_REVISION_H
-#define SCM_REVISION_H
+#pragma once
#include <trezor_types.h>
-extern const uint8_t SCM_REVISION[sizeof((const uint8_t[])SCM_REVISION_INIT)];
-
-#endif
+extern const uint8_t
+ SCM_REVISION_SHORT[sizeof((const uint8_t[])SCM_REVISION_SHORT_INIT)];
diff --git a/core/embed/rtl/scm_revision.c b/core/embed/rtl/scm_revision.c
index 9730d5e8..f422c0f1 100644
--- a/core/embed/rtl/scm_revision.c
+++ b/core/embed/rtl/scm_revision.c
@@ -19,4 +19,4 @@
#include <rtl/scm_revision.h>
-const uint8_t SCM_REVISION[] = SCM_REVISION_INIT;
+const uint8_t SCM_REVISION_SHORT[] = SCM_REVISION_SHORT_INIT;
diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c
index acd65ba8..452cafa4 100644
--- a/core/embed/upymod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c
@@ -34,14 +34,14 @@
#include "../trezorobj.h"
#include "modtrezorutils-meminfo.h"
-#include <io/usb.h>
-#include <sys/logging.h>
-
#include <io/notify.h>
+#include <io/usb.h>
#include <rtl/scm_revision.h>
#include <sec/fwutils.h>
#include <sec/unit_properties.h>
#include <sys/bootutils.h>
+#include <sys/logging.h>
+
#include "blake2s.h"
#include "memzero.h"
@@ -734,8 +734,50 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorutils_set_log_filter_obj,
mod_trezorutils_set_log_filter);
#endif
-STATIC const mp_obj_str_t mod_trezorutils_revision_obj = {
- {&mp_type_bytes}, 0, sizeof(SCM_REVISION), (const byte *)SCM_REVISION};
+/// def get_scm_revision(xor2: int) -> bytes:
+/// """
+/// Returns SCM revision of the firmware.
+/// """
+STATIC mp_obj_t mod_trezorutil_get_scm_revision(mp_obj_t xor2) {
+ // Why the revision ID is obfuscated:
+ //
+ // When a firmware update is loaded onto a fake Trezor device,
+ // a malicious bootloader may automatically extract the revision ID
+ // of the firmware update. The malicious firmware on the device may
+ // then use the extracted revision ID to convince Suite that the firmware
+ // was successfully updated when in fact the malicious firmware is running
+ // on the device all along.
+
+ uint8_t SCM_REVISION_LONG[] = SCM_REVISION_LONG_INIT;
+
+ uint8_t scm_revision[sizeof(SCM_REVISION_SHORT) + sizeof(SCM_REVISION_LONG)] =
+ {0};
+
+ for (size_t i = 0; i < sizeof(SCM_REVISION_SHORT); ++i) {
+ scm_revision[i] = SCM_REVISION_SHORT[i];
+ }
+
+ for (size_t i = 0; i < sizeof(SCM_REVISION_LONG); ++i) {
+ scm_revision[i + sizeof(SCM_REVISION_SHORT)] = SCM_REVISION_LONG[i];
+ }
+
+ uint8_t xor2_byte = trezor_obj_get_uint(xor2) & 0xFF;
+
+ _Static_assert(sizeof(SCM_REVISION_SHORT) % 2 == 0,
+ "SCM revision size must be even");
+ _Static_assert(sizeof(SCM_REVISION_LONG) % 2 == 0,
+ "SCM revision size must be even");
+
+ for (size_t i = sizeof(SCM_REVISION_SHORT); i < sizeof(scm_revision);
+ i += 2) {
+ scm_revision[i] ^= SCM_REVISION_XOR1;
+ scm_revision[i + 1] ^= xor2_byte;
+ }
+
+ return mp_obj_new_bytes(scm_revision, sizeof(scm_revision));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorutil_get_scm_revision_obj,
+ mod_trezorutil_get_scm_revision);
STATIC const mp_obj_str_t mod_trezorutils_model_name_obj = {
{&mp_type_str}, 0, sizeof(MODEL_NAME) - 1, (const byte *)MODEL_NAME};
@@ -764,8 +806,6 @@ STATIC const mp_obj_tuple_t mod_trezorutils_version_obj = {
{MP_OBJ_NEW_SMALL_INT(VERSION_MAJOR), MP_OBJ_NEW_SMALL_INT(VERSION_MINOR),
MP_OBJ_NEW_SMALL_INT(VERSION_PATCH), MP_OBJ_NEW_SMALL_INT(VERSION_BUILD)}};
-/// SCM_REVISION: bytes
-/// """Git commit hash of the firmware."""
/// VERSION: VersionTuple
/// """Firmware version as a tuple (major, minor, patch, build)."""
/// USE_BLE: bool
@@ -842,6 +882,8 @@ STATIC const mp_obj_tuple_t mod_trezorutils_version_obj = {
/// """Notification event: factory reset (wipe) invoked"""
/// NOTIFY_UNPAIR: int
/// """Notification event: BLE bonding for current connection deleted"""
+/// SCM_REVISION_XOR2: int
+/// """XOR2 byte for SCM revision obfuscation."""
///
/// if __debug__:
/// DISABLE_ANIMATION: bool
@@ -895,6 +937,15 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
#ifdef USE_DBG_CONSOLE
{MP_ROM_QSTR(MP_QSTR_set_log_filter),
MP_ROM_PTR(&mod_trezorutils_set_log_filter_obj)},
+#endif
+ {MP_ROM_QSTR(MP_QSTR_get_scm_revision),
+ MP_ROM_PTR(&mod_trezorutil_get_scm_revision_obj)},
+#if MICROPY_MODULE_FROZEN_MPY
+ // Hide xor2 key in the dictionary
+ // (direct constant is used instead)
+ {MP_ROM_QSTR(MP_QSTR_SCM_REVISION_XOR2), MP_ROM_INT(0)},
+#else
+ {MP_ROM_QSTR(MP_QSTR_SCM_REVISION_XOR2), MP_ROM_INT(SCM_REVISION_XOR2)},
#endif
{MP_ROM_QSTR(MP_QSTR_delegated_identity),
MP_ROM_PTR(&mod_trezorutils_delegated_identity_obj)},
@@ -943,8 +994,6 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_presize_module),
MP_ROM_PTR(&mod_trezorutils_presize_module_obj)},
// various built-in constants
- {MP_ROM_QSTR(MP_QSTR_SCM_REVISION),
- MP_ROM_PTR(&mod_trezorutils_revision_obj)},
{MP_ROM_QSTR(MP_QSTR_VERSION), MP_ROM_PTR(&mod_trezorutils_version_obj)},
#ifdef USE_SD_CARD
{MP_ROM_QSTR(MP_QSTR_USE_SD_CARD), mp_const_true},
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index 44fb593c..32bab40b 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -246,8 +246,13 @@ def set_log_filter(filter: str) -> None:
"""
Sets filter string for syslog
"""
-SCM_REVISION: bytes
-"""Git commit hash of the firmware."""
+
+
+# upymod/modtrezorutils/modtrezorutils.c
+def get_scm_revision(xor2: int) -> bytes:
+ """
+ Returns SCM revision of the firmware.
+ """
VERSION: VersionTuple
"""Firmware version as a tuple (major, minor, patch, build)."""
USE_BLE: bool
@@ -324,6 +329,8 @@ NOTIFY_WIPE: int
"""Notification event: factory reset (wipe) invoked"""
NOTIFY_UNPAIR: int
"""Notification event: BLE bonding for current connection deleted"""
+SCM_REVISION_XOR2: int
+"""XOR2 byte for SCM revision obfuscation."""
if __debug__:
DISABLE_ANIMATION: bool
diff --git a/core/src/apps/base.py b/core/src/apps/base.py
index 392512fa..fcf6ebc8 100644
--- a/core/src/apps/base.py
+++ b/core/src/apps/base.py
@@ -86,7 +86,7 @@ def get_features() -> Features:
minor_version=v_minor,
patch_version=v_patch,
build_version=v_build,
- revision=utils.SCM_REVISION,
+ revision=utils.get_scm_revision(utils.SCM_REVISION_XOR2),
model=utils.MODEL,
internal_model=utils.INTERNAL_MODEL,
device_id=storage_device.get_device_id(),
diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py
index e2874e76..83a09343 100644
--- a/core/src/trezor/utils.py
+++ b/core/src/trezor/utils.py
@@ -19,7 +19,7 @@ from trezorutils import ( # noqa: F401
NOTIFY_UNLOCK,
NOTIFY_UNPAIR,
NOTIFY_WIPE,
- SCM_REVISION,
+ SCM_REVISION_XOR2,
UI_LAYOUT,
USE_APP_LOADING,
USE_BACKLIGHT,
@@ -45,6 +45,7 @@ from trezorutils import ( # noqa: F401
consteq,
firmware_hash,
firmware_vendor,
+ get_scm_revision,
halt,
memcpy,
memzero,
Why this scored 37/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.