feat(core): Use Tropic in AuthenticateDevice.
What changed, and why it matters
This commit updates the Trezor hardware wallet's device-authentication feature so it can optionally use a second secure chip (Tropic) in addition to the existing Optiga chip. It refactors certificate parsing into a helper and renames the proof fields from generic names to optiga_* and tropic_*. There is no direct evidence in the commit that this fixes a security vulnerability; it appears to be a feature addition enabling a new hardware variant.
Treat as a normal feature/refactor commit. Reviewers may want to verify that the Tropic signing path and certificate parsing handle malformed input safely, and that callers consuming AuthenticityProof tolerate the new optional tropic_* fields being None on non-Tropic devices.
Security signals we found
Refactored certificate parsing into a reusable helper with explicit error raising
Fixed missing 'raise' keyword before wire.FirmwareError in corrupted-certificate handling
Added optional second signing path via Tropic secure element
Renamed AuthenticityProof fields to distinguish Optiga and Tropic evidence
No mention of vulnerability, CVE, or security bug in commit message or changelog
Evidence from the diff
The change modifies core/src/apps/management/authenticate_device.py to: (1) extract DER certificate-chain parsing into parse_cert_chain(); (2) build the challenge bytes in a bytearray and hash it with sha256() instead of using HashWriter; (3) sign with optiga and, when utils.USE_TROPIC is true, also sign with tropic and fetch the Tropic certificate chain; (4) return separate optiga_certificates/optiga_signature and tropic_certificates/tropic_signature fields. The test is updated to use the renamed optiga fields. A bug in the original loop—where wire.FirmwareError was raised without the raise keyword—is fixed in the new helper by adding raise. The commit message and changelog frame this as a feature (‘feat(core)’, ‘.added’ changelog fragment).
Changed components
core/src/apps/management/authenticate_device.pytests/device_tests/test_authenticate_device.pyInspect captured patch +50 / −21
diff --git a/core/.changelog.d/5760.added b/core/.changelog.d/5760.added
new file mode 100644
index 00000000..d837c0b4
--- /dev/null
+++ b/core/.changelog.d/5760.added
@@ -0,0 +1 @@
+Use Tropic in AuthenticateDevice.
diff --git a/core/src/apps/management/authenticate_device.py b/core/src/apps/management/authenticate_device.py
index d1c2fbe6..ad6d4033 100644
--- a/core/src/apps/management/authenticate_device.py
+++ b/core/src/apps/management/authenticate_device.py
@@ -2,12 +2,29 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from trezor.messages import AuthenticateDevice, AuthenticityProof
+ from trezor.utils import BufferReader
+
+
+def parse_cert_chain(r: BufferReader) -> list[bytes]:
+ from trezor import wire
+ from trezor.crypto.der import read_length
+
+ certificates = []
+ while r.remaining_count() > 0:
+ cert_begin = r.offset
+ if r.get() != 0x30:
+ raise wire.FirmwareError("Device certificate is corrupted.")
+ n = read_length(r)
+ cert_len = r.offset - cert_begin + n
+ r.seek(cert_begin)
+ certificates.append(r.read_memoryview(cert_len))
+
+ return certificates
async def authenticate_device(msg: AuthenticateDevice) -> AuthenticityProof:
from trezor import TR, utils, wire
from trezor.crypto import optiga
- from trezor.crypto.der import read_length
from trezor.crypto.hashlib import sha256
from trezor.loop import sleep
from trezor.messages import AuthenticityProof
@@ -29,30 +46,37 @@ async def authenticate_device(msg: AuthenticateDevice) -> AuthenticityProof:
)
header = b"AuthenticateDevice:"
- h = utils.HashWriter(sha256())
- write_compact_size(h, len(header))
- h.extend(header)
- write_compact_size(h, len(msg.challenge))
- h.extend(msg.challenge)
+ challenge_bytes = utils.empty_bytearray(1 + len(header) + 1 + len(msg.challenge))
+ write_compact_size(challenge_bytes, len(header))
+ challenge_bytes.extend(header)
+ write_compact_size(challenge_bytes, len(msg.challenge))
+ challenge_bytes.extend(msg.challenge)
spinner = progress(TR.progress__authenticity_check)
spinner.report(0)
try:
- signature = optiga.sign(optiga.DEVICE_ECC_KEY_INDEX, h.get_digest())
+ optiga_signature = optiga.sign(
+ optiga.DEVICE_ECC_KEY_INDEX, sha256(challenge_bytes).digest()
+ )
except optiga.SigningInaccessible:
- raise wire.ProcessError("Signing inaccessible.")
+ raise wire.ProcessError("Optiga signing inaccessible.")
- certificates = []
r = BufferReader(optiga.get_certificate(optiga.DEVICE_CERT_INDEX))
- while r.remaining_count() > 0:
- cert_begin = r.offset
- if r.get() != 0x30:
- wire.FirmwareError("Device certificate is corrupted.")
- n = read_length(r)
- cert_len = r.offset - cert_begin + n
- r.seek(cert_begin)
- certificates.append(r.read_memoryview(cert_len))
+ optiga_certificates = parse_cert_chain(r)
+
+ tropic_certificates = None
+ tropic_signature = None
+ if utils.USE_TROPIC:
+ from trezor.crypto import tropic
+
+ try:
+ tropic_signature = tropic.sign(tropic.DEVICE_KEY_SLOT, challenge_bytes)
+ except tropic.TropicError:
+ raise wire.ProcessError("Tropic signing failed.")
+
+ r = BufferReader(tropic.get_user_data(tropic.DEVICE_CERT_INDEX))
+ tropic_certificates = parse_cert_chain(r)
if not utils.DISABLE_ANIMATION:
frame_delay = sleep(60)
@@ -63,6 +87,8 @@ async def authenticate_device(msg: AuthenticateDevice) -> AuthenticityProof:
spinner.report(1000)
return AuthenticityProof(
- certificates=certificates,
- signature=signature,
+ optiga_certificates=optiga_certificates,
+ optiga_signature=optiga_signature,
+ tropic_certificates=tropic_certificates,
+ tropic_signature=tropic_signature,
)
diff --git a/tests/device_tests/test_authenticate_device.py b/tests/device_tests/test_authenticate_device.py
index 6a1238fe..339eafa9 100644
--- a/tests/device_tests/test_authenticate_device.py
+++ b/tests/device_tests/test_authenticate_device.py
@@ -46,7 +46,7 @@ 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.certificates]
+ certs = [x509.load_der_x509_certificate(cert) for cert in proof.optiga_certificates]
# Verify the last certificate in the certificate chain against trust anchor.
root_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
@@ -88,4 +88,6 @@ def test_authenticate_device(session: Session, challenge: bytes) -> None:
# Verify the signature of the challenge.
data = b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
- certs[0].public_key().verify(proof.signature, data, ec.ECDSA(hashes.SHA256()))
+ certs[0].public_key().verify(
+ proof.optiga_signature, data, ec.ECDSA(hashes.SHA256())
+ )
Why this scored 26/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.