Merge remote-tracking branch 'agent/benma-agent/require-20-byte-eip712-address'
What changed, and why it matters
This commit tightens validation for Ethereum addresses used when signing typed messages (EIP-712). Previously, an address of any hex length after the '0x' prefix was accepted and padded to 32 bytes. Now only exactly 20-byte (40 hex character) addresses are allowed. This prevents malformed or truncated addresses from being silently encoded and signed, which could lead to a user approving a signature for an unintended address or contract.
Treat as a security-relevant hardening fix. Ensure the fix is included in firmware releases and consider whether prior versions could have produced signatures over non-standard address encodings that downstream verifiers might misinterpret. No immediate incident response is indicated by the diff alone.
Security signals we found
Input validation hardening in cryptographic signing path
Explicit length check on Ethereum address before encoding
EIP-712 typed message signing affected
Potential silent acceptance of malformed/truncated addresses removed
Evidence from the diff
In sign_typed_msg.rs, the encode_value function handles DataType::Address by decoding the hex payload after an optional ‘0x’/‘0X’ prefix and left-padding it to 32 bytes. Before this patch, any successfully decoded hex string was passed to leftpad32. The patch adds an explicit length check: if address.len() != 20 { return Err(Error::InvalidInput); }. Unit tests were added verifying rejection of 19-byte and 21-byte addresses and acceptance of a 20-byte address. This is a validation hardening change in the EIP-712 signing path.
Changed components
src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rsEIP-712 typed message signing APIEthereum address encodingInspect captured patch +19 / −1
### src/rust/bitbox02-rust/src/hww/api/ethereum/sign_typed_msg.rs
@@ -346,7 +346,11 @@ fn encode_value(typ: &MemberType, value: Vec<u8>) -> Result<(Vec<u8>, String), E
// The address is sent as a string by the host, so we can display it in the same way as
// it is displayed on the host (mixed case vs lowercase, etc.).
let encoded = if let [b'0', b'x' | b'X', rest @ ..] = value.as_slice() {
- leftpad32(&hex::decode(rest).or(Err(Error::InvalidInput))?, false)?
+ let address = hex::decode(rest).or(Err(Error::InvalidInput))?;
+ if address.len() != 20 {
+ return Err(Error::InvalidInput);
+ }
+ leftpad32(&address, false)?
} else {
return Err(Error::InvalidInput);
};
@@ -1194,6 +1198,20 @@ mod tests {
);
}
+ #[test]
+ fn test_encode_value_address() {
+ let value = |len: usize| {
+ let mut value = b"0x".to_vec();
+ value.resize(2 + len * 2, b'1');
+ value
+ };
+ let typ = mk_type(DataType::Address);
+
+ assert_eq!(encode_value(&typ, value(19)), Err(Error::InvalidInput));
+ assert!(encode_value(&typ, value(20)).is_ok());
+ assert_eq!(encode_value(&typ, value(21)), Err(Error::InvalidInput));
+ }
+
#[test]
fn test_get_transitive_types() {
assert!(get_transitive_types(&[], "type-doesnt-exist").is_err());Why this scored 59/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.