chore(core): remove THP fallback support
What changed, and why it matters
This commit removes a temporary fallback mechanism from the Trezor hardware wallet's core firmware. The fallback feature was used during encrypted communication when normal memory buffers were busy. The commit message says it will be reimplemented later after internal refactoring. There is no indication in the commit itself that this fixes a security vulnerability; it appears to be a routine cleanup of unfinished code.
No immediate action required. Treat as a feature-removal refactor. If the fallback behavior was relied upon for multi-host concurrency, monitor subsequent commits for the promised reimplementation and verify that dropping packets when buffers are locked does not introduce denial-of-service or reliability issues.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change deletes core/src/trezor/wire/thp/fallback.py and removes all references to the Fallback class from the THP (Trezor Host Protocol) channel implementation. It also removes the associated test and UI fixtures. The fallback logic previously allowed the channel to decrypt and respond to messages using a local backup buffer when the shared memory_manager read/write buffer was locked by another channel. After this change, if the buffer is locked, the channel simply resets its state and drops the packet. The commit message frames this as a chore to be reimplemented after event-loop refactoring.
Changed components
core/src/trezor/wire/thp/channel.pycore/src/trezor/wire/thp/fallback.pycore/embed/upymod/qstrdefsport.htests/device_tests/thp/test_multiple_hosts.pytests/ui_tests/fixtures.jsonInspect captured patch +15 / −378
diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h
index a2f581230..350a8ee50 100644
--- a/core/embed/upymod/qstrdefsport.h
+++ b/core/embed/upymod/qstrdefsport.h
@@ -411,7 +411,6 @@ Q(control_byte)
Q(cpace)
Q(credential_manager)
Q(crypto)
-Q(fallback)
Q(interface_manager)
Q(memory_manager)
Q(pairing)
@@ -434,7 +433,6 @@ Q(trezor.wire.thp.checksum)
Q(trezor.wire.thp.control_byte)
Q(trezor.wire.thp.cpace)
Q(trezor.wire.thp.crypto)
-Q(trezor.wire.thp.fallback)
Q(trezor.wire.thp.interface_manager)
Q(trezor.wire.thp.memory_manager)
Q(trezor.wire.thp.pairing_context)
diff --git a/core/src/trezor/wire/thp/channel.py b/core/src/trezor/wire/thp/channel.py
index cbbbc8183..f69ff3735 100644
--- a/core/src/trezor/wire/thp/channel.py
+++ b/core/src/trezor/wire/thp/channel.py
@@ -21,7 +21,6 @@ from storage.cache_thp import (
)
from trezor import loop, protobuf, utils, workflow
from trezor.wire.errors import WireBufferError
-from trezor.wire.thp.fallback import Fallback
from . import ENCRYPTED, ChannelState, PacketHeader, ThpDecryptionError, ThpError
from . import alternating_bit_protocol as ABP
@@ -74,7 +73,6 @@ class Channel:
self.channel_cache: ChannelCache = channel_cache
# Shared variables
- self.buffer: utils.BufferType = bytearray(self.iface.TX_PACKET_LEN)
self.bytes_read: int = 0
self.expected_payload_length: int = 0
self.is_cont_packet_expected: bool = False
@@ -85,7 +83,6 @@ class Channel:
self.write_task_spawn: loop.spawn | None = None
# Temporary objects
- self._fallback: Fallback | None = None
self.handshake: crypto.Handshake | None = None
self.credential: ThpPairingCredential | None = None
self.connection_context: PairingContext | None = None
@@ -165,7 +162,7 @@ class Channel:
self.get_channel_id_int()
)
if __debug__:
- self._log("self.buffer: ", hexlify_if_bytes(buffer))
+ self._log("buffer: ", hexlify_if_bytes(buffer))
except WireBufferError:
if __debug__:
self._log(
@@ -174,56 +171,8 @@ class Channel:
logger=log.warning,
)
pass # TODO ??
- if (
- self._fallback is not None
- and self.expected_payload_length == self.bytes_read
- ):
-
- self._fallback.finish()
- if not self._fallback.is_crc_checksum_valid():
- if __debug__:
- self._log("INVALID FALLBACK CRC", logger=log.warning)
- return None
-
- # Check ABP seq bit
- seq_bit = control_byte.get_seq_bit(self._fallback.ctrl_byte)
- if not ABP.has_msg_correct_seq_bit(self.channel_cache, seq_bit):
- if __debug__:
- self._log(
- "Received message with an unexpected sequential bit!",
- logger=log.warning,
- )
- return received_message_handler._send_ack(self, ack_bit=seq_bit)
-
- # Check noise tag
- if not self._fallback.is_noise_tag_valid():
- if __debug__:
- self._log("Invalid fallback noise tag", logger=log.warning)
- raise ThpDecryptionError()
-
- # Update nonces and seq bit
- nonce_receive = self.channel_cache.get_int(CHANNEL_NONCE_RECEIVE)
- assert nonce_receive is not None
- self.channel_cache.set_int(CHANNEL_NONCE_RECEIVE, nonce_receive + 1)
- ABP.set_expected_receive_seq_bit(self.channel_cache, 1 - seq_bit)
- self._finish_message()
- sid = self._fallback.session_id or 0
- self._clear_fallback()
-
- from trezor.enums import FailureType
- from trezor.messages import Failure
-
- return self.write(
- Failure(code=FailureType.Busy, message="FALLBACK!"),
- session_id=sid,
- fallback=True,
- )
-
- if (
- self._fallback is None
- and self.expected_payload_length + INIT_HEADER_LENGTH == self.bytes_read
- ):
+ if self.expected_payload_length + INIT_HEADER_LENGTH == self.bytes_read:
self._finish_message()
return received_message_handler.handle_received_message(self, buffer)
elif self.expected_payload_length + INIT_HEADER_LENGTH > self.bytes_read:
@@ -241,17 +190,13 @@ class Channel:
)
return None
- def _handle_received_packet(
- self, packet: utils.BufferType
- ) -> Awaitable[None] | None:
+ def _handle_received_packet(self, packet: utils.BufferType) -> None:
ctrl_byte = packet[0]
if control_byte.is_continuation(ctrl_byte):
- self._handle_cont_packet(packet)
- return None
+ return self._handle_cont_packet(packet)
return self._handle_init_packet(packet)
- def _handle_init_packet(self, packet: utils.BufferType) -> Awaitable[None] | None:
- self._fallback = None
+ def _handle_init_packet(self, packet: utils.BufferType) -> None:
self.bytes_read = 0
self.expected_payload_length = 0
@@ -267,7 +212,7 @@ class Channel:
if self.expected_payload_length != CHECKSUM_LENGTH:
raise ThpError("Invalid ACK length, ignoring")
self.bytes_read = INIT_HEADER_LENGTH + CHECKSUM_LENGTH
- return None
+ return
# If the channel does not "own" the buffer lock, decrypt the first packet
@@ -276,51 +221,16 @@ class Channel:
try:
buffer = memory_manager.get_new_read_buffer(cid, length)
except WireBufferError:
- # Channel does not "own" the buffer lock, decrypt the first packet
-
- try:
- if not self._can_fallback():
- if __debug__:
- self._log(
- "Channel is in a state that does not support fallback.",
- logger=log.error,
- )
- raise Exception(
- "Channel is in a state that does not support fallback."
- )
- if __debug__:
- self._log("Started fallback read")
- self._fallback = Fallback(self, memoryview(packet))
-
- except Exception:
- self._fallback = None
- self.expected_payload_length = 0
- self.bytes_read = 0
- if __debug__:
- from ubinascii import hexlify
-
- self._log(
- "FAILED TO FALLBACK: ",
- hexlify(packet).decode(),
- logger=log.error,
- )
- return None
-
- to_read_len = min(len(packet) - INIT_HEADER_LENGTH, payload_length)
- buf = memoryview(self.buffer)[:to_read_len]
- utils.memcpy(buf, 0, packet, INIT_HEADER_LENGTH)
-
- # Fallback
- fallback_task = self._fallback.read_init_packet(buf)
- self.bytes_read += to_read_len
- return fallback_task
+ # Channel does not "own" the buffer lock
+ self.expected_payload_length = 0
+ self.bytes_read = 0
+ return
if __debug__:
self._log("handle_init_packet - payload len: ", str(payload_length))
self._log("handle_init_packet - buffer len: ", str(len(buffer)))
self._buffer_packet_data(buffer, packet, 0)
- return None
def _handle_cont_packet(self, packet: utils.BufferType) -> None:
if __debug__:
@@ -329,18 +239,6 @@ class Channel:
if not self.is_cont_packet_expected:
raise ThpError("Continuation packet is not expected, ignoring")
- if self._fallback is not None:
- to_read_len = min(
- len(packet) - CONT_HEADER_LENGTH,
- self.expected_payload_length - self.bytes_read,
- )
- buf = memoryview(self.buffer)[:to_read_len]
- utils.memcpy(buf, 0, packet, CONT_HEADER_LENGTH)
-
- self._fallback.read_cont_packet(buf)
-
- self.bytes_read += to_read_len
- return
try:
buffer = memory_manager.get_existing_read_buffer(self.get_channel_id_int())
except WireBufferError:
@@ -359,11 +257,6 @@ class Channel:
self.expected_payload_length = 0
self.is_cont_packet_expected = False
- def _clear_fallback(self) -> None:
- self._fallback = None
- if __debug__:
- self._log("Finish fallback")
-
def decrypt_buffer(
self, message_length: int, offset: int = INIT_HEADER_LENGTH
) -> None:
@@ -409,7 +302,6 @@ class Channel:
msg: protobuf.MessageType,
session_id: int = 0,
force: bool = False,
- fallback: bool = False,
) -> None:
if __debug__:
self._log(
@@ -428,44 +320,11 @@ class Channel:
msg_size = protobuf.encoded_length(msg)
payload_size = SESSION_ID_LENGTH + MESSAGE_TYPE_LENGTH + msg_size
length = payload_size + CHECKSUM_LENGTH + TAG_LENGTH + INIT_HEADER_LENGTH
- try:
- if fallback:
- buffer = self.buffer
- else:
- buffer = memory_manager.get_new_write_buffer(cid, length)
- noise_payload_len = memory_manager.encode_into_buffer(
- buffer, msg, session_id
- )
- except WireBufferError:
- from trezor.enums import FailureType
- from trezor.messages import Failure
- if length <= len(self.buffer):
- # Fallback write - Write buffer is locked, using backup buffer instead
- noise_payload_len = memory_manager.encode_into_buffer(
- self.buffer, msg, session_id
- )
- task = self._write_and_encrypt(noise_payload_len, fallback=True)
- if task is not None:
- await task
- return
+ buffer = memory_manager.get_new_write_buffer(cid, length)
+ noise_payload_len = memory_manager.encode_into_buffer(buffer, msg, session_id)
- # Message cannot be written - not even in fallback mode, killing channel
- if __debug__:
- self._log("Failed to get write buffer, killing channel.")
-
- noise_payload_len = memory_manager.encode_into_buffer(
- self.buffer,
- Failure(
- code=FailureType.FirmwareError,
- message="Failed to obtain write buffer.",
- ),
- session_id,
- )
- self.set_channel_state(ChannelState.INVALIDATED)
- task = self._write_and_encrypt(
- noise_payload_len=noise_payload_len, force=force, fallback=fallback
- )
+ task = self._write_and_encrypt(noise_payload_len=noise_payload_len, force=force)
if task is not None:
await task
@@ -485,12 +344,8 @@ class Channel:
self,
noise_payload_len: int,
force: bool = False,
- fallback: bool = False,
) -> Awaitable[None] | None:
- if fallback:
- buffer = self.buffer
- else:
- buffer = memory_manager.get_existing_write_buffer(self.get_channel_id_int())
+ buffer = memory_manager.get_existing_write_buffer(self.get_channel_id_int())
self._encrypt(buffer, noise_payload_len)
payload_length = noise_payload_len + TAG_LENGTH
@@ -500,18 +355,6 @@ class Channel:
if __debug__:
self._log("Closed write task", logger=log.warning)
self._prepare_write()
- if fallback:
- if __debug__:
- self._log(
- "Writing FALLBACK message (written only once without async or retransmission)."
- )
-
- return self._write_encrypted_payload_loop(
- ctrl_byte=ENCRYPTED,
- payload=memoryview(buffer[:payload_length]),
- only_once=True,
- )
-
if force:
if __debug__:
self._log("Writing FORCE message (without async or retransmission).")
@@ -588,14 +431,6 @@ class Channel:
not workflow.tasks
) and self.get_channel_state() is ChannelState.ENCRYPTED_TRANSPORT
- def _can_fallback(self) -> bool:
- state = self.get_channel_state()
- return state not in [
- ChannelState.TH1,
- ChannelState.TH2,
- ChannelState.UNALLOCATED,
- ]
-
if __debug__:
def _log(self, text_1: str, text_2: str = "", logger: Any = log.debug) -> None:
diff --git a/core/src/trezor/wire/thp/fallback.py b/core/src/trezor/wire/thp/fallback.py
deleted file mode 100644
index 759f1988f..000000000
--- a/core/src/trezor/wire/thp/fallback.py
+++ /dev/null
@@ -1,159 +0,0 @@
-from typing import TYPE_CHECKING
-
-from storage.cache_common import CHANNEL_KEY_RECEIVE, CHANNEL_NONCE_RECEIVE
-from storage.cache_thp import TAG_LENGTH
-from trezor import utils
-from trezor.wire.errors import DataError
-from trezor.wire.thp import received_message_handler
-from trezor.wire.thp.writer import INIT_HEADER_LENGTH
-
-from . import checksum, control_byte
-from .checksum import CHECKSUM_LENGTH
-from .crypto import BusyDecoder
-
-if TYPE_CHECKING:
- from typing import Awaitable
-
- from .channel import Channel
-
-
-class Fallback:
- _busy_decoder: BusyDecoder | None = None
- _noise_tag: bytearray | None = None
- session_id: int | None = None
-
- def __init__(self, channel: Channel, init_packet: memoryview) -> None:
- if len(init_packet) <= INIT_HEADER_LENGTH + CHECKSUM_LENGTH:
- raise ValueError("Invalid init packet - too short")
-
- self._channel: Channel = channel
- self.ctrl_byte: int = init_packet[0]
- self._crc_compare: bytearray = bytearray(4)
- self._is_finished: bool = False
-
- self._crc: int = checksum.compute_int(init_packet[:INIT_HEADER_LENGTH])
-
- def read_init_packet(self, buf: memoryview) -> Awaitable[None] | None:
- self._handle_crc(buf)
-
- # If the message has only one packet, handle ACK messages
- if len(buf) == self._channel.expected_payload_length:
- if not self._is_crc_checksum_valid():
- return None
- if control_byte.is_ack(self.ctrl_byte):
- ack_bit = control_byte.get_ack_bit(self.ctrl_byte)
- return received_message_handler.handle_ack(self._channel, ack_bit)
-
- self._prepare_decryption(buf)
- self._handle_decryption(buf)
- return None
-
- def read_cont_packet(self, buf: memoryview) -> None:
- self._handle_crc(buf)
- self._handle_decryption(buf)
-
- def finish(self) -> None:
- if self._is_finished:
- raise Exception("Fallback already finished!")
- self._is_finished = True
-
- def is_crc_checksum_valid(self) -> bool:
- if not self._is_finished:
- raise Exception("Fallback is not finished yet!")
- return self._is_crc_checksum_valid()
-
- def _is_crc_checksum_valid(self) -> bool:
- assert self._crc_compare is not None
- return self._crc.to_bytes(4, "big") == self._crc_compare
-
- def is_noise_tag_valid(self) -> bool:
- if not self._is_finished:
- raise Exception("Fallback is not finished yet!")
- assert self._busy_decoder is not None
- assert self._noise_tag is not None
- return self._busy_decoder.finish_and_check_tag(self._noise_tag)
-
- def _handle_crc(self, buf: memoryview) -> None:
- if (
- self._channel.expected_payload_length
- > len(buf) + self._channel.bytes_read + CHECKSUM_LENGTH
- ):
- # The CRC checksum is not in this packet, compute crc over whole buffer
- self._crc = checksum.compute_int(buf, self._crc)
- elif (
- self._channel.expected_payload_length >= len(buf) + self._channel.bytes_read
- ):
- # At least a part of the CRC checksum is in this packet, compute CRC over
- # the first (max(0, crc_copy_len)) bytes and add the rest of the bytes
- # (max 4) as the checksum from message into temp_crc_compare
- crc_copy_len = (
- self._channel.expected_payload_length
- - self._channel.bytes_read
- - CHECKSUM_LENGTH
- )
- self._crc = checksum.compute_int(buf[:crc_copy_len], self._crc)
-
- crc_checksum = buf[
- self._channel.expected_payload_length
- - CHECKSUM_LENGTH
- - len(buf)
- - self._channel.bytes_read :
- ]
- offset = CHECKSUM_LENGTH - len(buf[-CHECKSUM_LENGTH:])
- utils.memcpy(self._crc_compare, offset, crc_checksum, 0)
- else:
- raise DataError(
- f"Buffer (+bytes_read) ({len(buf)}+{self._channel.bytes_read})should not be bigger than payload{self._channel.expected_payload_length}"
- )
-
- def _prepare_decryption(self, buf: memoryview) -> None:
- key_receive = self._channel.channel_cache.get(CHANNEL_KEY_RECEIVE)
- nonce_receive = self._channel.channel_cache.get_int(CHANNEL_NONCE_RECEIVE)
-
- assert key_receive is not None
- assert nonce_receive is not None
-
- self._busy_decoder = BusyDecoder(key_receive, nonce_receive)
- self._noise_tag = bytearray(16)
-
- def _handle_decryption(self, buf: memoryview) -> None:
- if self._busy_decoder is None:
- raise Exception("Fallback decryption is not prepared")
-
- assert self._noise_tag is not None
-
- if (
- self._channel.expected_payload_length
- > len(buf) + self._channel.bytes_read + CHECKSUM_LENGTH + TAG_LENGTH
- ):
- # The noise tag is not in this packet, decrypt the whole buffer
- self._busy_decoder.decrypt_part(buf)
- elif (
- self._channel.expected_payload_length >= len(buf) + self._channel.bytes_read
- ):
- # At least a part of the noise tag is in this packet, decrypt
- # the first (max(0, dec_len)) bytes and add the rest of the bytes
- # as the noise_tag from message into temp_tag
- dec_len = (
- self._channel.expected_payload_length
- - self._channel.bytes_read
- - TAG_LENGTH
- - CHECKSUM_LENGTH
- )
- self._busy_decoder.decrypt_part(buf[:dec_len])
-
- noise_tag = buf[
- self._channel.expected_payload_length
- - CHECKSUM_LENGTH
- - TAG_LENGTH
- - len(buf)
- - self._channel.bytes_read :
- ]
- offset = (
- TAG_LENGTH + CHECKSUM_LENGTH - len(buf[-CHECKSUM_LENGTH - TAG_LENGTH :])
- )
- utils.memcpy(self._noise_tag, offset, noise_tag, 0)
- else:
- raise Exception("Buffer (+bytes_read) should not be bigger than payload")
- if self.session_id is None:
- self.session_id = buf[0]
diff --git a/tests/device_tests/thp/test_multiple_hosts.py b/tests/device_tests/thp/test_multiple_hosts.py
index 40ed6df0b..4dd7aa163 100644
--- a/tests/device_tests/thp/test_multiple_hosts.py
+++ b/tests/device_tests/thp/test_multiple_hosts.py
@@ -3,7 +3,7 @@ from time import sleep
import pytest
-from trezorlib import exceptions, messages
+from trezorlib import exceptions
from trezorlib.client import ProtocolV2Channel
from trezorlib.debuglink import TrezorClientDebugLink as Client
@@ -62,37 +62,6 @@ def _prepare_two_hosts(client: Client) -> tuple[ProtocolV2Channel, ProtocolV2Cha
return protocol_1, protocol_2
-def test_fallback_encrypted_transport(client: Client) -> None:
- client_1 = Client(transport=client.transport, open_transport=True)
- client_2 = Client(transport=client.transport, open_transport=True)
- session_1 = client_1.get_session()
- session_2 = client_2.get_session()
- msg = messages.GetFeatures()
- msg2 = messages.Ping(message="PONG")
-
- # Sequential calls should work without any problem
- _ = session_1.call(msg)
- _ = session_2.call(msg)
- _ = session_1.call(msg)
- _ = session_2.call(msg)
- _ = session_1.call(msg)
- _ = session_2.call(msg)
- _ = session_1.call(msg)
- _ = session_2.call(msg)
-
- # Zig-zag calls should invoke fallback
- session_1._write(msg2)
- session_2._write(msg)
- # BUG - sesssion 1 should be still retransmitting message
- sleep(2) # BUG 2 - without this sleep, the test fails
- resp = session_2._read()
- assert isinstance(resp, messages.Failure)
- assert resp.message == "FALLBACK!"
- sleep(LOCK_TIME)
- session_1._read() # TODO REMOVE
- session_2.call(msg)
-
-
def test_concurrent_handshakes_1(client: Client) -> None:
client = client.get_new_client()
protocol_1, protocol_2 = _prepare_two_hosts_for_handshake(client)
diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json
index c5f497316..cd2def07f 100644
--- a/tests/ui_tests/fixtures.json
+++ b/tests/ui_tests/fixtures.json
@@ -29946,7 +29946,6 @@
"T3W1_cs_thp-test_handshake.py::test_handshake": "599b7c45309b3e09f382610f3e2198c264fc35aefb4effe7816040e6ec29f91d",
"T3W1_cs_thp-test_multiple_hosts.py::test_concurrent_handshakes_1": "a8055674ce15f6440992bcd745f6da0f0a0367b08bf0b90ba8fdf74988a17dea",
"T3W1_cs_thp-test_multiple_hosts.py::test_concurrent_handshakes_2": "535037bfe5f1459cfdf305915835d8bf2a9a427c3f60264a8cc3ca6f306a61b1",
-"T3W1_cs_thp-test_multiple_hosts.py::test_fallback_encrypted_transport": "739f2c965e4668471688b0568f185d79c386d285d26714cf28a0478bb491d089",
"T3W1_cs_thp-test_pairing.py::test_autoconnect_credential_request_cancel": "457ab9e14416f52c1bb1ea257c7dd4bd43c781a71ba4400fb700cf376200cd27",
"T3W1_cs_thp-test_pairing.py::test_channel_replacement": "2ec597b20ac7bbd981d5b446e41c8ee99530b887232b0e6e050eb543776eef78",
"T3W1_cs_thp-test_pairing.py::test_connection_confirmation_cancel": "c22a79b68dd1f57a5db58388cd0344cb5111dbc68afef20e3362853d8ea54f8a",
@@ -31402,7 +31401,6 @@
"T3W1_de_thp-test_handshake.py::test_handshake": "4089127fa2d033a6d9c4a09781f54ce1f92fc1684ce2ffdacc153586685e8392",
"T3W1_de_thp-test_multiple_hosts.py::test_concurrent_handshakes_1": "e8a51f88f32744b43c7b996a50ec865033caa965b23dc70263fd442fe79c7729",
"T3W1_de_thp-test_multiple_hosts.py::test_concurrent_handshakes_2": "2bf2200d3f158d1cffae639cd632a3b651fe53af5942b65eb881cfce12817592",
-"T3W1_de_thp-test_multiple_hosts.py::test_fallback_encrypted_transport": "f41b346248ca426be3c35e380d1701c46e1176840d1ea7e04c551a1b68057242",
"T3W1_de_thp-test_pairing.py::test_autoconnect_credential_request_cancel": "356d3b7c5aed10f8b91d9e8e35d06edf483a25afa2ce9444b442f9810fa6fe8f",
"T3W1_de_thp-test_pairing.py::test_channel_replacement": "f6d77878c5385ae01b727db3d1253041ec8b8d5594084a68bb64c4ef9065d5bf",
"T3W1_de_thp-test_pairing.py::test_connection_confirmation_cancel": "34b20c5e6fdd46511dce62e10f92ec5b08cd55ad06ba0acef6704ddd931000e4",
@@ -32858,7 +32856,6 @@
"T3W1_en_thp-test_handshake.py::test_handshake": "e8338fb6f6d8dabdb3ba79c521852b3db26cf2d65de359a133074e1858173aba",
"T3W1_en_thp-test_multiple_hosts.py::test_concurrent_handshakes_1": "89b192fe654bedc8f9d3f11be08e8544abb55ee66ca6d4036242d67039c47401",
"T3W1_en_thp-test_multiple_hosts.py::test_concurrent_handshakes_2": "2b19d878184abddf53159d4acb504a1e86a7c2d5fd15de433495742ba7df9cc8",
-"T3W1_en_thp-test_multiple_hosts.py::test_fallback_encrypted_transport": "84eb2f10099c9f045ba417b4d5a4ead9b15235d7e4af7f22143a720779477260",
"T3W1_en_thp-test_pairing.py::test_autoconnect_credential_request_cancel": "4d40957d3dca48a1e792042d1e5b7fe90200336ddd7df008e66027844eba1fab",
"T3W1_en_thp-test_pairing.py::test_channel_replacement": "40cfae080c587feac48b4d4c781c0e4c15edf37fbf4468e53b6ac43b4fd53000",
"T3W1_en_thp-test_pairing.py::test_connection_confirmation_cancel": "a920958bb70e5be597f04a644eb529c77a7c0a6ac87fe77ce037d5299526e6da",
@@ -34314,7 +34311,6 @@
"T3W1_es_thp-test_handshake.py::test_handshake": "446c4b55e2b04cf4f12f20dd2a544a139cd1f04db2de732de69f0ab1595d34a9",
"T3W1_es_thp-test_multiple_hosts.py::test_concurrent_handshakes_1": "d4fa84772b45b0d5be2443fac20dca5f46108b1955317b23306b4974f571fef1",
"T3W1_es_thp-test_multiple_hosts.py::test_concurrent_handshakes_2": "991c2fedae415c4284948d276edc08daeea466ca5849934ed2784cc8884ee589",
-"T3W1_es_thp-test_multiple_hosts.py::test_fallback_encrypted_transport": "558601929c7d775947379e1381fd52b35a04d52594b927734286762c2b555966",
"T3W1_es_thp-test_pairing.py::test_autoconnect_credential_request_cancel": "cb482a59c593c3c8803c35dc8021e1ae9518a680caeaea4af39813a56eb435fd",
"T3W1_es_thp-test_pairing.py::test_channel_replacement": "55b7542cab8a983244e79de523287963e077e5237a4db04f4d41d418e03e1b68",
"T3W1_es_thp-test_pairing.py::test_connection_confirmation_cancel": "6f433c176315ce1654a4c675ccc4f20b83facaf78124d5004e91d3cd9b6cdd2b",
@@ -35770,7 +35766,6 @@
"T3W1_fr_thp-test_handshake.py::test_handshake": "4a1b056b6117bfb79336e6e174efff3006017d838bdcf4b16f46fd3ed4628d2e",
"T3W1_fr_thp-test_multiple_hosts.py::test_concurrent_handshakes_1": "ade0f0dfc01ce5f2bf20a7a7c063e11425a7b83ecb7173411aa766f169f461a1",
"T3W1_fr_thp-test_multiple_hosts.py::test_concurrent_handshakes_2": "250f727140e1737e1e25e4620b4557d886c40e53f8205987dacb8904050dc474",
-"T3W1_fr_thp-test_multiple_hosts.py::test_fallback_encrypted_transport": "6e6102b9508d8fba965c7900bcf9be22ea9c9e9940131a850e23c3e928dd5099",
"T3W1_fr_thp-test_pairing.py::test_autoconnect_credential_request_cancel": "cd3c958a5b358779765d7d2485ddff48d22fd9a5340d6aca2e48f0e0e939b013",
"T3W1_fr_thp-test_pairing.py::test_channel_replacement": "ed61fb08c9858fbb1148660d1228f7d7c497f7a89f9066486d5c7bb37f8ef873",
"T3W1_fr_thp-test_pairing.py::test_connection_confirmation_cancel": "7b1db2a82d9528fb3dda6ecfa6de511b2066df3eb157f898e33b5c49c9181767",
@@ -37226,7 +37221,6 @@
"T3W1_pt_thp-test_handshake.py::test_handshake": "c3514f5e9f61efb7bb753a10d9af0f9b932f1b741948bfb1ff576ee557c1ceb1",
"T3W1_pt_thp-test_multiple_hosts.py::test_concurrent_handshakes_1": "6900b07edb410c88a421f4d79b8e15cb8e1399fcff0bc06826e1f58c8c715de8",
"T3W1_pt_thp-test_multiple_hosts.py::test_concurrent_handshakes_2": "637658c0c6bbf3267f67895c12aeea91280f0471133d7865e4b0d19bdd1da874",
-"T3W1_pt_thp-test_multiple_hosts.py::test_fallback_encrypted_transport": "60ad7830c467e50bdadd837011aef4349e137c7261eb3b1220496d9875b63922",
"T3W1_pt_thp-test_pairing.py::test_autoconnect_credential_request_cancel": "475b9204de9b71fa3637f14787bea5b1612ad64789c3663936dc15498b362a56",
"T3W1_pt_thp-test_pairing.py::test_channel_replacement": "52dce5f754788e21d59789e6a0c5a3a72b4b1810e44d59d1f368896fe93229a1",
"T3W1_pt_thp-test_pairing.py::test_connection_confirmation_cancel": "f5ba7f22b3b5dd1038588d7433d5597318724811a2d208fa206434cbfed45c22",
Why this scored 12/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.