fix(core): make app_name required for THP pairing
What changed, and why it matters
This commit tightens the rules for a Trezor pairing request by making both the host name and app name mandatory. Previously, the app name was optional, which could have led to a confusing or misleading pairing screen on the device. The change helps ensure users see complete, accurate information before approving a connection.
Treat as a minor hardening improvement. Review whether any existing clients or integrations still send ThpPairingRequest without app_name, because they will now fail. No urgent security response is indicated by the diff alone.
Security signals we found
Input validation strengthened: missing host_name and app_name now both raise DataError
Protobuf schema changed from optional to required for ThpPairingRequest fields
UI pairing dialog now guaranteed to receive both host and app identifiers
No changelog entry present ([no changelog])
Evidence from the diff
The commit changes ThpPairingRequest fields host_name and app_name from optional to required in the protobuf definition and in generated Python/Rust bindings. The core pairing handler now explicitly raises DataError if either field is missing, and the UI pairing dialog is always called with both values. Tests are updated to supply both fields.
Changed components
common/protob/messages-thp.protocore/src/apps/thp/pairing.pycore/src/trezor/messages.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_thp.rstests/device_tests/thp/connect.pytests/device_tests/thp/test_pairing.pyInspect captured patch +30 / −19
diff --git a/common/protob/messages-thp.proto b/common/protob/messages-thp.proto
index 4581e2a6e..3e366f60c 100644
--- a/common/protob/messages-thp.proto
+++ b/common/protob/messages-thp.proto
@@ -98,8 +98,8 @@ message ThpCreateNewSession {
* @next ThpPairingRequestApproved
*/
message ThpPairingRequest {
- optional string host_name = 1; // Human-readable host name (browser name for web apps)
- optional string app_name = 2; // Human-readable application name
+ required string host_name = 1; // Human-readable host name (browser name for web apps)
+ required string app_name = 2; // Human-readable application name
}
/**
diff --git a/core/src/apps/thp/pairing.py b/core/src/apps/thp/pairing.py
index 266e62e97..44213c445 100644
--- a/core/src/apps/thp/pairing.py
+++ b/core/src/apps/thp/pairing.py
@@ -111,9 +111,10 @@ async def handle_pairing_request(
if not ThpPairingRequest.is_type_of(message):
raise UnexpectedMessage("Unexpected message")
- # TODO: make app_name required eventually
if not message.host_name:
raise DataError("Missing host_name.")
+ if not message.app_name:
+ raise DataError("Missing app_name.")
peer_addr = ctx.channel_ctx.iface_ctx.connected_addr()
await ui.show_pairing_dialog(message.host_name, message.app_name)
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index 907e4f3dd..cbabaa3b3 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -6377,14 +6377,14 @@ if TYPE_CHECKING:
return isinstance(msg, cls)
class ThpPairingRequest(protobuf.MessageType):
- host_name: "str | None"
- app_name: "str | None"
+ host_name: "str"
+ app_name: "str"
def __init__(
self,
*,
- host_name: "str | None" = None,
- app_name: "str | None" = None,
+ host_name: "str",
+ app_name: "str",
) -> None:
pass
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index 3a519e90a..da336e75a 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -8141,15 +8141,15 @@ class ThpCreateNewSession(protobuf.MessageType):
class ThpPairingRequest(protobuf.MessageType):
MESSAGE_WIRE_TYPE = 1008
FIELDS = {
- 1: protobuf.Field("host_name", "string", repeated=False, required=False, default=None),
- 2: protobuf.Field("app_name", "string", repeated=False, required=False, default=None),
+ 1: protobuf.Field("host_name", "string", repeated=False, required=True),
+ 2: protobuf.Field("app_name", "string", repeated=False, required=True),
}
def __init__(
self,
*,
- host_name: Optional["str"] = None,
- app_name: Optional["str"] = None,
+ host_name: "str",
+ app_name: "str",
) -> None:
self.host_name = host_name
self.app_name = app_name
diff --git a/rust/trezor-client/src/protos/generated/messages_thp.rs b/rust/trezor-client/src/protos/generated/messages_thp.rs
index d9788a32a..144d4aca9 100644
--- a/rust/trezor-client/src/protos/generated/messages_thp.rs
+++ b/rust/trezor-client/src/protos/generated/messages_thp.rs
@@ -737,7 +737,7 @@ impl ThpPairingRequest {
::std::default::Default::default()
}
- // optional string host_name = 1;
+ // required string host_name = 1;
pub fn host_name(&self) -> &str {
match self.host_name.as_ref() {
@@ -773,7 +773,7 @@ impl ThpPairingRequest {
self.host_name.take().unwrap_or_else(|| ::std::string::String::new())
}
- // optional string app_name = 2;
+ // required string app_name = 2;
pub fn app_name(&self) -> &str {
match self.app_name.as_ref() {
@@ -834,6 +834,12 @@ impl ::protobuf::Message for ThpPairingRequest {
const NAME: &'static str = "ThpPairingRequest";
fn is_initialized(&self) -> bool {
+ if self.host_name.is_none() {
+ return false;
+ }
+ if self.app_name.is_none() {
+ return false;
+ }
true
}
@@ -4699,8 +4705,8 @@ static file_descriptor_proto_data: &'static [u8] = b"\
ateNewSession\x12\x1e\n\npassphrase\x18\x01\x20\x01(\tR\npassphrase\x12\
\"\n\ton_device\x18\x02\x20\x01(\x08:\x05falseR\x08onDevice\x12,\n\x0ede\
rive_cardano\x18\x03\x20\x01(\x08:\x05falseR\rderiveCardano\"K\n\x11ThpP\
- airingRequest\x12\x1b\n\thost_name\x18\x01\x20\x01(\tR\x08hostName\x12\
- \x19\n\x08app_name\x18\x02\x20\x01(\tR\x07appName\"\x1b\n\x19ThpPairingR\
+ airingRequest\x12\x1b\n\thost_name\x18\x01\x20\x02(\tR\x08hostName\x12\
+ \x19\n\x08app_name\x18\x02\x20\x02(\tR\x07appName\"\x1b\n\x19ThpPairingR\
equestApproved\"s\n\x0fThpSelectMethod\x12`\n\x17selected_pairing_method\
\x18\x01\x20\x02(\x0e2(.hw.trezor.messages.thp.ThpPairingMethodR\x15sele\
ctedPairingMethod\"\x20\n\x1eThpPairingPreparationsFinished\"8\n\x16ThpC\
diff --git a/tests/device_tests/thp/connect.py b/tests/device_tests/thp/connect.py
index 85b45e847..9d0dff687 100644
--- a/tests/device_tests/thp/connect.py
+++ b/tests/device_tests/thp/connect.py
@@ -42,9 +42,9 @@ def get_encrypted_transport_protocol(
def handle_pairing_request(
- client: Client, protocol: ProtocolV2Channel, host_name: str | None = None
+ client: Client, protocol: ProtocolV2Channel, app_name: str | None = None
) -> None:
- protocol._send_message(ThpPairingRequest(host_name=host_name))
+ protocol._send_message(ThpPairingRequest(host_name="localhost", app_name=app_name))
button_req = protocol._read_message(ButtonRequest)
assert button_req.name == "thp_pairing_request"
diff --git a/tests/device_tests/thp/test_pairing.py b/tests/device_tests/thp/test_pairing.py
index 12b35eec1..d83e46485 100644
--- a/tests/device_tests/thp/test_pairing.py
+++ b/tests/device_tests/thp/test_pairing.py
@@ -209,7 +209,9 @@ def test_pairing_code_entry_cancel(
def test_pairing_cancel_1(client: Client) -> None:
protocol = prepare_protocol_for_pairing(client)
- protocol._send_message(ThpPairingRequest(host_name="TestTrezor Cancel 1"))
+ protocol._send_message(
+ ThpPairingRequest(host_name="localhost", app_name="TestTrezor Cancel 1")
+ )
button_req = protocol._read_message(ButtonRequest)
assert button_req.name == "thp_pairing_request"
@@ -224,7 +226,9 @@ def test_pairing_cancel_1(client: Client) -> None:
def test_pairing_cancel_2(client: Client) -> None:
protocol = prepare_protocol_for_pairing(client)
- protocol._send_message(ThpPairingRequest(host_name="TestTrezor Cancel 2"))
+ protocol._send_message(
+ ThpPairingRequest(host_name="localhost", app_name="TestTrezor Cancel 2")
+ )
button_req = protocol._read_message(ButtonRequest)
assert button_req.name == "thp_pairing_request"
Why this scored 37/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.