Skip to content

Commit 06cc039

Browse files
committed
bitcoin: allow multisig at arbitrary keypaths
Before, we would restrict the account-level keypaths for multisig to be: - m/48'/coin'/account'/1' for P2WSH_P2SH - m/48'/coin'/account'/2' for P2WSH Since the keypath is verified by the user and the coin network is confirmed for every receive/send, ransom and isolation bypass attacks are mitigated sufficiently, and we can lift this restriction. Note that for wallet policies (of which multisig is a subset of), arbitrary keypaths are already allowed. When exporting an xpub, we furthermore warned about "unusual" keypaths. In addition to the above keypaths, we also allow exporting the xpub at m/45' without warning, as this path is used by Unchained for their vaults.
1 parent b2d0f21 commit 06cc039

File tree

6 files changed

+68
-240
lines changed

6 files changed

+68
-240
lines changed

CHANGELOG.md

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

99
### [Unreleased]
1010
- Bitcoin: add support for payment requests
11+
- Bitcoin: allow multisig accounts at arbitrary keypaths
1112
- Ethereum: allow signing EIP-712 messages containing multi-line strings
1213

1314
### 9.18.0

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ use crate::keystore;
3939

4040
use pb::btc_pub_request::{Output, XPubType};
4141
use pb::btc_request::Request;
42-
use pb::btc_script_config::multisig::ScriptType as MultisigScriptType;
4342
use pb::btc_script_config::{Config, SimpleType};
4443
use pb::btc_script_config::{Multisig, Policy};
4544
use pb::response::Response;
@@ -128,6 +127,8 @@ async fn xpub(
128127
if display {
129128
let title = if is_unusual {
130129
"".into()
130+
} else if keypath == [45 + HARDENED] {
131+
format!("{}\nat\n{}", params.name, util::bip32::to_string(keypath))
131132
} else {
132133
format!("{}\naccount #{}", params.name, keypath[2] - HARDENED + 1)
133134
};
@@ -193,11 +194,9 @@ pub async fn address_multisig(
193194
display: bool,
194195
) -> Result<Response, Error> {
195196
let coin_params = params::get(coin);
196-
let script_type = MultisigScriptType::try_from(multisig.script_type)?;
197-
keypath::validate_address_multisig(keypath, coin_params.bip44_coin, script_type)
198-
.or(Err(Error::InvalidInput))?;
197+
keypath::validate_address_policy(keypath).or(Err(Error::InvalidInput))?;
199198
let account_keypath = &keypath[..keypath.len() - 2];
200-
multisig::validate(multisig, account_keypath, coin_params.bip44_coin)?;
199+
multisig::validate(multisig, account_keypath)?;
201200
let name = match multisig::get_name(coin, multisig, account_keypath)? {
202201
Some(name) => name,
203202
None => return Err(Error::InvalidInput),
@@ -320,6 +319,7 @@ mod tests {
320319
use bitbox02::testing::{
321320
mock, mock_memory, mock_unlocked, mock_unlocked_using_mnemonic, Data, TEST_MNEMONIC,
322321
};
322+
use pb::btc_script_config::multisig::ScriptType as MultisigScriptType;
323323
use util::bip32::HARDENED;
324324

325325
#[test]
@@ -376,6 +376,15 @@ mod tests {
376376
expected_xpub: "zpub6qMaznTYmLk3vtEVNKbozbjRJedYqnHPwSwDwEAVaDkuQd7YEnqBvcbmCDpgvEqw2sqHUMtrJTwD6yNYLoqULriz6PXDYsS14LuoLr3KxUC",
377377
expected_display_title: "Bitcoin\naccount #2",
378378
},
379+
// BTC m/45', no warning
380+
Test {
381+
mnemonic: TEST_MNEMONIC,
382+
coin: BtcCoin::Btc,
383+
keypath: &[45 + HARDENED],
384+
xpub_type: XPubType::Xpub,
385+
expected_xpub: "xpub67uTYzYstMMVao9Z7sseYh5m9N51ft82f6Wo3Lp773Qxe1JxFFDyP71C3xvo3jZ3p1Cg3xZQ8eqsFBHYEVFZt9iqoTBEcCigcmxF1xgqBPm",
386+
expected_display_title: "Bitcoin\nat\nm/45'",
387+
},
379388
// BTC P2TR
380389
Test {
381390
// Test vector from https://github.com/bitcoin/bips/blob/edffe529056f6dfd33d8f716fb871467c3c09263/bip-0086.mediawiki#test-vectors
@@ -909,6 +918,24 @@ mod tests {
909918
script_type: MultisigScriptType::P2wsh,
910919
expected_address: "tb1qndz49j0arp8g6jc8vcrgf9ugrsw96a0j5d7vqcun6jev6rlv47jsv99y5m",
911920
},
921+
// An arbitrary "non-standard" keypath
922+
Test {
923+
coin: BtcCoin::Btc,
924+
threshold: 1,
925+
xpubs: &[
926+
"xpub6FEZ9Bv73h1vnE4TJG4QFj2RPXJhhsPbnXgFyH3ErLvpcZrDcynY65bhWga8PazWHLSLi23PoBhGcLcYW6JRiJ12zXZ9Aop4LbAqsS3gtcy",
927+
"xpub68yJakxtRe3azab9rb8DJqxDeCG7oBY3zhsNnvZybjTE9qc9Hgw4bCqdLjVGykZrwD6CC6r6xHrnuep5Dmb9uq2R4emCm8YzBuddFyhgvAD",
928+
],
929+
expected_info: "1-of-2\nBitcoin multisig",
930+
our_xpub_index: 1,
931+
keypath: &[
932+
45 + HARDENED,
933+
1,
934+
2
935+
],
936+
script_type: MultisigScriptType::P2wsh,
937+
expected_address: "bc1qtsvlhzltl05etjjeqh00urwttu6ep4xn3c0ccndz77unttut9h0qvrcs04",
938+
},
912939
/* P2WSH-P2SH */
913940
Test {
914941
coin: BtcCoin::Btc,

src/rust/bitbox02-rust/src/hww/api/bitcoin/keypath.rs

Lines changed: 7 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub fn validate_account(
5858
/// - Electrum-style: m/48'/coin'/account'/script_type', where script_type is 1 for p2wsh-p2sh and 2
5959
/// for p2wsh.
6060
/// - Nunchuk-style: m/48'/coin'/account', independent of the script type.
61-
pub fn validate_account_multisig(
61+
fn validate_account_multisig(
6262
keypath: &[u32],
6363
expected_coin: u32,
6464
script_type: MultisigScriptType,
@@ -89,22 +89,6 @@ fn validate_change_address(change: u32, address: u32) -> Result<(), ()> {
8989
}
9090
}
9191

92-
/// Validates that the prefix (all but last two elements) of the keypath is a valid multisig account
93-
/// keypath and the last to elements are a valid change and receive element.
94-
pub fn validate_address_multisig(
95-
keypath: &[u32],
96-
expected_coin: u32,
97-
script_type: MultisigScriptType,
98-
) -> Result<(), ()> {
99-
if keypath.len() >= 2 {
100-
let (keypath_account, keypath_rest) = keypath.split_at(keypath.len() - 2);
101-
validate_account_multisig(keypath_account, expected_coin, script_type)?;
102-
validate_change_address(keypath_rest[0], keypath_rest[1])
103-
} else {
104-
Err(())
105-
}
106-
}
107-
10892
/// Validates that the address index is valid and that the account keypath prefix is not empty.
10993
///
11094
/// Note that the change index is not verified as in policies, it can be different to 0 and 1
@@ -166,7 +150,8 @@ pub fn validate_address_simple(
166150
}
167151
}
168152

169-
/// Checks if the keypath is a valid account-level keypath for any supported script type.
153+
/// Checks if the the xpub at this keypath can be exported without warning the user of that it is an
154+
/// unusual keypath.
170155
pub fn validate_xpub(keypath: &[u32], expected_coin: u32, taproot_support: bool) -> Result<(), ()> {
171156
for &script_type in ALL_MULTISCRIPT_SCRIPT_TYPES.iter() {
172157
if validate_account_multisig(keypath, expected_coin, script_type).is_ok() {
@@ -178,6 +163,10 @@ pub fn validate_xpub(keypath: &[u32], expected_coin: u32, taproot_support: bool)
178163
return Ok(());
179164
}
180165
}
166+
// m/45', used/exported by Unchained.
167+
if keypath == [45 + HARDENED] {
168+
return Ok(());
169+
}
181170
Err(())
182171
}
183172

@@ -265,161 +254,6 @@ mod tests {
265254
.is_err());
266255
}
267256

268-
#[test]
269-
fn test_validate_address_multisig() {
270-
let coin = 1 + HARDENED;
271-
// valid p2wsh
272-
assert!(validate_address_multisig(
273-
&[48 + HARDENED, coin, 0 + HARDENED, 2 + HARDENED, 0, 0],
274-
coin,
275-
MultisigScriptType::P2wsh
276-
)
277-
.is_ok());
278-
279-
// valid p2wsh-p2sh
280-
assert!(validate_address_multisig(
281-
&[48 + HARDENED, coin, 0 + HARDENED, 1 + HARDENED, 0, 0],
282-
coin,
283-
MultisigScriptType::P2wshP2sh
284-
)
285-
.is_ok());
286-
287-
// valid Nunchuk-style
288-
assert!(validate_address_multisig(
289-
&[48 + HARDENED, coin, 0 + HARDENED, 0, 0],
290-
coin,
291-
MultisigScriptType::P2wsh
292-
)
293-
.is_ok());
294-
assert!(validate_address_multisig(
295-
&[48 + HARDENED, coin, 0 + HARDENED, 0, 0],
296-
coin,
297-
MultisigScriptType::P2wshP2sh
298-
)
299-
.is_ok());
300-
301-
// wrong script type for p2wsh
302-
assert!(validate_address_multisig(
303-
&[
304-
48 + HARDENED,
305-
coin,
306-
0 + HARDENED,
307-
1 + HARDENED, // should be 2'
308-
0,
309-
0,
310-
],
311-
coin,
312-
MultisigScriptType::P2wsh
313-
)
314-
.is_err());
315-
316-
// wrong script type for p2wsh-p2sh
317-
assert!(validate_address_multisig(
318-
&[
319-
48 + HARDENED,
320-
coin,
321-
0 + HARDENED,
322-
2 + HARDENED, // should be 1'
323-
0,
324-
0,
325-
],
326-
coin,
327-
MultisigScriptType::P2wshP2sh
328-
)
329-
.is_err());
330-
331-
// too short
332-
assert!(validate_address_multisig(
333-
&[48 + HARDENED, coin, 0 + HARDENED, 2 + HARDENED, 0],
334-
coin,
335-
MultisigScriptType::P2wsh
336-
)
337-
.is_err());
338-
339-
// too long
340-
assert!(validate_address_multisig(
341-
&[48 + HARDENED, coin, 0 + HARDENED, 2 + HARDENED, 0, 0, 0],
342-
coin,
343-
MultisigScriptType::P2wsh
344-
)
345-
.is_err());
346-
347-
// wrong purpose
348-
assert!(validate_address_multisig(
349-
&[
350-
49 + HARDENED, // <- wrong
351-
coin,
352-
0 + HARDENED,
353-
2 + HARDENED,
354-
0,
355-
0,
356-
],
357-
coin,
358-
MultisigScriptType::P2wsh
359-
)
360-
.is_err());
361-
362-
// unhardened account
363-
assert!(validate_address_multisig(
364-
&[
365-
48 + HARDENED,
366-
coin,
367-
0, // <- wrong
368-
2 + HARDENED,
369-
0,
370-
0,
371-
],
372-
coin,
373-
MultisigScriptType::P2wsh
374-
)
375-
.is_err());
376-
377-
// account too high
378-
assert!(validate_address_multisig(
379-
&[
380-
48 + HARDENED,
381-
coin,
382-
100 + HARDENED, // <- wrong
383-
2 + HARDENED,
384-
0,
385-
0,
386-
],
387-
coin,
388-
MultisigScriptType::P2wsh
389-
)
390-
.is_err());
391-
392-
// wrong change
393-
assert!(validate_address_multisig(
394-
&[
395-
48 + HARDENED,
396-
coin,
397-
0 + HARDENED,
398-
2 + HARDENED,
399-
2, // <- wrong
400-
0,
401-
],
402-
coin,
403-
MultisigScriptType::P2wsh
404-
)
405-
.is_err());
406-
407-
// address too high
408-
assert!(validate_address_multisig(
409-
&[
410-
48 + HARDENED,
411-
coin,
412-
0 + HARDENED,
413-
2 + HARDENED,
414-
0,
415-
10000, // <- wrong
416-
],
417-
coin,
418-
MultisigScriptType::P2wsh
419-
)
420-
.is_err());
421-
}
422-
423257
#[test]
424258
fn test_validate_address_simple() {
425259
let bip44_account = 99 + HARDENED;

0 commit comments

Comments
 (0)