chore(core): make `host_name` & `app_name` required also in `ThpCredentialMetadata`
What changed, and why it matters
This commit tightens a data structure used during Trezor's new host pairing protocol (THP). It makes two text fields—host_name and app_name—mandatory instead of optional, and adds runtime checks that they are present before creating a pairing credential. On its own this is a defensive hardening change, not a fix for an active vulnerability. It reduces the chance that a paired host could be stored or shown to the user without an identifiable name, which could help prevent social-engineering or UI-confusion attacks.
Treat as a hardening commit. Review whether any existing callers or tests outside this diff still construct ThpCredentialMetadata without both host_name and app_name, because they will now fail serialization or runtime assertions. No urgent patch deployment is indicated unless this change is part of a larger security release.
Security signals we found
Schema-level requirement added for identity fields in pairing credential metadata
Runtime assertions added to fail closed if host_name or app_name is missing
UI type narrowing prevents pairing dialog from being invoked with null names
No changelog entry and no vendor security disclosure in commit message
Evidence from the diff
The protobuf definition for ThpCredentialMetadata changes host_name (field 1) and app_name (field 3) from optional to required. Generated bindings in Python, Rust, and the core MicroPython messages module are regenerated accordingly, and the pairing flow in core/src/apps/thp/pairing.py now asserts ctx.host_name and ctx.app_name are non-None before constructing ThpCredentialMetadata. The UI helper show_pairing_dialog also narrows its type hints from str | None to str. A unit test is updated to supply app_name and its expected serialized credential bytes are refreshed. No changelog entry is recorded and no security advisory or CVE is referenced.
Changed components
common/protob/messages-thp.protocore/src/apps/thp/pairing.pycore/src/trezor/messages.pycore/src/trezor/wire/thp/ui.pycore/tests/test_apps.thp.credential_manager.pypython/src/trezorlib/messages.pyrust/trezor-client/src/protos/generated/messages_thp.rsInspect captured patch +27 / −18
diff --git a/common/protob/messages-thp.proto b/common/protob/messages-thp.proto
index 03a7a40e..2667d341 100644
--- a/common/protob/messages-thp.proto
+++ b/common/protob/messages-thp.proto
@@ -246,9 +246,9 @@ message ThpEndResponse {}
*/
message ThpCredentialMetadata {
option (internal_only) = true;
- optional string host_name = 1; // Human-readable host name (browser name for web apps)
+ required string host_name = 1; // Human-readable host name (browser name for web apps)
optional bool autoconnect = 2; // Whether host is allowed to autoconnect without user confirmation
- optional string app_name = 3; // Human-readable application name
+ required string app_name = 3; // Human-readable application name
}
/**
diff --git a/core/src/apps/thp/pairing.py b/core/src/apps/thp/pairing.py
index c6e82f88..dbe98935 100644
--- a/core/src/apps/thp/pairing.py
+++ b/core/src/apps/thp/pairing.py
@@ -431,6 +431,9 @@ async def _handle_credential_request(
)
trezor_static_public_key = crypto.get_trezor_static_public_key()
+
+ assert ctx.host_name is not None
+ assert ctx.app_name is not None
credential_metadata = ThpCredentialMetadata(
host_name=ctx.host_name,
app_name=ctx.app_name,
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index 9abd4309..070562cf 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -6594,16 +6594,16 @@ if TYPE_CHECKING:
return isinstance(msg, cls)
class ThpCredentialMetadata(protobuf.MessageType):
- host_name: "str | None"
+ host_name: "str"
autoconnect: "bool | None"
- app_name: "str | None"
+ app_name: "str"
def __init__(
self,
*,
- host_name: "str | None" = None,
+ host_name: "str",
+ app_name: "str",
autoconnect: "bool | None" = None,
- app_name: "str | None" = None,
) -> None:
pass
diff --git a/core/src/trezor/wire/thp/ui.py b/core/src/trezor/wire/thp/ui.py
index c4e50db7..9e70f06c 100644
--- a/core/src/trezor/wire/thp/ui.py
+++ b/core/src/trezor/wire/thp/ui.py
@@ -46,7 +46,7 @@ def show_autoconnect_credential_confirmation_screen(
)
-def show_pairing_dialog(host_name: str | None, app_name: str | None) -> Awaitable[None]:
+def show_pairing_dialog(host_name: str, app_name: str) -> Awaitable[None]:
from trezor import TR
return confirm_pairing(
diff --git a/core/tests/test_apps.thp.credential_manager.py b/core/tests/test_apps.thp.credential_manager.py
index 50e9e05e..fde708a0 100644
--- a/core/tests/test_apps.thp.credential_manager.py
+++ b/core/tests/test_apps.thp.credential_manager.py
@@ -8,7 +8,7 @@ if utils.USE_THP:
from apps.thp import credential_manager
def _issue_credential(host_name: str, host_static_public_key: bytes) -> bytes:
- metadata = ThpCredentialMetadata(host_name=host_name)
+ metadata = ThpCredentialMetadata(host_name=host_name, app_name="APP")
return credential_manager.issue_credential(host_static_public_key, metadata)
def _dummy_log(name: str, msg: str, *args):
@@ -79,7 +79,7 @@ class TestTrezorHostProtocolCredentialManager(unittest.TestCase):
When the test fails, it might be necessary to create custom parser
of credentials to ensure that credentials remain valid after FW update.
"""
- expected = b"\x0a\x0b\x0a\x09\x68\x6f\x73\x74\x5f\x6e\x61\x6d\x65\x12\x20\xf4\x44\x86\x2d\x00\x23\x1d\x02\xf3\x20\xbb\x58\xed\x13\x8f\xc6\x84\x9b\x6b\x73\x7a\x33\x25\xc4\x71\x79\x3b\x45\x15\xe4\x76\x67"
+ expected = b"\n\x10\n\thost_name\x1a\x03APP\x12 p\x08Lv#\x185\xcd\x07sF\xb6f\x8e?P\x8d\x88\xf5\xa0\xd4\x16''\x90\x97$\xa4AQ@\x95"
# Use hard-coded bytes as a "credential auth key" when issuing a credential
credential_manager.derive_cred_auth_key = lambda: b"\xbe\xef"
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index d9cf684c..9dd387e5 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -8354,21 +8354,21 @@ class ThpEndResponse(protobuf.MessageType):
class ThpCredentialMetadata(protobuf.MessageType):
MESSAGE_WIRE_TYPE = None
FIELDS = {
- 1: protobuf.Field("host_name", "string", repeated=False, required=False, default=None),
+ 1: protobuf.Field("host_name", "string", repeated=False, required=True),
2: protobuf.Field("autoconnect", "bool", repeated=False, required=False, default=None),
- 3: protobuf.Field("app_name", "string", repeated=False, required=False, default=None),
+ 3: protobuf.Field("app_name", "string", repeated=False, required=True),
}
def __init__(
self,
*,
- host_name: Optional["str"] = None,
+ host_name: "str",
+ app_name: "str",
autoconnect: Optional["bool"] = None,
- app_name: Optional["str"] = None,
) -> None:
self.host_name = host_name
- self.autoconnect = autoconnect
self.app_name = app_name
+ self.autoconnect = autoconnect
class ThpPairingCredential(protobuf.MessageType):
diff --git a/rust/trezor-client/src/protos/generated/messages_thp.rs b/rust/trezor-client/src/protos/generated/messages_thp.rs
index 5985cf89..67c7c7dd 100644
--- a/rust/trezor-client/src/protos/generated/messages_thp.rs
+++ b/rust/trezor-client/src/protos/generated/messages_thp.rs
@@ -3492,7 +3492,7 @@ impl ThpCredentialMetadata {
::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() {
@@ -3547,7 +3547,7 @@ impl ThpCredentialMetadata {
self.autoconnect = ::std::option::Option::Some(v);
}
- // optional string app_name = 3;
+ // required string app_name = 3;
pub fn app_name(&self) -> &str {
match self.app_name.as_ref() {
@@ -3613,6 +3613,12 @@ impl ::protobuf::Message for ThpCredentialMetadata {
const NAME: &'static str = "ThpCredentialMetadata";
fn is_initialized(&self) -> bool {
+ if self.host_name.is_none() {
+ return false;
+ }
+ if self.app_name.is_none() {
+ return false;
+ }
true
}
@@ -4785,8 +4791,8 @@ static file_descriptor_proto_data: &'static [u8] = b"\
r_static_public_key\x18\x01\x20\x02(\x0cR\x15trezorStaticPublicKey\x12\
\x1e\n\ncredential\x18\x02\x20\x02(\x0cR\ncredential\"\x0f\n\rThpEndRequ\
est\"\x10\n\x0eThpEndResponse\"w\n\x15ThpCredentialMetadata\x12\x1b\n\th\
- ost_name\x18\x01\x20\x01(\tR\x08hostName\x12\x20\n\x0bautoconnect\x18\
- \x02\x20\x01(\x08R\x0bautoconnect\x12\x19\n\x08app_name\x18\x03\x20\x01(\
+ ost_name\x18\x01\x20\x02(\tR\x08hostName\x12\x20\n\x0bautoconnect\x18\
+ \x02\x20\x01(\x08R\x0bautoconnect\x12\x19\n\x08app_name\x18\x03\x20\x02(\
\tR\x07appName:\x04\x98\xb2\x19\x01\"\x82\x01\n\x14ThpPairingCredential\
\x12R\n\rcred_metadata\x18\x01\x20\x02(\x0b2-.hw.trezor.messages.thp.Thp\
CredentialMetadataR\x0ccredMetadata\x12\x10\n\x03mac\x18\x02\x20\x02(\
Why this scored 28/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.