././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1725370452.9459274 slip10-1.0.1/CHANGELOG.md0000644000000000000000000000014614665610125011367 0ustar00## 1.0.1 - Support Python 3.8 ## 1.0.0 - First release forked from Antoine Poinsot's python-bip32. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1724142092.9906137 slip10-1.0.1/README.md0000644000000000000000000001112114661051015011021 0ustar00# python-slip10 A reference implementation of the [SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) specification, which generalizes the [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) derivation scheme for private and public key pairs in hierarchical deterministic wallets for the curves secp256k1, NIST P-256, ed25519 and curve25519. ## Usage ```python >>> from slip10 import SLIP10, HARDENED_INDEX >>> slip10 = SLIP10.from_seed(bytes.fromhex("01")) # Specify the derivation path as a list ... >>> slip10.get_xpriv_from_path([1, HARDENED_INDEX, 9998]) 'xprv9y4sBgCuub5x2DtbdNBDDCZ3btybk8YZZaTvzV5rmYd3PbU63XLo2QEj6cUt4JAqpF8gJiRKFUW8Vm7thPkccW2DpUvBxASycypEHxmZzts' # ... Or in usual m/the/path/ >>> slip10.get_xpriv_from_path("m/1/0'/9998") 'xprv9y4sBgCuub5x2DtbdNBDDCZ3btybk8YZZaTvzV5rmYd3PbU63XLo2QEj6cUt4JAqpF8gJiRKFUW8Vm7thPkccW2DpUvBxASycypEHxmZzts' >>> slip10.get_xpub_from_path([HARDENED_INDEX, 42]) 'xpub69uEaVYoN1mZyMon8qwRP41YjYyevp3YxJ68ymBGV7qmXZ9rsbMy9kBZnLNPg3TLjKd2EnMw5BtUFQCGrTVDjQok859LowMV2SEooseLCt1' # You can also use "h" or "H" to signal for hardened derivation >>> slip10.get_xpub_from_path("m/0h/42") 'xpub69uEaVYoN1mZyMon8qwRP41YjYyevp3YxJ68ymBGV7qmXZ9rsbMy9kBZnLNPg3TLjKd2EnMw5BtUFQCGrTVDjQok859LowMV2SEooseLCt1' # You can use pubkey-only derivation >>> slip10 = SLIP10.from_xpub("xpub6AKC3u8URPxDojLnFtNdEPFkNsXxHfgRhySvVfEJy9SVvQAn14XQjAoFY48mpjgutJNfA54GbYYRpR26tFEJHTHhfiiZZ2wdBBzydVp12yU") >>> slip10.get_xpub_from_path([42, 43]) 'xpub6FL7T3s7GuVb4od1gvWuumhg47y6TZtf2DSr6ModQpX4UFGkQXw8oEVhJXcXJ4edmtAWCTrefD64B9RP4sYSkSumTW1wadTS3SYurBGYccT' >>> slip10.get_xpub_from_path("m/42/43") 'xpub6FL7T3s7GuVb4od1gvWuumhg47y6TZtf2DSr6ModQpX4UFGkQXw8oEVhJXcXJ4edmtAWCTrefD64B9RP4sYSkSumTW1wadTS3SYurBGYccT' >>> slip10.get_pubkey_from_path("m/1/1/1/1/1/1/1/1/1/1/1") b'\x02\x0c\xac\n\xa8\x06\x96C\x8e\x9b\xcf\x83]\x0c\rCm\x06\x1c\xe9T\xealo\xa2\xdf\x195\xebZ\x9b\xb8\x9e' ``` ## Installation ``` pip install slip10 ``` ### Dependencies This package uses [`ecdsa`](https://pypi.org/project/ecdsa/) as a wrapper for secp256k1 and secp256r1 elliptic curve operations and [`cryptography`](https://pypi.org/project/cryptography/) for Ed25519 and curve25519 operations. ### Running the test suite ``` pip3 install poetry git clone https://github.com/trezor/python-slip10 cd python-slip10 poetry install poetry run make test ``` ## Interface All public keys below are compressed. All `path` below are a list of integers representing the index of the key at each depth. `network` is "main" or "test". `curve_name` is one of "secp256k1", "secp256r1", "ed25519" or "curve25519". ### SLIP10 #### from_seed(seed, network="main", curve_name="secp256k1") __*classmethod*__ Instanciate from a raw seed (as `bytes`). See [SLIP-0010's master key generation](https://github.com/satoshilabs/slips/blob/master/slip-0010.md#master-key-generation). #### from_xpriv(xpriv) __*classmethod*__ Instanciate with an encoded serialized extended private key (as `str`) as master. #### from_xpub(xpub) __*classmethod*__ Instanciate with an encoded serialized extended public key (as `str`) as master. You'll only be able to derive unhardened public keys. #### get_child_from_path(path) Returns a SLIP10 instance of the child pointed by the path. #### get_extended_privkey_from_path(path) Returns `(chaincode (bytes), privkey (bytes))` of the private key pointed by the path. #### get_privkey_from_path(path) Returns `privkey (bytes)`, the private key pointed by the path. #### get_extended_pubkey_from_path(path) Returns `(chaincode (bytes), pubkey (bytes))` of the public key pointed by the path. Note that you don't need to have provided the master private key if the path doesn't include an index `>= HARDENED_INDEX`. #### get_pubkey_from_path(path) Returns `pubkey (bytes)`, the public key pointed by the path. Note that you don't need to have provided the master private key if the path doesn't include an index `>= HARDENED_INDEX`. #### get_xpriv_from_path(path) Returns `xpriv (str)` the serialized and encoded extended private key pointed by the given path. #### get_xpub_from_path(path) Returns `xpub (str)` the serialized and encoded extended public key pointed by the given path. Note that you don't need to have provided the master private key if the path doesn't include an index `>= HARDENED_INDEX`. #### get_xpriv() Equivalent to `get_xpriv_from_path([])`. #### get_xpriv_bytes() Equivalent to `get_xpriv([])`, but not serialized in base58 #### get_xpub() Equivalent to `get_xpub_from_path([])`. #### get_xpub_bytes() Equivalent to `get_xpub([])`, but not serialized in base58 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1725368953.0412414 slip10-1.0.1/pyproject.toml0000644000000000000000000000165114665605171012501 0ustar00[tool.poetry] name = "slip10" version = "1.0.1" description = "A reference implementation of the SLIP-0010 specification, which generalizes the BIP-0032 derivation scheme for private and public key pairs in hierarchical deterministic wallets for the curves secp256k1, NIST P-256, ed25519 and curve25519." authors = ["Antoine Poinsot ", "Andrew R. Kozlik "] maintainers = ["Andrew R. Kozlik "] license = "MIT" readme = [ "README.md", "CHANGELOG.md", ] repository = "https://github.com/trezor/python-slip10" keywords = ["bitcoin", "slip10", "hdwallet"] [tool.poetry.dependencies] cryptography = "*" ecdsa = "*" base58 = "^2" python = ">=3.8,<4.0" [tool.poetry.group.dev.dependencies] pytest = "*" black = ">=20" isort = "^5" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.isort] profile = "black" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1725370226.0016117 slip10-1.0.1/slip10/__init__.py0000644000000000000000000000054414665607562013014 0ustar00import importlib.metadata from .slip10 import SLIP10, InvalidInputError, PrivateDerivationError from .utils import HARDENED_INDEX, SLIP10DerivationError __version__ = importlib.metadata.version(__package__ or __name__) __all__ = [ "SLIP10", "SLIP10DerivationError", "PrivateDerivationError", "InvalidInputError", "HARDENED_INDEX", ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1724089278.8298416 slip10-1.0.1/slip10/ripemd160.py0000644000000000000000000001017214660701677012757 0ustar00# Copyright (c) 2021 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # Taken from https://github.com/bitcoin/bitcoin/blob/124e75a41ea0f3f0e90b63b0c41813184ddce2ab/test/functional/test_framework/ripemd160.py # fmt: off """ Pure Python RIPEMD160 implementation. WARNING: This implementation is NOT constant-time. Do not use without understanding the implications. """ # Message schedule indexes for the left path. ML = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 ] # Message schedule indexes for the right path. MR = [ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 ] # Rotation counts for the left path. RL = [ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ] # Rotation counts for the right path. RR = [ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ] # K constants for the left path. KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e] # K constants for the right path. KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0] def fi(x, y, z, i): """The f1, f2, f3, f4, and f5 functions from the specification.""" if i == 0: return x ^ y ^ z elif i == 1: return (x & y) | (~x & z) elif i == 2: return (x | ~y) ^ z elif i == 3: return (x & z) | (y & ~z) elif i == 4: return x ^ (y | ~z) else: assert False def rol(x, i): """Rotate the bottom 32 bits of x left by i bits.""" return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff def compress(h0, h1, h2, h3, h4, block): """Compress state (h0, h1, h2, h3, h4) with block.""" # Left path variables. al, bl, cl, dl, el = h0, h1, h2, h3, h4 # Right path variables. ar, br, cr, dr, er = h0, h1, h2, h3, h4 # Message variables. x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)] # Iterate over the 80 rounds of the compression. for j in range(80): rnd = j >> 4 # Perform left side of the transformation. al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl # Perform right side of the transformation. ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr # Compose old state, left transform, and right transform into new state. return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr def ripemd160(data): """Compute the RIPEMD-160 hash of data.""" # Initialize state. state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) # Process full 64-byte blocks in the input. for b in range(len(data) >> 6): state = compress(*state, data[64*b:64*(b+1)]) # Construct final blocks (with padding and size). pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63) fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little') # Process final blocks. for b in range(len(fin) >> 6): state = compress(*state, fin[64*b:64*(b+1)]) # Produce output. return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1724156869.4625254 slip10-1.0.1/slip10/slip10.py0000644000000000000000000003343214661105705012353 0ustar00import hashlib import hmac import base58 from .utils import ( HARDENED_INDEX, _deriv_path_str_to_list, _get_curve_by_name, _hardened_index_in_path, _pubkey_to_fingerprint, _serialize_extended_key, _unserialize_extended_key, ) class PrivateDerivationError(ValueError): """ Tried to use a derivation requiring private keys, without private keys. """ pass class InvalidInputError(ValueError): def __init__(self, message): self.message = message class ParsingError(ValueError): def __init__(self, message): self.message = message class SerializationError(ValueError): def __init__(self, message): self.message = message class SLIP10: def __init__( self, chaincode, privkey=None, pubkey=None, fingerprint=bytes(4), depth=0, index=0, network="main", curve_name="secp256k1", ): """ :param chaincode: The master chaincode, used to derive keys. As bytes. :param privkey: The master private key for this index (default 0). Can be None for pubkey-only derivation. As bytes. :param pubkey: The master public key for this index (default 0). Can be None if private key is specified. Compressed format. As bytes. :param fingeprint: If we are instanciated from an xpub/xpriv, we need to remember the parent's pubkey fingerprint to reserialize ! :param depth: If we are instanciated from an existing extended key, we need this for serialization. :param index: If we are instanciated from an existing extended key, we need this for serialization. :param network: Either "main" or "test". :param curve_name: Either "secp256k1", "secp256r1", "ed25519" or "curve25519". """ try: curve = _get_curve_by_name(curve_name) except ValueError as e: raise InvalidInputError(e) from None if network not in ["main", "test"]: raise InvalidInputError("'network' must be one of 'main' or 'test'") if not isinstance(chaincode, bytes): raise InvalidInputError("'chaincode' must be bytes") if privkey is None and pubkey is None: raise InvalidInputError("Need at least a 'pubkey' or a 'privkey'") if privkey is not None: if not isinstance(privkey, bytes): raise InvalidInputError("'privkey' must be bytes") if not curve.privkey_is_valid(privkey): raise InvalidInputError("Invalid private key") if pubkey is not None: if not isinstance(pubkey, bytes): raise InvalidInputError("'pubkey' must be bytes") if not curve.pubkey_is_valid(pubkey): raise InvalidInputError("Invalid public key") if privkey is not None and pubkey != curve.privkey_to_pubkey(privkey): raise InvalidInputError("Public key does not match private key") else: pubkey = curve.privkey_to_pubkey(privkey) if depth == 0: if fingerprint != bytes(4): raise InvalidInputError( "Fingerprint must be 0 if depth is 0 (master xpub)" ) if index != 0: raise InvalidInputError("Index must be 0 if depth is 0 (master xpub)") if network not in ["main", "test"]: raise InvalidInputError("Unknown network") self.chaincode = chaincode self.privkey = privkey self.pubkey = pubkey self.parent_fingerprint = fingerprint self.depth = depth self.index = index self.network = network self.curve = curve def get_child_from_path(self, path): """Get an child node from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: SLIP10 object """ if isinstance(path, str): path = _deriv_path_str_to_list(path) if len(path) == 0: return self if _hardened_index_in_path(path) and self.privkey is None: raise PrivateDerivationError chaincode = self.chaincode privkey = self.privkey if privkey is not None: pubkey = None for index in path: parent_privkey = privkey privkey, chaincode = self.curve.derive_private_child( privkey, chaincode, index ) parent_pubkey = self.curve.privkey_to_pubkey(parent_privkey) else: pubkey = self.pubkey for index in path: parent_pubkey = pubkey pubkey, chaincode = self.curve.derive_public_child( pubkey, chaincode, index ) return SLIP10( chaincode, privkey, pubkey, _pubkey_to_fingerprint(parent_pubkey), depth=self.depth + len(path), index=path[-1], network=self.network, curve_name=self.curve.name, ) def get_extended_privkey_from_path(self, path): """Get an extended privkey from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: chaincode (bytes), privkey (bytes) """ if self.privkey is None: raise PrivateDerivationError if isinstance(path, str): path = _deriv_path_str_to_list(path) chaincode, privkey = self.chaincode, self.privkey for index in path: privkey, chaincode = self.curve.derive_private_child( privkey, chaincode, index ) return chaincode, privkey def get_privkey_from_path(self, path): """Get a privkey from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: privkey (bytes) """ if self.privkey is None: raise PrivateDerivationError return self.get_extended_privkey_from_path(path)[1] def get_extended_pubkey_from_path(self, path): """Get an extended pubkey from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: chaincode (bytes), pubkey (bytes) """ if isinstance(path, str): path = _deriv_path_str_to_list(path) if _hardened_index_in_path(path) and self.privkey is None: raise PrivateDerivationError chaincode, key = self.chaincode, self.privkey pubkey = self.pubkey # We'll need the private key at some point anyway, so let's derive # everything from private keys. if _hardened_index_in_path(path): for index in path: key, chaincode = self.curve.derive_private_child(key, chaincode, index) pubkey = self.curve.privkey_to_pubkey(key) # We won't need private keys for the whole path, so let's only use # public key derivation. else: for index in path: pubkey, chaincode = self.curve.derive_public_child( pubkey, chaincode, index ) return chaincode, pubkey def get_pubkey_from_path(self, path): """Get a pubkey from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: privkey (bytes) """ return self.get_extended_pubkey_from_path(path)[1] def get_xpriv_from_path(self, path): """Get an encoded extended privkey from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: The encoded extended pubkey as str. """ if self.curve.name != "secp256k1": raise SerializationError( "xpriv serialization is supported only for secp256k1" ) if self.privkey is None: raise PrivateDerivationError if isinstance(path, str): path = _deriv_path_str_to_list(path) if len(path) == 0: return self.get_xpriv() elif len(path) == 1: parent_pubkey = self.pubkey else: parent_pubkey = self.get_pubkey_from_path(path[:-1]) chaincode, privkey = self.get_extended_privkey_from_path(path) extended_key = _serialize_extended_key( privkey, self.depth + len(path), parent_pubkey, path[-1], chaincode, self.network, ) return base58.b58encode_check(extended_key).decode() def get_xpub_from_path(self, path): """Get an encoded extended pubkey from a derivation path. :param path: A list of integers (index of each depth) or a string with m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2). :return: The encoded extended pubkey as str. """ if self.curve.name != "secp256k1": raise SerializationError( "xpub serialization is supported only for secp256k1" ) if isinstance(path, str): path = _deriv_path_str_to_list(path) if _hardened_index_in_path(path) and self.privkey is None: raise PrivateDerivationError if len(path) == 0: return self.get_xpub() elif len(path) == 1: parent_pubkey = self.pubkey else: parent_pubkey = self.get_pubkey_from_path(path[:-1]) chaincode, pubkey = self.get_extended_pubkey_from_path(path) extended_key = _serialize_extended_key( pubkey, self.depth + len(path), parent_pubkey, path[-1], chaincode, self.network, ) return base58.b58encode_check(extended_key).decode() def get_xpriv(self): """Get the base58 encoded extended private key.""" return base58.b58encode_check(self.get_xpriv_bytes()).decode() def get_xpriv_bytes(self): """Get the encoded extended private key.""" if self.curve.name != "secp256k1": raise SerializationError( "xpriv serialization is supported only for secp256k1" ) if self.privkey is None: raise PrivateDerivationError return _serialize_extended_key( self.privkey, self.depth, self.parent_fingerprint, self.index, self.chaincode, self.network, ) def get_xpub(self): """Get the encoded extended public key.""" return base58.b58encode_check(self.get_xpub_bytes()).decode() def get_xpub_bytes(self): """Get the encoded extended public key.""" if self.curve.name != "secp256k1": raise SerializationError( "xpub serialization is supported only for secp256k1" ) return _serialize_extended_key( self.pubkey, self.depth, self.parent_fingerprint, self.index, self.chaincode, self.network, ) @classmethod def from_xpriv(cls, xpriv): """Get a SLIP10 "wallet" out of this xpriv :param xpriv: (str) The encoded serialized extended private key. """ if not isinstance(xpriv, str): raise InvalidInputError("'xpriv' must be a string") extended_key = base58.b58decode_check(xpriv) ( network, depth, fingerprint, index, chaincode, key, ) = _unserialize_extended_key(extended_key) if key[0] != 0: raise ParsingError("Invalid xpriv: private key prefix must be 0") try: # We need to remove the trailing `0` before the actual private key !! return SLIP10(chaincode, key[1:], None, fingerprint, depth, index, network) except InvalidInputError as e: raise ParsingError(f"Invalid xpriv: '{e}'") @classmethod def from_xpub(cls, xpub): """Get a SLIP10 "wallet" out of this xpub :param xpub: (str) The encoded serialized extended public key. """ if not isinstance(xpub, str): raise InvalidInputError("'xpub' must be a string") extended_key = base58.b58decode_check(xpub) ( network, depth, fingerprint, index, chaincode, key, ) = _unserialize_extended_key(extended_key) try: return SLIP10(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", curve_name="secp256k1"): """Get a SLIP10 "wallet" out of this seed seed byte sequence, which can be a BIP39 binary seed or a SLIP39 master secret. :param seed: The seed as bytes. """ try: curve = _get_curve_by_name(curve_name) except ValueError as e: raise InvalidInputError(e) from None privkey, chaincode = curve.generate_master(seed) return SLIP10(chaincode, privkey, network=network, curve_name=curve_name) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1724156183.5386457 slip10-1.0.1/slip10/utils.py0000644000000000000000000002621014661104430012371 0ustar00import hashlib import hmac import re import ecdsa from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, ) from cryptography.hazmat.primitives.asymmetric.x25519 import ( X25519PrivateKey, X25519PublicKey, ) REGEX_DERIVATION_PATH = re.compile("^m(/[0-9]+['hH]?)*$") HARDENED_INDEX = 0x80000000 ENCODING_PREFIX = { "main": { "private": 0x0488ADE4, "public": 0x0488B21E, }, "test": { "private": 0x04358394, "public": 0x043587CF, }, } class SLIP10DerivationError(Exception): pass class WeierstrassCurve: def __init__(self, name, modifier, curve): self.name = name self.modifier = modifier self.curve = curve def generate_master(self, seed): """Master key generation in SLIP-0010 :param seed: Seed byte sequence (BIP-0039 binary seed or SLIP-0039 master secret), as bytes :return: (master_privatekey, master_chaincode) """ while True: payload = hmac.new(self.modifier, seed, hashlib.sha512).digest() if self.privkey_is_valid(payload[:32]): return payload[:32], payload[32:] seed = payload def derive_private_child(self, privkey, chaincode, index): """A.k.a CKDpriv, in SLIP-0010, but the hardened way :param privkey: The parent's private key, as bytes :param chaincode: The parent's chaincode, as bytes :param index: The index of the node to derive, as int :return: (child_privatekey, child_chaincode) """ assert isinstance(privkey, bytes) and isinstance(chaincode, bytes) # payload is the I from the SLIP. Index is 32 bits unsigned int, BE. if index & HARDENED_INDEX != 0: payload = hmac.new( chaincode, b"\x00" + privkey + index.to_bytes(4, "big"), hashlib.sha512 ).digest() else: pubkey = self.privkey_to_pubkey(privkey) payload = hmac.new( chaincode, pubkey + index.to_bytes(4, "big"), hashlib.sha512 ).digest() while True: tweak = int.from_bytes(payload[:32], "big") child_private = (tweak + int.from_bytes(privkey, "big")) % self.curve.order if tweak <= self.curve.order and child_private != 0: break payload = hmac.new( chaincode, b"\x01" + payload[32:] + index.to_bytes(4, "big"), hashlib.sha512, ).digest() return child_private.to_bytes(len(privkey), "big"), payload[32:] def derive_public_child(self, pubkey, chaincode, index): """A.k.a CKDpub, in SLIP-0010. :param pubkey: The parent's (compressed) public key, as bytes :param chaincode: The parent's chaincode, as bytes :param index: The index of the node to derive, as int :return: (child_pubkey, child_chaincode) """ from ecdsa.ellipticcurve import INFINITY assert isinstance(pubkey, bytes) and isinstance(chaincode, bytes) if index & HARDENED_INDEX != 0: raise SLIP10DerivationError("Hardened derivation is not possible.") # payload is the I from the SLIP. Index is 32 bits unsigned int, BE. payload = hmac.new( chaincode, pubkey + index.to_bytes(4, "big"), hashlib.sha512 ).digest() while True: tweak = int.from_bytes(payload[:32], "big") point = ecdsa.VerifyingKey.from_string(pubkey, self.curve).pubkey.point point += self.curve.generator * tweak if tweak <= self.curve.order and point != INFINITY: break payload = hmac.new( chaincode, b"\x01" + payload[32:] + index.to_bytes(4, "big"), hashlib.sha512, ).digest() return point.to_bytes("compressed"), payload[32:] def privkey_is_valid(self, privkey): key = int.from_bytes(privkey, "big") return 0 < key < self.curve.order def pubkey_is_valid(self, pubkey): try: ecdsa.VerifyingKey.from_string(pubkey, self.curve) return True except ecdsa.errors.MalformedPointError: return False def privkey_to_pubkey(self, privkey): sk = ecdsa.SigningKey.from_string(privkey, self.curve) return sk.get_verifying_key().to_string("compressed") class EdwardsCurve: def __init__(self, name, modifier, private_key_class, public_key_class): self.name = name self.modifier = modifier self.private_key_class = private_key_class self.public_key_class = public_key_class def generate_master(self, seed): """Master key generation in SLIP-0010 :param seed: Seed byte sequence (BIP-0039 binary seed or SLIP-0039 master secret), as bytes :return: (master_privatekey, master_chaincode) """ secret = hmac.new(self.modifier, seed, hashlib.sha512).digest() return secret[:32], secret[32:] def derive_private_child(self, privkey, chaincode, index): """A.k.a CKDpriv, in SLIP-0010, but the hardened way :param privkey: The parent's private key, as bytes :param chaincode: The parent's chaincode, as bytes :param index: The index of the node to derive, as int :return: (child_privatekey, child_chaincode) """ assert isinstance(privkey, bytes) and isinstance(chaincode, bytes) # payload is the I from the SLIP. Index is 32 bits unsigned int, BE. if index & HARDENED_INDEX == 0: raise SLIP10DerivationError("Normal derivation is not supported.") payload = hmac.new( chaincode, b"\x00" + privkey + index.to_bytes(4, "big"), hashlib.sha512 ).digest() return payload[:32], payload[32:] def derive_public_child(self, pubkey, chaincode, index): raise SLIP10DerivationError("Normal derivation is not supported.") def privkey_is_valid(self, privkey): try: self.private_key_class.from_private_bytes(privkey) except ValueError: return False return True def pubkey_is_valid(self, pubkey): if pubkey[0] != 0: return False try: self.public_key_class.from_public_bytes(pubkey[1:]) except ValueError: return False return True def privkey_to_pubkey(self, privkey): from cryptography.hazmat.primitives import serialization sk = self.private_key_class.from_private_bytes(privkey) key_encoding = serialization.Encoding.Raw key_format = serialization.PublicFormat.Raw return b"\x00" + sk.public_key().public_bytes(key_encoding, key_format) SECP256K1 = WeierstrassCurve("secp256k1", b"Bitcoin seed", ecdsa.SECP256k1) SECP256R1 = WeierstrassCurve("secp256r1", b"Nist256p1 seed", ecdsa.NIST256p) ED25519 = EdwardsCurve("ed25519", b"ed25519 seed", Ed25519PrivateKey, Ed25519PublicKey) X25519 = EdwardsCurve( "curve25519", b"curve25519 seed", X25519PrivateKey, X25519PublicKey ) CURVES = (SECP256K1, SECP256R1, ED25519, X25519) def _get_curve_by_name(name): for curve in CURVES: if curve.name == name: return curve raise ValueError( "'curve' must be one of " + ", ".join(curve.name for curve in CURVES) ) def _ripemd160(data): try: rip = hashlib.new("ripemd160") rip.update(data) return rip.digest() except BaseException: # Implementations may ship hashlib without ripemd160. # In that case, fallback to custom pure Python implementation. # WARNING: the implementation in ripemd160.py is not constant-time. from . import ripemd160 return ripemd160.ripemd160(data) def _pubkey_to_fingerprint(pubkey): return _ripemd160(hashlib.sha256(pubkey).digest())[:4] def _serialize_extended_key(key, depth, parent, index, chaincode, network="main"): """Serialize an extended private *OR* public key, as spec by SLIP-0010. :param key: The public or private key to serialize. Note that if this is a public key it MUST be compressed. :param depth: 0x00 for master nodes, 0x01 for level-1 derived keys, etc.. :param parent: The parent pubkey used to derive the fingerprint, or the fingerprint itself None if master. :param index: The index of the key being serialized. 0x00000000 if master. :param chaincode: The chain code (not the labs !!). :return: The serialized extended key. """ for param in {key, chaincode}: assert isinstance(param, bytes) for param in {depth, index}: assert isinstance(param, int) if parent: assert isinstance(parent, bytes) if len(parent) == 33: fingerprint = _pubkey_to_fingerprint(parent) elif len(parent) == 4: fingerprint = parent else: raise ValueError("Bad parent, a fingerprint or a pubkey is" " required") else: fingerprint = bytes(4) # master # A privkey or a compressed pubkey assert len(key) in {32, 33} if network not in {"main", "test"}: raise ValueError("Unsupported network") is_privkey = len(key) == 32 prefix = ENCODING_PREFIX[network]["private" if is_privkey else "public"] extended = prefix.to_bytes(4, "big") extended += depth.to_bytes(1, "big") extended += fingerprint extended += index.to_bytes(4, "big") extended += chaincode if is_privkey: extended += b"\x00" extended += key return extended def _unserialize_extended_key(extended_key): """Unserialize an extended private *OR* public key, as spec by SLIP-0010. :param extended_key: The extended key to unserialize __as bytes__ :return: network (str), depth (int), fingerprint (bytes), index (int), chaincode (bytes), key (bytes) """ 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" elif prefix in list(ENCODING_PREFIX["test"].values()): network = "test" depth = extended_key[4] fingerprint = extended_key[5:9] index = int.from_bytes(extended_key[9:13], "big") chaincode, key = extended_key[13:45], extended_key[45:] return network, depth, fingerprint, index, chaincode, key def _hardened_index_in_path(path): return len([i for i in path if i & HARDENED_INDEX]) > 0 def _deriv_path_str_to_list(strpath): """Converts a derivation path as string to a list of integers (index of each depth) :param strpath: Derivation path as string with "m/x/x'/x" notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2 or m/0h/1/2h/2) :return: Derivation path as a list of integers (index of each depth) """ if not REGEX_DERIVATION_PATH.match(strpath): raise ValueError("invalid format") indexes = strpath.split("/")[1:] list_path = [] for i in indexes: # if HARDENED if i[-1:] in ["'", "h", "H"]: list_path.append(int(i[:-1]) + HARDENED_INDEX) else: list_path.append(int(i)) return list_path slip10-1.0.1/PKG-INFO0000644000000000000000000001341000000000000010604 0ustar00Metadata-Version: 2.1 Name: slip10 Version: 1.0.1 Summary: A reference implementation of the SLIP-0010 specification, which generalizes the BIP-0032 derivation scheme for private and public key pairs in hierarchical deterministic wallets for the curves secp256k1, NIST P-256, ed25519 and curve25519. Home-page: https://github.com/trezor/python-slip10 License: MIT Keywords: bitcoin,slip10,hdwallet Author: Antoine Poinsot Author-email: darosior@protonmail.com Maintainer: Andrew R. Kozlik Maintainer-email: andrew.kozlik@satoshilabs.com Requires-Python: >=3.8,<4.0 Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Dist: base58 (>=2,<3) Requires-Dist: cryptography Requires-Dist: ecdsa Project-URL: Repository, https://github.com/trezor/python-slip10 Description-Content-Type: text/markdown # python-slip10 A reference implementation of the [SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) specification, which generalizes the [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) derivation scheme for private and public key pairs in hierarchical deterministic wallets for the curves secp256k1, NIST P-256, ed25519 and curve25519. ## Usage ```python >>> from slip10 import SLIP10, HARDENED_INDEX >>> slip10 = SLIP10.from_seed(bytes.fromhex("01")) # Specify the derivation path as a list ... >>> slip10.get_xpriv_from_path([1, HARDENED_INDEX, 9998]) 'xprv9y4sBgCuub5x2DtbdNBDDCZ3btybk8YZZaTvzV5rmYd3PbU63XLo2QEj6cUt4JAqpF8gJiRKFUW8Vm7thPkccW2DpUvBxASycypEHxmZzts' # ... Or in usual m/the/path/ >>> slip10.get_xpriv_from_path("m/1/0'/9998") 'xprv9y4sBgCuub5x2DtbdNBDDCZ3btybk8YZZaTvzV5rmYd3PbU63XLo2QEj6cUt4JAqpF8gJiRKFUW8Vm7thPkccW2DpUvBxASycypEHxmZzts' >>> slip10.get_xpub_from_path([HARDENED_INDEX, 42]) 'xpub69uEaVYoN1mZyMon8qwRP41YjYyevp3YxJ68ymBGV7qmXZ9rsbMy9kBZnLNPg3TLjKd2EnMw5BtUFQCGrTVDjQok859LowMV2SEooseLCt1' # You can also use "h" or "H" to signal for hardened derivation >>> slip10.get_xpub_from_path("m/0h/42") 'xpub69uEaVYoN1mZyMon8qwRP41YjYyevp3YxJ68ymBGV7qmXZ9rsbMy9kBZnLNPg3TLjKd2EnMw5BtUFQCGrTVDjQok859LowMV2SEooseLCt1' # You can use pubkey-only derivation >>> slip10 = SLIP10.from_xpub("xpub6AKC3u8URPxDojLnFtNdEPFkNsXxHfgRhySvVfEJy9SVvQAn14XQjAoFY48mpjgutJNfA54GbYYRpR26tFEJHTHhfiiZZ2wdBBzydVp12yU") >>> slip10.get_xpub_from_path([42, 43]) 'xpub6FL7T3s7GuVb4od1gvWuumhg47y6TZtf2DSr6ModQpX4UFGkQXw8oEVhJXcXJ4edmtAWCTrefD64B9RP4sYSkSumTW1wadTS3SYurBGYccT' >>> slip10.get_xpub_from_path("m/42/43") 'xpub6FL7T3s7GuVb4od1gvWuumhg47y6TZtf2DSr6ModQpX4UFGkQXw8oEVhJXcXJ4edmtAWCTrefD64B9RP4sYSkSumTW1wadTS3SYurBGYccT' >>> slip10.get_pubkey_from_path("m/1/1/1/1/1/1/1/1/1/1/1") b'\x02\x0c\xac\n\xa8\x06\x96C\x8e\x9b\xcf\x83]\x0c\rCm\x06\x1c\xe9T\xealo\xa2\xdf\x195\xebZ\x9b\xb8\x9e' ``` ## Installation ``` pip install slip10 ``` ### Dependencies This package uses [`ecdsa`](https://pypi.org/project/ecdsa/) as a wrapper for secp256k1 and secp256r1 elliptic curve operations and [`cryptography`](https://pypi.org/project/cryptography/) for Ed25519 and curve25519 operations. ### Running the test suite ``` pip3 install poetry git clone https://github.com/trezor/python-slip10 cd python-slip10 poetry install poetry run make test ``` ## Interface All public keys below are compressed. All `path` below are a list of integers representing the index of the key at each depth. `network` is "main" or "test". `curve_name` is one of "secp256k1", "secp256r1", "ed25519" or "curve25519". ### SLIP10 #### from_seed(seed, network="main", curve_name="secp256k1") __*classmethod*__ Instanciate from a raw seed (as `bytes`). See [SLIP-0010's master key generation](https://github.com/satoshilabs/slips/blob/master/slip-0010.md#master-key-generation). #### from_xpriv(xpriv) __*classmethod*__ Instanciate with an encoded serialized extended private key (as `str`) as master. #### from_xpub(xpub) __*classmethod*__ Instanciate with an encoded serialized extended public key (as `str`) as master. You'll only be able to derive unhardened public keys. #### get_child_from_path(path) Returns a SLIP10 instance of the child pointed by the path. #### get_extended_privkey_from_path(path) Returns `(chaincode (bytes), privkey (bytes))` of the private key pointed by the path. #### get_privkey_from_path(path) Returns `privkey (bytes)`, the private key pointed by the path. #### get_extended_pubkey_from_path(path) Returns `(chaincode (bytes), pubkey (bytes))` of the public key pointed by the path. Note that you don't need to have provided the master private key if the path doesn't include an index `>= HARDENED_INDEX`. #### get_pubkey_from_path(path) Returns `pubkey (bytes)`, the public key pointed by the path. Note that you don't need to have provided the master private key if the path doesn't include an index `>= HARDENED_INDEX`. #### get_xpriv_from_path(path) Returns `xpriv (str)` the serialized and encoded extended private key pointed by the given path. #### get_xpub_from_path(path) Returns `xpub (str)` the serialized and encoded extended public key pointed by the given path. Note that you don't need to have provided the master private key if the path doesn't include an index `>= HARDENED_INDEX`. #### get_xpriv() Equivalent to `get_xpriv_from_path([])`. #### get_xpriv_bytes() Equivalent to `get_xpriv([])`, but not serialized in base58 #### get_xpub() Equivalent to `get_xpub_from_path([])`. #### get_xpub_bytes() Equivalent to `get_xpub([])`, but not serialized in base58 ## 1.0.1 - Support Python 3.8 ## 1.0.0 - First release forked from Antoine Poinsot's python-bip32.