eth: loosen EIP-712 identifier validation
What changed, and why it matters
This firmware update relaxes the rules for valid Ethereum typed-data (EIP-712) names so they can contain a colon (:), which some decentralized apps use as a namespace separator. Member names still cannot contain colons. The change is presented as safe because a colon cannot be confused with the punctuation that separates type names, fields, and arrays in EIP-712 encoding.
No immediate action required. Users relying on EIP-712 signing with dapps that use namespaced type names should upgrade to v9.26.4. Reviewers may want to confirm that no other EIP-712 parser or downstream component treats ':' as a delimiter in a way that could create ambiguity.
Security signals we found
Input validation relaxation for externally supplied EIP-712 type names
Explicit claim that ':' cannot forge encodeType boundaries
Member-name validation remains strict
CHANGELOG labels this as a routine compatibility change, not a security fix
Evidence from the diff
The commit replaces strict EIP-712/Solidity identifier validation for struct type names and the primary type with a new validate_type_name function that splits on ‘:’ and validates each segment with the original validate_identifier rules. Member names continue to use the strict validator. Unit tests confirm allowed forms such as ‘Namespace:Type’ and ‘Foo:Bar:Baz’, and reject malformed cases like empty segments, leading/trailing/double colons, and boundary-forging characters.
Changed components
Ethereum EIP-712 typed-message signing (src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs)Inspect captured patch +50 / −4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83802bb..9b3730f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,9 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
### [Unreleased]
+### v9.26.4
+- Ethereum: allow ':' in EIP-712 type names, used by some dapps as a namespace separator
+
### v9.26.3
- Security improvements
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
index c6fe74f..56257bd 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
@@ -80,21 +80,28 @@ fn validate_identifier(name: &str) -> Result<(), Error> {
}
}
+// Some deployed dapps use ':' as a namespace separator in struct type names. A colon
+// cannot forge encodeType boundaries ('(', ')', ',', ' '), so it is safe to accept
+// despite EIP-712's Solidity-identifier wording. Member names stay strict.
+fn validate_type_name(name: &str) -> Result<(), Error> {
+ name.split(':').try_for_each(validate_identifier)
+}
+
fn validate_member_type_identifiers(typ: &MemberType) -> Result<(), Error> {
match DataType::try_from(typ.r#type)? {
DataType::Unknown => Err(Error::InvalidInput),
DataType::Array => {
validate_member_type_identifiers(typ.array_type.as_ref().ok_or(Error::InvalidInput)?)
}
- DataType::Struct => validate_identifier(&typ.struct_name),
+ DataType::Struct => validate_type_name(&typ.struct_name),
_ => Ok(()),
}
}
fn validate_typed_msg_schema(types: &[StructType], primary_type: &str) -> Result<(), Error> {
- validate_identifier(primary_type)?;
+ validate_type_name(primary_type)?;
for typ in types {
- validate_identifier(&typ.name)?;
+ validate_type_name(&typ.name)?;
for member in &typ.members {
validate_identifier(&member.name)?;
validate_member_type_identifiers(member.r#type.as_ref().ok_or(Error::InvalidInput)?)?;
@@ -1395,6 +1402,29 @@ mod tests {
}
}
+ #[test]
+ fn test_validate_type_name() {
+ for name in ["Msg", "Namespace:Type", "Foo:Bar:Baz", "_Type1"] {
+ assert_eq!(validate_type_name(name), Ok(()));
+ }
+
+ for name in [
+ "",
+ ":Type",
+ "Type:",
+ "Foo::Bar",
+ "Foo:123Bar",
+ "123Type",
+ "Bad\nType",
+ "Bad Type",
+ "Type(",
+ "Type,",
+ "Typé",
+ ] {
+ assert_eq!(validate_type_name(name), Err(Error::InvalidInput));
+ }
+ }
+
#[test]
fn test_validate_typed_msg_schema() {
let valid_types = vec![
@@ -1416,6 +1446,19 @@ mod tests {
];
assert_eq!(validate_typed_msg_schema(&valid_types, "Msg"), Ok(()));
+ // Colons are allowed in type names, but not in member names.
+ let mut types = valid_types.clone();
+ types[2].name = "Namespace:Msg".into();
+ types[2].members[0] = mk_member("data", mk_struct_type("Inner"));
+ assert_eq!(validate_typed_msg_schema(&types, "Namespace:Msg"), Ok(()));
+
+ let mut types = valid_types.clone();
+ types[2].members[0] = mk_member("field:name", mk_type(DataType::String));
+ assert_eq!(
+ validate_typed_msg_schema(&types, "Msg"),
+ Err(Error::InvalidInput)
+ );
+
assert_eq!(
validate_typed_msg_schema(&valid_types, "Bad\nType"),
Err(Error::InvalidInput)
diff --git a/versions.json b/versions.json
index 7b6ba91..d43dc12 100644
--- a/versions.json
+++ b/versions.json
@@ -1,4 +1,4 @@
{
- "firmware": "v9.26.3",
+ "firmware": "v9.26.4",
"bootloader": "v1.1.3"
}
Why this scored 21/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.