fix(core): Features.language_version_matches true even if build_version differs
What changed, and why it matters
This commit fixes a minor logic bug in the Trezor hardware wallet firmware. Previously, the device reported that its stored language translation data matched the firmware version only when the full four-part version numbers matched exactly, including the rarely-changing 'build' number. The fix makes the comparison ignore the build number, so the device correctly reports a match when only the major, minor, and patch versions match. This is a correctness/UX fix rather than a security vulnerability: a mismatch did not bypass any protections, it only caused a misleading status flag.
No security action required. Treat as a normal bugfix/UX improvement. Reviewers may verify that translation blob signature and header validation remain unchanged and that the build-version relaxation is intentional product behavior.
Security signals we found
Strict version equality relaxed to prefix comparison
No cryptographic or signature verification changes
No privilege boundary or authorization change
Test updated to assert relaxed matching behavior
Evidence from the diff
In core/src/apps/base.py, _language_version_matches() changed from comparing header.version == utils.VERSION to comparing only the first three components (header.version[:3] == utils.VERSION[:3]). The translation blob version is a 4-tuple (major, minor, patch, build), while the firmware’s reported Features version historically exposed only the first three components to clients. The strict equality caused language_version_matches to be False whenever the build component differed, even though the translation data is intentionally allowed to differ at build level. The test changes confirm this is expected behavior: test_build_version_mismatch now asserts language_version_matches is True after installing a blob whose build version was bumped. No access-control, signature, or cryptographic checks were altered.
Changed components
core/src/apps/base.py:_language_version_matches()Features.language_version_matches flagTranslation blob version handling in trezorlib testing helpersInspect captured patch +17 / −12
diff --git a/core/src/apps/base.py b/core/src/apps/base.py
index a144d469..28eaa360 100644
--- a/core/src/apps/base.py
+++ b/core/src/apps/base.py
@@ -63,7 +63,7 @@ def _language_version_matches() -> bool:
if header is None:
return True
- return header.version == utils.VERSION
+ return header.version[:3] == utils.VERSION[:3]
def get_features() -> Features:
diff --git a/python/src/trezorlib/testing/translations.py b/python/src/trezorlib/testing/translations.py
index 9234ec15..ed77a185 100644
--- a/python/src/trezorlib/testing/translations.py
+++ b/python/src/trezorlib/testing/translations.py
@@ -50,7 +50,7 @@ _CURRENT_TRANSLATION = threading.local()
def prepare_blob(
lang_or_def: translations.JsonDef | Path | str,
model: models.TrezorModel,
- version: translations.VersionTuple | tuple[int, int, int] | None = None,
+ version: translations.VersionTuple | None = None,
) -> translations.TranslationsBlob:
"""
Prepare a translation blob for a given language and device model.
@@ -72,9 +72,6 @@ def prepare_blob(
# generate raw blob
if version is None:
version = translations.version_from_json(lang_or_def["header"]["version"])
- elif len(version) == 3:
- # version coming from client object does not have build item
- version = *version, 0
return translations.blob_from_defs(lang_or_def, order, model, version, FONTS_DIR)
@@ -113,7 +110,9 @@ def build_and_sign_blob(
Returns:
bytes: The signed translation blob.
"""
- blob = prepare_blob(lang_or_def, session.model, session.version)
+ f = session.client.features
+ version = (f.major_version, f.minor_version, f.patch_version, f.build_version or 0)
+ blob = prepare_blob(lang_or_def, session.model, version)
return sign_blob(blob)
diff --git a/tests/device_tests/test_language.py b/tests/device_tests/test_language.py
index 589b02dc..96f60e53 100644
--- a/tests/device_tests/test_language.py
+++ b/tests/device_tests/test_language.py
@@ -60,6 +60,11 @@ def get_ping_title(lang: str) -> str:
return content["translations"]["words__confirm"]
+def get_version(session: Session) -> tuple[int, int, int, int]:
+ f = session.client.features
+ return (f.major_version, f.minor_version, f.patch_version, f.build_version)
+
+
@pytest.fixture
def client(client: Client) -> Iterator[Client]:
session = client.get_seedless_session()
@@ -201,7 +206,7 @@ def test_error_invalid_signature(session: Session):
pytest.raises(exceptions.TrezorFailure, match="Invalid translations data"),
session.test_ctx,
):
- blob = prepare_blob("cs", session.model, session.version)
+ blob = prepare_blob("cs", session.model, get_version(session))
blob.proof = translations.Proof(
merkle_proof=[],
sigmask=0b011,
@@ -243,18 +248,19 @@ def test_build_version_mismatch(session: Session):
assert session.features.language == "en-US"
# Translations build version is allowed to differ from FW build version.
# Change the build version to one not matching the current device
- version = session.version
- assert len(session.version) == 3
- version = version + (1,)
- blob = prepare_blob("cs", session.model, version)
+ cur_version = get_version(session)
+ bumped_version = cur_version[:3] + (cur_version[3] + 1,)
+ blob = prepare_blob("cs", session.model, bumped_version)
device.change_language(
session,
language_data=sign_blob(blob),
)
assert session.features.language == "cs-CZ"
+ assert session.features.language_version_matches is True
_check_ping_screen_texts(
session, get_ping_title("cs"), get_ping_button("cs", session.client)
)
+ # Would be nice to test lower build_version but firmware's is usually 0 and hard to change
def test_language_is_removed_after_wipe(client: Client):
@@ -460,7 +466,7 @@ def test_header_trailing_data(session: Session):
assert session.features.language == "en-US"
lang = "cs"
- blob = prepare_blob(lang, session.model, session.version)
+ blob = prepare_blob(lang, session.model, get_version(session))
blob.header_bytes += b"trailing dataa"
assert len(blob.header_bytes) % 2 == 0, "Trailing data must keep the 2-alignment"
language_data = sign_blob(blob)
Why this scored 27/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.