fix(tests): fix Evolu tests on HW devices
What changed, and why it matters
This commit only changes test code for the Trezor hardware wallet. It refactors how tests compute expected cryptographic values so they can run on real hardware devices instead of only on emulators, and moves certificate-verification helper code into a shared test utility. There is no change to the actual firmware or wallet behavior, and no security vulnerability is introduced or fixed.
No security action required. This is a test-only refactoring commit. Reviewers may optionally verify that the new dynamic proof generation matches the protocol's expected signing format.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit modifies Python test files under tests/device_tests/. It removes hardcoded test vectors for Evolu delegated-identity and registration tests, replacing them with dynamically computed proofs from the device’s delegated_identity_key. It merges THP and non-THP test paths, adds an xfail guard for devices without Optiga, and extracts Optiga/Tropic certificate-chain verification helpers from test_authenticate_device.py into a new certificate.py module for reuse. No firmware source code is changed.
Changed components
tests/device_tests/certificate.py (new shared test helper)tests/device_tests/evolu/common.py (new shared test helper)tests/device_tests/evolu/test_get_delegated_identity_key.pytests/device_tests/evolu/test_get_delegated_identity_key_thp.py (deleted)tests/device_tests/evolu/test_get_node.pytests/device_tests/evolu/test_sign_registration.pytests/device_tests/test_authenticate_device.pyInspect captured patch +375 / −287
diff --git a/tests/device_tests/certificate.py b/tests/device_tests/certificate.py
new file mode 100644
index 00000000..88737661
--- /dev/null
+++ b/tests/device_tests/certificate.py
@@ -0,0 +1,117 @@
+from typing import Sequence
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import ec, ed25519
+from cryptography.x509 import extensions as ext
+
+from trezorlib import models
+from trezorlib.models import TrezorModel
+
+OPTIGA_ROOT_PUBLIC_KEY = {
+ models.T2B1: bytes.fromhex(
+ "047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
+ ),
+ models.T3T1: bytes.fromhex(
+ "04e48b69cd7962068d3cca3bcc6b1747ef496c1e28b5529e34ad7295215ea161dbe8fb08ae0479568f9d2cb07630cb3e52f4af0692102da5873559e45e9fa72959"
+ ),
+ models.T3B1: bytes.fromhex(
+ "047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
+ ),
+ models.T3W1: bytes.fromhex(
+ "04521192e173a9da4e3023f747d836563725372681eba3079c56ff11b2fc137ab189eb4155f371127651b5594f8c332fc1e9c0f3b80d4212822668b63189706578"
+ ),
+}
+
+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)
+
+
+def check_signature_optiga(
+ 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) >= 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(), OPTIGA_ROOT_PUBLIC_KEY[model]
+ )
+ root_public_key.verify(
+ certs[-1].signature,
+ certs[-1].tbs_certificate_bytes,
+ certs[-1].signature_algorithm_parameters,
+ )
+
+ verify_cert_chain(certs, model.internal_name)
+
+ # Verify the signature of the challenge.
+ certs[0].public_key().verify(signature, data, ec.ECDSA(hashes.SHA256()))
+
+
+def check_signature_tropic(
+ signature: bytes,
+ certificate_chain: Sequence[bytes],
+ model: TrezorModel,
+ data: bytes,
+) -> None:
+ certs = [x509.load_der_x509_certificate(cert) for cert in certificate_chain]
+
+ # 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[model]
+ )
+ root_public_key.verify(
+ certs[-1].signature,
+ certs[-1].tbs_certificate_bytes,
+ )
+
+ verify_cert_chain(certs, model.internal_name)
+
+ # Verify the signature of the challenge.
+ certs[0].public_key().verify(signature, bytearray(data))
diff --git a/tests/device_tests/evolu/common.py b/tests/device_tests/evolu/common.py
new file mode 100644
index 00000000..0703287d
--- /dev/null
+++ b/tests/device_tests/evolu/common.py
@@ -0,0 +1,90 @@
+import os
+from hashlib import sha256
+from typing import List
+
+from ecdsa import NIST256p, SigningKey
+
+from trezorlib import evolu
+from trezorlib.debuglink import SessionDebugWrapper as Session
+from trezorlib.debuglink import TrezorClientDebugLink as Client
+from trezorlib.messages import ThpCredentialResponse
+from trezorlib.transport.thp import curve25519
+
+from ...common import compact_size
+
+TEST_host_static_private_key = curve25519.get_private_key(os.urandom(32))
+TEST_host_static_public_key = curve25519.get_public_key(TEST_host_static_private_key)
+
+
+def get_proof(client: Client, header: bytes, arguments: List[bytes]) -> bytes:
+ private_key = get_delegated_identity_key(client)
+ signing_key = SigningKey.from_string(private_key, curve=NIST256p)
+
+ ctx = sha256()
+ ctx.update(compact_size(len(header)))
+ ctx.update(header)
+ for arg in arguments:
+ ctx.update(compact_size(len(arg)))
+ ctx.update(arg)
+ return signing_key.sign_digest(ctx.digest())
+
+
+def get_invalid_proof(client: Client, header: bytes, arguments: List[bytes]) -> bytes:
+ valid_proof = get_proof(client, header, arguments)
+ # tamper with the proof to make it invalid
+ invalid_proof = (
+ valid_proof[:-2]
+ + bytes([valid_proof[-2] ^ 0xFF])
+ + bytes([valid_proof[-1] ^ 0xFF])
+ )
+ return invalid_proof
+
+
+class ThpPairingResult:
+ def __init__(self, session, credential):
+ self.session: Session = session
+ self.credential: ThpCredentialResponse = credential
+
+
+def pair_and_get_credential(client: Client) -> ThpPairingResult:
+ from trezorlib.messages import (
+ ThpCredentialRequest,
+ ThpCredentialResponse,
+ ThpEndRequest,
+ ThpEndResponse,
+ )
+
+ from ..thp.connect import prepare_protocol_for_pairing
+ from ..thp.test_pairing import nfc_pairing
+
+ protocol = prepare_protocol_for_pairing(client)
+ nfc_pairing(client, protocol)
+ protocol._send_message(
+ ThpCredentialRequest(
+ host_static_public_key=TEST_host_static_public_key,
+ autoconnect=False,
+ )
+ )
+ credential_response = protocol._read_message(ThpCredentialResponse)
+
+ protocol._send_message(ThpEndRequest())
+ protocol._read_message(ThpEndResponse)
+ protocol._is_paired = True
+
+ client.protocol = protocol
+ session = client.get_session()
+ return ThpPairingResult(session, credential_response)
+
+
+def get_delegated_identity_key(client: Client) -> bytes:
+ if client.protocol_version == 2:
+ pairing_data = pair_and_get_credential(client)
+ return evolu.get_delegated_identity_key(
+ client.get_session(),
+ thp_credential=pairing_data.credential.credential,
+ host_static_public_key=TEST_host_static_public_key,
+ )
+ elif client.protocol_version == 1:
+ return evolu.get_delegated_identity_key(client.get_session())
+ else:
+ raise ValueError("Unsupported protocol version")
diff --git a/tests/device_tests/evolu/test_get_delegated_identity_key.py b/tests/device_tests/evolu/test_get_delegated_identity_key.py
index 930a36a4..4ea80e4a 100644
--- a/tests/device_tests/evolu/test_get_delegated_identity_key.py
+++ b/tests/device_tests/evolu/test_get_delegated_identity_key.py
@@ -1,22 +1,27 @@
import pytest
-from trezorlib import evolu
-from trezorlib.debuglink import SessionDebugWrapper as Session
+from trezorlib.debuglink import TrezorClientDebugLink as Client
-pytestmark = [pytest.mark.models("core"), pytest.mark.protocol("protocol_v1")]
+from .common import get_delegated_identity_key
+pytestmark = [pytest.mark.models("core")]
-def test_evolu_get_delegated_identity_is_constant(session: Session):
- private_key = evolu.get_delegated_identity_key(session)
+
+def test_evolu_get_delegated_identity_is_constant(client: Client):
+ private_key = get_delegated_identity_key(client)
assert len(private_key) == 32
- private_key_2 = evolu.get_delegated_identity_key(session)
+ private_key_2 = get_delegated_identity_key(client)
assert private_key_2 == private_key
-def test_evolu_get_delegated_identity_test_vector(session: Session):
+def test_evolu_get_delegated_identity_test_vector(client: Client):
# on emulator, the master key is all zeroes. So the delegated identity key is constant.
- private_key = evolu.get_delegated_identity_key(session)
+ if client.get_session().features.fw_vendor != "EMULATOR":
+ pytest.skip("Only for emulator")
+
+ private_key = get_delegated_identity_key(client)
+ # hardcoded expected value for the emulator with zeroed master key
assert private_key == bytes.fromhex(
"10e39ed3a40dd63a47a14608d4bccd4501170cf9f2188223208084d39c37b369"
)
diff --git a/tests/device_tests/evolu/test_get_delegated_identity_key_thp.py b/tests/device_tests/evolu/test_get_delegated_identity_key_thp.py
deleted file mode 100644
index 8e49fada..00000000
--- a/tests/device_tests/evolu/test_get_delegated_identity_key_thp.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import os
-
-import pytest
-
-from trezorlib.debuglink import SessionDebugWrapper as Session
-from trezorlib.debuglink import TrezorClientDebugLink as Client
-from trezorlib.messages import (
- EvoluDelegatedIdentityKey,
- EvoluGetDelegatedIdentityKey,
- ThpCredentialResponse,
-)
-from trezorlib.transport.thp import curve25519
-
-pytestmark = [pytest.mark.protocol("protocol_v2"), pytest.mark.models("core")]
-
-TEST_host_static_private_key = curve25519.get_private_key(os.urandom(32))
-TEST_host_static_public_key = curve25519.get_public_key(TEST_host_static_private_key)
-
-
-class ThpPairingResult:
- def __init__(self, session, credential):
- self.session: Session = session
- self.credential: ThpCredentialResponse = credential
-
-
-def pair_and_get_credential(client: Client) -> ThpPairingResult:
- from trezorlib.messages import (
- ThpCredentialRequest,
- ThpCredentialResponse,
- ThpEndRequest,
- ThpEndResponse,
- )
-
- from ..thp.connect import prepare_protocol_for_pairing
- from ..thp.test_pairing import nfc_pairing
-
- protocol = prepare_protocol_for_pairing(client)
- nfc_pairing(client, protocol)
- protocol._send_message(
- ThpCredentialRequest(
- host_static_public_key=TEST_host_static_public_key,
- autoconnect=False,
- )
- )
- credential_response = protocol._read_message(ThpCredentialResponse)
-
- protocol._send_message(ThpEndRequest())
- protocol._read_message(ThpEndResponse)
- protocol._is_paired = True
-
- client.protocol = protocol
- session = client.get_session()
- return ThpPairingResult(session, credential_response)
-
-
-def test_evolu_get_delegated_identity_is_constant(client: Client):
- pairing_data = pair_and_get_credential(client)
- credential_data = pairing_data.credential
- session = pairing_data.session
-
- response = session.call(
- EvoluGetDelegatedIdentityKey(
- thp_credential=credential_data.credential,
- host_static_public_key=TEST_host_static_public_key,
- ),
- expect=EvoluDelegatedIdentityKey,
- )
-
- private_key = response.private_key
- assert len(private_key) == 32
-
- response_2 = session.call(
- EvoluGetDelegatedIdentityKey(
- thp_credential=credential_data.credential,
- host_static_public_key=TEST_host_static_public_key,
- ),
- expect=EvoluDelegatedIdentityKey,
- )
- assert response_2.private_key == private_key
-
-
-def test_evolu_get_delegated_identity_test_vector(client: Client):
- # on emulator, the master key is all zeroes. So the delegated identity key is constant.
-
- pairing_data = pair_and_get_credential(client)
- credential_data = pairing_data.credential
- session = pairing_data.session
-
- response = session.call(
- EvoluGetDelegatedIdentityKey(
- thp_credential=credential_data.credential,
- host_static_public_key=TEST_host_static_public_key,
- ),
- expect=EvoluDelegatedIdentityKey,
- )
-
- private_key = response.private_key
- assert private_key == bytes.fromhex(
- "10e39ed3a40dd63a47a14608d4bccd4501170cf9f2188223208084d39c37b369"
- )
diff --git a/tests/device_tests/evolu/test_get_node.py b/tests/device_tests/evolu/test_get_node.py
index fec1f950..880a3578 100644
--- a/tests/device_tests/evolu/test_get_node.py
+++ b/tests/device_tests/evolu/test_get_node.py
@@ -1,47 +1,70 @@
import pytest
from trezorlib import evolu
-from trezorlib.debuglink import SessionDebugWrapper as Session
+from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import TrezorFailure
-pytestmark = pytest.mark.models("core")
+from .common import get_invalid_proof, get_proof
+pytestmark = [
+ pytest.mark.models("core"),
+ # the tests vectors in this test are for the SLIP-14 seed. It should be initialized from `conftest.py` already but we set it explicitly to be sure
+ pytest.mark.setup_client(
+ mnemonic="all all all all all all all all all all all all", passphrase=False
+ ),
+]
-def test_evolu_get_node(session: Session):
- proof = bytes.fromhex(
- "1fb521e8a4e4580377d530a9d6eb0a394ec8340fa42094d9f2e822bb944ce6a2074b81241b3b65dfa15d66e052f2504aba3ad1644844d695b181b3cdc9666cb66b"
- )
- node = evolu.get_node(session, proof=proof)
+def test_evolu_get_node(client: Client):
+ proof = get_proof(client, b"EvoluGetNode", [])
+ node = evolu.get_node(client.get_session(), proof=proof)
+
+ # expected node for the SLIP-14 seed
check_value = bytes.fromhex(
"a81aaf51997b6ddfa33d11c038d6aba5f711754a2c823823ff8b777825cdbb32b0e71c301fa381c75081bd3bcc134b63306aa6fc9a9f52d835ad4df8cd507be6"
)
assert node == check_value
-def test_evolu_get_node_invalid_proof(session: Session):
- proof = bytes.fromhex(
- "1f354fbb47b4679c1cb0c2c6b96a27f9a147c61ec5ef6f6c42491c839f4b7a95792d099be0f138274e5ef7896058b4de4f383f497792bb157b925e2644a79a0000" # altered last 2 bytes
+@pytest.mark.setup_client(
+ # a different seed
+ mnemonic="valve multiply shuffle venue then cruel genre venture fruit hammer sponsor luxury",
+ passphrase=False,
+)
+def test_evolu_get_node_different_seed(client: Client):
+ proof = get_proof(client, b"EvoluGetNode", [])
+ node = evolu.get_node(client.get_session(), proof=proof)
+
+ # expected node for the SLIP-14 seed
+ check_value = bytes.fromhex(
+ "a81aaf51997b6ddfa33d11c038d6aba5f711754a2c823823ff8b777825cdbb32b0e71c301fa381c75081bd3bcc134b63306aa6fc9a9f52d835ad4df8cd507be6"
)
+ # check that the generated node is different
+ assert node != check_value
+
+
+def test_evolu_get_node_invalid_proof(client: Client):
+ invalid_proof = get_invalid_proof(client, b"EvoluGetNode", [])
+
with pytest.raises(
TrezorFailure,
match="Invalid proof",
):
- evolu.get_node(session, proof=proof)
+ evolu.get_node(client.get_session(), proof=invalid_proof)
-def test_evolu_get_node_no_proof(session: Session):
+def test_evolu_get_node_no_proof(client: Client):
with pytest.raises(
TrezorFailure,
match="Invalid proof",
):
- evolu.get_node(session, proof=b"")
+ evolu.get_node(client.get_session(), proof=b"")
-def test_evolu_get_node_none_proof(session: Session):
+def test_evolu_get_node_none_proof(client: Client):
with pytest.raises(
TrezorFailure,
match="DataError: Failed to decode message: Missing required field. proof_of_delegated_identity",
):
- evolu.get_node(session, proof=None) # type: ignore
+ evolu.get_node(client.get_session(), proof=None) # type: ignore
diff --git a/tests/device_tests/evolu/test_sign_registration.py b/tests/device_tests/evolu/test_sign_registration.py
index 4c5d52b1..3368ab6c 100644
--- a/tests/device_tests/evolu/test_sign_registration.py
+++ b/tests/device_tests/evolu/test_sign_registration.py
@@ -1,18 +1,39 @@
import pytest
+from ecdsa import NIST256p, SigningKey, VerifyingKey
from trezorlib import evolu
-from trezorlib.debuglink import SessionDebugWrapper as Session
+from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import TrezorFailure
+from ...common import compact_size
+from ..certificate import check_signature_optiga
+from .common import get_delegated_identity_key, get_invalid_proof, get_proof
+
pytestmark = pytest.mark.models("core")
+def signing_buffer(private_key: bytes, challenge: bytes, size: int) -> bytes:
+ public_key: VerifyingKey = SigningKey.from_string(private_key, curve=NIST256p).get_verifying_key() # type: ignore
+ components = [
+ b"EvoluSignRegistrationRequestV1:",
+ public_key.to_string("uncompressed"),
+ challenge,
+ size.to_bytes(4, "big"),
+ ]
+ return b"".join((compact_size(len(comp)) + comp) for comp in components)
+
+
+def optiga_unavailable(client: Client) -> bool:
+ """Check if Optiga is unavailable from the presence of its security counter."""
+ return client.features.optiga_sec is None
+
+
@pytest.mark.models("t2t1")
-def test_evolu_sign_request_t2t1(session: Session):
- challenge = "1234"
+def test_evolu_sign_request_t2t1(client: Client):
+ challenge = bytes.fromhex("1234")
size = 10
- proposed_value = bytes.fromhex(
- "1b161be2bfc622b4ffd9943138ab5931e77b4c6835e29b1ac25221c74492495a912c00f488fd5f95b43085f721f36574813785c011c60cf81877ccd057df6bed0c"
+ proof = get_proof(
+ client, b"EvoluSignRegistrationRequest", [challenge, size.to_bytes(4, "big")]
)
with pytest.raises(
@@ -20,40 +41,47 @@ def test_evolu_sign_request_t2t1(session: Session):
match="Optiga is not available",
):
evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=proof,
)
@pytest.mark.models("safe")
-def test_evolu_sign_request(session: Session):
- challenge = "1234"
+def test_evolu_sign_request(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ delegated_identity_key = get_delegated_identity_key(client)
+ challenge = bytes.fromhex("1234")
size = 10
- proposed_value = bytes.fromhex(
- "1fb4ca7b8d956cc50ac652e383691af8e59b200adedde3a898b86795fd94d49241559a1699de1110617a91c44c70c4b9509fdb36f5057a52c0ef28fce7afa10734"
+ proposed_value = get_proof(
+ client,
+ b"EvoluSignRegistrationRequest",
+ [challenge, size.to_bytes(4, "big")],
)
+
response = evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
proof=proposed_value,
)
- check_signature = bytes.fromhex(
- "30440220148c0a0026828532e5a2e7ce5cf2dcd2491e7eea5f5c6eafd49779d1502c5ba102204b1ca171045969e38ac815de09462d6c5b496d04851266fe71abcf55b9aee672"
+ data = signing_buffer(delegated_identity_key, challenge, size)
+ check_signature_optiga(
+ response.signature, response.certificate_chain, client.model, data
)
- assert response.signature == check_signature
-
@pytest.mark.models("safe")
-def test_evolu_sign_request_invalid_proof(session: Session):
- challenge = "1234"
+def test_evolu_sign_request_invalid_proof(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ challenge = bytes.fromhex("1234")
size = 10
- proposed_value = bytes.fromhex(
- "20dc125b51c2f596df4a9ae9ef816353dcdbf068b91ac687962742b8bd434276f60258c337e0d03211e599701a87cae8d8ac3258ce01bd484921743c2a5e990000" # altered last 2 bytes
+ invalid_proof = get_invalid_proof(
+ client, b"EvoluSignRegistrationRequest", [challenge, size.to_bytes(4, "big")]
)
with pytest.raises(
@@ -61,19 +89,21 @@ def test_evolu_sign_request_invalid_proof(session: Session):
match="Invalid proof",
):
evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=invalid_proof,
)
@pytest.mark.models("safe")
-def test_evolu_sign_request_challenge_too_long(session: Session):
- challenge = "01" * 300 # 300 bytes, max is 255
+def test_evolu_sign_request_challenge_too_long(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ challenge = b"\x01" * 300 # 300 bytes, max is 255
size = 10
- proposed_value = bytes.fromhex(
- "1fd0b4cd0a04806eaa74ae59cc2f5a740680fc784b877deff6ffa6b9eda7d5a7d4207958c48e679b18c64d0e7fcd0e5be25eb27bcf186fbf9531eb20bce7234a23"
+ proof = get_proof(
+ client, b"EvoluSignRegistrationRequest", [challenge, size.to_bytes(4, "big")]
)
with pytest.raises(
@@ -81,19 +111,21 @@ def test_evolu_sign_request_challenge_too_long(session: Session):
match="Invalid challenge length",
):
evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=proof,
)
@pytest.mark.models("safe")
-def test_evolu_sign_request_challenge_too_short(session: Session):
- challenge = "" # 0 bytes, min is 1
+def test_evolu_sign_request_challenge_too_short(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ challenge = b"" # 0 bytes, minimum is 1
size = 10
- proposed_value = bytes.fromhex(
- "1fa386d20efb38dbb3f7ae0509651fa36c8128324ef89fa1cfd104e10dced08c594f0e8f0a525a839b4fbfaa92b8c2b51163cef593f5c14fc9f1c8c48d1192270d"
+ proof = get_proof(
+ client, b"EvoluSignRegistrationRequest", [challenge, size.to_bytes(4, "big")]
)
with pytest.raises(
@@ -101,19 +133,23 @@ def test_evolu_sign_request_challenge_too_short(session: Session):
match="Invalid challenge length",
):
evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=proof,
)
@pytest.mark.models("safe")
-def test_evolu_sign_request_size_too_small(session: Session):
- challenge = "1234"
+def test_evolu_sign_request_size_too_small(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ challenge = bytes.fromhex("1234")
size = -10
- proposed_value = bytes.fromhex(
- "1fa386d20efb38dbb3f7ae0509651fa36c8128324ef89fa1cfd104e10dced08c594f0e8f0a525a839b4fbfaa92b8c2b51163cef593f5c14fc9f1c8c48d1192270d"
+ proof = get_proof(
+ client,
+ b"EvoluSignRegistrationRequest",
+ [challenge, size.to_bytes(4, "big", signed=True)],
)
with pytest.raises(
@@ -121,19 +157,21 @@ def test_evolu_sign_request_size_too_small(session: Session):
match=f"Value {size} in field size_to_acquire does not fit into uint32",
):
evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=proof,
)
@pytest.mark.models("safe")
-def test_evolu_sign_request_size_too_large(session: Session):
- challenge = "1234"
+def test_evolu_sign_request_size_too_large(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ challenge = bytes.fromhex("1234")
size = 0xFFFFFFFF + 1
- proposed_value = bytes.fromhex(
- "1fa386d20efb38dbb3f7ae0509651fa36c8128324ef89fa1cfd104e10dced08c594f0e8f0a525a839b4fbfaa92b8c2b51163cef593f5c14fc9f1c8c48d1192270d"
+ proof = get_proof(
+ client, b"EvoluSignRegistrationRequest", [challenge, size.to_bytes(5, "big")]
)
with pytest.raises(
@@ -141,28 +179,34 @@ def test_evolu_sign_request_size_too_large(session: Session):
match=f"Value {size} in field size_to_acquire does not fit into uint32",
):
evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=proof,
)
@pytest.mark.models("safe")
-def test_evolu_sign_request_data_higher_bound(session: Session):
- challenge = "12" * 255
+def test_evolu_sign_request_data_higher_bound(client: Client):
+ if optiga_unavailable(client):
+ pytest.xfail("Optiga is not available on this device.")
+ delegated_identity_key = get_delegated_identity_key(client)
+ challenge = b"\x12" * 255
size = 0xFFFFFFFF
- proposed_value = bytes.fromhex(
- "1f1971f6ce302562e737520c0de2338cdaaac4e676fa02ff857b3b6081ebde794545f25905128ae9c9e7861e2358fe2e94821dd9e902564ec11478e5c6b60527c8"
+ proof = get_proof(
+ client,
+ b"EvoluSignRegistrationRequest",
+ [challenge, size.to_bytes(4, "big")],
)
response = evolu.sign_registration_request(
- session,
- challenge=bytes.fromhex(challenge),
+ client.get_session(),
+ challenge=challenge,
size=size,
- proof=proposed_value,
+ proof=proof,
)
- check_signature = bytes.fromhex(
- "304402202fedb9dee42c4cb19c27daab8c5f8cbfb74047fa65a5521e1f410a14cb0ab41502202d76b31fe1c97e4577191825bc39e0b01a3bafcba3615130175cbe11d5714832"
+
+ data = signing_buffer(delegated_identity_key, challenge, size)
+ check_signature_optiga(
+ response.signature, response.certificate_chain, client.model, data
)
- assert response.signature == check_signature
diff --git a/tests/device_tests/test_authenticate_device.py b/tests/device_tests/test_authenticate_device.py
index 2ecb97cb..2e7d5daa 100644
--- a/tests/device_tests/test_authenticate_device.py
+++ b/tests/device_tests/test_authenticate_device.py
@@ -1,73 +1,13 @@
import pytest
-from cryptography import x509
-from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.asymmetric import ec, ed25519
-from cryptography.x509 import extensions as ext
-from trezorlib import device, models
+from trezorlib import device
from trezorlib.debuglink import SessionDebugWrapper as Session
from ..common import compact_size
+from .certificate import check_signature_optiga, check_signature_tropic
pytestmark = pytest.mark.models("safe")
-OPTIGA_ROOT_PUBLIC_KEY = {
- models.T2B1: bytes.fromhex(
- "047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
- ),
- models.T3T1: bytes.fromhex(
- "04e48b69cd7962068d3cca3bcc6b1747ef496c1e28b5529e34ad7295215ea161dbe8fb08ae0479568f9d2cb07630cb3e52f4af0692102da5873559e45e9fa72959"
- ),
- models.T3B1: bytes.fromhex(
- "047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
- ),
- models.T3W1: bytes.fromhex(
- "04521192e173a9da4e3023f747d836563725372681eba3079c56ff11b2fc137ab189eb4155f371127651b5594f8c332fc1e9c0f3b80d4212822668b63189706578"
- ),
-}
-
-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",
@@ -89,26 +29,9 @@ def test_authenticate_device_optiga(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(), OPTIGA_ROOT_PUBLIC_KEY[session.model]
- )
- root_public_key.verify(
- certs[-1].signature,
- certs[-1].tbs_certificate_bytes,
- certs[-1].signature_algorithm_parameters,
- )
-
- 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())
+ check_signature_optiga(
+ proof.optiga_signature, proof.optiga_certificates, session.model, data
)
@@ -133,24 +56,10 @@ def test_authenticate_device_tropic(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.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 = bytearray(
- b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
+ data = b"\x13AuthenticateDevice:" + compact_size(len(challenge)) + challenge
+ check_signature_tropic(
+ proof.tropic_signature,
+ proof.tropic_certificates,
+ session.model,
+ data,
)
- 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.