From 448c4866fa7a7abcaf49caabc33dea96844abaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C4=8Ce=C5=A1ka?= Date: Sun, 8 Mar 2026 11:44:50 +0100 Subject: [PATCH] patch bip32_key_unserialize and add tests for TV5 uncrustify --- src/bip32.c | 38 +++++++++++++-------- src/test/test_bip32.py | 76 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/bip32.c b/src/bip32.c index 55b08eee5..8b21c60e1 100644 --- a/src/bip32.c +++ b/src/bip32.c @@ -276,7 +276,7 @@ int bip32_path_from_str_len(const char *str, uint32_t child_num, size_t *written) { return bip32_path_from_str_n_len(str, str ? strlen(str) : 0, child_num, - multi_index, flags, written); + multi_index, flags, written); } int bip32_path_str_n_get_features(const char *str, size_t str_len, @@ -419,10 +419,10 @@ int bip32_key_from_seed(const unsigned char *bytes, size_t bytes_len, } #define ALLOC_KEY() \ - if (!output) \ + if (!output) \ return WALLY_EINVAL; \ - *output = wally_calloc(sizeof(struct ext_key)); \ - if (!*output) \ + *output = wally_calloc(sizeof(struct ext_key)); \ + if (!*output) \ return WALLY_ENOMEM int bip32_key_from_seed_custom_alloc(const unsigned char *bytes, size_t bytes_len, @@ -471,6 +471,10 @@ static bool key_is_valid(const struct ext_key *hdkey) mem_is_zero(hdkey->pub_key + 1, sizeof(hdkey->pub_key) - 1)) return false; + if (wally_ec_public_key_verify(hdkey->pub_key, + sizeof(hdkey->pub_key)) != WALLY_OK) + return false; + if (hdkey->priv_key[0] != BIP32_FLAG_KEY_PUBLIC && hdkey->priv_key[0] != BIP32_FLAG_KEY_PRIVATE) return false; @@ -480,7 +484,8 @@ static bool key_is_valid(const struct ext_key *hdkey) return false; if (is_master && - !mem_is_zero(hdkey->parent160, sizeof(hdkey->parent160))) + (hdkey->child_num != 0 || + !mem_is_zero(hdkey->parent160, sizeof(hdkey->parent160)))) return false; return true; @@ -581,6 +586,11 @@ int bip32_key_unserialize(const unsigned char *bytes, size_t bytes_len, bip32_key_strip_private_key(key_out); } + /* Validate the fully populated key (covers depth-0 fingerprint/child_num, + * public key point-on-curve, and all other structural checks) */ + if (!key_is_valid(key_out)) + return wipe_key_fail(key_out); + key_compute_hash160(key_out); return WALLY_OK; } @@ -1137,9 +1147,9 @@ static int getb_impl(const struct ext_key *hdkey, } #define GET_B(name) \ - int bip32_key_get_ ## name(const struct ext_key *hdkey, unsigned char *bytes_out, size_t len) { \ - return getb_impl(hdkey, hdkey->name, sizeof(hdkey->name), bytes_out, len); \ - } + int bip32_key_get_ ## name(const struct ext_key *hdkey, unsigned char *bytes_out, size_t len) { \ + return getb_impl(hdkey, hdkey->name, sizeof(hdkey->name), bytes_out, len); \ + } GET_B(chain_code) GET_B(parent160) @@ -1161,12 +1171,12 @@ int bip32_key_get_priv_key(const struct ext_key *hdkey, unsigned char *bytes_out #define GET_I(name) \ - int bip32_key_get_ ## name(const struct ext_key *hdkey, size_t *written) { \ - if (written) *written = 0; \ - if (!hdkey || !written) return WALLY_EINVAL; \ - *written = hdkey->name; \ - return WALLY_OK; \ - } + int bip32_key_get_ ## name(const struct ext_key *hdkey, size_t *written) { \ + if (written) *written = 0; \ + if (!hdkey || !written) return WALLY_EINVAL; \ + *written = hdkey->name; \ + return WALLY_OK; \ + } GET_I(depth) GET_I(child_num) diff --git a/src/test/test_bip32.py b/src/test/test_bip32.py index cef95d7b4..72708e529 100755 --- a/src/test/test_bip32.py +++ b/src/test/test_bip32.py @@ -388,6 +388,82 @@ def test_key_init(self): def test_bip32_vectors(self): self.do_test_vector(vec_1) self.do_test_vector(vec_3) + + def test_vector_5_invalid_keys(self): + """BIP32 Test Vector 5: invalid extended keys must be rejected""" + # Each entry is (base58_key, description) + invalid_keys = [ + # pubkey version / prvkey mismatch + ('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3' + 'zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm', + 'pubkey version / prvkey mismatch'), + # prvkey version / pubkey mismatch + ('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63o' + 'StZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH', + 'prvkey version / pubkey mismatch'), + # invalid pubkey prefix 04 + ('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3' + 'zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn', + 'invalid pubkey prefix 04'), + # invalid prvkey prefix 04 + ('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63o' + 'StZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ', + 'invalid prvkey prefix 04'), + # invalid pubkey prefix 01 + ('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3' + 'zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4', + 'invalid pubkey prefix 01'), + # invalid prvkey prefix 01 + ('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63o' + 'StZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J', + 'invalid prvkey prefix 01'), + # zero depth with non-zero parent fingerprint (xprv) + ('xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNT' + 'uLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv', + 'zero depth with non-zero parent fingerprint (xprv)'), + # zero depth with non-zero parent fingerprint (xpub) + ('xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgi' + 'T91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ', + 'zero depth with non-zero parent fingerprint (xpub)'), + # zero depth with non-zero index (xprv) + ('xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF4' + '2acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN', + 'zero depth with non-zero index (xprv)'), + # zero depth with non-zero index (xpub) + ('xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJ' + 'aNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8', + 'zero depth with non-zero index (xpub)'), + # unknown extended key version (1) + ('DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8X' + 'nmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4', + 'unknown extended key version (1)'), + # unknown extended key version (2) + ('DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8X' + 'nmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9', + 'unknown extended key version (2)'), + # private key 0 not in 1..n-1 + ('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63o' + 'StZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx', + 'private key 0 not in 1..n-1'), + # private key n not in 1..n-1 + ('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63o' + 'StZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G', + 'private key n not in 1..n-1'), + # invalid pubkey 020000000000000000000000000000000000000000000000000000000000000007 + ('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3' + 'zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY', + 'invalid pubkey'), + # invalid checksum + ('xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPP' + 'qjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL', + 'invalid checksum'), + ] + + key_out = POINTER(ext_key)() + for base58_key, description in invalid_keys: + ret = bip32_key_from_base58_alloc(utf8(base58_key), byref(key_out)) + self.assertEqual(ret, WALLY_EINVAL, + 'Expected EINVAL for: ' + description) def do_test_vector(self, vec):