Skip to content

Commit bfc0360

Browse files
committed
descriptor: verify checksum if one is provided at parse time
Signed-off-by: Antoine Poinsot <[email protected]>
1 parent e4486a0 commit bfc0360

File tree

2 files changed

+87
-20
lines changed

2 files changed

+87
-20
lines changed

src/descriptor/checksum.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
66
use std::iter::FromIterator;
77

8-
use crate::descriptor::Error;
8+
use Error;
99

1010
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
1111
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1212

1313
fn poly_mod(mut c: u64, val: u64) -> u64 {
1414
let c0 = c >> 35;
15+
1516
c = ((c & 0x7ffffffff) << 5) ^ val;
1617
if c0 & 1 > 0 {
1718
c ^= 0xf5dee51989
@@ -33,14 +34,16 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
3334
}
3435

3536
/// Compute the checksum of a descriptor
36-
pub fn get_checksum(desc: &str) -> Result<String, Error> {
37+
pub fn desc_checksum(desc: &str) -> Result<String, Error> {
3738
let mut c = 1;
3839
let mut cls = 0;
3940
let mut clscount = 0;
41+
4042
for ch in desc.chars() {
41-
let pos = INPUT_CHARSET
42-
.find(ch)
43-
.ok_or(Error::InvalidDescriptorCharacter(ch))? as u64;
43+
let pos = INPUT_CHARSET.find(ch).ok_or(Error::BadDescriptor(format!(
44+
"Invalid character in checksum: '{}'",
45+
ch
46+
)))? as u64;
4447
c = poly_mod(c, pos & 31);
4548
cls = cls * 3 + (pos >> 5);
4649
clscount += 1;
@@ -72,31 +75,52 @@ pub fn get_checksum(desc: &str) -> Result<String, Error> {
7275
#[cfg(test)]
7376
mod test {
7477
use super::*;
75-
use crate::descriptor::get_checksum;
78+
use std::str;
79+
80+
macro_rules! check_expected {
81+
($desc: expr, $checksum: expr) => {
82+
assert_eq!(desc_checksum($desc).unwrap(), $checksum);
83+
};
84+
}
7685

77-
// test get_checksum() function; it should return the same value as Bitcoin Core
7886
#[test]
79-
fn test_get_checksum() {
80-
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
81-
assert_eq!(get_checksum(desc).unwrap(), "tqz0nc62");
87+
fn test_valid_descriptor_checksum() {
88+
check_expected!(
89+
"wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)",
90+
"tqz0nc62"
91+
);
92+
check_expected!(
93+
"pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)",
94+
"lasegmfs"
95+
);
8296

83-
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
84-
assert_eq!(get_checksum(desc).unwrap(), "lasegmfs");
97+
// https://github.com/bitcoin/bitcoin/blob/7ae86b3c6845873ca96650fc69beb4ae5285c801/src/test/descriptor_tests.cpp#L352-L354
98+
check_expected!(
99+
"sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))",
100+
"ggrsrxfy"
101+
);
102+
check_expected!(
103+
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))",
104+
"tjg09x5t"
105+
);
85106
}
86107

87108
#[test]
88-
fn test_get_checksum_invalid_character() {
109+
fn test_desc_checksum_invalid_character() {
89110
let sparkle_heart = vec![240, 159, 146, 150];
90-
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
111+
let sparkle_heart = str::from_utf8(&sparkle_heart)
91112
.unwrap()
92113
.chars()
93114
.next()
94115
.unwrap();
95116
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
96117

97-
assert!(matches!(
98-
get_checksum(&invalid_desc).err(),
99-
Some(Error::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
100-
));
118+
assert_eq!(
119+
desc_checksum(&invalid_desc).err().unwrap().to_string(),
120+
format!(
121+
"Invalid descriptor: Invalid character in checksum: '{}'",
122+
sparkle_heart
123+
)
124+
);
101125
}
102126
}

src/descriptor/mod.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ use MiniscriptKey;
4949
use Satisfier;
5050
use ToPublicKey;
5151

52+
mod checksum;
5253
mod key;
54+
use self::checksum::desc_checksum;
5355
pub use self::key::{
5456
DescriptorKeyParseError, DescriptorPublicKey, DescriptorPublicKeyCtx, DescriptorSecretKey,
5557
DescriptorSinglePriv, DescriptorSinglePub, DescriptorXKey,
@@ -787,7 +789,19 @@ where
787789
}
788790
}
789791

790-
let top = expression::Tree::from_str(s)?;
792+
let mut parts = s.splitn(2, '#');
793+
let desc_str = parts.next().unwrap();
794+
if let Some(checksum_str) = parts.next() {
795+
let expected_sum = desc_checksum(desc_str)?;
796+
if checksum_str != expected_sum {
797+
return Err(Error::BadDescriptor(format!(
798+
"Invalid checksum '{}', expected '{}'",
799+
checksum_str, expected_sum
800+
)));
801+
}
802+
}
803+
804+
let top = expression::Tree::from_str(desc_str)?;
791805
expression::FromTree::from_tree(&top)
792806
}
793807
}
@@ -1055,7 +1069,7 @@ mod tests {
10551069
use std::cmp;
10561070
use std::collections::HashMap;
10571071
use std::str::FromStr;
1058-
use {Descriptor, DummyKey, Miniscript, NullCtx, Satisfier};
1072+
use {Descriptor, DummyKey, Error, Miniscript, NullCtx, Satisfier};
10591073

10601074
#[cfg(feature = "compiler")]
10611075
use policy;
@@ -1774,6 +1788,35 @@ mod tests {
17741788
let (descriptor, key_map) = Descriptor::parse_descriptor("wpkh(tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/44'/0'/0'/0/*)").unwrap();
17751789
assert_eq!(descriptor.to_string(), "wpkh([2cbe2a6d/44'/0'/0']tpubDCvNhURocXGZsLNqWcqD3syHTqPXrMSTwi8feKVwAcpi29oYKsDD3Vex7x2TDneKMVN23RbLprfxB69v94iYqdaYHsVz3kPR37NQXeqouVz/0/*)");
17761790
assert_eq!(key_map.len(), 1);
1791+
1792+
// https://github.com/bitcoin/bitcoin/blob/7ae86b3c6845873ca96650fc69beb4ae5285c801/src/test/descriptor_tests.cpp#L355-L360
1793+
macro_rules! check_invalid_checksum {
1794+
($($desc: literal),*) => {
1795+
$(
1796+
match Descriptor::parse_descriptor($desc) {
1797+
Err(Error::BadDescriptor(_)) => {},
1798+
Err(e) => panic!("Expected bad checksum for {}, got '{}'", $desc, e),
1799+
_ => panic!("Invalid checksum treated as valid: {}", $desc),
1800+
};
1801+
)*
1802+
};
1803+
}
1804+
check_invalid_checksum!(
1805+
"sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#",
1806+
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#",
1807+
"sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfyq",
1808+
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5tq",
1809+
"sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf",
1810+
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5",
1811+
"sh(multi(3,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy",
1812+
"sh(multi(3,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t",
1813+
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjq09x4t",
1814+
"sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))##ggssrxfy",
1815+
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))##tjq09x4t"
1816+
);
1817+
1818+
Descriptor::parse_descriptor("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy").expect("Valid descriptor with checksum");
1819+
Descriptor::parse_descriptor("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t").expect("Valid descriptor with checksum");
17771820
}
17781821

17791822
#[test]

0 commit comments

Comments
 (0)