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:
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'"))