Skip to content

Commit 4a29247

Browse files
committed
bip32: a specific error for privkey-requiring derivation
The assertions one layer below will do the same, but it's not a nice error for the caller. Reported-by: Wladimir J. van der Laan <laanwj@protonmail.com> Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
1 parent 689b212 commit 4a29247

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

bip32/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from .bip32 import BIP32
1+
from .bip32 import BIP32, PrivateDerivationError
22
from .utils import BIP32DerivationError, HARDENED_INDEX
33

44
__version__ = "0.0.8"
55

66
__all__ = [
77
"BIP32",
88
"BIP32DerivationError",
9+
"PrivateDerivationError",
910
"HARDENED_INDEX",
1011
]

bip32/bip32.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
)
1111

1212

13+
class PrivateDerivationError(ValueError):
14+
"""
15+
Tried to use a derivation requiring private keys, without private keys.
16+
"""
17+
pass
18+
19+
1320
class BIP32:
1421
def __init__(self, chaincode, privkey=None, pubkey=None, fingerprint=None,
1522
depth=0, index=0, network="main"):
@@ -53,8 +60,12 @@ def get_extended_privkey_from_path(self, path):
5360
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
5461
:return: chaincode (bytes), privkey (bytes)
5562
"""
63+
if self.master_privkey is None:
64+
raise PrivateDerivationError
65+
5666
if isinstance(path, str):
5767
path = _deriv_path_str_to_list(path)
68+
5869
chaincode, privkey = self.master_chaincode, self.master_privkey
5970
for index in path:
6071
if index & HARDENED_INDEX:
@@ -63,6 +74,7 @@ def get_extended_privkey_from_path(self, path):
6374
else:
6475
privkey, chaincode = \
6576
_derive_unhardened_private_child(privkey, chaincode, index)
77+
6678
return chaincode, privkey
6779

6880
def get_privkey_from_path(self, path):
@@ -72,6 +84,9 @@ def get_privkey_from_path(self, path):
7284
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
7385
:return: privkey (bytes)
7486
"""
87+
if self.master_privkey is None:
88+
raise PrivateDerivationError
89+
7590
return self.get_extended_privkey_from_path(path)[1]
7691

7792
def get_extended_pubkey_from_path(self, path):
@@ -83,6 +98,10 @@ def get_extended_pubkey_from_path(self, path):
8398
"""
8499
if isinstance(path, str):
85100
path = _deriv_path_str_to_list(path)
101+
102+
if _hardened_index_in_path(path) and self.master_privkey is None:
103+
raise PrivateDerivationError
104+
86105
chaincode, key = self.master_chaincode, self.master_privkey
87106
pubkey = self.master_pubkey
88107
# We'll need the private key at some point anyway, so let's derive
@@ -102,6 +121,7 @@ def get_extended_pubkey_from_path(self, path):
102121
for index in path:
103122
pubkey, chaincode = \
104123
_derive_public_child(pubkey, chaincode, index)
124+
105125
return chaincode, pubkey
106126

107127
def get_pubkey_from_path(self, path):
@@ -120,8 +140,12 @@ def get_xpriv_from_path(self, path):
120140
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
121141
:return: The encoded extended pubkey as str.
122142
"""
143+
if self.master_privkey is None:
144+
raise PrivateDerivationError
145+
123146
if isinstance(path, str):
124147
path = _deriv_path_str_to_list(path)
148+
125149
if len(path) == 0:
126150
return self.get_master_xpriv()
127151
elif len(path) == 1:
@@ -133,6 +157,7 @@ def get_xpriv_from_path(self, path):
133157
parent_pubkey,
134158
path[-1], chaincode,
135159
self.network)
160+
136161
return base58.b58encode_check(extended_key).decode()
137162

138163
def get_xpub_from_path(self, path):
@@ -144,6 +169,10 @@ def get_xpub_from_path(self, path):
144169
"""
145170
if isinstance(path, str):
146171
path = _deriv_path_str_to_list(path)
172+
173+
if _hardened_index_in_path(path) and self.master_privkey is None:
174+
raise PrivateDerivationError
175+
147176
if len(path) == 0:
148177
return self.get_master_xpub()
149178
elif len(path) == 1:
@@ -155,10 +184,13 @@ def get_xpub_from_path(self, path):
155184
parent_pubkey,
156185
path[-1], chaincode,
157186
self.network)
187+
158188
return base58.b58encode_check(extended_key).decode()
159189

160190
def get_master_xpriv(self):
161191
"""Get the encoded extended private key of the master private key"""
192+
if self.master_privkey is None:
193+
raise PrivateDerivationError
162194
extended_key = _serialize_extended_key(self.master_privkey, self.depth,
163195
self.parent_fingerprint,
164196
self.index,

tests/test_bip32.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import pytest
44

5-
from bip32 import BIP32, HARDENED_INDEX
5+
from bip32 import BIP32, HARDENED_INDEX, PrivateDerivationError
66

77

88
def test_vector_1():
@@ -162,3 +162,15 @@ def test_sanity_checks():
162162
# But getting from "m'" does not make sense
163163
with pytest.raises(ValueError, match="invalid format"):
164164
bip32.get_pubkey_from_path("m'")
165+
166+
# We raise if we attempt to use a privkey without privkey access
167+
bip32 = BIP32.from_xpub("xpub6C6zm7YgrLrnd7gXkyYDjQihT6F2ei9EYbNuSiDAjok7Ht56D5zbnv8WDoAJGg1RzKzK4i9U2FUwXG7TFGETFc35vpQ4sZBuYKntKMLshiq")
168+
bip32.get_master_xpub()
169+
bip32.get_pubkey_from_path("m/0/1")
170+
bip32.get_xpub_from_path("m/10000/18")
171+
with pytest.raises(PrivateDerivationError):
172+
bip32.get_master_xpriv()
173+
bip32.get_extended_privkey_from_path("m/0/1/2")
174+
bip32.get_privkey_from_path([9, 8])
175+
bip32.get_pubkey_from_path("m/0'/1")
176+
bip32.get_xpub_from_path("m/10000'/18")

0 commit comments

Comments
 (0)