Skip to content

Commit 18f2c90

Browse files
committed
ethereum: allow mainnet on testnet keypath and vice versa
Until now: Mainnet coins were under the xpub: m/44'/60'/0'/0 Testnet coins were under the xpub: m/44'/1'/0'/0 With this commit, one can use either path with any Ethereum network. If it is not matching the above, a warning is displayed. The reason this is changed is that some wallets, like Metamask, use the 60' path for all networks. Using testnet coins on Metamask hence fails since the BitBox02 rejected the transactions based on the keypath. Mixing keypaths like this is safe even if the user ignores the warning, as the chain id (unique per ETH network) is part of the transaction sighash, so one cannot be tricked into sending mainnet coins instead of testnet coins. The only risk is that mainnet coins might not be found if sent to the testnet keypath, hence the warning.
1 parent bfd444f commit 18f2c90

File tree

7 files changed

+276
-52
lines changed

7 files changed

+276
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
77
## Firmware
88

99
### [Unreleased]
10+
- Allow mainnet keypaths (`m/44'/60'/0'/0/*`) Rinkeby and Ropsten, and testnet keypaths (`m/44'/1'/0'/0/*`) for Ethereum mainnet
1011
- Display granular error codes when unlock fails unexpectedly
1112
- Remove RandomNumber API endpoint
1213

src/rust/apps/ethereum/src/keypath.rs

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,23 @@ use util::bip32::HARDENED;
1717
const ACCOUNT_MAX: u32 = 99; // 100 accounts
1818

