Disable signing devices that are not related to the actual spending path

test

test

test

rebase i

rebase i
This commit is contained in:
Thomas Ballivet 2025-05-06 21:11:38 +02:00
parent 41747538ed
commit 86c4896534
7 changed files with 232 additions and 34 deletions

View File

@ -187,6 +187,7 @@ impl PsbtState {
cache.datadir_path.clone(),
cache.network,
self.saved,
self.tx.recovery_timelock(),
);
let cmd = modal.load(daemon);
self.modal = Some(PsbtModal::Sign(modal));
@ -438,6 +439,7 @@ pub struct SignModal {
signed: HashSet<Fingerprint>,
is_saved: bool,
display_modal: bool,
recovery_timelock: Option<u16>,
}
impl SignModal {
@ -447,6 +449,7 @@ impl SignModal {
datadir_path: LianaDirectory,
network: Network,
is_saved: bool,
recovery_timelock: Option<u16>,
) -> Self {
Self {
signing: HashSet::new(),
@ -456,6 +459,7 @@ impl SignModal {
signed,
is_saved,
display_modal: true,
recovery_timelock,
}
}
}
@ -565,6 +569,7 @@ impl Modal for SignModal {
view::psbt::sign_action(
self.error.as_ref(),
&self.hws.list,
&self.wallet.main_descriptor,
self.wallet.signer.as_ref().map(|s| s.fingerprint()),
self.wallet
.signer
@ -572,6 +577,7 @@ impl Modal for SignModal {
.and_then(|signer| self.wallet.keys_aliases.get(&signer.fingerprint)),
&self.signed,
&self.signing,
self.recovery_timelock,
),
)
.on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel)))

View File

