fix(core): remove host static public key from protobuf message
What changed, and why it matters
This commit changes how a Trezor hardware wallet verifies the computer (host) it is talking to when generating a special delegated identity key. Previously, the host had to send its own public key inside the request message, and the device used that key to check a credential. Now the device fetches the host's public key from its own secure channel cache instead. This is a defensive design improvement: it removes an opportunity for a malicious or buggy caller to supply the wrong public key and potentially trick the device into trusting a credential it shouldn't. The commit also adds tests for missing or invalid credentials.
Treat as a hardening/design-cleanup commit rather than an active vulnerability fix. Reviewers should confirm that get_host_static_public_key() is always populated before EvoluGetDelegatedIdentityKey is accepted on a THP channel, and that the channel cache key cannot be influenced by the message sender. No urgent vendor advisory appears required based solely on this diff.
Security signals we found
Removal of attacker-controllable public-key field from protobuf message
Credential validation now uses channel-cache-derived host static public key
Addition of negative tests for invalid/missing credentials
Error type changed from ValueError to DataError for malformed/missing inputs
Evidence from the diff
The protobuf message EvoluGetDelegatedIdentityKey loses the optional host_static_public_key field (reserved as deprecated). The core handler confirm_thp() now retrieves the host static public key via get_channel_context().channel_cache.get_host_static_public_key() rather than from the message. Error handling is tightened: missing credentials raise DataError, and invalid credentials raise DataError. The host-side Python/Rust clients and tests are updated to stop sending the field. New tests verify rejection of invalid and missing credentials under protocol v2.
Changed components
common/protob/messages-evolu.protocore/src/apps/evolu/get_delegated_identity_key.pycore/src/storage/cache_thp.pycore/src/trezor/wire/context.pypython/src/trezorlib/cli/evolu.pypython/src/trezorlib/evolu.pyrust/trezor-client/src/protos/generated/messages_evolu.rstests/device_tests/evolu/common.pytests/device_tests/evolu/test_get_delegated_identity_key.pyInspect captured patch +122 / −114
diff --git a/common/protob/messages-evolu.proto b/common/protob/messages-evolu.proto
index 47aca3d0..27db7a76 100644
--- a/common/protob/messages-evolu.proto
+++ b/common/protob/messages-evolu.proto
@@ -57,7 +57,7 @@ message EvoluRegistrationRequest {
*/
message EvoluGetDelegatedIdentityKey {
optional bytes thp_credential = 1; // THP credential so that we can display the host on the device
- optional bytes host_static_public_key = 2; // Host static public key for THP so that we can validate the credentials
+ reserved 2; // host_static_public_key is deprecated, the key stored in the channel cache is used instead
}
/**
diff --git a/core/src/apps/evolu/get_delegated_identity_key.py b/core/src/apps/evolu/get_delegated_identity_key.py
index 2bfb507e..7acca6cf 100644
--- a/core/src/apps/evolu/get_delegated_identity_key.py
+++ b/core/src/apps/evolu/get_delegated_identity_key.py
@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from trezor.messages import EvoluDelegatedIdentityKey, EvoluGetDelegatedIdentityKey
+from trezor import utils
+
async def get_delegated_identity_key(
msg: EvoluGetDelegatedIdentityKey,
@@ -29,7 +31,6 @@ async def get_delegated_identity_key(
from trezorutils import delegated_identity
- from trezor import utils
from trezor.messages import EvoluDelegatedIdentityKey
if utils.USE_THP:
@@ -42,37 +43,41 @@ async def get_delegated_identity_key(
return EvoluDelegatedIdentityKey(private_key=private_key)
-async def confirm_thp(msg: EvoluGetDelegatedIdentityKey) -> None:
+async def confirm_no_thp() -> None:
from trezor import TR
from trezor.ui.layouts import confirm_action
- from apps.thp.credential_manager import decode_credential, validate_credential
-
- if msg.thp_credential is None:
- raise ValueError("THP credentials must be provided when THP is enabled")
- if msg.host_static_public_key is None:
- raise ValueError("Host static public key must be provided when THP is enabled")
-
- credential_received = decode_credential(msg.thp_credential)
-
- if not validate_credential(credential_received, msg.host_static_public_key):
- raise ValueError("Invalid credential")
-
- app_name = credential_received.cred_metadata.app_name
- host_name = credential_received.cred_metadata.host_name
await confirm_action(
"secure_sync",
TR.secure_sync__header,
- TR.secure_sync__delegated_identity_key_thp.format(app_name, host_name),
+ TR.secure_sync__delegated_identity_key_no_thp,
)
-async def confirm_no_thp() -> None:
- from trezor import TR
- from trezor.ui.layouts import confirm_action
+if utils.USE_THP:
- await confirm_action(
- "secure_sync",
- TR.secure_sync__header,
- TR.secure_sync__delegated_identity_key_no_thp,
- )
+ async def confirm_thp(msg: EvoluGetDelegatedIdentityKey) -> None:
+ from trezor import TR
+ from trezor.ui.layouts import confirm_action
+ from trezor.wire.context import get_channel_context
+ from trezor.wire.errors import DataError
+
+ from apps.thp.credential_manager import decode_credential, validate_credential
+
+ if msg.thp_credential is None:
+ raise DataError("THP credential must be provided when THP is enabled")
+ credential_received = decode_credential(msg.thp_credential)
+ host_static_public_key = (
+ get_channel_context().channel_cache.get_host_static_public_key()
+ )
+
+ if not validate_credential(credential_received, host_static_public_key):
+ raise DataError("Invalid credential")
+
+ app_name = credential_received.cred_metadata.app_name
+ host_name = credential_received.cred_metadata.host_name
+ await confirm_action(
+ "secure_sync",
+ TR.secure_sync__header,
+ TR.secure_sync__delegated_identity_key_thp.format(app_name, host_name),
+ )
diff --git a/core/src/storage/cache_thp.py b/core/src/storage/cache_thp.py
index 6b5d080d..5fdeee8a 100644
--- a/core/src/storage/cache_thp.py
+++ b/core/src/storage/cache_thp.py
@@ -79,9 +79,15 @@ class ChannelCache(ThpDataCache):
def set_host_static_public_key(self, key: memoryview) -> None:
if len(key) != KEY_LENGTH:
- raise ValueError("Invalid key length")
+ raise ValueError # Invalid key length
self.set(CHANNEL_HOST_STATIC_PUBKEY, key)
+ def get_host_static_public_key(self) -> bytes:
+ key = self.get(CHANNEL_HOST_STATIC_PUBKEY)
+ if key is None:
+ raise ValueError # Host static public key is not set in the channel cache.
+ return key
+
class SessionThpCache(ThpDataCache):
def __init__(self) -> None:
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index 743e578f..b7456dbb 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -4184,13 +4184,11 @@ if TYPE_CHECKING:
class EvoluGetDelegatedIdentityKey(protobuf.MessageType):
thp_credential: "AnyBytes | None"
- host_static_public_key: "AnyBytes | None"
def __init__(
self,
*,
thp_credential: "AnyBytes | None" = None,
- host_static_public_key: "AnyBytes | None" = None,
) -> None:
pass
diff --git a/core/src/trezor/wire/context.py b/core/src/trezor/wire/context.py
index d101d80f..d424a5d0 100644
--- a/core/src/trezor/wire/context.py
+++ b/core/src/trezor/wire/context.py
@@ -35,6 +35,9 @@ if TYPE_CHECKING:
T = TypeVar("T")
+ if utils.USE_THP:
+ from trezor.wire.thp.channel import Channel
+
class UnexpectedMessageException(Exception):
"""A message was received that is not part of the current workflow.
@@ -116,6 +119,17 @@ def get_context() -> Context:
return CURRENT_CONTEXT
+if utils.USE_THP:
+
+ def get_channel_context() -> Channel:
+ from trezor.wire.thp.session_context import GenericSessionContext
+
+ ctx = get_context()
+ if not isinstance(ctx, GenericSessionContext):
+ raise TypeError("Current context is not a THP session context")
+ return ctx.channel
+
+
def with_context(ctx: Context, workflow: loop.Task) -> Generator:
"""Run a workflow in a particular context.
diff --git a/python/src/trezorlib/cli/evolu.py b/python/src/trezorlib/cli/evolu.py
index 0a3dcedb..cfb1774f 100644
--- a/python/src/trezorlib/cli/evolu.py
+++ b/python/src/trezorlib/cli/evolu.py
@@ -70,13 +70,11 @@ def sign_registration_request(
@click.option("--credential", "-c", type=str)
-@click.option("--pubkey", "-p", type=str)
@cli.command()
@with_session
def get_delegated_identity_key(
session: Session,
credential: Optional[str] = None,
- pubkey: Optional[str] = None,
) -> str:
"""
Request the delegated identity key of this device.
@@ -85,10 +83,8 @@ def get_delegated_identity_key(
"""
thp_credential = bytes.fromhex(credential) if credential else None
- host_static_public_key = bytes.fromhex(pubkey) if pubkey else None
return evolu.get_delegated_identity_key(
session=session,
thp_credential=thp_credential,
- host_static_public_key=host_static_public_key,
).hex()
diff --git a/python/src/trezorlib/evolu.py b/python/src/trezorlib/evolu.py
index 77925f8d..921d5f1b 100644
--- a/python/src/trezorlib/evolu.py
+++ b/python/src/trezorlib/evolu.py
@@ -47,13 +47,11 @@ def sign_registration_request(
def get_delegated_identity_key(
session: Session,
thp_credential: Optional[bytes] = None,
- host_static_public_key: Optional[bytes] = None,
) -> bytes:
return session.call(
messages.EvoluGetDelegatedIdentityKey(
thp_credential=thp_credential,
- host_static_public_key=host_static_public_key,
),
expect=messages.EvoluDelegatedIdentityKey,
).private_key
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index faee6c14..1c764c03 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -5657,17 +5657,14 @@ class EvoluGetDelegatedIdentityKey(protobuf.MessageType):
MESSAGE_WIRE_TYPE = 2104
FIELDS = {
1: protobuf.Field("thp_credential", "bytes", repeated=False, required=False, default=None),
- 2: protobuf.Field("host_static_public_key", "bytes", repeated=False, required=False, default=None),
}
def __init__(
self,
*,
thp_credential: Optional["bytes"] = None,
- host_static_public_key: Optional["bytes"] = None,
) -> None:
self.thp_credential = thp_credential
- self.host_static_public_key = host_static_public_key
class EvoluDelegatedIdentityKey(protobuf.MessageType):
diff --git a/rust/trezor-client/src/protos/generated/messages_evolu.rs b/rust/trezor-client/src/protos/generated/messages_evolu.rs
index 73991578..853897b4 100644
--- a/rust/trezor-client/src/protos/generated/messages_evolu.rs
+++ b/rust/trezor-client/src/protos/generated/messages_evolu.rs
@@ -789,8 +789,6 @@ pub struct EvoluGetDelegatedIdentityKey {
// message fields
// @@protoc_insertion_point(field:hw.trezor.messages.evolu.EvoluGetDelegatedIdentityKey.thp_credential)
pub thp_credential: ::std::option::Option<::std::vec::Vec<u8>>,
- // @@protoc_insertion_point(field:hw.trezor.messages.evolu.EvoluGetDelegatedIdentityKey.host_static_public_key)
- pub host_static_public_key: ::std::option::Option<::std::vec::Vec<u8>>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.evolu.EvoluGetDelegatedIdentityKey.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@@ -843,55 +841,14 @@ impl EvoluGetDelegatedIdentityKey {
self.thp_credential.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
- // optional bytes host_static_public_key = 2;
-
- pub fn host_static_public_key(&self) -> &[u8] {
- match self.host_static_public_key.as_ref() {
- Some(v) => v,
- None => &[],
- }
- }
-
- pub fn clear_host_static_public_key(&mut self) {
- self.host_static_public_key = ::std::option::Option::None;
- }
-
- pub fn has_host_static_public_key(&self) -> bool {
- self.host_static_public_key.is_some()
- }
-
- // Param is passed by value, moved
- pub fn set_host_static_public_key(&mut self, v: ::std::vec::Vec<u8>) {
- self.host_static_public_key = ::std::option::Option::Some(v);
- }
-
- // Mutable pointer to the field.
- // If field is not initialized, it is initialized with default value first.
- pub fn mut_host_static_public_key(&mut self) -> &mut ::std::vec::Vec<u8> {
- if self.host_static_public_key.is_none() {
- self.host_static_public_key = ::std::option::Option::Some(::std::vec::Vec::new());
- }
- self.host_static_public_key.as_mut().unwrap()
- }
-
- // Take field
- pub fn take_host_static_public_key(&mut self) -> ::std::vec::Vec<u8> {
- self.host_static_public_key.take().unwrap_or_else(|| ::std::vec::Vec::new())
- }
-
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
- let mut fields = ::std::vec::Vec::with_capacity(2);
+ let mut fields = ::std::vec::Vec::with_capacity(1);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"thp_credential",
|m: &EvoluGetDelegatedIdentityKey| { &m.thp_credential },
|m: &mut EvoluGetDelegatedIdentityKey| { &mut m.thp_credential },
));
- fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
- "host_static_public_key",
- |m: &EvoluGetDelegatedIdentityKey| { &m.host_static_public_key },
- |m: &mut EvoluGetDelegatedIdentityKey| { &mut m.host_static_public_key },
- ));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<EvoluGetDelegatedIdentityKey>(
"EvoluGetDelegatedIdentityKey",
fields,
@@ -913,9 +870,6 @@ impl ::protobuf::Message for EvoluGetDelegatedIdentityKey {
10 => {
self.thp_credential = ::std::option::Option::Some(is.read_bytes()?);
},
- 18 => {
- self.host_static_public_key = ::std::option::Option::Some(is.read_bytes()?);
- },
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@@ -931,9 +885,6 @@ impl ::protobuf::Message for EvoluGetDelegatedIdentityKey {
if let Some(v) = self.thp_credential.as_ref() {
my_size += ::protobuf::rt::bytes_size(1, &v);
}
- if let Some(v) = self.host_static_public_key.as_ref() {
- my_size += ::protobuf::rt::bytes_size(2, &v);
- }
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
@@ -943,9 +894,6 @@ impl ::protobuf::Message for EvoluGetDelegatedIdentityKey {
if let Some(v) = self.thp_credential.as_ref() {
os.write_bytes(1, v)?;
}
- if let Some(v) = self.host_static_public_key.as_ref() {
- os.write_bytes(2, v)?;
- }
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@@ -964,14 +912,12 @@ impl ::protobuf::Message for EvoluGetDelegatedIdentityKey {
fn clear(&mut self) {
self.thp_credential = ::std::option::Option::None;
- self.host_static_public_key = ::std::option::Option::None;
self.special_fields.clear();
}
fn default_instance() -> &'static EvoluGetDelegatedIdentityKey {
static instance: EvoluGetDelegatedIdentityKey = EvoluGetDelegatedIdentityKey {
thp_credential: ::std::option::Option::None,
- host_static_public_key: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@@ -1166,12 +1112,11 @@ static file_descriptor_proto_data: &'static [u8] = b"\
uire\x12=\n\x1bproof_of_delegated_identity\x18\x03\x20\x02(\x0cR\x18proo\
fOfDelegatedIdentity\"e\n\x18EvoluRegistrationRequest\x12+\n\x11certific\
ate_chain\x18\x01\x20\x03(\x0cR\x10certificateChain\x12\x1c\n\tsignature\
- \x18\x02\x20\x02(\x0cR\tsignature\"z\n\x1cEvoluGetDelegatedIdentityKey\
- \x12%\n\x0ethp_credential\x18\x01\x20\x01(\x0cR\rthpCredential\x123\n\
- \x16host_static_public_key\x18\x02\x20\x01(\x0cR\x13hostStaticPublicKey\
- \"<\n\x19EvoluDelegatedIdentityKey\x12\x1f\n\x0bprivate_key\x18\x01\x20\
- \x02(\x0cR\nprivateKeyB=\n#com.satoshilabs.trezor.lib.protobufB\x12Trezo\
- rMessageEvolu\x80\xa6\x1d\x01\
+ \x18\x02\x20\x02(\x0cR\tsignature\"K\n\x1cEvoluGetDelegatedIdentityKey\
+ \x12%\n\x0ethp_credential\x18\x01\x20\x01(\x0cR\rthpCredentialJ\x04\x08\
+ \x02\x10\x03\"<\n\x19EvoluDelegatedIdentityKey\x12\x1f\n\x0bprivate_key\
+ \x18\x01\x20\x02(\x0cR\nprivateKeyB=\n#com.satoshilabs.trezor.lib.protob\
+ ufB\x12TrezorMessageEvolu\x80\xa6\x1d\x01\
";
/// `FileDescriptorProto` object which was a source for this generated file
diff --git a/tests/device_tests/evolu/common.py b/tests/device_tests/evolu/common.py
index 0703287d..a58826ed 100644
--- a/tests/device_tests/evolu/common.py
+++ b/tests/device_tests/evolu/common.py
@@ -7,12 +7,20 @@ from ecdsa import NIST256p, SigningKey
from trezorlib import evolu
from trezorlib.debuglink import SessionDebugWrapper as Session
from trezorlib.debuglink import TrezorClientDebugLink as Client
-from trezorlib.messages import ThpCredentialResponse
+from trezorlib.messages import (
+ ThpCredentialRequest,
+ ThpCredentialResponse,
+ ThpEndRequest,
+ ThpEndResponse,
+)
from trezorlib.transport.thp import curve25519
from ...common import compact_size
+from ..thp.connect import prepare_protocol_for_pairing
+from ..thp.test_pairing import nfc_pairing
-TEST_host_static_private_key = curve25519.get_private_key(os.urandom(32))
+TEST_randomness = os.urandom(32)
+TEST_host_static_private_key = curve25519.get_private_key(TEST_randomness)
TEST_host_static_public_key = curve25519.get_public_key(TEST_host_static_private_key)
@@ -47,17 +55,7 @@ class ThpPairingResult:
def pair_and_get_credential(client: Client) -> ThpPairingResult:
- from trezorlib.messages import (
- ThpCredentialRequest,
- ThpCredentialResponse,
- ThpEndRequest,
- ThpEndResponse,
- )
-
- from ..thp.connect import prepare_protocol_for_pairing
- from ..thp.test_pairing import nfc_pairing
-
- protocol = prepare_protocol_for_pairing(client)
+ protocol = prepare_protocol_for_pairing(client, TEST_randomness)
nfc_pairing(client, protocol)
protocol._send_message(
ThpCredentialRequest(
@@ -76,13 +74,26 @@ def pair_and_get_credential(client: Client) -> ThpPairingResult:
return ThpPairingResult(session, credential_response)
+def pair_and_get_invalid_credential(client: Client) -> ThpPairingResult:
+ pairing_result = pair_and_get_credential(client)
+ credential = pairing_result.credential.credential
+
+ # Corrupt the credential to make it invalid
+ invalid_credential = (
+ credential[:-2]
+ + bytes([credential[-2] ^ 0xFF])
+ + bytes([credential[-1] ^ 0xFF])
+ )
+ pairing_result.credential.credential = invalid_credential
+ return pairing_result
+
+
def get_delegated_identity_key(client: Client) -> bytes:
if client.protocol_version == 2:
pairing_data = pair_and_get_credential(client)
return evolu.get_delegated_identity_key(
client.get_session(),
thp_credential=pairing_data.credential.credential,
- host_static_public_key=TEST_host_static_public_key,
)
elif client.protocol_version == 1:
return evolu.get_delegated_identity_key(client.get_session())
diff --git a/tests/device_tests/evolu/test_get_delegated_identity_key.py b/tests/device_tests/evolu/test_get_delegated_identity_key.py
index 4ea80e4a..72d545ff 100644
--- a/tests/device_tests/evolu/test_get_delegated_identity_key.py
+++ b/tests/device_tests/evolu/test_get_delegated_identity_key.py
@@ -1,8 +1,14 @@
import pytest
from trezorlib.debuglink import TrezorClientDebugLink as Client
+from trezorlib.exceptions import TrezorFailure
+from trezorlib.messages import EvoluDelegatedIdentityKey, EvoluGetDelegatedIdentityKey
-from .common import get_delegated_identity_key
+from .common import (
+ get_delegated_identity_key,
+ pair_and_get_credential,
+ pair_and_get_invalid_credential,
+)
pytestmark = [pytest.mark.models("core")]
@@ -25,3 +31,35 @@ def test_evolu_get_delegated_identity_test_vector(client: Client):
assert private_key == bytes.fromhex(
"10e39ed3a40dd63a47a14608d4bccd4501170cf9f2188223208084d39c37b369"
)
+
+
+@pytest.mark.protocol("protocol_v2")
+def test_evolu_get_delegated_identity_invalid_credential(client: Client):
+ pairing_data = pair_and_get_invalid_credential(client)
+ credential_data = pairing_data.credential
+ session = pairing_data.session
+
+ with pytest.raises(TrezorFailure, match="DataError: Invalid credential"):
+ session.call(
+ EvoluGetDelegatedIdentityKey(
+ thp_credential=credential_data.credential,
+ ),
+ expect=EvoluDelegatedIdentityKey,
+ )
+
+
+@pytest.mark.protocol("protocol_v2")
+def test_evolu_get_delegated_identity_missing_credential(client: Client):
+ pairing_data = pair_and_get_credential(client)
+ session = pairing_data.session
+
+ with pytest.raises(
+ TrezorFailure,
+ match="DataError: THP credential must be provided when THP is enabled",
+ ):
+ session.call(
+ EvoluGetDelegatedIdentityKey(
+ thp_credential=None, # Missing credential
+ ),
+ expect=EvoluDelegatedIdentityKey,
+ )
Why this scored 42/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.