Skip to content

Commit 3e73a6d

Browse files
committed
eth: add streaming support for large data for EIP-712 typed data
1 parent 574c05a commit 3e73a6d

6 files changed

Lines changed: 101 additions & 15 deletions

File tree

CHANGELOG-npm.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Changelog
22

33
## [Unreleased]
4-
- eth: add support for streaming transactions with large data
4+
- eth: add support for streaming transactions and EIP-712 typed data with large data
55
- eth: add optional `useAntiklepto` argument to `ethSignTypedMessage()` (set to `false` for
66
deterministic typed-message signatures, firmware >=9.26.0)
77

CHANGELOG-rust.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Changelog
22

33
## [Unreleased]
4-
- eth: add support for streaming transactions with large data
4+
- eth: add support for streaming transactions and EIP-712 typed data with large data
55
- eth: add `use_antiklepto` toggle to `eth_sign_typed_message()` (set `false` for deterministic
66
typed-message signatures, firmware >=9.26.0)
77

messages/eth.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ message ETHTypedMessageValueResponse {
144144

145145
message ETHTypedMessageValueRequest {
146146
bytes value = 1;
147+
// If non-zero, value should be empty and data will be streamed via
148+
// DataRequestChunk/DataResponseChunk.
149+
uint32 data_length = 2;
147150
}
148151

149152
message ETHRequest {

src/eth.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ fn encode_value(typ: &MemberType, value: &Value) -> Result<Vec<u8>, String> {
383383
fn get_value(
384384
what: &pb::EthTypedMessageValueResponse,
385385
msg: &Eip712Message,
386-
) -> Result<Vec<u8>, String> {
386+
) -> Result<(Vec<u8>, DataType), String> {
387387
enum Either<'a> {
388388
HashMap(&'a HashMap<String, Value>),
389389
JsonValue(Value),
@@ -446,8 +446,10 @@ fn get_value(
446446
_ => return Err("path element does not point to struct or array".into()),
447447
}
448448
}
449+
let data_type =
450+
DataType::try_from(typ.r#type).map_err(|_| format!("invalid data type: {}", typ.r#type))?;
449451
if let Either::JsonValue(value) = &value {
450-
encode_value(&typ, value)
452+
encode_value(&typ, value).map(|v| (v, data_type))
451453
} else {
452454
Err("path points to struct or array; value expected".to_string())
453455
}
@@ -700,12 +702,25 @@ impl<R: Runtime> PairedBitBox<R> {
700702
))
701703
.await?;
702704
while let pb::eth_response::Response::TypedMsgValue(typed_msg_value) = &response {
703-
let value = get_value(typed_msg_value, &msg).map_err(Error::EthTypedMessage)?;
705+
let (value, data_type) =
706+
get_value(typed_msg_value, &msg).map_err(Error::EthTypedMessage)?;
707+
if data_type == DataType::String && value.len() > STREAMING_THRESHOLD {
708+
return Err(Error::EthTypedMessage(
709+
"string value exceeds maximum size".into(),
710+
));
711+
}
712+
let use_streaming = value.len() > STREAMING_THRESHOLD;
704713
response = self
705714
.query_proto_eth(pb::eth_request::Request::TypedMsgValue(
706-
pb::EthTypedMessageValueRequest { value },
715+
pb::EthTypedMessageValueRequest {
716+
value: if use_streaming { vec![] } else { value.clone() },
717+
data_length: if use_streaming { value.len() as u32 } else { 0 },
718+
},
707719
))
708720
.await?;
721+
if use_streaming {
722+
response = self.handle_eth_data_streaming(&value, response).await?;
723+
}
709724
}
710725
let mut signature = if use_antiklepto {
711726
self.handle_antiklepto(&response, host_nonce.unwrap())
@@ -1224,7 +1239,7 @@ mod tests {
12241239
.is_err());
12251240

12261241
// domain.name
1227-
let value = get_value(
1242+
let (value, _) = get_value(
12281243
&pb::EthTypedMessageValueResponse {
12291244
root_object: RootObject::Domain as _,
12301245
path: vec![0],
@@ -1235,7 +1250,7 @@ mod tests {
12351250
assert_eq!(value, b"Ether Mail".to_vec());
12361251

12371252
// domain.version
1238-
let value = get_value(
1253+
let (value, _) = get_value(
12391254
&pb::EthTypedMessageValueResponse {
12401255
root_object: RootObject::Domain as _,
12411256
path: vec![1],
@@ -1246,7 +1261,7 @@ mod tests {
12461261
assert_eq!(value, b"1".to_vec());
12471262

12481263
// domain.chainId
1249-
let value = get_value(
1264+
let (value, _) = get_value(
12501265
&pb::EthTypedMessageValueResponse {
12511266
root_object: RootObject::Domain as _,
12521267
path: vec![2],
@@ -1257,7 +1272,7 @@ mod tests {
12571272
assert_eq!(value, b"\x01".to_vec());
12581273

12591274
// domain.verifyingContract
1260-
let value = get_value(
1275+
let (value, _) = get_value(
12611276
&pb::EthTypedMessageValueResponse {
12621277
root_object: RootObject::Domain as _,
12631278
path: vec![3],
@@ -1282,7 +1297,7 @@ mod tests {
12821297
// MESSAGE
12831298

12841299
// message.from.name
1285-
let value = get_value(
1300+
let (value, _) = get_value(
12861301
&pb::EthTypedMessageValueResponse {
12871302
root_object: RootObject::Message as _,
12881303
path: vec![0, 0],
@@ -1293,7 +1308,7 @@ mod tests {
12931308
assert_eq!(value, b"Cow".to_vec());
12941309

12951310
// message.from.wallet
1296-
let value = get_value(
1311+
let (value, _) = get_value(
12971312
&pb::EthTypedMessageValueResponse {
12981313
root_object: RootObject::Message as _,
12991314
path: vec![0, 1],
@@ -1307,7 +1322,7 @@ mod tests {
13071322
);
13081323

13091324
// message.to.wallet
1310-
let value = get_value(
1325+
let (value, _) = get_value(
13111326
&pb::EthTypedMessageValueResponse {
13121327
root_object: RootObject::Message as _,
13131328
path: vec![1, 1],
@@ -1321,7 +1336,7 @@ mod tests {
13211336
);
13221337

13231338
// message.attachments.0.contents
1324-
let value = get_value(
1339+
let (value, _) = get_value(
13251340
&pb::EthTypedMessageValueResponse {
13261341
root_object: RootObject::Message as _,
13271342
path: vec![3, 0, 0],
@@ -1332,7 +1347,7 @@ mod tests {
13321347
assert_eq!(value, b"attachment1".to_vec());
13331348

13341349
// message.attachments.1.contents
1335-
let value = get_value(
1350+
let (value, _) = get_value(
13361351
&pb::EthTypedMessageValueResponse {
13371352
root_object: RootObject::Message as _,
13381353
path: vec![3, 1, 0],

src/shiftcrypto.bitbox02.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,6 +1973,10 @@ pub mod eth_typed_message_value_response {
19731973
pub struct EthTypedMessageValueRequest {
19741974
#[prost(bytes = "vec", tag = "1")]
19751975
pub value: ::prost::alloc::vec::Vec<u8>,
1976+
/// If non-zero, value should be empty and data will be streamed via
1977+
/// DataRequestChunk/DataResponseChunk.
1978+
#[prost(uint32, tag = "2")]
1979+
pub data_length: u32,
19761980
}
19771981
#[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))]
19781982
#[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))]

tests/test_eth.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@ fn eip1559_sighash(tx: &EIP1559Transaction) -> [u8; 32] {
103103
keccak256(&prefixed)
104104
}
105105

106+
fn eip712_sighash(primary_type: &str, data_type: &str, data: &[u8]) -> [u8; 32] {
107+
let domain_type_hash = keccak256(b"EIP712Domain(string name)");
108+
let name_hash = keccak256(b"Test");
109+
let mut domain_input = Vec::new();
110+
domain_input.extend_from_slice(&domain_type_hash);
111+
domain_input.extend_from_slice(&name_hash);
112+
let domain_separator = keccak256(&domain_input);
113+
114+
let type_hash = keccak256(format!("{primary_type}({data_type} data)").as_bytes());
115+
let data_hash = keccak256(data);
116+
let mut struct_input = Vec::new();
117+
struct_input.extend_from_slice(&type_hash);
118+
struct_input.extend_from_slice(&data_hash);
119+
let struct_hash = keccak256(&struct_input);
120+
121+
let mut sig_input = Vec::new();
122+
sig_input.extend_from_slice(b"\x19\x01");
123+
sig_input.extend_from_slice(&domain_separator);
124+
sig_input.extend_from_slice(&struct_hash);
125+
keccak256(&sig_input)
126+
}
127+
106128
fn verify_eth_signature(sighash: &[u8; 32], signature: &[u8; 65]) {
107129
let secp = secp256k1::Secp256k1::new();
108130
let path: bitcoin::bip32::DerivationPath = "m/44'/60'/0'/0/0".parse().unwrap();
@@ -312,3 +334,45 @@ async fn test_eth_sign_typed_message_antiklepto_disabled() {
312334
})
313335
.await
314336
}
337+
338+
#[tokio::test]
339+
async fn test_eth_sign_typed_message_streaming_bytes() {
340+
test_initialized_simulators(async |paired_bitbox| {
341+
if !semver::VersionReq::parse(">=9.26.0")
342+
.unwrap()
343+
.matches(paired_bitbox.version())
344+
{
345+
return;
346+
}
347+
348+
let large_bytes_hex = "aa".repeat(10000);
349+
let msg = format!(
350+
r#"{{
351+
"types": {{
352+
"EIP712Domain": [
353+
{{ "name": "name", "type": "string" }}
354+
],
355+
"Msg": [
356+
{{ "name": "data", "type": "bytes" }}
357+
]
358+
}},
359+
"primaryType": "Msg",
360+
"domain": {{
361+
"name": "Test"
362+
}},
363+
"message": {{
364+
"data": "0x{large_bytes_hex}"
365+
}}
366+
}}"#
367+
);
368+
369+
let signature = paired_bitbox
370+
.eth_sign_typed_message(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), &msg, false)
371+
.await
372+
.unwrap();
373+
assert_eq!(signature.len(), 65);
374+
let sighash = eip712_sighash("Msg", "bytes", &vec![0xaa; 10000]);
375+
verify_eth_signature(&sighash, &signature);
376+
})
377+
.await
378+
}

0 commit comments

Comments
 (0)