@ -13,6 +13,7 @@ pub fn hw_list_view(
hw: &HardwareWallet,
signed: bool,
signing: bool,
can_sign: bool,
) -> Element<Message> {
let mut bttn = Button::new(match hw {
HardwareWallet::Supported {
@ -40,6 +41,8 @@ pub fn hw_list_view(
alias.as_ref(),
"The wallet descriptor is not registered on the device.\n You can register it in the settings.",
)
} else if !can_sign {
hw::disabled_hardware_wallet(kind, version.as_ref(), fingerprint, "This signing device is not part of this spending path.")
} else {
hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
}
@ -71,7 +74,7 @@ pub fn hw_list_view(
})
.style(theme::button::secondary)
.width(Length::Fill);
if !signing {
if can_sign && !signing {
if let HardwareWallet::Supported { registered, .. } = hw {
if *registered != Some(false) {
bttn = bttn.on_press(Message::SelectHardwareWallet(i));

View File

@ -5,6 +5,7 @@ use iced::{
Alignment, Length,
};
use liana::descriptors::LianaDescriptor;
use liana::{
descriptors::{LianaPolicy, PathInfo, PathSpendInfo},
miniscript::bitcoin::{
@ -1014,13 +1015,16 @@ fn change_view(output: &TxOut, network: Network) -> Element<Message> {
.into()
}
#[allow(clippy::too_many_arguments)]
pub fn sign_action<'a>(
warning: Option<&Error>,
hws: &'a [HardwareWallet],
descriptor: &LianaDescriptor,
signer: Option<Fingerprint>,
signer_alias: Option<&'a String>,
signed: &HashSet<Fingerprint>,
signing: &HashSet<Fingerprint>,
recovery_timelock: Option<u16>,
) -> Element<'a, Message> {
Column::new()
.push_maybe(warning.map(|w| warn(Some(w))))
@ -1037,6 +1041,9 @@ pub fn sign_action<'a>(
.push(hws.iter().enumerate().fold(
Column::new().spacing(10),
|col, (i, hw)| {
let can_sign = hw.fingerprint().map_or(false, |f| {
descriptor.contains_fingerprint_in_path(f, recovery_timelock)
});
col.push(hw_list_view(
i,
hw,
@ -1046,20 +1053,29 @@ pub fn sign_action<'a>(
hw.fingerprint()
.map(|f| signing.contains(&f))
.unwrap_or(false),
can_sign,
))
},
))
.push_maybe(signer.map(|fingerprint| {
Button::new(if signed.contains(&fingerprint) {
hw::sign_success_hot_signer(fingerprint, signer_alias)
} else {
hw::hot_signer(fingerprint, signer_alias)
.push_maybe({
signer.map(|fingerprint| {
let can_sign = descriptor
.contains_fingerprint_in_path(fingerprint, recovery_timelock);
let btn = Button::new(if signed.contains(&fingerprint) {
hw::sign_success_hot_signer(fingerprint, signer_alias)
} else {
hw::hot_signer(fingerprint, signer_alias, can_sign)
})
.padding(10)
.style(theme::button::secondary)
.width(Length::Fill);
if can_sign {
btn.on_press(Message::Spend(SpendTxMessage::SelectHotSigner))
} else {
btn
}
})
.on_press(Message::Spend(SpendTxMessage::SelectHotSigner))
.padding(10)
.style(theme::button::secondary)
.width(Length::Fill)
}))
})
.width(Length::Fill),
)
.spacing(20)

View File

@ -204,6 +204,10 @@ impl SpendTx {
.find(|&path| path.sigs_count >= path.threshold)
}
pub fn recovery_timelock(&self) -> Option<u16> {
self.sigs.recovery_paths().keys().max().cloned()
}
pub fn signers(&self) -> HashSet<Fingerprint> {
let mut signers = HashSet::new();
for fg in self.sigs.primary_path().signed_pubkeys.keys() {

View File

@ -125,10 +125,11 @@ pub fn unimplemented_method_hardware_wallet<'a, T: 'a, K: Display, V: Display, F
.width(Length::Fill)
}
pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
pub fn disabled_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
kind: K,
version: Option<V>,
fingerprint: F,
label: &'static str,
) -> Container<'a, T> {
let key = column(vec![
text::p1_regular(format!("#{}", fingerprint)).into(),
@ -144,9 +145,7 @@ pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
.push(key)
.push(Space::with_width(15))
.push(Space::with_width(Length::Fill))
.push(text::text(
"This signing device is not related to this Liana wallet.",
))
.push(text::text(label))
.push(Space::with_width(Length::Fill))
.align_y(Vertical::Center),
)
@ -157,6 +156,19 @@ pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
.width(Length::Fill)
}
pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
kind: K,
version: Option<V>,
fingerprint: F,
) -> Container<'a, T> {
disabled_hardware_wallet(
kind,
version,
fingerprint,
"This signing device is not related to this Liana wallet.",
)
}
pub fn processing_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
kind: K,
version: Option<V>,
@ -484,20 +496,31 @@ pub fn unselected_hot_signer<'a, T: 'a, F: Display>(
pub fn hot_signer<'a, T: 'a, F: Display>(
fingerprint: F,
alias: Option<impl Into<Cow<'a, str>> + Display>,
can_sign: bool,
) -> Container<'a, T> {
Container::new(
column(vec![
Row::new()
.spacing(5)
.push_maybe(alias.map(|a| text::p1_bold(a)))
.push(text::p1_regular(format!("#{}", fingerprint)))
.into(),
Row::new()
.spacing(5)
.push(text::caption("This computer"))
.into(),
])
.width(Length::Fill),
Row::new()
.push(column(vec![
Row::new()
.spacing(5)
.push_maybe(alias.map(|a| text::p1_bold(a)))
.push(text::p1_regular(format!("#{}", fingerprint)))
.into(),
Row::new()
.spacing(5)
.push(text::caption("This computer"))
.into(),
]))
.push(Space::with_width(Length::Fixed(20.0)))
.push_maybe(if !can_sign {
Some(text::text(
"This hot signer is not part of this spending path.",
))
} else {
None
})
.push(Space::with_width(Length::Fill))
.align_y(Vertical::Center),
)
.padding(10)
}

View File

@ -9,6 +9,7 @@ use miniscript::{
RelLockTime, ScriptContext, Threshold,
};
use miniscript::bitcoin::bip32::Fingerprint;
use std::{
collections::{BTreeMap, HashMap, HashSet},
convert::TryFrom,
@ -368,6 +369,11 @@ impl PathInfo {
),
})
}
/// Determine whether the fingerprint is part of this path.
pub fn contains_fingerprint(&self, fingerprint: Fingerprint) -> bool {
self.thresh_origins().1.contains_key(&fingerprint)
}
}
// See

View File

@ -217,6 +217,39 @@ impl LianaDescriptor {
.for_any_key(|k| k.master_fingerprint() == fg)
}
/// Determine whether the fingerprint is part of a specific path of this descriptor.
/// If recovery_timelock is None, checks in the primary path.
/// If recovery_timelock is Some(timelock), checks in the recovery path with specified timelock.
pub fn contains_fingerprint_in_path(
&self,
fingerprint: Fingerprint,
recovery_timelock: Option<u16>,
) -> bool {
match recovery_timelock {
None => self.contains_fingerprint_in_primary_path(fingerprint),
Some(timelock) => self.contains_fingerprint_in_recovery_path(fingerprint, timelock),
}
}
/// Determine whether the fingerprint is part of the primary path of this descriptor.
fn contains_fingerprint_in_primary_path(&self, fingerprint: Fingerprint) -> bool {
self.policy().primary_path.contains_fingerprint(fingerprint)
}
/// Determine whether the fingerprint is part of the recovery path of this descriptor for the
/// specified timelock.
fn contains_fingerprint_in_recovery_path(
&self,
fingerprint: Fingerprint,
recovery_timelock: u16,
) -> bool {
self.policy()
.recovery_paths
.get(&recovery_timelock)
.map(|path_info| path_info.contains_fingerprint(fingerprint))
.unwrap_or(false)
}
/// Get the descriptor for receiving addresses.
pub fn receive_descriptor(&self) -> &SinglePathLianaDesc {
&self.receive_desc
@ -751,9 +784,9 @@ impl DerivedSinglePathLianaDesc {
mod tests {
use super::*;
use bitcoin::{hashes::Hash, Sequence};
use crate::signer::HotSigner;
use bitcoin::{hashes::Hash, Sequence};
use miniscript::bitcoin::bip32::Fingerprint;
fn random_desc_key(
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
@ -2210,13 +2243,120 @@ mod tests {
}
#[test]
fn descriptor_contains_fingerprint() {
fn descriptor_contains_fingerprint_in_primary_path_multi() {
let descr = LianaDescriptor::from_str("wsh(or_d(multi(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*),and_v(v:thresh(2,pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*),a:pkh([aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*),a:pkh([aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*)),older(26352))))").unwrap();
assert!(descr.contains_fingerprint(Fingerprint::from_str("aabb0011").unwrap()));
assert!(descr.contains_fingerprint(Fingerprint::from_str("aabb0012").unwrap()));
assert!(descr.contains_fingerprint(Fingerprint::from_str("aabb0013").unwrap()));
assert!(!descr.contains_fingerprint(Fingerprint::from_str("aabb0014").unwrap()));
assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0011").unwrap())
);
assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0012").unwrap())
);
assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0013").unwrap())
);
assert!(
!descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0014").unwrap())
);
}
#[test]
fn descriptor_contains_fingerprint_in_primary_path_single_key() {
let descr = LianaDescriptor::from_str("wsh(or_d(pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*),and_v(v:thresh(1,pkh([bbcc2233/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*)),older(26352))))").unwrap();
assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0011").unwrap())
);
assert!(
!descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("bbcc2233").unwrap())
);
assert!(
!descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("ddeeff00").unwrap())
);
}
#[test]
fn descriptor_contains_fingerprint_in_recovery_path_multi() {
let descr = LianaDescriptor::from_str("wsh(or_d(multi(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*),and_v(v:thresh(2,pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*),a:pkh([aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*),a:pkh([aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*)),older(26352))))").unwrap();
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0011").unwrap(),
26352
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0012").unwrap(),
26352
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0013").unwrap(),
26352
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0013").unwrap(),
1000
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0014").unwrap(),
26352
));
}
#[test]
fn descriptor_contains_fingerprint_in_recovery_path_single_key() {
let descr = LianaDescriptor::from_str("wsh(or_d(pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*),and_v(v:thresh(1,pkh([bbcc2233/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*)),older(26352))))").unwrap();
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0011").unwrap(),
26352
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("bbcc2233").unwrap(),
26352
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("ddeeff00").unwrap(),
26352
));
}
#[test]
fn descriptor_contains_fingerprint_in_recovery_path_multiple_keys() {
let descr = LianaDescriptor::from_str("wsh(or_d(c:or_i(and_v(v:older(38305),and_v(v:pkh([d6bba22a/84'/1'/0']tpubDCwMfgJWBGfJuZFmgTAP9qdSwJeC4fEKaYXQx7CNiTzsB5WrpVSmySdPFnKDu8ChWZweYNh7MoAoFsCNY7gTRFSGtDYbG9s6vAKNKzT1Hii/<0;1>/*),pk_h([e9e6c583/48'/1'/0'/2']tpubDEWn2LRKdyREaweKHxj7XzSjcxXGTVbFkL5Qi5AWsJzGvN28cKQwGqCND9TP6EPtPaE13eK9SnyuiQ4qsfy5UuGD3p32Ew36mWfKmYCJRcz/<0;1>/*))),pk_k([de8abde2/48'/1'/0'/2']tpubDES5ZQEwEuj7Fpe6d6wkwD8SdequEa2cqq57QHQ43pb1x2HxbLp6anHwutDNzrMhDAbx1YgxCFAbRi6EhWwQLaGMSSmxJRaAzCUgn6VwpVD/<0;1>/*)),and_v(v:thresh(1,pkh([d6bba22a/84'/1'/0']tpubDCwMfgJWBGfJuZFmgTAP9qdSwJeC4fEKaYXQx7CNiTzsB5WrpVSmySdPFnKDu8ChWZweYNh7MoAoFsCNY7gTRFSGtDYbG9s6vAKNKzT1Hii/<2;3>/*),a:pkh([e9e6c583/48'/1'/0'/2']tpubDEWn2LRKdyREaweKHxj7XzSjcxXGTVbFkL5Qi5AWsJzGvN28cKQwGqCND9TP6EPtPaE13eK9SnyuiQ4qsfy5UuGD3p32Ew36mWfKmYCJRcz/<2;3>/*)),older(52596))))").unwrap();
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("d6bba22a").unwrap(),
38305
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("e9e6c583").unwrap(),
38305
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("d6bba22a").unwrap(),
52596
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("e9e6c583").unwrap(),
52596
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("d6bba22a").unwrap(),
12345
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("e9e6c583").unwrap(),
12345
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("ffffffff").unwrap(),
38305
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("ffffffff").unwrap(),
52596
));
}
// TODO: test error conditions of deserialization.