Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG-npm.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]
- eth: add support for streaming transactions with large data
- eth: add optional `useAntiklepto` argument to `ethSignTypedMessage()` (set to `false` for
deterministic typed-message signatures, firmware >=9.26.0)

## 0.12.0
- btc: add support for OP_RETURN outputs
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]
- eth: add support for streaming transactions with large data
- eth: add `use_antiklepto` toggle to `eth_sign_typed_message()` (set `false` for deterministic
typed-message signatures, firmware >=9.26.0)

## 0.11.0
- btc: add support for OP_RETURN outputs
Expand Down
2 changes: 1 addition & 1 deletion examples/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async fn eth_demo<R: bitbox_api::runtime::Runtime>() {

println!("Signign typed message...");
let signature = paired_bitbox
.eth_sign_typed_message(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), EIP712_MSG)
.eth_sign_typed_message(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), EIP712_MSG, true)
.await
.unwrap();
println!("Signature: {}", hex::encode(signature));
Expand Down
2 changes: 1 addition & 1 deletion sandbox/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion sandbox/src/Ethereum.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ function EthSignTypedMessage({ bb02 } : Props) {
const [chainID, setChainID] = useState(1);
const [keypath, setKeypath] = useState('m/44\'/60\'/0\'/0/0');
const [msg, setMsg] = useState(exampleMsg);
const [useAntiklepto, setUseAntiklepto] = useState(true);
const [result, setResult] = useState<bitbox.EthSignature | undefined>();
const [running, setRunning] = useState(false);
const [err, setErr] = useState<bitbox.Error>();
Expand All @@ -404,7 +405,7 @@ function EthSignTypedMessage({ bb02 } : Props) {
setResult(undefined);
setErr(undefined);
try {
setResult(await bb02.ethSignTypedMessage(BigInt(chainID), keypath, JSON.parse(msg)));
setResult(await bb02.ethSignTypedMessage(BigInt(chainID), keypath, JSON.parse(msg), useAntiklepto));
} catch (err) {
setErr(bitbox.ensureError(err));
} finally {
Expand All @@ -424,6 +425,10 @@ function EthSignTypedMessage({ bb02 } : Props) {
Keypath
<input type='text' value={keypath} onChange={e => setKeypath(e.target.value)} />
</label>
<label>
Use anti-klepto
<input type='checkbox' checked={useAntiklepto} onChange={e => setUseAntiklepto(e.target.checked)} />
</label>
<label>
EIP-712 typed message
</label>
Expand Down
30 changes: 26 additions & 4 deletions src/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,13 +642,18 @@ impl<R: Runtime> PairedBitBox<R> {

/// Signs an Ethereum EIP-712 typed message. It returns a 65 byte signature (R, S, and 1 byte
/// recID). 27 is added to the recID to denote an uncompressed pubkey.
/// If `use_antiklepto` is false, signing is deterministic and requires firmware >=9.26.0.
pub async fn eth_sign_typed_message(
&self,
chain_id: u64,
keypath: &Keypath,
json_msg: &str,
use_antiklepto: bool,
) -> Result<[u8; 65], Error> {
self.validate_version(">=9.12.0")?;
if !use_antiklepto {
self.validate_version(">=9.26.0")?;
}

let msg: Eip712Message = serde_json::from_str(json_msg)
.map_err(|_| Error::EthTypedMessage("Could not parse EIP-712 JSON message".into()))?;
Expand All @@ -673,7 +678,11 @@ impl<R: Runtime> PairedBitBox<R> {
.collect::<Result<Vec<StructType>, String>>()
.map_err(Error::EthTypedMessage)?;

let host_nonce = crate::antiklepto::gen_host_nonce()?;
let host_nonce = if use_antiklepto {
Some(crate::antiklepto::gen_host_nonce()?)
} else {
None
};

let mut response = self
.query_proto_eth(pb::eth_request::Request::SignTypedMsg(
Expand All @@ -682,8 +691,10 @@ impl<R: Runtime> PairedBitBox<R> {
keypath: keypath.to_vec(),
types: parsed_types,
primary_type: msg.primary_type.clone(),
host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
host_nonce_commitment: host_nonce.as_ref().map(|host_nonce| {
pb::AntiKleptoHostNonceCommitment {
commitment: crate::antiklepto::host_commit(host_nonce).to_vec(),
}
}),
},
))
Expand All @@ -696,7 +707,18 @@ impl<R: Runtime> PairedBitBox<R> {
))
.await?;
}
let mut signature = self.handle_antiklepto(&response, host_nonce).await?;
let mut signature = if use_antiklepto {
self.handle_antiklepto(&response, host_nonce.unwrap())
.await?
} else {
match response {
pb::eth_response::Response::Sign(pb::EthSignResponse { signature }) => signature
.as_slice()
.try_into()
.map_err(|_| Error::UnexpectedResponse)?,
_ => return Err(Error::UnexpectedResponse),
}
};
// 27 is the magic constant to add to the recoverable ID to denote an uncompressed pubkey.
signature[64] += 27;
Ok(signature)
Expand Down
9 changes: 8 additions & 1 deletion src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,17 +526,24 @@ impl PairedBitBox {

/// Signs an Ethereum EIP-712 typed message. It returns a 65 byte signature (R, S, and 1 byte
/// recID). 27 is added to the recID to denote an uncompressed pubkey.
/// `use_antiklepto` defaults to `true` if omitted.
#[wasm_bindgen(js_name = ethSignTypedMessage)]
pub async fn eth_sign_typed_message(
&self,
chain_id: u64,
keypath: types::TsKeypath,
msg: JsValue,
use_antiklepto: Option<bool>,
) -> Result<types::TsEthSignature, JavascriptError> {
let json_msg: String = js_sys::JSON::stringify(&msg).unwrap().into();
let signature = self
.device
.eth_sign_typed_message(chain_id, &keypath.try_into()?, &json_msg)
.eth_sign_typed_message(
chain_id,
&keypath.try_into()?,
&json_msg,
use_antiklepto.unwrap_or(true),
)
.await?;

Ok(serde_wasm_bindgen::to_value(&types::EthSignature {
Expand Down
111 changes: 111 additions & 0 deletions tests/test_eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,54 @@ use bitcoin::secp256k1;
use tiny_keccak::{Hasher, Keccak};
use util::test_initialized_simulators;

const EIP712_MSG: &str = r#"
{
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"Attachment": [
{ "name": "contents", "type": "string" }
],
"Person": [
{ "name": "name", "type": "string" },
{ "name": "wallet", "type": "address" },
{ "name": "age", "type": "uint8" }
],
"Mail": [
{ "name": "from", "type": "Person" },
{ "name": "to", "type": "Person" },
{ "name": "contents", "type": "string" },
{ "name": "attachments", "type": "Attachment[]" }
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
"age": 20
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
"age": "0x1e"
},
"contents": "Hello, Bob!",
"attachments": [{ "contents": "attachment1" }, { "contents": "attachment2" }]
}
}
"#;

fn keccak256(data: &[u8]) -> [u8; 32] {
let mut hasher = Keccak::v256();
hasher.update(data);
Expand Down Expand Up @@ -201,3 +249,66 @@ async fn test_eth_sign_1559_transaction_streaming() {
})
.await
}

#[tokio::test]
async fn test_eth_sign_typed_message_antiklepto_enabled() {
test_initialized_simulators(async |paired_bitbox| {
let signature_antiklepto_1 = paired_bitbox
.eth_sign_typed_message(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), EIP712_MSG, true)
.await
.unwrap();
let signature_antiklepto_2 = paired_bitbox
.eth_sign_typed_message(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), EIP712_MSG, true)
.await
.unwrap();
assert_eq!(signature_antiklepto_1.len(), 65);
assert_eq!(signature_antiklepto_2.len(), 65);
assert_ne!(signature_antiklepto_1, signature_antiklepto_2);
})
.await
}

#[tokio::test]
async fn test_eth_sign_typed_message_antiklepto_disabled() {
test_initialized_simulators(async |paired_bitbox| {
if semver::VersionReq::parse(">=9.26.0")
.unwrap()
.matches(paired_bitbox.version())
{
let signature_no_antiklepto_1 = paired_bitbox
.eth_sign_typed_message(
1,
&"m/44'/60'/0'/0/0".try_into().unwrap(),
EIP712_MSG,
false,
)
.await
.unwrap();
let signature_no_antiklepto_2 = paired_bitbox
.eth_sign_typed_message(
1,
&"m/44'/60'/0'/0/0".try_into().unwrap(),
EIP712_MSG,
false,
)
.await
.unwrap();
assert_eq!(signature_no_antiklepto_1.len(), 65);
assert_eq!(signature_no_antiklepto_2.len(), 65);
assert_eq!(signature_no_antiklepto_1, signature_no_antiklepto_2);
return;
}

let err = paired_bitbox
.eth_sign_typed_message(
1,
&"m/44'/60'/0'/0/0".try_into().unwrap(),
EIP712_MSG,
false,
)
.await
.unwrap_err();
assert!(matches!(err, bitbox_api::error::Error::Version(">=9.26.0")));
})
.await
}