1919
/// Does limit checks the keypath, whitelisting bip44 purpose, account and change.
20-
/// Only allows the well-known xpub of m'/44'/60'/0'/0 for now.
20+
/// Only allows the well-known xpubs of m'/44'/60'/0'/0 and m'/44'/1'/0'/0 for now.
2121
/// Since ethereum doesn't use the "change" path part it is always 0 and have become part of the
2222
/// xpub keypath.
2323
/// @return true if the keypath is valid, false if it is invalid.
24-
pub fn is_valid_keypath_xpub(keypath: &[u32], expected_coin: u32) -> bool {
25-
keypath.len() == 4 && keypath[..4] == [44 + HARDENED, expected_coin, 0 + HARDENED, 0]
24+
pub fn is_valid_keypath_xpub(keypath: &[u32]) -> bool {
25+
keypath.len() == 4
26+
&& (keypath[..4] == [44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0]
27+
|| keypath[..4] == [44 + HARDENED, 1 + HARDENED, 0 + HARDENED, 0])
2628
}
2729

2830
/// Does limit checks the keypath, whitelisting bip44 purpose, account and change.
2931
/// Returns true if the keypath is valid, false if it is invalid.
30-
pub fn is_valid_keypath_address(keypath: &[u32], expected_coin: u32) -> bool {
32+
pub fn is_valid_keypath_address(keypath: &[u32]) -> bool {
3133
if keypath.len() != 5 {
3234
return false;
3335
}
34-
if !is_valid_keypath_xpub(&keypath[..4], expected_coin) {
36+
if !is_valid_keypath_xpub(&keypath[..4]) {
3537
return false;
3638
}
3739
if keypath[4] > ACCOUNT_MAX {
@@ -46,66 +48,98 @@ mod tests {
4648

4749
#[test]
4850
fn test_is_valid_keypath_xpub() {
49-
let expected_coin = 60 + HARDENED;
50-
assert!(is_valid_keypath_xpub(
51-
&[44 + HARDENED, expected_coin, 0 + HARDENED, 0],
52-
expected_coin
53-
));
51+
assert!(is_valid_keypath_xpub(&[
52+
44 + HARDENED,
53+
60 + HARDENED,
54+
0 + HARDENED,
55+
0
56+
]));
57+
assert!(is_valid_keypath_xpub(&[
58+
44 + HARDENED,
59+
1 + HARDENED,
60+
0 + HARDENED,
61+
0
62+
]));
5463
// wrong coin.
55-
assert!(!is_valid_keypath_xpub(
56-
&[44 + HARDENED, expected_coin, 0 + HARDENED, 0],
57-
expected_coin + 1,
58-
));
64+
assert!(!is_valid_keypath_xpub(&[
65+
44 + HARDENED,
66+
0 + HARDENED,
67+
0 + HARDENED,
68+
0
69+
]));
5970
// too short
60-
assert!(!is_valid_keypath_xpub(
61-
&[44 + HARDENED, expected_coin, 0 + HARDENED],
62-
expected_coin + 1,
63-
));
71+
assert!(!is_valid_keypath_xpub(&[
72+
44 + HARDENED,
73+
60 + HARDENED,
74+
0 + HARDENED
75+
]));
6476
// too long
65-
assert!(!is_valid_keypath_xpub(
66-
&[44 + HARDENED, expected_coin, 0 + HARDENED, 0, 0],
67-
expected_coin + 1,
68-
));
77+
assert!(!is_valid_keypath_xpub(&[
78+
44 + HARDENED,
79+
60 + HARDENED,
80+
0 + HARDENED,
81+
0,
82+
0
83+
]));
6984
}
7085

7186
#[test]
7287
fn test_is_valid_keypath_address() {
73-
let expected_coin = 60 + HARDENED;
74-
let keypath_for_account =
75-
|account| [44 + HARDENED, expected_coin, 0 + HARDENED, 0, account];
76-
7788
// 100 good paths.
7889
for account in 0..100 {
79-
assert!(is_valid_keypath_address(
80-
&keypath_for_account(account),
81-
expected_coin
82-
));
90+
assert!(is_valid_keypath_address(&[
91+
44 + HARDENED,
92+
60 + HARDENED,
93+
0 + HARDENED,
94+
0,
95+
account
96+
]));
97+
assert!(is_valid_keypath_address(&[
98+
44 + HARDENED,
99+
1 + HARDENED,
100+
0 + HARDENED,
101+
0,
102+
account
103+
]));
83104
// wrong coin
84-
assert!(!is_valid_keypath_address(
85-
&keypath_for_account(account),
86-
expected_coin + 1
87-
));
105+
assert!(!is_valid_keypath_address(&[
106+
44 + HARDENED,
107+
0 + HARDENED,
108+
0 + HARDENED,
109+
0,
110+
account
111+
]));
88112
}
89-
assert!(!is_valid_keypath_address(
90-
&keypath_for_account(100),
91-
expected_coin
92-
));
113+
// account too high
114+
assert!(!is_valid_keypath_address(&[
115+
44 + HARDENED,
116+
60 + HARDENED,
117+
0 + HARDENED,
118+
0,
119+
100
120+
]));
93121

94122
// too short
95-
assert!(!is_valid_keypath_address(
96-
&[44 + HARDENED, expected_coin, 0 + HARDENED, 0],
97-
expected_coin
98-
));
123+
assert!(!is_valid_keypath_address(&[
124+
44 + HARDENED,
125+
60 + HARDENED,
126+
0 + HARDENED,
127+
0
128+
]));
99129
// too long
100-
assert!(!is_valid_keypath_address(
101-
&[44 + HARDENED, expected_coin, 0 + HARDENED, 0, 0, 0],
102-
expected_coin
103-
));
130+
assert!(!is_valid_keypath_address(&[
131+
44 + HARDENED,
132+
60 + HARDENED,
133+
0 + HARDENED,
134+
0,
135+
0,
136+
0
137+
]));
104138
// tweak keypath elements
105139
for i in 0..4 {
106-
let mut keypath = keypath_for_account(0);
140+
let mut keypath = [44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
107141
keypath[i] += 1;
108-
assert!(!is_valid_keypath_address(&keypath, expected_coin));
142+
assert!(!is_valid_keypath_address(&keypath));
109143
}
110144
}
111145
}

src/rust/bitbox02-rust/src/hww/api/ethereum.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ compile_error!(
1818
);
1919

2020
mod amount;
21+
mod keypath;
2122
mod pubrequest;
2223
mod sign;
2324
mod signmsg;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2021 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use super::Error;
16+
use crate::workflow::confirm;
17+
use bitbox02::app_eth::Params;
18+
19+
/// If the second element of `keypath` does not match the expected bip44 coin value for the given
20+
/// coin, we warn the user about an unusual keypath.
21+
///
22+
/// The keypath is already assumed to be validated and that the second element is one of the
23+
/// Ethereum bip44 values, e.g. `60'` or `1'`.
24+
///
25+
/// A warning suffices so the user does not accidentally send e.g. mainnet coins to a testnet path
26+
/// (m/44'/1'/...). It is safe to make a transaction on the 'wrong' keypath as the chain id is
27+
/// unique and part of the transaction sighash.
28+
pub async fn warn_unusual_keypath(
29+
params: &Params,
30+
title: &str,
31+
keypath: &[u32],
32+
) -> Result<(), Error> {
33+
if keypath.len() < 2 {
34+
return Err(Error::InvalidInput);
35+
}
36+
if keypath[1] != params.bip44_coin {
37+
let body = format!(
38+
"Unusual keypath warning: {}. Proceed only if you know what you are doing.",
39+
util::bip32::to_string(keypath)
40+
);
41+
let params = confirm::Params {
42+
title,
43+
body: &body,
44+
scrollable: true,
45+
..Default::default()
46+
};
47+
return Ok(confirm::confirm(&params).await?);
48+
}
49+
Ok(())
50+
}

src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async fn process_address(request: &pb::EthPubRequest) -> Result<Response, Error>
4242
)
4343
};
4444

45-
if !ethereum::keypath::is_valid_keypath_address(&request.keypath, params.bip44_coin) {
45+
if !ethereum::keypath::is_valid_keypath_address(&request.keypath) {
4646
return Err(Error::InvalidInput);
4747
}
4848
let pubkey = bitbox02::keystore::secp256k1_pubkey_uncompressed(&request.keypath)
@@ -54,6 +54,7 @@ async fn process_address(request: &pb::EthPubRequest) -> Result<Response, Error>
5454
Some(erc20_params) => erc20_params.name,
5555
None => params.name,
5656
};
57+
super::keypath::warn_unusual_keypath(&params, title, &request.keypath).await?;
5758
let params = confirm::Params {
5859
title,
5960
title_autowrap: true,
@@ -73,8 +74,8 @@ fn process_xpub(request: &pb::EthPubRequest) -> Result<Response, Error> {
7374
return Err(Error::InvalidInput);
7475
}
7576

76-
let params = bitbox02::app_eth::params_get(request.coin as _).ok_or(Error::InvalidInput)?;
77-
if !ethereum::keypath::is_valid_keypath_xpub(&request.keypath, params.bip44_coin) {
77+
bitbox02::app_eth::params_get(request.coin as _).ok_or(Error::InvalidInput)?;
78+
if !ethereum::keypath::is_valid_keypath_xpub(&request.keypath) {
7879
return Err(Error::InvalidInput);
7980
}
8081
let xpub = keystore::encode_xpub_at_keypath(&request.keypath, keystore::xpub_type_t::XPUB)
@@ -201,6 +202,44 @@ mod tests {
201202
}))
202203
);
203204

205+
static mut CONFIRM_COUNTER: u32 = 0;
206+
207+
// All good, with display, unusual keypath.
208+
mock(Data {
209+
ui_confirm_create: Some(Box::new(|params| {
210+
match unsafe {
211+
CONFIRM_COUNTER += 1;
212+
CONFIRM_COUNTER
213+
} {
214+
1 => {
215+
assert_eq!(params.title, "Ropsten");
216+
assert_eq!(params.body, "Unusual keypath warning: m/44'/60'/0'/0/0. Proceed only if you know what you are doing.");
217+
}
218+
2 => {
219+
assert_eq!(params.title, "Ropsten");
220+
assert_eq!(params.body, ADDRESS);
221+
}
222+
_ => panic!("too many user confirmations"),
223+
}
224+
true
225+
})),
226+
..Default::default()
227+
});
228+
mock_unlocked();
229+
assert_eq!(
230+
block_on(process(&pb::EthPubRequest {
231+
output_type: OutputType::Address as _,
232+
keypath: [44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0].to_vec(),
233+
coin: pb::EthCoin::RopstenEth as _,
234+
display: true,
235+
contract_address: b"".to_vec(),
236+
})),
237+
Ok(Response::Pub(pb::PubResponse {
238+
r#pub: ADDRESS.into()
239+
}))
240+
);
241+
assert_eq!(unsafe { CONFIRM_COUNTER }, 2);
242+
204243
// Keystore locked.
205244
mock(Data {
206245
ui_confirm_create: Some(Box::new(|_| true)),

src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,10 @@ async fn verify_standard_transaction(
186186
pub async fn process(request: &pb::EthSignRequest) -> Result<Response, Error> {
187187
let params = params_get(request.coin as _).ok_or(Error::InvalidInput)?;
188188

189-
if !ethereum::keypath::is_valid_keypath_address(&request.keypath, params.bip44_coin) {
189+
if !ethereum::keypath::is_valid_keypath_address(&request.keypath) {
190190
return Err(Error::InvalidInput);
191191
}
192+
super::keypath::warn_unusual_keypath(&params, params.name, &request.keypath).await?;
192193

193194
// Size limits.
194195
if request.nonce.len() > 16
@@ -376,6 +377,59 @@ mod tests {
376377
);
377378
}
378379

380+
/// Standard ETH transaction on an unusual keypath (Ropsten on mainnet keypath)
381+
#[test]
382+
pub fn test_process_warn_unusual_keypath() {
383+
let _guard = MUTEX.lock().unwrap();
384+
385+
const KEYPATH: &[u32] = &[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
386+
387+
static mut CONFIRM_COUNTER: u32 = 0;
388+
mock(Data {
389+
ui_confirm_create: Some(Box::new(|params| {
390+
match unsafe {
391+
CONFIRM_COUNTER += 1;
392+
CONFIRM_COUNTER
393+
} {
394+
1 => {
395+
assert_eq!(params.title, "Ropsten");
396+
assert_eq!(params.body, "Unusual keypath warning: m/44'/60'/0'/0/0. Proceed only if you know what you are doing.");
397+
true
398+
}
399+
_ => panic!("too many user confirmations"),
400+
}
401+
})),
402+
ui_transaction_address_create: Some(Box::new(|amount, address| {
403+
assert_eq!(amount, "0.530564 TETH");
404+
assert_eq!(address, "0x04F264Cf34440313B4A0192A352814FBe927b885");
405+
true
406+
})),
407+
ui_transaction_fee_create: Some(Box::new(|total, fee| {
408+
assert_eq!(total, "0.53069 TETH");
409+
assert_eq!(fee, "0.000126 TETH");
410+
true
411+
})),
412+
..Default::default()
413+
});
414+
mock_unlocked();
415+
416+
block_on(process(&pb::EthSignRequest {
417+
coin: pb::EthCoin::RopstenEth as _,
418+
keypath: KEYPATH.to_vec(),
419+
nonce: b"\x1f\xdc".to_vec(),
420+
gas_price: b"\x01\x65\xa0\xbc\x00".to_vec(),
421+
gas_limit: b"\x52\x08".to_vec(),
422+
recipient:
423+
b"\x04\xf2\x64\xcf\x34\x44\x03\x13\xb4\xa0\x19\x2a\x35\x28\x14\xfb\xe9\x27\xb8\x85"
424+
.to_vec(),
425+
value: b"\x07\x5c\xf1\x25\x9e\x9c\x40\x00".to_vec(),
426+
data: b"".to_vec(),
427+
host_nonce_commitment: None,
428+
}))
429+
.unwrap();
430+
assert_eq!(unsafe { CONFIRM_COUNTER }, 1);
431+
}
432+
379433
/// Standard ETH transaction with an unknown data field.
380434
#[test]
381435
pub fn test_process_standard_transaction_with_data() {

0 commit comments

Comments
 (0)