feat(python): enable per-workflow THP ACK piggybacking
What changed, and why it matters
This commit refactors how the Trezor Python client handles low-level 'ACK' messages during encrypted THP communication. It replaces a single-workflow marker with a stack of nested contexts so multiple sessions can share a channel without sending duplicate acknowledgements. The change is a feature/cleanup, not a security fix, and does not appear to introduce a vulnerability. It mainly touches test helpers and the THP transport layer.
No security action required. Treat as normal feature/maintenance commit. Reviewers may want to confirm that nested `_ThpInteractiveContext` entries are always balanced and that `_flush_ack()` is called exactly once when the outermost context exits, to avoid protocol stalls or duplicate ACKs.
Security signals we found
Refactor of transport-layer acknowledgement logic only
No input validation, parsing, or cryptographic changes
No privilege boundary crossed
No changelog entry, consistent with internal feature work
Test-only helper changes dominate the diff
Evidence from the diff
The patch enables per-workflow ACK piggybacking in the THP (Trezor Host Protocol) Python client. Previously Channel used a single _active_workflow marker and a piggyback_acks() context manager to suppress explicit ACKs during a workflow. The new code introduces _ThpInteractiveContext, a stack-based context manager (_active_contexts) that allows nested sessions/clients to suppress redundant ACKs and flush a final ACK when the outermost context exits. TrezorClient now creates an _interact() context on __enter__ and exits it on __exit__. TrezorClientThp overrides _interact() to return a _ThpInteractiveContext. Tests were refactored to use the new helper and to pass force_flush=True in one backup-device test. No security bug is fixed or introduced by this diff.
Changed components
python/src/trezorlib/client.pypython/src/trezorlib/thp/channel.pypython/src/trezorlib/thp/client.pypython/src/trezorlib/thp/pairing.pypython/src/trezorlib/debuglink.pytests/device_tests/evolu/common.pytests/device_tests/test_msg_backup_device.pytests/device_tests/thp/connect.pytests/device_tests/thp/test_pairing.pyInspect captured patch +249 / −238
diff --git a/python/src/trezorlib/client.py b/python/src/trezorlib/client.py
index 6d805adb..90a1ad29 100644
--- a/python/src/trezorlib/client.py
+++ b/python/src/trezorlib/client.py
@@ -24,6 +24,7 @@ import typing as t
import unicodedata
import warnings
from abc import ABCMeta, abstractmethod
+from contextlib import AbstractContextManager, nullcontext
from dataclasses import dataclass
import typing_extensions as tx
@@ -245,6 +246,7 @@ class TrezorClient(t.Generic[SessionType], metaclass=ABCMeta):
self._mapping = mapping
self._features = None
self.pairing = pairing
+ self._interact_ctx = self._interact()
# ===== Internal methods for overriding in subclasses =====
@@ -284,11 +286,15 @@ class TrezorClient(t.Generic[SessionType], metaclass=ABCMeta):
"""
raise NotImplementedError
+ def _interact(self, *, force_flush: bool = False) -> AbstractContextManager:
+ return nullcontext()
+
# ===== Common implementations =====
def __enter__(self) -> tx.Self:
"""(Re)Open a connection to the device."""
self.transport.__enter__()
+ self._interact_ctx.__enter__()
return self
def __exit__(
@@ -297,7 +303,10 @@ class TrezorClient(t.Generic[SessionType], metaclass=ABCMeta):
exc_value: BaseException | None,
traceback: t.Any,
) -> None:
- self.transport.__exit__(exc_type, exc_value, traceback)
+ try:
+ self._interact_ctx.__exit__(exc_type, exc_value, traceback)
+ finally:
+ self.transport.__exit__(exc_type, exc_value, traceback)
def connect(self) -> None:
"""Establish a connection to the device.
diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py
index 1e7dcf6c..c0ac25f4 100644
--- a/python/src/trezorlib/debuglink.py
+++ b/python/src/trezorlib/debuglink.py
@@ -1372,9 +1372,8 @@ class TrezorTestContext:
self.client = self._get_client()
- self.pairing = self.client.pairing
if self.client.is_connected():
- self.pairing.skip()
+ self.client.pairing.skip()
self.sync_responses()
def reset_debug_features(self) -> None:
diff --git a/python/src/trezorlib/thp/channel.py b/python/src/trezorlib/thp/channel.py
index 00ef7e50..48e883f4 100644
--- a/python/src/trezorlib/thp/channel.py
+++ b/python/src/trezorlib/thp/channel.py
@@ -22,7 +22,6 @@ import logging
import secrets
import time
import typing as t
-from contextlib import contextmanager
from enum import Enum, IntEnum, auto
import typing_extensions as tx
@@ -40,6 +39,8 @@ from .credentials import TrezorPublicKeys, find_credential
from .message import Message
if t.TYPE_CHECKING:
+ from contextlib import AbstractContextManager
+
from noise.functions.keypair import KeyPair
from noise.noise_protocol import NoiseProtocol
@@ -111,6 +112,35 @@ class ChannelClosedError(TrezorException):
super().__init__(self.__doc__)
+class _ThpInteractiveContext:
+ """
+ Used to create THP ACK piggybacking context per THP channel.
+
+ Allows nesting via multiple sessions/clients.
+ """
+
+ def __init__(self, channel: Channel, force_flush: bool = False) -> None:
+ self.channel = channel
+ self.force_flush = force_flush
+
+ def __enter__(self) -> tx.Self:
+ self.channel._active_contexts.append(self)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: t.Any,
+ ) -> None:
+ assert self.channel._active_contexts.pop() is self
+ if not self.channel.is_ack_piggybacking_allowed:
+ return
+
+ if not self.channel._active_contexts or self.force_flush:
+ self.channel._flush_ack()
+
+
class Channel:
CHUNK_SIZE: t.ClassVar[int | None] = None
@@ -141,7 +171,7 @@ class Channel:
self._noise: NoiseConnection | None = None
self.state = channel_state
self.trezor_public_keys: TrezorPublicKeys | None = None
- self._active_workflow: object | None = None
+ self._active_contexts: list[AbstractContextManager] = []
@functools.cached_property
def is_ack_piggybacking_allowed(self) -> bool:
@@ -300,8 +330,7 @@ class Channel:
if e.code == exceptions.ThpErrorCode.DEVICE_LOCKED:
raise DeviceLockedError from e
raise
- if not self.is_ack_piggybacking_allowed:
- self._send_ack(message)
+ self._send_ack(message)
if not message.is_handshake_init_response():
raise ProtocolError(f"Not a valid handshake init response: {message}")
@@ -409,16 +438,19 @@ class Channel:
continue
raise
- def _send_ack(self, acked_message: Message | None) -> None:
- if self.is_ack_piggybacking_allowed and self._active_workflow is not None:
+ def _send_ack(self, acked_message: Message) -> None:
+ if self.is_ack_piggybacking_allowed and self._active_contexts:
+ # Skip explicit THP ACK during workflow - the next request will piggyback the correct ACK bit
return
- if acked_message is not None:
- ack = control_byte.make_ack_for(acked_message.ctrl_byte)
- ack_message = Message(ack, acked_message.cid, b"")
- else:
- ack = control_byte.make_ack(not self.sync_bit_receive)
- ack_message = Message(ack, self.channel_id, b"")
+ ack = control_byte.make_ack_for(acked_message.ctrl_byte)
+ ack_message = Message(ack, acked_message.cid, b"")
+
+ thp_io.write_payload_to_wire(self.transport, ack_message)
+
+ def _flush_ack(self) -> None:
+ ack = control_byte.make_ack(not self.sync_bit_receive)
+ ack_message = Message(ack, self.channel_id, b"")
thp_io.write_payload_to_wire(self.transport, ack_message)
@@ -441,23 +473,6 @@ class Channel:
f"Failed to read ACK in {retries} retries for message: {message}"
)
- @contextmanager
- def piggyback_acks(self, marker: object) -> t.Generator[None, None, None]:
- # Make sure the previous workflow is over.
- assert self._active_workflow is None
- self._active_workflow = marker
- # Skip explicit ACKs during this workflow
- try:
- yield
- finally:
- active = self._active_workflow
- self._active_workflow = None
- assert active is marker
- if self.is_ack_piggybacking_allowed:
- # Explicitly ACK the latest received message. The device may restart
- # the event loop, so the next request will be sent in a separate message.
- self._send_ack(None)
-
def write_chunk(self, data: bytes, /) -> None:
self._assert_handshake_done()
encrypted_data = self.noise.encrypt(data)
diff --git a/python/src/trezorlib/thp/client.py b/python/src/trezorlib/thp/client.py
index 8c240223..2ce81e0f 100644
--- a/python/src/trezorlib/thp/client.py
+++ b/python/src/trezorlib/thp/client.py
@@ -23,7 +23,7 @@ from collections import defaultdict
from .. import client, exceptions, messages, models, protobuf
from ..log import DUMP_BYTES
-from .channel import Channel
+from .channel import Channel, _ThpInteractiveContext
from .pairing import PairingController
if t.TYPE_CHECKING:
@@ -67,6 +67,7 @@ class TrezorClientThp(client.TrezorClient[ThpSession]):
mapping: ProtobufMapping | None,
model: models.TrezorModel | None,
) -> None:
+ # used to override channel creation logic in tests
channel = Channel.allocate(transport)
try:
# try to open the channel
@@ -135,6 +136,14 @@ class TrezorClientThp(client.TrezorClient[ThpSession]):
session.derive(passphrase, derive_cardano)
return session
+ def _interact(self, *, force_flush: bool = False) -> _ThpInteractiveContext:
+ """
+ Used internally by `TrezorClient` to create an THP ACK piggybacking context.
+
+ Can be also used by tests for unconditionally sending THP ACKs.
+ """
+ return _ThpInteractiveContext(channel=self.channel, force_flush=force_flush)
+
def _invalidate(self) -> None:
super()._invalidate()
# Close the channel. The client cannot be used until a channel is
diff --git a/python/src/trezorlib/thp/pairing.py b/python/src/trezorlib/thp/pairing.py
index ac5c2e80..bf2ed7ee 100644
--- a/python/src/trezorlib/thp/pairing.py
+++ b/python/src/trezorlib/thp/pairing.py
@@ -262,6 +262,7 @@ class CodeEntry(PairingMethod):
messages.ThpCodeEntryChallenge(challenge=challenge),
expect=messages.ThpCodeEntryCpaceTrezor,
)
+ self.controller.channel._flush_ack()
self.code_entry_state = CodeEntryState(
challenge=challenge,
commitment=commitment_msg.commitment,
diff --git a/tests/device_tests/evolu/common.py b/tests/device_tests/evolu/common.py
index f63b0731..a986be2a 100644
--- a/tests/device_tests/evolu/common.py
+++ b/tests/device_tests/evolu/common.py
@@ -47,17 +47,16 @@ class ThpPairingResult:
self.credential: ThpCredentialResponse = credential
-def pair_and_get_credential(client: Client) -> ThpPairingResult:
- from ..thp.connect import nfc_pairing, prepare_channel_for_pairing
+def pair_and_get_credential(test_ctx: Client) -> ThpPairingResult:
+ from ..thp.connect import prepare_channel_for_pairing
- prepare_channel_for_pairing(
- client, host_static_privkey=TEST_host_static_private_key
+ pairing = prepare_channel_for_pairing(
+ test_ctx, host_static_privkey=TEST_host_static_private_key, nfc_pairing=True
)
- nfc_pairing(client)
- credential = client.pairing.request_credential(autoconnect=False)
- client.pairing.finish()
+ credential = pairing.request_credential(autoconnect=False)
+ pairing.finish()
- return ThpPairingResult(client.get_session(), credential)
+ return ThpPairingResult(test_ctx.get_session(), credential)
def pair_and_get_invalid_credential(client: Client) -> ThpPairingResult:
@@ -78,7 +77,7 @@ def get_delegated_identity_key(client: Client) -> bytes:
if client.is_thp():
pairing_data = pair_and_get_credential(client)
return evolu.get_delegated_identity_key(
- client.get_session(),
+ session=pairing_data.session,
thp_credential=pairing_data.credential.credential,
)
elif client.is_protocol_v1():
diff --git a/tests/device_tests/test_msg_backup_device.py b/tests/device_tests/test_msg_backup_device.py
index 7948a36a..7e5af8dd 100644
--- a/tests/device_tests/test_msg_backup_device.py
+++ b/tests/device_tests/test_msg_backup_device.py
@@ -60,10 +60,13 @@ def _try_to_cancel(
next(gen)
while True:
br = yield
- # Try to cancel the backup flow on Core
- resp = session.call_raw(messages.Cancel())
- # Following #6483, backup is not cancellable
- assert resp == BACKUP_IN_PROGRESS
+ # Entering session's context will send an explicit THP ACK after `BACKUP_IN_PROGRESS` is received.
+ with session.client._interact(force_flush=True):
+ # Try to cancel the backup flow on Core
+ with pytest.raises(TrezorFailure) as exc_info:
+ session.call(messages.Cancel(), expect=messages.Failure)
+ # Following #6483, backup is not cancellable
+ assert exc_info.value.failure == BACKUP_IN_PROGRESS
try:
gen.send(br)
except StopIteration:
diff --git a/tests/device_tests/thp/connect.py b/tests/device_tests/thp/connect.py
index fc408ae4..14a5c38f 100644
--- a/tests/device_tests/thp/connect.py
+++ b/tests/device_tests/thp/connect.py
@@ -41,15 +41,16 @@ def prepare_channel_for_handshake(test_ctx: TrezorTestContext) -> None:
def prepare_channel_for_pairing(
test_ctx: TrezorTestContext,
+ *,
credential: Credential | None = None,
host_static_privkey: bytes | None = None,
fixed_entropy: bool = False,
-) -> None:
- """Create a fresh channel, perform the handshake using the provided fixed entropy
- and credentials, and leave it in the pairing phase.
- """
+ nfc_pairing: bool = False,
+) -> PairingController:
# set up a fresh channel
- prepare_channel_for_handshake(test_ctx)
+ assert isinstance(test_ctx.client, TrezorClientThp)
+ test_ctx.channel = Channel.allocate(test_ctx.transport)
+
assert host_static_privkey is None or not fixed_entropy, "don't use both"
if host_static_privkey is not None:
test_ctx.channel._init_noise(static_privkey=host_static_privkey)
@@ -57,45 +58,43 @@ def prepare_channel_for_pairing(
test_ctx.channel._init_noise(
static_privkey=b"\x12" * 32, ephemeral_privkey=b"\x24" * 32
)
+ else:
+ test_ctx.channel._init_noise()
+
credentials = []
if credential is not None:
credentials.append(credential)
# run the handshake
test_ctx.channel.open(credentials)
- assert isinstance(test_ctx.client, TrezorClientThp)
- test_ctx.client.pairing = test_ctx.pairing = PairingController(test_ctx.client)
-
-def get_encrypted_transport_protocol(test_ctx: TrezorTestContext) -> None:
- prepare_channel_for_pairing(test_ctx)
- test_ctx.pairing.skip()
+ test_ctx.client._interact_ctx = test_ctx.client._interact()
+ test_ctx.client.pairing = PairingController(test_ctx.client)
+ if nfc_pairing:
+ method = Nfc(test_ctx.client.pairing)
+ # NFC screen shown
-def break_channel(test_ctx: TrezorTestContext) -> None:
- cse = test_ctx.channel._noise.noise_protocol.cipher_state_encrypt
- cse.n = cse.n + 1
-
- session = test_ctx.client._get_any_session()
- session.write(messages.ButtonAck())
- with pytest.raises(ThpError):
- session.read(1)
+ # Read `nfc_secret` and `handshake_hash` from Trezor using debuglink
+ pairing_info = test_ctx.debug.pairing_info(
+ thp_channel_id=test_ctx.channel.channel_id.to_bytes(2, "big"),
+ handshake_hash=test_ctx.channel.handshake_hash,
+ nfc_secret_host=method.nfc_host_secret,
+ )
+ assert pairing_info.handshake_hash is not None
+ assert pairing_info.nfc_secret_trezor is not None
+ assert pairing_info.handshake_hash[:16] == test_ctx.channel.handshake_hash[:16]
+ method.send_nfc_tag(pairing_info.nfc_secret_trezor)
-def nfc_pairing(test_ctx: TrezorTestContext) -> None:
- assert isinstance(test_ctx.client, TrezorClientThp)
- method = Nfc(test_ctx.client.pairing)
+ return test_ctx.client.pairing
- # NFC screen shown
- # Read `nfc_secret` and `handshake_hash` from Trezor using debuglink
- pairing_info = test_ctx.debug.pairing_info(
- thp_channel_id=test_ctx.channel.channel_id.to_bytes(2, "big"),
- handshake_hash=test_ctx.channel.handshake_hash,
- nfc_secret_host=method.nfc_host_secret,
- )
- assert pairing_info.handshake_hash is not None
- assert pairing_info.nfc_secret_trezor is not None
- assert pairing_info.handshake_hash[:16] == test_ctx.channel.handshake_hash[:16]
+def break_channel(client: TrezorClientThp) -> None:
+ cse = client.channel._noise.noise_protocol.cipher_state_encrypt
+ cse.n = cse.n + 1
- method.send_nfc_tag(pairing_info.nfc_secret_trezor)
+ session = client._get_any_session()
+ session.write(messages.ButtonAck())
+ with pytest.raises(ThpError):
+ session.read(timeout=1)
diff --git a/tests/device_tests/thp/test_pairing.py b/tests/device_tests/thp/test_pairing.py
index 70c4267e..b25210b0 100644
--- a/tests/device_tests/thp/test_pairing.py
+++ b/tests/device_tests/thp/test_pairing.py
@@ -10,34 +10,25 @@ import typing_extensions as tx
from tests.common import get_test_address
from trezorlib import exceptions, protobuf
-from trezorlib.debuglink import TrezorTestContext as Client
+from trezorlib.debuglink import TrezorTestContext
from trezorlib.messages import (
ButtonAck,
ButtonRequest,
Cancel,
Failure,
FailureType,
- ThpCodeEntryChallenge,
- ThpCodeEntryCommitment,
- ThpCodeEntryCpaceTrezor,
ThpCredentialRequest,
ThpCredentialResponse,
ThpEndRequest,
ThpEndResponse,
- ThpPairingMethod,
ThpPairingRequest,
- ThpSelectMethod,
)
from trezorlib.models import T2T1
-from trezorlib.thp import channel, curve25519, pairing
+from trezorlib.thp import channel, curve25519
from trezorlib.thp.credentials import StaticCredential
+from trezorlib.thp.pairing import CodeEntry, ControllerLifecycle, QrCode
-from .connect import (
- break_channel,
- nfc_pairing,
- prepare_channel_for_handshake,
- prepare_channel_for_pairing,
-)
+from .connect import break_channel, prepare_channel_for_pairing
if t.TYPE_CHECKING:
P = tx.ParamSpec("P")
@@ -56,62 +47,62 @@ def deterministic_secrets() -> t.Generator[None, None, None]:
yield
-def test_pairing_qr_code(client: Client) -> None:
- if client.model != T2T1:
+def test_pairing_qr_code(test_ctx: TrezorTestContext) -> None:
+ if test_ctx.model != T2T1:
pytest.xfail(reason="UI is implemented only for T2T1")
- prepare_channel_for_pairing(client)
- method = pairing.QrCode(client.pairing)
+ pairing = prepare_channel_for_pairing(test_ctx)
+ method = QrCode(pairing)
# QR Code shown
# Read code from "Trezor's display" using debuglink
- pairing_info = client.debug.pairing_info(
- thp_channel_id=client.channel.channel_id.to_bytes(2, "big")
+ pairing_info = test_ctx.debug.pairing_info(
+ thp_channel_id=pairing.channel.channel_id.to_bytes(2, "big")
)
code = pairing_info.code_qr_code
# Compute tag for response
- sha_ctx = sha256(client.channel.handshake_hash)
+ sha_ctx = sha256(pairing.channel.handshake_hash)
assert code is not None
sha_ctx.update(code)
tag = sha_ctx.digest()
method.send_qr_code(tag)
- client.pairing.finish()
+ pairing.finish()
@pytest.mark.filterwarnings(
"ignore:One of ephemeral keypairs is already set. This is OK for testing, but should NEVER happen in production!"
)
@deterministic_secrets()
-def test_pairing_code_entry(client: Client) -> None:
- prepare_channel_for_pairing(client, fixed_entropy=True)
- method = pairing.CodeEntry(client.pairing)
+def test_pairing_code_entry(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, fixed_entropy=True)
+ method = CodeEntry(pairing)
# Code Entry code shown
- pairing_info = client.debug.pairing_info(
- thp_channel_id=client.channel.channel_id.to_bytes(2, "big")
+ pairing_info = test_ctx.debug.pairing_info(
+ thp_channel_id=pairing.channel.channel_id.to_bytes(2, "big")
)
code = pairing_info.code_entry_code
assert code is not None
method.send_code(f"{code:06}")
- client.pairing.finish()
+ pairing.finish()
@pytest.mark.filterwarnings(
"ignore:One of ephemeral keypairs is already set. This is OK for testing, but should NEVER happen in production!"
)
@deterministic_secrets()
-def test_pairing_code_entry_invalid_cpace_key(client: Client) -> None:
- prepare_channel_for_pairing(client, fixed_entropy=True)
- method = pairing.CodeEntry(client.pairing)
+def test_pairing_code_entry_invalid_cpace_key(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, fixed_entropy=True)
+ method = CodeEntry(pairing)
# Code Entry code shown
- pairing_info = client.debug.pairing_info(
- thp_channel_id=client.channel.channel_id.to_bytes(2, "big")
+ pairing_info = test_ctx.debug.pairing_info(
+ thp_channel_id=pairing.channel.channel_id.to_bytes(2, "big")
)
code = pairing_info.code_entry_code
assert code is not None
@@ -130,13 +121,15 @@ def test_pairing_code_entry_invalid_cpace_key(client: Client) -> None:
"ignore:One of ephemeral keypairs is already set. This is OK for testing, but should NEVER happen in production!"
)
@deterministic_secrets()
-def test_pairing_code_entry_invalid_cpace_key_length(client: Client) -> None:
- prepare_channel_for_pairing(client, fixed_entropy=True)
- method = pairing.CodeEntry(client.pairing)
+def test_pairing_code_entry_invalid_cpace_key_length(
+ test_ctx: TrezorTestContext,
+) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, fixed_entropy=True)
+ method = CodeEntry(pairing)
# Code Entry code shown
- pairing_info = client.debug.pairing_info(
- thp_channel_id=client.channel.channel_id.to_bytes(2, "big")
+ pairing_info = test_ctx.debug.pairing_info(
+ thp_channel_id=pairing.channel.channel_id.to_bytes(2, "big")
)
code = pairing_info.code_entry_code
assert code is not None
@@ -156,31 +149,21 @@ def test_pairing_code_entry_invalid_cpace_key_length(client: Client) -> None:
"ignore:One of ephemeral keypairs is already set. This is OK for testing, but should NEVER happen in production!"
)
@deterministic_secrets()
-def test_pairing_code_entry_cancel(client: Client) -> None:
- prepare_channel_for_pairing(client, fixed_entropy=True)
- client.pairing.start()
- session = client.pairing.session
- session.call(
- ThpSelectMethod(selected_pairing_method=ThpPairingMethod.CodeEntry),
- expect=ThpCodeEntryCommitment,
- )
- session.call(
- ThpCodeEntryChallenge(challenge=secrets.token_bytes(16)),
- expect=ThpCodeEntryCpaceTrezor,
- )
-
+def test_pairing_code_entry_cancel(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, fixed_entropy=True)
+ CodeEntry(pairing)
# Code Entry code shown
# Press Cancel button
- client.debug.press_yes()
- failure = Failure.ensure_isinstance(session.read())
+ test_ctx.debug.press_yes()
+ failure = Failure.ensure_isinstance(pairing.session.read())
assert failure.code is FailureType.ActionCancelled
-def test_pairing_cancel_1(client: Client) -> None:
- prepare_channel_for_pairing(client)
+def test_pairing_cancel_1(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx)
- session = client.pairing.session
+ session = pairing.session
session.write(
ThpPairingRequest(host_name="localhost", app_name="TestTrezor Cancel 1")
)
@@ -193,42 +176,39 @@ def test_pairing_cancel_1(client: Client) -> None:
assert failure.code == FailureType.ActionCancelled
-def test_pairing_cancel_2(client: Client) -> None:
- prepare_channel_for_pairing(client)
+def test_pairing_cancel_2(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx)
- session = client.pairing.session
+ session = pairing.session
session.write(
ThpPairingRequest(host_name="localhost", app_name="TestTrezor Cancel 2")
)
button_req = ButtonRequest.ensure_isinstance(session.read())
assert button_req.name == "thp_pairing_request"
session.write(ButtonAck())
- client.debug.press_no()
+ test_ctx.debug.press_no()
failure = Failure.ensure_isinstance(session.read())
assert failure.code == FailureType.ActionCancelled
-def test_pairing_nfc(client: Client) -> None:
- prepare_channel_for_pairing(client)
- nfc_pairing(client)
- client.pairing.finish()
+def test_pairing_nfc(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, nfc_pairing=True)
+ pairing.finish()
-def test_connection_confirmation_cancel(client: Client) -> None:
- prepare_channel_for_pairing(client)
- nfc_pairing(client)
+def test_connection_confirmation_cancel(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, nfc_pairing=True)
# Request credential with confirmation after pairing
- credential = client.pairing.request_credential()
- client.pairing.finish()
+ credential = pairing.request_credential()
+ pairing.finish()
- break_channel(client)
+ break_channel(pairing.client)
# Connect using credential with confirmation
- prepare_channel_for_handshake(client)
- client.channel.open([credential])
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
- session = client.pairing.session
+ session = pairing.session
session.write(ThpEndRequest())
button_req = ButtonRequest.ensure_isinstance(session.read())
assert button_req.name == "thp_connection_request"
@@ -236,34 +216,32 @@ def test_connection_confirmation_cancel(client: Client) -> None:
failure = Failure.ensure_isinstance(session.read())
assert failure.code == FailureType.ActionCancelled
- time.sleep(0.2) # TODO fix this behavior
- prepare_channel_for_handshake(client)
- client.channel.open([credential])
- assert client.pairing.is_paired()
- client.pairing.finish()
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
+ assert pairing.is_paired()
+ pairing.finish()
-def test_autoconnect_credential_request_cancel(client: Client) -> None:
- prepare_channel_for_pairing(client)
- nfc_pairing(client)
+def test_autoconnect_credential_request_cancel(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, nfc_pairing=True)
# Request credential with confirmation after pairing
- credential = client.pairing.request_credential()
- client.pairing.finish()
- break_channel(client)
+ credential = pairing.request_credential()
+ pairing.finish()
+ break_channel(pairing.client)
+
# Connect using credential with confirmation and request autoconnect
- prepare_channel_for_pairing(client, credential=credential)
- session = client.pairing.session
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
+ session = pairing.session
session.write(
ThpCredentialRequest(
- host_static_public_key=client.channel.get_host_static_pubkey(),
+ host_static_public_key=pairing.channel.get_host_static_pubkey(),
autoconnect=True,
)
)
button_req = ButtonRequest.ensure_isinstance(session.read())
assert button_req.name == "thp_connection_request"
session.write(ButtonAck())
- client.debug.press_yes()
+ test_ctx.debug.press_yes()
button_req = ButtonRequest.ensure_isinstance(session.read())
assert button_req.name == "thp_autoconnect_credential_request"
session.write(Cancel())
@@ -271,34 +249,33 @@ def test_autoconnect_credential_request_cancel(client: Client) -> None:
assert failure.code == FailureType.ActionCancelled
-def test_credential_phase(client: Client) -> None:
- prepare_channel_for_pairing(client)
- nfc_pairing(client)
+def test_credential_phase(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, nfc_pairing=True)
# Request credential with confirmation after pairing
- credential = client.pairing.request_credential(autoconnect=False)
- client.pairing.finish()
+ credential = pairing.request_credential(autoconnect=False)
+ pairing.finish()
- break_channel(client)
+ break_channel(pairing.client)
# Connect using credential with confirmation
- prepare_channel_for_handshake(client)
- client.channel.open([credential])
- assert client.pairing.is_paired()
- with client:
- client.set_expected_responses(
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
+ assert pairing.is_paired()
+ with test_ctx:
+ test_ctx.set_expected_responses(
[ButtonRequest(name="thp_connection_request"), ThpEndResponse]
)
- client.pairing.finish()
+ pairing.finish()
# Delete channel from the device by sending badly encrypted message
# This is done to prevent channel replacement and trigerring of autoconnect false -> true
- break_channel(client)
+ break_channel(pairing.client)
# Connect using credential with confirmation and ask for autoconnect credential.
- prepare_channel_for_pairing(client, credential=credential)
- with client:
- client.set_expected_responses(
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
+ assert pairing.is_paired()
+ with test_ctx:
+ test_ctx.set_expected_responses(
[
ButtonRequest(name="thp_connection_request"),
ButtonRequest(name="thp_autoconnect_credential_request"),
@@ -306,46 +283,50 @@ def test_credential_phase(client: Client) -> None:
ThpEndResponse,
]
)
- credential_auto = client.pairing.request_credential(autoconnect=True)
- client.pairing.finish()
+ credential_auto = pairing.request_credential(autoconnect=True)
+ pairing.finish()
# Connect using credential with confirmation
- prepare_channel_for_pairing(client, credential=credential)
- with client:
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
+ assert pairing.is_paired()
+ with test_ctx:
# Confirmation dialog is not shown as channel in ENCRYPTED TRANSPORT state with the same
# host static public key is still available in Trezor's cache. (Channel replacement is triggered.)
- client.set_expected_responses([ThpEndResponse])
- client.pairing.finish()
+ test_ctx.set_expected_responses([ThpEndResponse])
+ pairing.finish()
# Connect using autoconnect credential
- prepare_channel_for_pairing(client, credential=credential_auto)
- with client:
- client.set_expected_responses([ThpEndResponse])
- client.pairing.finish()
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential_auto)
+ assert pairing.is_paired()
+ with test_ctx:
+ test_ctx.set_expected_responses([ThpEndResponse])
+ pairing.finish()
# Delete channel from the device by sending badly encrypted message
# This is done to prevent channel replacement and trigerring of autoconnect false -> true
- break_channel(client)
+ break_channel(pairing.client)
# Connect using autoconnect credential - should work the same as above
- prepare_channel_for_pairing(client, credential=credential_auto)
- with client:
- client.set_expected_responses([ThpEndResponse])
- client.pairing.finish()
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential_auto)
+ assert pairing.is_paired()
+ with test_ctx:
+ test_ctx.set_expected_responses([ThpEndResponse])
+ pairing.finish()
-def test_credential_request_in_encrypted_transport_phase(client: Client) -> None:
- prepare_channel_for_pairing(client)
- nfc_pairing(client)
+def test_credential_request_in_encrypted_transport_phase(
+ test_ctx: TrezorTestContext,
+) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, nfc_pairing=True)
# Request credential with confirmation after pairing
- credential = client.pairing.request_credential()
- client.pairing.finish()
+ credential = pairing.request_credential()
+ pairing.finish()
- session = client.get_seedless_session()
- session.call(
+ seedless_session = test_ctx.client.get_session(None)
+ seedless_session.call(
ThpCredentialRequest(
- host_static_public_key=client.channel.get_host_static_pubkey(),
+ host_static_public_key=pairing.channel.get_host_static_pubkey(),
autoconnect=True,
credential=credential.credential,
),
@@ -354,70 +335,66 @@ def test_credential_request_in_encrypted_transport_phase(client: Client) -> None
@pytest.mark.setup_client(passphrase=True)
-def test_channel_replacement(client: Client) -> None:
- assert client.features.passphrase_protection is True
+def test_channel_replacement(test_ctx: TrezorTestContext) -> None:
+ assert test_ctx.features.passphrase_protection is True
- session = client.get_session(passphrase="TREZOR")
- address = get_test_address(session)
+ session_1 = test_ctx.get_session(passphrase="TREZOR")
+ address_1 = get_test_address(session_1)
- session_2 = client.get_session(passphrase="ROZERT")
+ session_2 = test_ctx.get_session(passphrase="ROZERT")
address_2 = get_test_address(session_2)
- assert address != address_2
+ assert address_1 != address_2
# create new channel using the same host_static_private_key
- prepare_channel_for_pairing(
- client, host_static_privkey=client.channel.host_static_privkey
+ pairing = prepare_channel_for_pairing(
+ test_ctx,
+ host_static_privkey=test_ctx.channel.host_static_privkey,
)
- client.pairing.skip()
+ pairing.skip()
- session_3 = client.get_session(passphrase="OKIDOKI")
+ session_3 = test_ctx.get_session(passphrase="OKIDOKI")
address_3 = get_test_address(session_3)
assert address_3 != address_2
# test address on regenerated channel
- new_address = get_test_address(session)
- assert address == new_address
- new_address_3 = get_test_address(session_3)
- assert address_3 == new_address_3
+ assert address_1 == get_test_address(session_1)
+ assert address_3 == get_test_address(session_3)
- host_static_privkey_orig = client.channel.host_static_privkey
+ host_static_privkey_orig = pairing.channel.host_static_privkey
# create new channel using different host_static_private_key
- prepare_channel_for_pairing(client)
- assert client.channel.host_static_privkey != host_static_privkey_orig
- client.pairing.skip()
+ pairing = prepare_channel_for_pairing(test_ctx)
+ assert pairing.channel.host_static_privkey != host_static_privkey_orig
+ pairing.skip()
with pytest.raises(exceptions.InvalidSessionError):
- _ = get_test_address(session)
+ _ = get_test_address(session_1)
with pytest.raises(exceptions.InvalidSessionError):
_ = get_test_address(session_3)
- session_4 = client.get_session(passphrase="TREZOR")
- super_new_address = get_test_address(session_4)
- assert address == super_new_address
+ assert address_1 == get_test_address(test_ctx.get_session(passphrase="TREZOR"))
-def test_credential_for_different_key(client: Client) -> None:
- prepare_channel_for_pairing(client)
- nfc_pairing(client)
+def test_credential_for_different_key(test_ctx: TrezorTestContext) -> None:
+ pairing = prepare_channel_for_pairing(test_ctx, nfc_pairing=True)
- assert client.pairing.state is pairing.ControllerLifecycle.PAIRING_COMPLETED
- assert client.channel.state is channel.ChannelState.CREDENTIAL_PHASE
+ assert pairing.state is ControllerLifecycle.PAIRING_COMPLETED
+ assert pairing.channel.state is channel.ChannelState.CREDENTIAL_PHASE
host_secret = secrets.token_bytes(32)
host_privkey = curve25519.get_private_key(host_secret)
host_pubkey = curve25519.get_public_key(host_privkey)
- assert host_privkey != client.channel.host_static_privkey
+ assert host_privkey != pairing.channel.host_static_privkey
- credential_response = client.pairing._call(
+ credential_response = pairing._call(
ThpCredentialRequest(
host_static_public_key=host_pubkey,
autoconnect=False,
),
expect=ThpCredentialResponse,
)
- client.pairing.finish()
+ pairing.finish()
credential = StaticCredential(
trezor_pubkey=credential_response.trezor_static_public_key,
@@ -426,6 +403,6 @@ def test_credential_for_different_key(client: Client) -> None:
)
# try to connect using the new credential
- prepare_channel_for_pairing(client, credential=credential)
- assert client.channel.state is channel.ChannelState.CREDENTIAL_PHASE
- client.pairing.finish()
+ pairing = prepare_channel_for_pairing(test_ctx, credential=credential)
+ assert pairing.channel.state is channel.ChannelState.CREDENTIAL_PHASE
+ pairing.finish()
Why this scored 19/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.