feat(tests): add `test_authenticate_device_mcu()`
What changed, and why it matters
This commit only adds a new automated test that checks whether Trezor hardware devices can prove their identity using an MCU-based digital signature. It does not change any firmware code that runs on the device, nor does it fix or introduce any security vulnerability. It is a testing-only change.
No security action required; this is a test-only addition. Review the test logic for correctness if auditing device attestation coverage.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds test_authenticate_device_mcu() and supporting certificate verification helpers for ML-DSA44 (a post-quantum signature scheme) in the Python test suite. It imports mldsa from the cryptography library, adds a root public key mapping for the T3W1 model, and records expected UI hash fixtures. No device firmware, bootloader, or host library production code is modified.
Changed components
tests/device_tests/certificate.pytests/device_tests/test_authenticate_device.pytests/ui_tests/fixtures.jsonInspect captured patch +64 / −2
diff --git a/tests/device_tests/certificate.py b/tests/device_tests/certificate.py
index 1f0b29fc..83a6014a 100644
--- a/tests/device_tests/certificate.py
+++ b/tests/device_tests/certificate.py
@@ -2,7 +2,7 @@ from typing import Sequence
from cryptography import x509
from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.asymmetric import ec, ed25519
+from cryptography.hazmat.primitives.asymmetric import ec, ed25519, mldsa
from cryptography.x509 import extensions as ext
from trezorlib import _root_keys, models
@@ -19,6 +19,10 @@ TROPIC_ROOT_PUBLIC_KEY = {
models.T3W1: bytes.fromhex(_root_keys.T3W1_DEV_AUTH_ROOT_DEBUG_ED25519_HEX),
}
+MCU_ROOT_PUBLIC_KEY = {
+ models.T3W1: bytes.fromhex(_root_keys.T3W1_DEV_AUTH_ROOT_DEBUG_MLDSA44_HEX),
+}
+
def verify_cert_chain(certs, model_name, root_public_key):
def verify_cert_signature(public_key, cert):
@@ -30,6 +34,8 @@ def verify_cert_chain(certs, model_name, root_public_key):
cert.tbs_certificate_bytes,
cert.signature_algorithm_parameters,
)
+ elif isinstance(public_key, mldsa.MLDSA44PublicKey):
+ public_key.verify(cert.signature, cert.tbs_certificate_bytes)
else:
raise ValueError("Unsupported public key type")
@@ -101,6 +107,23 @@ def check_signature_optiga(
certs[0].public_key().verify(signature, data, ec.ECDSA(hashes.SHA256()))
+def check_signature_mcu(
+ signature: bytes,
+ certificate_chain: Sequence[bytes],
+ model: TrezorModel,
+ data: bytes,
+) -> None:
+ certs = [x509.load_der_x509_certificate(cert) for cert in certificate_chain]
+ assert len(certs) >= 1
+
+ root_public_key = mldsa.MLDSA44PublicKey.from_public_bytes(
+ MCU_ROOT_PUBLIC_KEY[model]
+ )
+ verify_cert_chain(certs, model.internal_name, root_public_key)
+
+ certs[0].public_key().verify(signature, data)
+
+
def check_signature_tropic(
signature: bytes,
certificate_chain: Sequence[bytes],
diff --git a/tests/device_tests/test_authenticate_device.py b/tests/device_tests/test_authenticate_device.py
index 68a56f5f..a9c58b91 100644
--- a/tests/device_tests/test_authenticate_device.py
+++ b/tests/device_tests/test_authenticate_device.py
@@ -5,7 +5,11 @@ from trezorlib import device, exceptions, messages
from trezorlib.debuglink import DebugSession as Session
from trezorlib.testing.common import compact_size
-from .certificate import check_signature_optiga, check_signature_tropic
+from .certificate import (
+ check_signature_mcu,
+ check_signature_optiga,
+ check_signature_tropic,
+)
# The tests below require Optiga (and some require Tropic)
pytestmark = pytest.mark.models("safe")
@@ -55,6 +59,29 @@ def test_authenticate_device_optiga(
)
+@pytest.mark.models(skip=["safe3", "safe5"])
+def test_authenticate_device_mcu(
+ session: Session, challenge: bytes, chunk_size: int
+) -> None:
+ # NOTE Applications must generate a random challenge for each request.
+
+ if not session.features.bootloader_locked:
+ pytest.xfail("unlocked bootloader")
+
+ if chunk_size == 0:
+ # MCU attestation is sent only when streaming is supported.
+ pytest.skip("MCU attestation requires streaming (chunk_size > 0)")
+
+ proof = device.authenticate(session, challenge, chunk_size)
+ assert proof.mcu_signature is not None
+ assert len(proof.mcu_certificates) >= 1
+
+ data = b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
+ check_signature_mcu(
+ proof.mcu_signature, proof.mcu_certificates, session.model, data
+ )
+
+
@pytest.mark.models(skip=["safe3", "safe5"], reason="Not using Tropic")
def test_authenticate_device_tropic(
session: Session, challenge: bytes, chunk_size: int
diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json
index e2e978ee..e3236e89 100644
--- a/tests/ui_tests/fixtures.json
+++ b/tests/ui_tests/fixtures.json
@@ -40349,6 +40349,18 @@
"T3W1_en_stellar-test_stellar.py::test_sign_tx[tx_source_account_not_equal_signing_key]": "ff0c7728defcc817063ef019fa53e8ade924c1b879e7b921ced331f956e1c53c",
"T3W1_en_test_authenticate_device.py::test_authenticate_device_invalid_range_offset": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
"T3W1_en_test_authenticate_device.py::test_authenticate_device_invalid_range_size": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[1kB_zeroes-0]": "778dbcfb96e575c652b06902ef2842f6cc261dd3d457cc66b8b6c9338e82e1fb",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[1kB_zeroes-1024]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[1kB_zeroes-16]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[32B_digest-0]": "778dbcfb96e575c652b06902ef2842f6cc261dd3d457cc66b8b6c9338e82e1fb",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[32B_digest-1024]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[32B_digest-16]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[empty-0]": "778dbcfb96e575c652b06902ef2842f6cc261dd3d457cc66b8b6c9338e82e1fb",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[empty-1024]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[empty-16]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[hello_world-0]": "778dbcfb96e575c652b06902ef2842f6cc261dd3d457cc66b8b6c9338e82e1fb",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[hello_world-1024]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
+"T3W1_en_test_authenticate_device.py::test_authenticate_device_mcu[hello_world-16]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
"T3W1_en_test_authenticate_device.py::test_authenticate_device_no_certificate": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
"T3W1_en_test_authenticate_device.py::test_authenticate_device_optiga[1kB_zeroes-0]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
"T3W1_en_test_authenticate_device.py::test_authenticate_device_optiga[1kB_zeroes-1024]": "fd6c57e10b64ae0c1f964e0c5e2bd4bfc5b92d222a9370d51e9afefe37875b0d",
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.