diff --git a/bip32/bip32.py b/bip32/bip32.py index 7dcc026..ba68366 100644 --- a/bip32/bip32.py +++ b/bip32/bip32.py @@ -12,6 +12,8 @@ _hardened_index_in_path, _privkey_to_pubkey, _deriv_path_str_to_list, + _pubkey_is_valid, + _privkey_is_valid, ) @@ -28,6 +30,11 @@ def __init__(self, message): self.message = message +class ParsingError(ValueError): + def __init__(self, message): + self.message = message + + class BIP32: def __init__( self, @@ -65,9 +72,13 @@ def __init__( if privkey is not None: if not isinstance(privkey, bytes): raise InvalidInputError("'privkey' must be bytes") + if not _privkey_is_valid(privkey): + raise InvalidInputError("Invalid secp256k1 private key") if pubkey is not None: if not isinstance(pubkey, bytes): raise InvalidInputError("'pubkey' must be bytes") + if not _pubkey_is_valid(pubkey): + raise InvalidInputError("Invalid secp256k1 public key") else: pubkey = _privkey_to_pubkey(privkey) @@ -268,6 +279,23 @@ def from_xpriv(cls, xpriv): chaincode, key, ) = _unserialize_extended_key(extended_key) + + if key[0] != 0: + raise ParsingError("Invalid xpriv: private key prefix must be 0") + + if depth == 0: + if fingerprint != b"\x00\x00\x00\x00": + raise ParsingError( + "Invalid xpriv: fingerprint must be 0 if depth is 0 (master xpriv)" + ) + if index != 0: + raise ParsingError( + "Invalid xpriv: index must be 0 if depth is 0 (master xpriv)" + ) + + if network is None: + raise ParsingError("Invalid xpriv: unknown network") + # We need to remove the trailing `0` before the actual private key !! return BIP32(chaincode, key[1:], None, fingerprint, depth, index, network) @@ -289,7 +317,24 @@ def from_xpub(cls, xpub): chaincode, key, ) = _unserialize_extended_key(extended_key) - return BIP32(chaincode, None, key, fingerprint, depth, index, network) + + if depth == 0: + if fingerprint != b"\x00\x00\x00\x00": + raise ParsingError( + "Invalid xpub: fingerprint must be 0 if depth is 0 (master xpub)" + ) + if index != 0: + raise ParsingError( + "Invalid xpub: index must be 0 if depth is 0 (master xpub)" + ) + + if network is None: + raise ParsingError("Invalid xpub: unknown network") + + try: + return BIP32(chaincode, None, key, fingerprint, depth, index, network) + except InvalidInputError as e: + raise ParsingError(f"Invalid xpub: '{e}'") @classmethod def from_seed(cls, seed, network="main"): diff --git a/bip32/utils.py b/bip32/utils.py index 6c1ce41..9601992 100644 --- a/bip32/utils.py +++ b/bip32/utils.py @@ -22,6 +22,24 @@ class BIP32DerivationError(Exception): """We derived an invalid (secret > N or point(secret) is infinity) key!""" +def _privkey_is_valid(privkey): + """Takes bytes and returns True if it's a valid secp256k1 privkey""" + try: + coincurve.PrivateKey(privkey) + return True + except ValueError: + return False + + +def _pubkey_is_valid(pubkey): + """Takes bytes and returns True if it's a valid secp256k1 pubkey""" + try: + coincurve.PublicKey(pubkey) + return True + except ValueError: + return False + + def _privkey_to_pubkey(privkey): """Takes a 32 bytes privkey and returns a 33 bytes secp256k1 pubkey""" return coincurve.PublicKey.from_secret(privkey).format() @@ -167,9 +185,10 @@ def _unserialize_extended_key(extended_key): """ assert isinstance(extended_key, bytes) and len(extended_key) == 78 prefix = int.from_bytes(extended_key[:4], "big") + network = None if prefix in list(ENCODING_PREFIX["main"].values()): network = "main" - else: + elif prefix in list(ENCODING_PREFIX["test"].values()): network = "test" depth = extended_key[4] fingerprint = extended_key[5:9] diff --git a/tests/test_bip32.py b/tests/test_bip32.py index 7cd93ff..fc04b8f 100644 --- a/tests/test_bip32.py +++ b/tests/test_bip32.py @@ -272,6 +272,38 @@ def test_vector_4(): ) +def test_vector_5(): + invalid_xpubs = [ + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm", + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn", + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4", + "xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ", + "xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8", + "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4", + "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9", + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY", + ] + for xpub in invalid_xpubs: + with pytest.raises(ValueError): + BIP32.from_xpub(xpub) + + invalid_xprivs = [ + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G", + "xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J", + "xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH", + "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4", + "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9", + ] + for xpriv in invalid_xprivs: + with pytest.raises(ValueError): + BIP32.from_xpriv(xpriv) + + def test_sanity_checks(): seed = bytes.fromhex( "1077a46dc8545d372f22d9e110ae6c5c2bf7620fe9c4c911f5404d112233e1aa270567dd3554092e051ba3ba86c303590b0309116ac89964ff284db2219d7511"