diff --git a/tests/common.py b/tests/common.py index fb37e69732..4379aad560 100644 --- a/tests/common.py +++ b/tests/common.py @@ -263,3 +263,14 @@ def get_test_address(client: "Client") -> str: """Fetch a testnet address on a fixed path. Useful to make a pin/passphrase protected call, or to identify the root secret (seed+passphrase)""" return btc.get_address(client, "Testnet", TEST_ADDRESS_N) + + +def compact_size(n) -> bytes: + if n < 253: + return n.to_bytes(1, "little") + elif n < 0x1_0000: + return bytes([253]) + n.to_bytes(2, "little") + elif n < 0x1_0000_0000: + return bytes([254]) + n.to_bytes(4, "little") + else: + return bytes([255]) + n.to_bytes(8, "little") diff --git a/tests/device_tests/bitcoin/payment_req.py b/tests/device_tests/bitcoin/payment_req.py index 9ab2d4dad8..5d987706df 100644 --- a/tests/device_tests/bitcoin/payment_req.py +++ b/tests/device_tests/bitcoin/payment_req.py @@ -5,6 +5,8 @@ from ecdsa import ECDH, SECP256k1, SigningKey from trezorlib import btc, messages +from ...common import compact_size + SLIP44 = 1 # Testnet TextMemo = namedtuple("TextMemo", "text") @@ -20,13 +22,7 @@ payment_req_signer = SigningKey.from_string( def hash_bytes_prefixed(hasher, data): - n = len(data) - if n < 253: - hasher.update(n.to_bytes(1, "little")) - elif n < 0x1_0000: - hasher.update(bytes([253])) - hasher.update(n.to_bytes(2, "little")) - + hasher.update(compact_size(len(data))) hasher.update(data) diff --git a/tests/device_tests/test_authenticate_device.py b/tests/device_tests/test_authenticate_device.py new file mode 100644 index 0000000000..f197b13f4e --- /dev/null +++ b/tests/device_tests/test_authenticate_device.py @@ -0,0 +1,77 @@ +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509 import extensions as ext + +from trezorlib import device +from trezorlib.debuglink import TrezorClientDebugLink as Client + +from ..common import compact_size + +pytestmark = [pytest.mark.skip_t1, pytest.mark.skip_t2] + +ROOT_PUBLIC_KEY = bytes.fromhex( + "04626d58aca84f0fcb52ea63f0eb08de1067b8d406574a715d5e7928f4b67f113a00fb5c5918e74d2327311946c446b242c20fe7347482999bdc1e229b94e27d96" +) + + +@pytest.mark.parametrize( + "challenge", + ( + b"", + b"hello world", + b"\x00" * 1024, + bytes.fromhex( + "21f3d40e63c304d0312f62eb824113efd72ba1ee02bef6777e7f8a7b6f67ba16" + ), + ), +) +def test_authenticate_device(client: Client, challenge: bytes) -> None: + # NOTE Applications must generate a random challenge for each request. + + # Issue an AuthenticateDevice challenge to Trezor. + proof = device.authenticate(client, challenge) + certs = [x509.load_der_x509_certificate(cert) for cert in proof.certificates] + + # Verify the last certificate in the certificate chain against trust anchor. + root_public_key = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), ROOT_PUBLIC_KEY + ) + root_public_key.verify( + certs[-1].signature, + certs[-1].tbs_certificate_bytes, + certs[-1].signature_algorithm_parameters, + ) + + # Verify the certificate chain. + 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_cert.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(client.features.internal_model) + + # 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())) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 3b4c1708e6..fd000334da 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1760,6 +1760,10 @@ "TR_stellar-test_stellar.py::test_sign_tx[timebounds-0-1575234180]": "9e1f884816d80415ef9b925309d5502852d8bade9c632f2d715d0a2cdecb1204", "TR_stellar-test_stellar.py::test_sign_tx[timebounds-461535181-0]": "afc84260d5f7991a516ead25a0f77c34ec14ef2b3de3595adc1c50624657ac0a", "TR_stellar-test_stellar.py::test_sign_tx[timebounds-461535181-1575234180]": "8fcc98ebb2f76f36f85c313131478d29b3b2ba79e2ef5993afa3cb7a663ed63a", +"TR_test_authenticate_device.py::test_authenticate_device[!\\xf3\\xd4\\x0ec\\xc3\\x04\\xd01-b\\xeb\\x82-e4b4eb3a": "5bbde07a26ce37bd72d0c792c4dc9807c169c9fc731d1bcf30a72a5f2da7f602", +"TR_test_authenticate_device.py::test_authenticate_device[\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\-d824e03c": "5bbde07a26ce37bd72d0c792c4dc9807c169c9fc731d1bcf30a72a5f2da7f602", +"TR_test_authenticate_device.py::test_authenticate_device[]": "5bbde07a26ce37bd72d0c792c4dc9807c169c9fc731d1bcf30a72a5f2da7f602", +"TR_test_authenticate_device.py::test_authenticate_device[hello world]": "5bbde07a26ce37bd72d0c792c4dc9807c169c9fc731d1bcf30a72a5f2da7f602", "TR_test_autolock.py::test_apply_auto_lock_delay": "cf93576944395035a45cf62f4b4b22387c595af7646e1863b07bdcc38ccfbb20", "TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[0]": "674ca13c50b342d80ca47ed49bcfaa846bb07db2ba30d6e1414c96db8375457b", "TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[1]": "674ca13c50b342d80ca47ed49bcfaa846bb07db2ba30d6e1414c96db8375457b",