feat(core): sign rotation index into Evolu sign registration
What changed, and why it matters
This commit updates the Trezor firmware's Evolu (delegated identity) registration signing feature so that the device now includes a 'rotation index' in the signed registration request. The rotation index tells the Quota Manager which version of the device's delegated identity key was used. The signature format was also bumped from V1 to V2 by adding the rotation index into the signed data. This is a feature enhancement, not a fix for an obvious security vulnerability, but it touches cryptographic signing and identity-key rotation, which are security-sensitive areas.
Review the V2 signing scheme for replay or downgrade risks: ensure the Quota Manager rejects V1 signatures and validates the rotation_index against the current or expected key rotation. Verify that the rotation index cannot be manipulated before signing and that key rotation boundaries are enforced. No urgent patch is indicated by the diff alone, but treat as a security-relevant protocol change.
Security signals we found
Cryptographic signing protocol version bump (V1 -> V2)
New required protobuf field in a security/identity message
Delegated identity key rotation logic touched
No explicit vulnerability description or CVE in commit or references
Evidence from the diff
The change adds a required rotation_index field to the EvoluRegistrationRequest protobuf message and includes that index in the signed payload. The signing header changes from EvoluSignRegistrationRequestV1: to V2:. A new helper get_public_key(rotation_index) is added in core/src/apps/evolu/common.py, and _get_signature now returns both the signature and the rotation index used. Tests and generated protobuf bindings (Python, Rust) are updated accordingly. The commit message frames this as enabling the Quota Manager to distinguish indices and store the current one.
Changed components
core/src/apps/evolu/sign_registration_request.pycore/src/apps/evolu/common.pycommon/protob/messages-evolu.protocore/src/trezor/messages.pypython/src/trezorlib/messages.pypython/src/trezorlib/cli/evolu.pyrust/trezor-client/src/protos/generated/messages_evolu.rstests/device_tests/evolu/test_sign_registration.pyInspect captured patch +106 / −28
diff --git a/common/protob/messages-evolu.proto b/common/protob/messages-evolu.proto
index cdb08bc8..56152894 100644
--- a/common/protob/messages-evolu.proto
+++ b/common/protob/messages-evolu.proto
@@ -49,6 +49,7 @@ message EvoluSignRegistrationRequest {
message EvoluRegistrationRequest {
repeated bytes certificate_chain = 1;
required bytes signature = 2;
+ required uint32 rotation_index = 3; // the rotation index of the delegated identity key
}
/**
diff --git a/core/src/apps/evolu/common.py b/core/src/apps/evolu/common.py
index 236c6423..ae70d2ed 100644
--- a/core/src/apps/evolu/common.py
+++ b/core/src/apps/evolu/common.py
@@ -55,3 +55,15 @@ def get_public_key_from_private_key(private_key: AnyBytes) -> bytes:
public_key = nist256p1.publickey(private_key, False)
return public_key
+
+
+def get_public_key(rotation_index: int | None = None) -> bytes:
+ from trezorutils import delegated_identity
+
+ from storage.device import get_delegated_identity_key_rotation_index
+
+ if rotation_index is None:
+ rotation_index = get_delegated_identity_key_rotation_index() or 0
+ private_key = delegated_identity(rotation_index)
+
+ return get_public_key_from_private_key(private_key)
diff --git a/core/src/apps/evolu/sign_registration_request.py b/core/src/apps/evolu/sign_registration_request.py
index 404f5ccc..9817562e 100644
--- a/core/src/apps/evolu/sign_registration_request.py
+++ b/core/src/apps/evolu/sign_registration_request.py
@@ -59,13 +59,14 @@ async def sign_registration_request(
):
raise ValueError("Invalid proof")
- signature = _get_signature(challenge_bytes, size_bytes)
+ signature, rotation_index = _get_signature(challenge_bytes, size_bytes)
r = BufferReader(optiga.get_certificate(optiga.DEVICE_CERT_INDEX))
certificates = parse_cert_chain(r)
return EvoluRegistrationRequest(
certificate_chain=certificates,
signature=signature,
+ rotation_index=rotation_index,
)
@@ -82,9 +83,7 @@ def _check_data(challenge: AnyBytes, size: int) -> tuple[AnyBytes, bytes]:
return challenge, size_to_acquire_bytes
-def _get_signature(challenge_bytes: AnyBytes, size_bytes: bytes) -> bytes:
- from trezorutils import delegated_identity
-
+def _get_signature(challenge_bytes: AnyBytes, size_bytes: bytes) -> tuple[bytes, int]:
from storage.device import get_delegated_identity_key_rotation_index
from trezor import utils, wire
from trezor.crypto import optiga
@@ -92,18 +91,20 @@ def _get_signature(challenge_bytes: AnyBytes, size_bytes: bytes) -> bytes:
from apps.common.writers import write_compact_size
- from .common import get_public_key_from_private_key
+ from .common import get_public_key
- private_key = delegated_identity(get_delegated_identity_key_rotation_index() or 0)
- public_key = get_public_key_from_private_key(private_key)
+ rotation_index = get_delegated_identity_key_rotation_index() or 0
+ public_key = get_public_key()
- header = b"EvoluSignRegistrationRequestV1:"
+ header = b"EvoluSignRegistrationRequestV2:"
components = [
header,
public_key,
challenge_bytes,
size_bytes,
+ rotation_index.to_bytes(BYTES_IN_UINT32, "big"),
]
+
hash_writer = utils.HashWriter(sha256())
for component in components:
write_compact_size(hash_writer, len(component))
@@ -113,4 +114,5 @@ def _get_signature(challenge_bytes: AnyBytes, size_bytes: bytes) -> bytes:
signature = optiga.sign(optiga.DEVICE_ECC_KEY_INDEX, hash_writer.get_digest())
except optiga.SigningInaccessible:
raise wire.ProcessError("Signing inaccessible.")
- return signature
+
+ return signature, rotation_index
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index b791d320..0970a10f 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -4471,11 +4471,13 @@ if TYPE_CHECKING:
class EvoluRegistrationRequest(protobuf.MessageType):
certificate_chain: "list[AnyBytes]"
signature: "AnyBytes"
+ rotation_index: "int"
def __init__(
self,
*,
signature: "AnyBytes",
+ rotation_index: "int",
certificate_chain: "list[AnyBytes] | None" = None,
) -> None:
pass
diff --git a/python/src/trezorlib/cli/evolu.py b/python/src/trezorlib/cli/evolu.py
index 1650d721..6a64fd16 100644
--- a/python/src/trezorlib/cli/evolu.py
+++ b/python/src/trezorlib/cli/evolu.py
@@ -60,7 +60,7 @@ def sign_registration_request(
proof: str,
challenge: str,
size: int,
-) -> dict[str, str]:
+) -> dict[str, str | int | None]:
"""Sign a registration request for this device to be registered at the Quota Manager server."""
response = evolu.sign_registration_request(
@@ -72,6 +72,7 @@ def sign_registration_request(
return {
"certificates": ",".join([cert.hex() for cert in response.certificate_chain]),
"signature": response.signature.hex(),
+ "rotation_index": response.rotation_index,
}
diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py
index 1f42efd3..7c3ea631 100644
--- a/python/src/trezorlib/messages.py
+++ b/python/src/trezorlib/messages.py
@@ -6035,16 +6035,19 @@ class EvoluRegistrationRequest(protobuf.MessageType):
FIELDS = {
1: protobuf.Field("certificate_chain", "bytes", repeated=True, required=False, default=None),
2: protobuf.Field("signature", "bytes", repeated=False, required=True),
+ 3: protobuf.Field("rotation_index", "uint32", repeated=False, required=True),
}
def __init__(
self,
*,
signature: "bytes",
+ rotation_index: "int",
certificate_chain: Optional[Sequence["bytes"]] = None,
) -> None:
self.certificate_chain: Sequence["bytes"] = certificate_chain if certificate_chain is not None else []
self.signature = signature
+ self.rotation_index = rotation_index
class EvoluGetDelegatedIdentityKey(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 45372c31..82a92267 100644
--- a/rust/trezor-client/src/protos/generated/messages_evolu.rs
+++ b/rust/trezor-client/src/protos/generated/messages_evolu.rs
@@ -649,6 +649,8 @@ pub struct EvoluRegistrationRequest {
pub certificate_chain: ::std::vec::Vec<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:hw.trezor.messages.evolu.EvoluRegistrationRequest.signature)
pub signature: ::std::option::Option<::std::vec::Vec<u8>>,
+ // @@protoc_insertion_point(field:hw.trezor.messages.evolu.EvoluRegistrationRequest.rotation_index)
+ pub rotation_index: ::std::option::Option<u32>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.evolu.EvoluRegistrationRequest.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@@ -701,8 +703,27 @@ impl EvoluRegistrationRequest {
self.signature.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
+ // required uint32 rotation_index = 3;
+
+ pub fn rotation_index(&self) -> u32 {
+ self.rotation_index.unwrap_or(0)
+ }
+
+ pub fn clear_rotation_index(&mut self) {
+ self.rotation_index = ::std::option::Option::None;
+ }
+
+ pub fn has_rotation_index(&self) -> bool {
+ self.rotation_index.is_some()
+ }
+
+ // Param is passed by value, moved
+ pub fn set_rotation_index(&mut self, v: u32) {
+ self.rotation_index = ::std::option::Option::Some(v);
+ }
+
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(3);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>(
"certificate_chain",
@@ -714,6 +735,11 @@ impl EvoluRegistrationRequest {
|m: &EvoluRegistrationRequest| { &m.signature },
|m: &mut EvoluRegistrationRequest| { &mut m.signature },
));
+ fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+ "rotation_index",
+ |m: &EvoluRegistrationRequest| { &m.rotation_index },
+ |m: &mut EvoluRegistrationRequest| { &mut m.rotation_index },
+ ));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<EvoluRegistrationRequest>(
"EvoluRegistrationRequest",
fields,
@@ -729,6 +755,9 @@ impl ::protobuf::Message for EvoluRegistrationRequest {
if self.signature.is_none() {
return false;
}
+ if self.rotation_index.is_none() {
+ return false;
+ }
true
}
@@ -741,6 +770,9 @@ impl ::protobuf::Message for EvoluRegistrationRequest {
18 => {
self.signature = ::std::option::Option::Some(is.read_bytes()?);
},
+ 24 => {
+ self.rotation_index = ::std::option::Option::Some(is.read_uint32()?);
+ },
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@@ -759,6 +791,9 @@ impl ::protobuf::Message for EvoluRegistrationRequest {
if let Some(v) = self.signature.as_ref() {
my_size += ::protobuf::rt::bytes_size(2, &v);
}
+ if let Some(v) = self.rotation_index {
+ my_size += ::protobuf::rt::uint32_size(3, 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
@@ -771,6 +806,9 @@ impl ::protobuf::Message for EvoluRegistrationRequest {
if let Some(v) = self.signature.as_ref() {
os.write_bytes(2, v)?;
}
+ if let Some(v) = self.rotation_index {
+ os.write_uint32(3, v)?;
+ }
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@@ -790,6 +828,7 @@ impl ::protobuf::Message for EvoluRegistrationRequest {
fn clear(&mut self) {
self.certificate_chain.clear();
self.signature = ::std::option::Option::None;
+ self.rotation_index = ::std::option::Option::None;
self.special_fields.clear();
}
@@ -797,6 +836,7 @@ impl ::protobuf::Message for EvoluRegistrationRequest {
static instance: EvoluRegistrationRequest = EvoluRegistrationRequest {
certificate_chain: ::std::vec::Vec::new(),
signature: ::std::option::Option::None,
+ rotation_index: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@@ -1541,18 +1581,19 @@ static file_descriptor_proto_data: &'static [u8] = b"\
ignRegistrationRequest\x122\n\x15challenge_from_server\x18\x01\x20\x02(\
\x0cR\x13challengeFromServer\x12&\n\x0fsize_to_acquire\x18\x02\x20\x02(\
\rR\rsizeToAcquire\x12=\n\x1bproof_of_delegated_identity\x18\x03\x20\x02\
- (\x0cR\x18proofOfDelegatedIdentity\"e\n\x18EvoluRegistrationRequest\x12+\
- \n\x11certificate_chain\x18\x01\x20\x03(\x0cR\x10certificateChain\x12\
- \x1c\n\tsignature\x18\x02\x20\x02(\x0cR\tsignature\"\x8a\x01\n\x1cEvoluG\
- etDelegatedIdentityKey\x12%\n\x0ethp_credential\x18\x01\x20\x01(\x0cR\rt\
- hpCredential\x12%\n\x0erotation_index\x18\x03\x20\x01(\rR\rrotationIndex\
- \x12\x16\n\x06rotate\x18\x04\x20\x01(\x08R\x06rotateJ\x04\x08\x02\x10\
- \x03\"c\n\x19EvoluDelegatedIdentityKey\x12\x1f\n\x0bprivate_key\x18\x01\
- \x20\x02(\x0cR\nprivateKey\x12%\n\x0erotation_index\x18\x02\x20\x01(\rR\
- \rrotationIndex\"=\n\x14EvoluIndexManagement\x12%\n\x0erotation_index\
- \x18\x01\x20\x01(\rR\rrotationIndex\"E\n\x1cEvoluIndexManagementResponse\
- \x12%\n\x0erotation_index\x18\x01\x20\x01(\rR\rrotationIndexB=\n#com.sat\
- oshilabs.trezor.lib.protobufB\x12TrezorMessageEvolu\x80\xa6\x1d\x01\
+ (\x0cR\x18proofOfDelegatedIdentity\"\x8c\x01\n\x18EvoluRegistrationReque\
+ st\x12+\n\x11certificate_chain\x18\x01\x20\x03(\x0cR\x10certificateChain\
+ \x12\x1c\n\tsignature\x18\x02\x20\x02(\x0cR\tsignature\x12%\n\x0erotatio\
+ n_index\x18\x03\x20\x02(\rR\rrotationIndex\"\x8a\x01\n\x1cEvoluGetDelega\
+ tedIdentityKey\x12%\n\x0ethp_credential\x18\x01\x20\x01(\x0cR\rthpCreden\
+ tial\x12%\n\x0erotation_index\x18\x03\x20\x01(\rR\rrotationIndex\x12\x16\
+ \n\x06rotate\x18\x04\x20\x01(\x08R\x06rotateJ\x04\x08\x02\x10\x03\"c\n\
+ \x19EvoluDelegatedIdentityKey\x12\x1f\n\x0bprivate_key\x18\x01\x20\x02(\
+ \x0cR\nprivateKey\x12%\n\x0erotation_index\x18\x02\x20\x01(\rR\rrotation\
+ Index\"=\n\x14EvoluIndexManagement\x12%\n\x0erotation_index\x18\x01\x20\
+ \x01(\rR\rrotationIndex\"E\n\x1cEvoluIndexManagementResponse\x12%\n\x0er\
+ otation_index\x18\x01\x20\x01(\rR\rrotationIndexB=\n#com.satoshilabs.tre\
+ zor.lib.protobufB\x12TrezorMessageEvolu\x80\xa6\x1d\x01\
";
/// `FileDescriptorProto` object which was a source for this generated file
diff --git a/tests/device_tests/evolu/test_sign_registration.py b/tests/device_tests/evolu/test_sign_registration.py
index d3529065..c5bcfa2d 100644
--- a/tests/device_tests/evolu/test_sign_registration.py
+++ b/tests/device_tests/evolu/test_sign_registration.py
@@ -12,14 +12,18 @@ from .common import get_delegated_identity_key, get_invalid_proof, get_proof
pytestmark = pytest.mark.models("core")
-def signing_buffer(private_key: bytes, challenge: bytes, size: int) -> bytes:
+def signing_buffer(
+ private_key: bytes, challenge: bytes, size: int, rotation_index: int | None = None
+) -> bytes:
public_key: VerifyingKey = SigningKey.from_string(private_key, curve=NIST256p).get_verifying_key() # type: ignore
components = [
- b"EvoluSignRegistrationRequestV1:",
+ b"EvoluSignRegistrationRequestV2:",
public_key.to_string("uncompressed"),
challenge,
size.to_bytes(4, "big"),
]
+ if rotation_index is not None:
+ components.append(rotation_index.to_bytes(4, "big"))
return b"".join((compact_size(len(comp)) + comp) for comp in components)
@@ -68,7 +72,9 @@ def test_evolu_sign_request(client: Client):
proof=proposed_value,
)
- data = signing_buffer(delegated_identity_key, challenge, size)
+ data = signing_buffer(
+ delegated_identity_key, challenge, size, rotation_index=response.rotation_index
+ )
check_signature_optiga(
response.signature, response.certificate_chain, client.model, data
)
@@ -206,7 +212,9 @@ def test_evolu_sign_request_data_higher_bound(client: Client):
proof=proof,
)
- data = signing_buffer(delegated_identity_key, challenge, size)
+ data = signing_buffer(
+ delegated_identity_key, challenge, size, rotation_index=response.rotation_index
+ )
check_signature_optiga(
response.signature, response.certificate_chain, client.model, data
)
@@ -238,7 +246,15 @@ def test_evolu_sign_request_with_different_rotation_indices(
proof=proof,
)
- data = signing_buffer(delegated_identity_key, challenge, size)
+ data = signing_buffer(
+ delegated_identity_key, challenge, size, rotation_index=response.rotation_index
+ )
check_signature_optiga(
response.signature, response.certificate_chain, client.model, data
)
+
+ assert response.rotation_index is not None
+ if rotation_index is None:
+ assert response.rotation_index == 0
+ else:
+ assert response.rotation_index == rotation_index
Why this scored 24/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.