feat(tests): check Tropic signature and cert chain
What changed, and why it matters
This commit only adds and updates automated tests for Trezor's device-authentication feature. It does not change the firmware code that runs on the device, so it cannot introduce a security vulnerability in the product itself. The tests verify that certificate chains and signatures from two hardware security chips (Optiga and Tropic) are accepted correctly.
No security action required. Review the new test assertions for correctness if desired, but treat this as routine test maintenance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies tests/device_tests/test_authenticate_device.py. It renames the existing Optiga test, extracts certificate-chain verification into a shared helper, and adds a new test case for the Tropic secure-element authentication path using Ed25519 root keys. No production firmware or host-library logic is changed; this is purely test-coverage expansion.
Changed components
tests/device_tests/test_authenticate_device.pyInspect captured patch +93 / −30
diff --git a/tests/device_tests/test_authenticate_device.py b/tests/device_tests/test_authenticate_device.py
index 339eafa9..2ecb97cb 100644
--- a/tests/device_tests/test_authenticate_device.py
+++ b/tests/device_tests/test_authenticate_device.py
@@ -1,7 +1,7 @@
import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric import ec, ed25519
from cryptography.x509 import extensions as ext
from trezorlib import device, models
@@ -11,7 +11,7 @@ from ..common import compact_size
pytestmark = pytest.mark.models("safe")
-ROOT_PUBLIC_KEY = {
+OPTIGA_ROOT_PUBLIC_KEY = {
models.T2B1: bytes.fromhex(
"047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
),
@@ -26,6 +26,48 @@ ROOT_PUBLIC_KEY = {
),
}
+TROPIC_ROOT_PUBLIC_KEY = {
+ models.T3W1: bytes.fromhex(
+ "1ab1c5f12f4570e0de5c16a8d9feea381f53c8d813feeb0eb2fb7f393f2b6b5f"
+ ),
+}
+
+
+def verify_cert_chain(certs, model_name):
+ for cert, ca_cert in zip(certs, certs[1:]):
+ assert cert.issuer == ca_cert.subject
+
+ ca_basic_constraints = ca_cert.extensions.get_extension_for_class(
+ ext.BasicConstraints
+ ).value
+ assert ca_basic_constraints.ca is True
+
+ try:
+ basic_constraints = cert.extensions.get_extension_for_class(
+ ext.BasicConstraints
+ ).value
+ if basic_constraints.ca:
+ assert basic_constraints.path_length < ca_basic_constraints.path_length
+ except ext.ExtensionNotFound:
+ pass
+
+ ca_public_key = ca_cert.public_key()
+ if isinstance(ca_public_key, ed25519.Ed25519PublicKey):
+ ca_public_key.verify(
+ cert.signature,
+ cert.tbs_certificate_bytes,
+ )
+ else:
+ ca_public_key.verify(
+ cert.signature,
+ cert.tbs_certificate_bytes,
+ cert.signature_algorithm_parameters,
+ )
+
+ # Verify that the common name matches the Trezor model.
+ common_name = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0]
+ assert common_name.value.startswith(model_name)
+
@pytest.mark.parametrize(
"challenge",
@@ -38,7 +80,7 @@ ROOT_PUBLIC_KEY = {
),
),
)
-def test_authenticate_device(session: Session, challenge: bytes) -> None:
+def test_authenticate_device_optiga(session: Session, challenge: bytes) -> None:
# NOTE Applications must generate a random challenge for each request.
if not session.features.bootloader_locked:
@@ -46,11 +88,14 @@ def test_authenticate_device(session: Session, challenge: bytes) -> None:
# Issue an AuthenticateDevice challenge to Trezor.
proof = device.authenticate(session, challenge)
+
certs = [x509.load_der_x509_certificate(cert) for cert in proof.optiga_certificates]
+ assert len(certs) >= 2 # at least one root and one device cert from Optiga
+
# Verify the last certificate in the certificate chain against trust anchor.
root_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
- ec.SECP256R1(), ROOT_PUBLIC_KEY[session.model]
+ ec.SECP256R1(), OPTIGA_ROOT_PUBLIC_KEY[session.model]
)
root_public_key.verify(
certs[-1].signature,
@@ -58,36 +103,54 @@ def test_authenticate_device(session: Session, challenge: bytes) -> None:
certs[-1].signature_algorithm_parameters,
)
- # Verify the certificate chain.
- for cert, ca_cert in zip(certs, certs[1:]):
- assert cert.issuer == ca_cert.subject
+ verify_cert_chain(certs, session.model.internal_name)
- ca_basic_constraints = ca_cert.extensions.get_extension_for_class(
- ext.BasicConstraints
- ).value
- assert ca_basic_constraints.ca is True
+ # Verify the signature of the challenge.
+ data = b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
+ certs[0].public_key().verify(
+ proof.optiga_signature, data, ec.ECDSA(hashes.SHA256())
+ )
- try:
- basic_constraints = cert.extensions.get_extension_for_class(
- ext.BasicConstraints
- ).value
- if basic_constraints.ca:
- assert basic_constraints.path_length < ca_basic_constraints.path_length
- except ext.ExtensionNotFound:
- pass
- ca_cert.public_key().verify(
- cert.signature,
- cert.tbs_certificate_bytes,
- cert.signature_algorithm_parameters,
- )
+@pytest.mark.parametrize(
+ "challenge",
+ (
+ b"",
+ b"hello world",
+ b"\x00" * 1024,
+ bytes.fromhex(
+ "21f3d40e63c304d0312f62eb824113efd72ba1ee02bef6777e7f8a7b6f67ba16"
+ ),
+ ),
+)
+@pytest.mark.models("core", skip=["safe3", "safe5"], reason="Not using Tropic")
+def test_authenticate_device_tropic(session: Session, challenge: bytes) -> None:
+ # NOTE Applications must generate a random challenge for each request.
- # Verify that the common name matches the Trezor model.
- common_name = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0]
- assert common_name.value.startswith(session.model.internal_name)
+ if not session.features.bootloader_locked:
+ pytest.xfail("unlocked bootloader")
+
+ # Issue an AuthenticateDevice challenge to Trezor.
+ proof = device.authenticate(session, challenge)
+
+ certs = [x509.load_der_x509_certificate(cert) for cert in proof.tropic_certificates]
+
+ # If this fails, make sure the emulator was built with DISABLE_TROPIC=0
+ assert len(certs) >= 2 # at least one root and one device cert from Tropic
+
+ # Verify the last certificate in the certificate chain against trust anchor.
+ root_public_key = ed25519.Ed25519PublicKey.from_public_bytes(
+ TROPIC_ROOT_PUBLIC_KEY[session.model]
+ )
+ root_public_key.verify(
+ certs[-1].signature,
+ certs[-1].tbs_certificate_bytes,
+ )
+
+ verify_cert_chain(certs, session.model.internal_name)
# Verify the signature of the challenge.
- data = b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
- certs[0].public_key().verify(
- proof.optiga_signature, data, ec.ECDSA(hashes.SHA256())
+ data = bytearray(
+ b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
)
+ certs[0].public_key().verify(proof.tropic_signature, data)
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.