fix(signer): match BIP32 origin by path prefix, not string prefix#81
fix(signer): match BIP32 origin by path prefix, not string prefix#81evanlinjin wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #81 +/- ##
==========================================
+ Coverage 54.01% 54.16% +0.15%
==========================================
Files 12 12
Lines 1620 1621 +1
Branches 62 62
==========================================
+ Hits 875 878 +3
+ Misses 716 714 -2
Partials 29 29
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`get_key` decided whether a key origin covered a `KeyRequest::Bip32` derivation by stringifying both paths and using `starts_with`. Since `DerivationPath` renders components as `/`-joined decimals, this matched on character boundaries rather than components: `m/1` spuriously matched `m/10`, and an unhardened origin matched its hardened sibling. The signer could then return a private key for a derivation the descriptor never meant to expose. Compare the paths component-by-component with a length check instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3e7e399 to
58fa5e5
Compare
| let is_path_prefix = derivation.len() >= path.len() | ||
| && derivation.into_iter().zip(path).all(|(a, b)| a == b); | ||
| if fingerprint == fp && is_path_prefix { |
There was a problem hiding this comment.
it's probably better if you make use of the descriptor/xkey matches() method here, it already handles checking for the fingerprint and matching prefix.
There was a problem hiding this comment.
That won't work as matches checks for equality whereas we want a "prefix matches" check.
There was a problem hiding this comment.
If it helps you can take a look here: https://github.com/rust-bitcoin/rust-miniscript/pull/872/changes
Description
Signer::get_keydecided whether a key origin(fingerprint, path)covered aKeyRequest::Bip32derivation by stringifying both paths and usingstarts_with:Because
DerivationPathrenders itsChildNumbercomponents as/-joined decimals, this matched on character boundaries rather than path components. That produces false positives whenever the origin's stringified form is a substring of the request's:m/1vs requestm/10("10".starts_with("1"))m/84'/1'/0'/1vs requestm/84'/1'/0'/10.../0vs hardened request.../0'When a spurious match fires and the two paths share a component count,
skip(path.len())leaves an emptyto_derive, so the signer returns the private key at the origin path while claiming to satisfy an unrelated request — leaking a key the descriptor was never meant to expose.This PR compares the paths component-by-component with a length check instead of stringifying them.
Notes to the reviewers
Added a regression test
get_key_bip32_string_prefix_not_path_prefixthat builds a descriptor with originm/84'/1'/0'/1and requestsm/84'/1'/0'/10: on the old code the signer returnsSome(key at m/84'/1'/0'/1); with the fix it correctly returnsNone.Reported via
llm-code-review(CWE-697). Discovered by Project Loupe.Changelog notice
Fixed:
Signermatched a BIP32 key origin against a derivation request by string prefix instead of path-component prefix, which could cause it to return a private key for an unrelated derivation path.Before submitting
🤖 Generated with Claude Code