From 4973cd4e82d06432676dd192194b95bf2cc38410 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Sat, 11 Jul 2026 14:12:15 +0300 Subject: [PATCH 1/2] fix(key_map): do not fulfill BIP32 requests outside descriptor path Remove the early `Xpriv::get_key` return in `DescriptorSecretKey::get_key`. That path matched on fingerprint only and ignored derivation path / wildcard, so a master xprv in e.g. `.../0/*` would sign sibling paths like `.../9/7`. Rely on `matches()` only. Assisted-by: Grok 4.5 --- src/descriptor/key_map.rs | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/descriptor/key_map.rs b/src/descriptor/key_map.rs index 3e193ec87..687f41445 100644 --- a/src/descriptor/key_map.rs +++ b/src/descriptor/key_map.rs @@ -128,29 +128,29 @@ impl GetKey for DescriptorSecretKey { Ok(None) } } - ( - DescriptorSecretKey::XPrv(descriptor_xkey), - ref key_request @ KeyRequest::Bip32(ref key_source), - ) => { - if let Some(key) = descriptor_xkey.xkey.get_key(key_request.clone(), secp)? { - return Ok(Some(key)); - } - - if let Some(matched_path) = descriptor_xkey.matches(key_source, secp) { - let (_, full_path) = key_source; - - let derivation_path = &full_path[matched_path.len()..]; - - return Ok(Some( - descriptor_xkey - .xkey - .derive_priv(secp, &derivation_path) - .map_err(GetKeyError::Bip32)? - .to_priv(), - )); - } - - Ok(None) + (DescriptorSecretKey::XPrv(descriptor_xkey), KeyRequest::Bip32(ref key_source)) => { + // A successful `matches()` already guarantees the requested key source's fingerprint equals our origin + // (or, when there is no origin, the xkey's own) fingerprint. + // + // `xkey` is anchored at the origin, but the request path is master-relative, either: + // - origin: strip the origin prefix and use the remaining suffix. + // - no origin: use the full request path. + let (_, full_path) = key_source; + let derivation_path = match descriptor_xkey.matches(key_source, secp) { + Some(_) => match &descriptor_xkey.origin { + Some((_, origin_path)) => &full_path[origin_path.len()..], + None => full_path.as_ref(), + }, + None => return Ok(None), + }; + + Ok(Some( + descriptor_xkey + .xkey + .derive_priv(secp, &derivation_path) + .map_err(GetKeyError::Bip32)? + .to_priv(), + )) } (DescriptorSecretKey::XPrv(_), KeyRequest::XOnlyPubkey(_)) => { Err(GetKeyError::NotSupported) From 651715a58d5f4dd728817a0941deb16de2245520 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Sat, 11 Jul 2026 14:12:16 +0300 Subject: [PATCH 2/2] test(key_map): reject sibling BIP32 path for master xprv Regression: master xprv descriptors such as `.../0/*` must not fulfill `KeyRequest::Bip32` for sibling paths like `.../9/7`. Assisted-by: Grok 4.5 --- src/descriptor/key_map.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/descriptor/key_map.rs b/src/descriptor/key_map.rs index 687f41445..c6f57335e 100644 --- a/src/descriptor/key_map.rs +++ b/src/descriptor/key_map.rs @@ -425,4 +425,23 @@ mod tests { let result = keymap.get_key(request_x, &secp).unwrap(); assert!(result.is_none(), "Should return None even on error"); } + + // Master xprv in the descriptor must not sign sibling paths (`/9/7` vs `/0/*`). + #[test] + fn get_key_bip32_rejects_sibling_with_master_xprv() { + let secp = Secp256k1::new(); + let master_xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + .parse::() + .unwrap(); + let fp = master_xprv.fingerprint(&secp); + let account_path: DerivationPath = "86h/1h/0h".parse().unwrap(); + let descriptor = format!("tr({master_xprv}/{account_path}/0/*)"); + let request_path: DerivationPath = format!("{account_path}/9/7").parse().unwrap(); + let key_request = KeyRequest::Bip32((fp, request_path)); + let (_, keymap) = Descriptor::parse_descriptor(&secp, &descriptor).unwrap(); + let result = keymap + .get_key(key_request, &secp) + .expect("key lookup should not fail"); + assert!(result.is_none(), "expected no matching key, got: {result:?}"); + } }