What changed, and why it matters
This commit is a code cleanup in the BitBox02 firmware's Ethereum transaction signing module. It replaces a generic/template-based approach with dynamic dispatch (using Rust trait objects) to reduce firmware size by about 1,776 bytes. There is no indication in the commit or supplied references that this fixes a security vulnerability or changes security-relevant behavior.
No security action required. Treat as a normal refactoring/size-optimization commit. Standard regression testing of Ethereum transaction signing is sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors sighash.rs and sign.rs to use dyn DataProducer trait objects instead of generic DataProducer implementations with RefCell. The DataProducer trait’s next() method now returns a Pin<Box<dyn Future>> to support async dynamic dispatch. ParamsLegacy and ParamsEIP1559 no longer carry generic type parameters and hold &mut dyn DataProducer instead of RefCell<D>. The Write::write_producer, hash_producer, compute_legacy, and compute_eip1559 functions are similarly de-genericized. The functional behavior of Ethereum sighash computation appears unchanged; the primary motivation stated is firmware size reduction.
Changed components
src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rssrc/rust/bitbox02-rust/src/hww/api/ethereum/sign.rsInspect captured patch +106 / −113
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs
index 881c0b3..8f0aa98 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sighash.rs
@@ -5,25 +5,25 @@ use sha3::{Digest, Keccak256};
const RLP_SMALL_TAG: u8 = 0xc0;
const RLP_LARGE_TAG: u8 = 0xf7;
-use core::cell::RefCell;
-use core::ops::DerefMut;
+use core::future::Future;
+use core::pin::Pin;
+use alloc::boxed::Box;
use alloc::vec::Vec;
use super::Error;
/// An async producer/generator of a bytes array. This is used to be able to accumulate the RLP hash
/// of the `data` field, which can be very large and has to be streamed in chunks in that case.
-#[allow(async_fn_in_trait)]
pub trait DataProducer {
- type Error;
/// Returns the length of the data.
fn len(&self) -> u32;
/// Returns the first byte of the data.
fn first_byte(&self) -> u8;
/// Produces a chunk of the data. Returns `Ok(Some(data))` if data was available,
/// `Ok(None)` when there are no more chunks, or `Err` on failure.
- async fn next(&mut self) -> Result<Option<Vec<u8>>, Self::Error>;
+ fn next<'a>(&'a mut self)
+ -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + 'a>>;
}
pub enum ChunkingProducer<'a> {
@@ -56,8 +56,6 @@ impl<'a> ChunkingProducer<'a> {
}
impl DataProducer for ChunkingProducer<'_> {
- type Error = Error;
-
fn len(&self) -> u32 {
match self {
Self::Inline { data, .. } => data.len() as u32,
@@ -74,73 +72,77 @@ impl DataProducer for ChunkingProducer<'_> {
}
}
- async fn next(&mut self) -> Result<Option<Vec<u8>>, Self::Error> {
- match self {
- Self::Inline { data, consumed } => {
- if !*consumed {
- *consumed = true;
- Ok(Some(data.to_vec()))
- } else {
- Ok(None)
- }
- }
- Self::Host {
- total_length,
- offset,
- first_byte_cached,
- } => {
- if *offset >= *total_length {
- return Ok(None);
+ fn next<'a>(
+ &'a mut self,
+ ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + 'a>> {
+ Box::pin(async move {
+ match self {
+ Self::Inline { data, consumed } => {
+ if !*consumed {
+ *consumed = true;
+ Ok(Some(data.to_vec()))
+ } else {
+ Ok(None)
+ }
}
+ Self::Host {
+ total_length,
+ offset,
+ first_byte_cached,
+ } => {
+ if *offset >= *total_length {
+ return Ok(None);
+ }
- const CHUNK_SIZE: u32 = 4096;
- let remaining = *total_length - *offset;
- let chunk_length = core::cmp::min(CHUNK_SIZE, remaining);
-
- let response =
- super::next_request(super::pb::eth_response::Response::DataRequestChunk(
- super::pb::EthSignDataRequestChunkResponse {
- offset: *offset,
- length: chunk_length,
- },
- ))
- .await?;
-
- match response {
- super::pb::eth_request::Request::DataResponseChunk(
- super::pb::EthSignDataResponseChunkRequest { chunk },
- ) => {
- // Error: chunk size mismatch
- if chunk.len() as u32 != chunk_length {
- return Err(Error::InvalidInput);
- }
-
- if *offset == 0 && !chunk.is_empty() {
- *first_byte_cached = Some(chunk[0]);
+ const CHUNK_SIZE: u32 = 4096;
+ let remaining = *total_length - *offset;
+ let chunk_length = core::cmp::min(CHUNK_SIZE, remaining);
+
+ let response =
+ super::next_request(super::pb::eth_response::Response::DataRequestChunk(
+ super::pb::EthSignDataRequestChunkResponse {
+ offset: *offset,
+ length: chunk_length,
+ },
+ ))
+ .await?;
+
+ match response {
+ super::pb::eth_request::Request::DataResponseChunk(
+ super::pb::EthSignDataResponseChunkRequest { chunk },
+ ) => {
+ // Error: chunk size mismatch
+ if chunk.len() as u32 != chunk_length {
+ return Err(Error::InvalidInput);
+ }
+
+ if *offset == 0 && !chunk.is_empty() {
+ *first_byte_cached = Some(chunk[0]);
+ }
+
+ *offset += chunk.len() as u32;
+ Ok(Some(chunk))
}
-
- *offset += chunk.len() as u32;
- Ok(Some(chunk))
+ // Error: wrong response type
+ _ => Err(Error::InvalidInput),
}
- // Error: wrong response type
- _ => Err(Error::InvalidInput),
}
}
- }
+ })
}
}
-pub struct ParamsLegacy<'a, D: DataProducer> {
+pub struct ParamsLegacy<'a> {
pub nonce: &'a [u8],
pub gas_price: &'a [u8],
pub gas_limit: &'a [u8],
pub recipient: &'a [u8],
pub value: &'a [u8],
- pub data: RefCell<D>,
+ pub data: &'a mut dyn DataProducer,
pub chain_id: u64,
}
-pub struct ParamsEIP1559<'a, D: DataProducer> {
+pub struct ParamsEIP1559<'a> {
pub chain_id: u64,
pub nonce: &'a [u8],
pub max_priority_fee_per_gas: &'a [u8],
@@ -148,17 +150,14 @@ pub struct ParamsEIP1559<'a, D: DataProducer> {
pub gas_limit: &'a [u8],
pub recipient: &'a [u8],
pub value: &'a [u8],
- pub data: RefCell<D>,
+ pub data: &'a mut dyn DataProducer,
}
trait Write {
// Writes the given data to the writer.
fn write(&mut self, data: &[u8]);
// Same as `write`, but it writes all the data produced by the async data producer.
- async fn write_producer<D: DataProducer, T: DerefMut<Target = D>>(
- &mut self,
- producer: T,
- ) -> Result<(), D::Error>;
+ async fn write_producer(&mut self, producer: &mut dyn DataProducer) -> Result<(), Error>;
}
struct Hasher(Keccak256);
@@ -168,10 +167,7 @@ impl Write for Hasher {
self.0.update(data);
}
- async fn write_producer<D: DataProducer, T: DerefMut<Target = D>>(
- &mut self,
- mut producer: T,
- ) -> Result<(), D::Error> {
+ async fn write_producer(&mut self, producer: &mut dyn DataProducer) -> Result<(), Error> {
while let Some(data) = producer.next().await? {
self.0.update(&data);
}
@@ -186,10 +182,7 @@ impl Write for Counter {
self.0 += data.len() as u32;
}
- async fn write_producer<D: DataProducer, T: DerefMut<Target = D>>(
- &mut self,
- producer: T,
- ) -> Result<(), D::Error> {
+ async fn write_producer(&mut self, producer: &mut dyn DataProducer) -> Result<(), Error> {
self.0 += producer.len();
Ok(())
}
@@ -218,10 +211,10 @@ fn hash_element<W: Write>(writer: &mut W, bytes: &[u8]) {
}
// Async version of `hash_element()` that streams the data to be hashed from the producer.
-async fn hash_producer<W: Write, D: DataProducer, T: DerefMut<Target = D>>(
+async fn hash_producer<W: Write>(
writer: &mut W,
- producer: T,
-) -> Result<(), D::Error> {
+ producer: &mut dyn DataProducer,
+) -> Result<(), Error> {
// hash header
let len = producer.len();
if len != 1 || producer.first_byte() > 0x7f {
@@ -239,16 +232,16 @@ fn hash_u64<W: Write>(writer: &mut W, value: u64) {
hash_element(writer, stripped)
}
-async fn hash_params_legacy<W: Write, D: DataProducer>(
+async fn hash_params_legacy<W: Write>(
writer: &mut W,
- params: &ParamsLegacy<'_, D>,
-) -> Result<(), D::Error> {
+ params: &mut ParamsLegacy<'_>,
+) -> Result<(), Error> {
hash_element(writer, params.nonce);
hash_element(writer, params.gas_price);
hash_element(writer, params.gas_limit);
hash_element(writer, params.recipient);
hash_element(writer, params.value);
- hash_producer(writer, params.data.borrow_mut()).await?;
+ hash_producer(writer, &mut *params.data).await?;
{
// EIP155, encodes <chainID><0><0>
hash_u64(writer, params.chain_id);
@@ -258,10 +251,10 @@ async fn hash_params_legacy<W: Write, D: DataProducer>(
Ok(())
}
-async fn hash_params_eip1559<W: Write, D: DataProducer>(
+async fn hash_params_eip1559<W: Write>(
writer: &mut W,
- params: &ParamsEIP1559<'_, D>,
-) -> Result<(), D::Error> {
+ params: &mut ParamsEIP1559<'_>,
+) -> Result<(), Error> {
hash_u64(writer, params.chain_id);
hash_element(writer, params.nonce);
hash_element(writer, params.max_priority_fee_per_gas);
@@ -269,7 +262,7 @@ async fn hash_params_eip1559<W: Write, D: DataProducer>(
hash_element(writer, params.gas_limit);
hash_element(writer, params.recipient);
hash_element(writer, params.value);
- hash_producer(writer, params.data.borrow_mut()).await?;
+ hash_producer(writer, &mut *params.data).await?;
hash_header(writer, RLP_SMALL_TAG, RLP_LARGE_TAG, 0); // access list not currently supported and hashed as empty list
Ok(())
}
@@ -279,9 +272,7 @@ async fn hash_params_eip1559<W: Write, D: DataProducer>(
/// not allowed to have leading zeros (unchecked).
///
/// See https://github.com/ethereum/wiki/wiki/RLP
-pub async fn compute_legacy<D: DataProducer<Error = Error>>(
- params: &ParamsLegacy<'_, D>,
-) -> Result<[u8; 32], Error> {
+pub async fn compute_legacy(params: &mut ParamsLegacy<'_>) -> Result<[u8; 32], Error> {
// We hash [nonce, gas price, gas limit, recipient, value, data], RLP encoded.
// The list length prefix is (0xc0 + length of the encoding of all elements).
@@ -301,9 +292,7 @@ pub async fn compute_legacy<D: DataProducer<Error = Error>>(
Ok(hasher.0.finalize().into())
}
-pub async fn compute_eip1559<D: DataProducer<Error = Error>>(
- params: &ParamsEIP1559<'_, D>,
-) -> Result<[u8; 32], Error> {
+pub async fn compute_eip1559(params: &mut ParamsEIP1559<'_>) -> Result<[u8; 32], Error> {
// https://eips.ethereum.org/EIPS/eip-1559
// We hash [chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas limit, recipient, value, data, access list]
// RLP encoded. Prefixed with 0x02 for EIP1559 transaction type
@@ -406,7 +395,8 @@ pub mod tests {
let expected_sighash: [u8; 32] = decode_hex(&test.expected_sighash).try_into().unwrap();
if data.len() < DATA_THRESHOLD {
- let params = ParamsEIP1559 {
+ let mut producer = ChunkingProducer::from_data(&data);
+ let mut params = ParamsEIP1559 {
chain_id: test.chain_id,
nonce: &nonce,
max_priority_fee_per_gas: &max_priority_fee,
@@ -414,9 +404,9 @@ pub mod tests {
gas_limit: &gas_limit,
recipient: &recipient,
value: &value,
- data: RefCell::new(ChunkingProducer::from_data(&data)),
+ data: &mut producer,
};
- let result = block_on(compute_eip1559(¶ms)).unwrap();
+ let result = block_on(compute_eip1559(&mut params)).unwrap();
assert_eq!(
result, expected_sighash,
"EIP1559 test {} failed (ChunkingProducer::from_data)",
@@ -424,7 +414,8 @@ pub mod tests {
);
} else {
setup_chunk_responder(data.clone());
- let params = ParamsEIP1559 {
+ let mut producer = ChunkingProducer::from_host(data.len() as u32);
+ let mut params = ParamsEIP1559 {
chain_id: test.chain_id,
nonce: &nonce,
max_priority_fee_per_gas: &max_priority_fee,
@@ -432,9 +423,9 @@ pub mod tests {
gas_limit: &gas_limit,
recipient: &recipient,
value: &value,
- data: RefCell::new(ChunkingProducer::from_host(data.len() as u32)),
+ data: &mut producer,
};
- let result = block_on(compute_eip1559(¶ms)).unwrap();
+ let result = block_on(compute_eip1559(&mut params)).unwrap();
assert_eq!(
result, expected_sighash,
"EIP1559 test {} failed (ChunkingProducer::from_host)",
@@ -460,16 +451,17 @@ pub mod tests {
let expected_sighash: [u8; 32] = decode_hex(&test.expected_sighash).try_into().unwrap();
if data.len() < DATA_THRESHOLD {
- let params = ParamsLegacy {
+ let mut producer = ChunkingProducer::from_data(&data);
+ let mut params = ParamsLegacy {
nonce: &nonce,
gas_price: &gas_price,
gas_limit: &gas_limit,
recipient: &recipient,
value: &value,
- data: RefCell::new(ChunkingProducer::from_data(&data)),
+ data: &mut producer,
chain_id: test.chain_id,
};
- let result = block_on(compute_legacy(¶ms)).unwrap();
+ let result = block_on(compute_legacy(&mut params)).unwrap();
assert_eq!(
result, expected_sighash,
"Legacy test {} failed (ChunkingProducer::from_data)",
@@ -477,16 +469,17 @@ pub mod tests {
);
} else {
setup_chunk_responder(data.clone());
- let params = ParamsLegacy {
+ let mut producer = ChunkingProducer::from_host(data.len() as u32);
+ let mut params = ParamsLegacy {
nonce: &nonce,
gas_price: &gas_price,
gas_limit: &gas_limit,
recipient: &recipient,
value: &value,
- data: RefCell::new(ChunkingProducer::from_host(data.len() as u32)),
+ data: &mut producer,
chain_id: test.chain_id,
};
- let result = block_on(compute_legacy(¶ms)).unwrap();
+ let result = block_on(compute_legacy(&mut params)).unwrap();
assert_eq!(
result, expected_sighash,
"Legacy test {} failed (ChunkingProducer::from_host)",
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 a8851c5..a016365 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
@@ -162,32 +162,32 @@ fn parse_fee<'a>(request: &Transaction<'_>, params: &'a Params) -> Amount<'a> {
}
async fn hash_legacy(chain_id: u64, request: &pb::EthSignRequest) -> Result<[u8; 32], Error> {
- let producer = if request.data_length > 0 {
+ let mut producer = if request.data_length > 0 {
super::sighash::ChunkingProducer::from_host(request.data_length)
} else {
super::sighash::ChunkingProducer::from_data(&request.data)
};
- let hash = super::sighash::compute_legacy(&super::sighash::ParamsLegacy {
+ let mut params = super::sighash::ParamsLegacy {
nonce: &request.nonce,
gas_price: &request.gas_price,
gas_limit: &request.gas_limit,
recipient: &request.recipient,
value: &request.value,
- data: core::cell::RefCell::new(producer),
+ data: &mut producer,
chain_id,
- })
- .await
- .map_err(|_| Error::InvalidInput)?;
- Ok(hash)
+ };
+ super::sighash::compute_legacy(&mut params)
+ .await
+ .map_err(|_| Error::InvalidInput)
}
async fn hash_eip1559(request: &pb::EthSignEip1559Request) -> Result<[u8; 32], Error> {
- let producer = if request.data_length > 0 {
+ let mut producer = if request.data_length > 0 {
super::sighash::ChunkingProducer::from_host(request.data_length)
} else {
super::sighash::ChunkingProducer::from_data(&request.data)
};
- let hash = super::sighash::compute_eip1559(&super::sighash::ParamsEIP1559 {
+ let mut params = super::sighash::ParamsEIP1559 {
chain_id: request.chain_id,
nonce: &request.nonce,
max_priority_fee_per_gas: &request.max_priority_fee_per_gas,
@@ -195,11 +195,11 @@ async fn hash_eip1559(request: &pb::EthSignEip1559Request) -> Result<[u8; 32], E
gas_limit: &request.gas_limit,
recipient: &request.recipient,
value: &request.value,
- data: core::cell::RefCell::new(producer),
- })
- .await
- .map_err(|_| Error::InvalidInput)?;
- Ok(hash)
+ data: &mut producer,
+ };
+ super::sighash::compute_eip1559(&mut params)
+ .await
+ .map_err(|_| Error::InvalidInput)
}
/// Verifies an ERC20 transfer.
Why this scored 12/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.