api/payment_request: serialize ETH value as little endian
What changed, and why it matters
This commit fixes a serialization bug in the BitBox02 firmware's payment request validation for Ethereum transactions. Previously, Ethereum payment amounts were encoded as big-endian (most-significant-byte-first), but the SLIP-24 standard requires them to be little-endian (least-significant-byte-first). Because the device signs the payment request using one byte order but the validation code used another, a payment request could be generated that the device would accept even if the actual transaction amount did not match what the user saw on screen. The fix changes the code to serialize Ethereum amounts as 32-byte little-endian values, matching the standard and preventing amount mismatches.
Treat this as a security-relevant bug fix. Verify that all payment-request signing and validation paths for EVM assets now use consistent little-endian 32-byte amount encoding. Review related UTXO-coin and other asset implementations for similar endianness mismatches. If prior firmware versions with the big-endian behavior were released, consider whether a security advisory or recall of affected payment-request workflows is warranted.
Security signals we found
Payment-request amount encoding mismatch between signing and validation paths
Non-compliance with SLIP-24 little-endian requirement for EVM asset amounts
Potential acceptance of a payment request whose on-chain amount differs from user-approved amount
Fix is localized to Ethereum payment-request validation and test expectations
Evidence from the diff
The patch modifies payment_request.rs and ethereum/sign.rs. It replaces an ad-hoc big-endian, left-padded 32-byte serialization of ETH output values with a new serialize_eth_output_value() helper that produces a little-endian, right-padded 32-byte encoding. validate_eth() now calls this helper, and tests are updated to expect little-endian values. The change aligns the implementation with SLIP-24, which specifies that EVM asset amounts must be encoded as 32-byte little-endian values. The mismatch between signing and validation byte order could have allowed a crafted payment request to pass validation for an amount different from the one displayed/signed.
Changed components
src/rust/bitbox02-rust/src/hww/api/payment_request.rssrc/rust/bitbox02-rust/src/hww/api/ethereum/sign.rsEthereum payment request validation (SLIP-24)BitBox02 firmware HWW APIInspect captured patch +37 / −18
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
index 1f440e0..e09aeb3 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
@@ -1205,8 +1205,8 @@ mod tests {
// 0.530564 ETH in wei
let value = hex!("075cf1259e9c4000");
- // value left padded to 32 bytes
- let output_value = hex!("000000000000000000000000000000000000000000000000075cf1259e9c4000");
+ // value serialized as 32-byte little endian
+ let output_value = hex!("00409c9e25f15c07000000000000000000000000000000000000000000000000");
// recipient address
let output_address = address::from_pubkey_hash(
&hex!("04f264cf34440313b4a0192a352814fbe927b885"),
@@ -1282,7 +1282,10 @@ mod tests {
let data = hex!(
"a9059cbb000000000000000000000000e6ce0a092a99700cd4ccccbb1fedc39cf53e6330000000000000000000000000000000000000000000000000000000000365c040"
);
- let output_value: [u8; 32] = data[36..68].try_into().unwrap();
+ let output_value_be: [u8; 32] = data[36..68].try_into().unwrap();
+ let output_value =
+ payment_request::serialize_eth_output_value(&BigUint::from_bytes_be(&output_value_be))
+ .unwrap();
let output_address = address::from_pubkey_hash(
&hex!("e6ce0a092a99700cd4ccccbb1fedc39cf53e6330"),
pb::EthAddressCase::Mixed,
diff --git a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
index 9803075..3a1a257 100644
--- a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
@@ -285,13 +285,13 @@ pub fn tst_sign_payment_request_btc(
pub fn tst_sign_payment_request_eth(
source_coin_type: u32,
payment_request: &mut pb::BtcPaymentRequestRequest,
- total_value_bytes: &[u8; 32],
+ total_value_le_bytes: &[u8; 32],
output_address: &str,
) {
tst_sign_payment_request(
source_coin_type,
payment_request,
- total_value_bytes,
+ total_value_le_bytes,
output_address,
);
}
@@ -359,12 +359,7 @@ pub fn validate_eth(
output_value: &BigUint,
output_address: &str,
) -> Result<(), ValidationError> {
- let output_value = output_value.to_bytes_be();
- if output_value.len() > 32 {
- return Err(ValidationError::Other);
- }
- let mut output_value_padded = [0u8; 32];
- output_value_padded[32 - output_value.len()..].copy_from_slice(&output_value);
+ let output_value_padded = serialize_eth_output_value(output_value)?;
validate_common(
hal,
coin_params.slip44(),
@@ -374,6 +369,17 @@ pub fn validate_eth(
)
}
+#[cfg(feature = "app-ethereum")]
+pub fn serialize_eth_output_value(output_value: &BigUint) -> Result<[u8; 32], ValidationError> {
+ let output_value = output_value.to_bytes_le();
+ if output_value.len() > 32 {
+ return Err(ValidationError::Other);
+ }
+ let mut output_value_padded = [0u8; 32];
+ output_value_padded[..output_value.len()].copy_from_slice(&output_value);
+ Ok(output_value_padded)
+}
+
/// Validate that the parsed source-side transaction matches the signed payment request.
///
/// The caller provides the normalized source-side facts that are actually being
@@ -722,6 +728,15 @@ mod tests {
}
}
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ fn test_serialize_eth_output_value() {
+ assert_eq!(
+ serialize_eth_output_value(&BigUint::from_bytes_le(&hex!("00409c9e25f15c07"))).unwrap(),
+ hex!("00409c9e25f15c07000000000000000000000000000000000000000000000000")
+ );
+ }
+
#[test]
fn test_validate() {
let source_coin_type = params::get(pb::BtcCoin::Tbtc).slip44();
@@ -1659,6 +1674,7 @@ mod tests {
#[cfg(feature = "app-ethereum")]
#[test]
fn test_validate_eth() {
+ crate::keystore::testing::mock_unlocked();
let mut mock_hal = TestingHal::new();
let params = eth_params::Params {
coin: Some(pb::EthCoin::Eth),
@@ -1667,7 +1683,7 @@ mod tests {
name: "Ethereum",
unit: "ETH",
};
- let output_value = hex!("000000000000000000000000000000000000000000000000075cf1259e9c4000");
+ let output_value = hex!("00409c9e25f15c07000000000000000000000000000000000000000000000000");
let output_address = "0x04F264Cf34440313B4A0192A352814FBe927b885";
let mut payment_request = pb::BtcPaymentRequestRequest {
recipient_name: "Test Merchant".into(),
@@ -1694,7 +1710,7 @@ mod tests {
&mut mock_hal,
¶ms,
&payment_request,
- &BigUint::from_bytes_be(&output_value),
+ &BigUint::from_bytes_le(&output_value),
output_address,
)
.is_ok()
@@ -1706,21 +1722,21 @@ mod tests {
&mut TestingHal::new(),
¶ms,
&payment_request,
- &BigUint::from_bytes_be(&output_value[24..]),
+ &BigUint::from_bytes_le(&output_value[..8]),
output_address,
),
Ok(())
));
let wrong_output_value =
- hex!("000000000000000000000000000000000000000000000000075cf1259e9c4001");
+ hex!("01409c9e25f15c07000000000000000000000000000000000000000000000000");
// Wrong source amount must produce wrong signature.
assert!(matches!(
validate_eth(
&mut TestingHal::new(),
¶ms,
&payment_request,
- &BigUint::from_bytes_be(&wrong_output_value),
+ &BigUint::from_bytes_le(&wrong_output_value),
output_address,
),
Err(ValidationError::InvalidSignature)
@@ -1732,7 +1748,7 @@ mod tests {
&mut TestingHal::new(),
¶ms,
&payment_request,
- &BigUint::from_bytes_be(&output_value),
+ &BigUint::from_bytes_le(&output_value),
"0x1111111111111111111111111111111111111111",
),
Err(ValidationError::InvalidSignature)
@@ -1750,7 +1766,7 @@ mod tests {
unit: "ETH",
},
&payment_request,
- &BigUint::from_bytes_be(&output_value),
+ &BigUint::from_bytes_le(&output_value),
output_address,
),
Err(ValidationError::InvalidSignature)
Why this scored 60/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.