Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion bip32/bip32.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
_hardened_index_in_path,
_privkey_to_pubkey,
_deriv_path_str_to_list,
_pubkey_is_valid,
_privkey_is_valid,
)


Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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"):
Expand Down
21 changes: 20 additions & 1 deletion bip32/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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]
Expand Down
32 changes: 32 additions & 0 deletions tests/test_bip32.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down