electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit c744fc4e3d30939e8690d8e591056b64d5fad38e
parent a987a2bbbee67ebfa79b1c3dbe1550e3b8c38f5f
Author: SomberNight <somber.night@protonmail.com>
Date:   Thu, 27 Feb 2020 05:13:31 +0100

follow-up prev: do all checks, and add tests

Diffstat:
Melectrum/bip32.py | 23+++++++++++++++++++++++
Melectrum/keystore.py | 13+++++--------
Melectrum/tests/test_bitcoin.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 100 insertions(+), 8 deletions(-)

diff --git a/electrum/bip32.py b/electrum/bip32.py @@ -401,3 +401,26 @@ def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int]) root_fingerprint = node.fingerprint.hex() return root_fingerprint, derivation_prefix + + +def is_xkey_consistent_with_key_origin_info(xkey: str, *, + derivation_prefix: str = None, + root_fingerprint: str = None) -> bool: + bip32node = BIP32Node.from_xkey(xkey) + int_path = None + if derivation_prefix is not None: + int_path = convert_bip32_path_to_list_of_uint32(derivation_prefix) + if int_path is not None and len(int_path) != bip32node.depth: + return False + if bip32node.depth == 0: + if bfh(root_fingerprint) != bip32node.calc_fingerprint_of_this_node(): + return False + if bip32node.child_number != bytes(4): + return False + if int_path is not None and bip32node.depth > 0: + if int.from_bytes(bip32node.child_number, 'big') != int_path[-1]: + return False + if bip32node.depth == 1: + if bfh(root_fingerprint) != bip32node.fingerprint: + return False + return True diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -36,7 +36,7 @@ from .bitcoin import deserialize_privkey, serialize_privkey from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, - convert_bip32_intpath_to_strpath) + convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info) from .ecc import string_to_number from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160) @@ -468,13 +468,10 @@ class Xpub(MasterPublicKeyMixin): if not (root_fingerprint is None or (is_hex_str(root_fingerprint) and len(root_fingerprint) == 8)): raise Exception("root fp must be 8 hex characters") derivation_prefix = normalize_bip32_derivation(derivation_prefix) - calc_root_fp, calc_der_prefix = bip32.root_fp_and_der_prefix_from_xkey(self.xpub) - if (calc_root_fp is not None and root_fingerprint is not None - and calc_root_fp != root_fingerprint): - raise Exception("provided root fp inconsistent with xpub") - if (calc_der_prefix is not None and derivation_prefix is not None - and calc_der_prefix != derivation_prefix): - raise Exception("provided der prefix inconsistent with xpub") + if not is_xkey_consistent_with_key_origin_info(self.xpub, + derivation_prefix=derivation_prefix, + root_fingerprint=root_fingerprint): + raise Exception("xpub inconsistent with provided key origin info") if root_fingerprint is not None: self._root_fingerprint = root_fingerprint if derivation_prefix is not None: diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py @@ -9,6 +9,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, is_compressed_privkey, EncodeBase58Check, DecodeBase58Check, script_num_to_hex, push_script, add_number_to_script, int_to_hex, opcodes, base_encode, base_decode, BitcoinException) +from electrum import bip32 from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, is_xpub, convert_bip32_path_to_list_of_uint32, @@ -457,6 +458,77 @@ class Test_xprv_xpub(ElectrumTestCase): self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/")) self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h")) + def test_is_xkey_consistent_with_key_origin_info(self): + ### actual data (high depth path) + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + derivation_prefix="m/48'/1'/0'/2'", + root_fingerprint="b2768d2f")) + # ok to skip args + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + derivation_prefix="m/48'/1'/0'/2'")) + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + root_fingerprint="b2768d2f")) + # path changed: wrong depth + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + derivation_prefix="m/48'/0'/2'", + root_fingerprint="b2768d2f")) + # path changed: wrong child index + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + derivation_prefix="m/48'/1'/0'/3'", + root_fingerprint="b2768d2f")) + # path changed: but cannot tell + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + derivation_prefix="m/48'/1'/1'/2'", + root_fingerprint="b2768d2f")) + # fp changed: but cannot tell + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "Zpub75NQordWKAkaF7utBw95GEodyxqwFdR3idtTqQtrvWkYFeiuYdg5c3Q9L9bLjPLhEahLCTjmmS2YQcXPwr6twYCEJ55k6uhE5JxRqvUowmd", + derivation_prefix="m/48'/1'/0'/2'", + root_fingerprint="aaaaaaaa")) + + ### actual data (depth=1 path) + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ", + derivation_prefix="m/0'", + root_fingerprint="b2e35a7d")) + # path changed: wrong depth + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ", + derivation_prefix="m/0'/0'", + root_fingerprint="b2e35a7d")) + # path changed: wrong child index + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ", + derivation_prefix="m/1'", + root_fingerprint="b2e35a7d")) + # fp changed: can tell + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "zpub6nsHdRuY92FsMKdbn9BfjBCG6X8pyhCibNP6uDvpnw2cyrVhecvHRMa3Ne8kdJZxjxgwnpbHLkcR4bfnhHy6auHPJyDTQ3kianeuVLdkCYQ", + derivation_prefix="m/0'", + root_fingerprint="aaaaaaaa")) + + ### actual data (depth=0 path) + self.assertTrue(bip32.is_xkey_consistent_with_key_origin_info( + "xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U", + derivation_prefix="m", + root_fingerprint="48adc7a0")) + # path changed: wrong depth + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U", + derivation_prefix="m/0", + root_fingerprint="48adc7a0")) + # fp changed: can tell + self.assertFalse(bip32.is_xkey_consistent_with_key_origin_info( + "xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U", + derivation_prefix="m", + root_fingerprint="aaaaaaaa")) + def test_is_all_public_derivation(self): self.assertFalse(is_all_public_derivation("m/0/1'/1'")) self.assertFalse(is_all_public_derivation("m/0/2/1'"))