123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- ###############################################################################
- #
- # The MIT License (MIT)
- #
- # Copyright (c) typedef int GmbH
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in
- # all copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- # THE SOFTWARE.
- #
- ###############################################################################
- import binascii
- import os
- import configparser
- from collections.abc import MutableMapping
- from typing import Optional, Union, Dict, Any, List, Iterator
- from threading import Lock
-
- import txaio
- import nacl
-
- from eth_account.account import Account
- from eth_account.signers.local import LocalAccount
-
- from py_eth_sig_utils.eip712 import encode_typed_data
- from py_eth_sig_utils.utils import ecsign, ecrecover_to_pub, checksum_encode, sha3
- from py_eth_sig_utils.signing import v_r_s_to_signature, signature_to_v_r_s
-
- from autobahn.wamp.interfaces import ISecurityModule, IEthereumKey
- from autobahn.xbr._mnemonic import mnemonic_to_private_key
- from autobahn.util import parse_keyfile
- from autobahn.wamp.cryptosign import CryptosignKey
-
- __all__ = ('EthereumKey', 'SecurityModuleMemory', )
-
-
- class EthereumKey(object):
- """
- Base class to implement :class:`autobahn.wamp.interfaces.IEthereumKey`.
- """
-
- def __init__(self, key_or_address: Union[LocalAccount, str, bytes], can_sign: bool,
- security_module: Optional[ISecurityModule] = None,
- key_no: Optional[int] = None) -> None:
- if can_sign:
- # https://eth-account.readthedocs.io/en/latest/eth_account.html#eth_account.account.Account
- assert type(key_or_address) == LocalAccount
- self._key = key_or_address
- self._address = key_or_address.address
- else:
- assert type(key_or_address) in (str, bytes)
- self._key = None
- self._address = key_or_address
- self._can_sign = can_sign
- self._security_module = security_module
- self._key_no = key_no
-
- @property
- def security_module(self) -> Optional['ISecurityModule']:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.security_module`.
- """
- return self._security_module
-
- @property
- def key_no(self) -> Optional[int]:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.key_no`.
- """
- return self._key_no
-
- @property
- def key_type(self) -> str:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.key_type`.
- """
- return 'ethereum'
-
- def public_key(self, binary: bool = False) -> Union[str, bytes]:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.public_key`.
- """
- raise NotImplementedError()
-
- @property
- def can_sign(self) -> bool:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.can_sign`.
- """
- return self._can_sign
-
- def address(self, binary: bool = False) -> Union[str, bytes]:
- """
- Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.address`.
- """
- if binary:
- return binascii.a2b_hex(self._address[2:])
- else:
- return self._address
-
- def sign(self, data: bytes) -> bytes:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.sign`.
- """
- # FIXME: implement signing of raw data
- raise NotImplementedError()
-
- def recover(self, data: bytes, signature: bytes) -> bytes:
- """
- Implements :meth:`autobahn.wamp.interfaces.IKey.recover`.
- """
- # FIXME: implement signing address recovery from signature of raw data
- raise NotImplementedError()
-
- def sign_typed_data(self, data: Dict[str, Any], binary=True) -> bytes:
- """
- Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.sign_typed_data`.
- """
- if self._security_module:
- assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
- try:
- # encode typed data dict and return message hash
- msg_hash = encode_typed_data(data)
-
- # ECDSA signatures in Ethereum consist of three parameters: v, r and s.
- # The signature is always 65-bytes in length.
- # r = first 32 bytes of signature
- # s = second 32 bytes of signature
- # v = final 1 byte of signature
- signature_vrs = ecsign(msg_hash, self._key.key)
-
- # concatenate signature components into byte string
- signature = v_r_s_to_signature(*signature_vrs)
- except Exception as e:
- return txaio.create_future_error(e)
- else:
- if binary:
- return txaio.create_future_success(signature)
- else:
- return txaio.create_future_success(binascii.b2a_hex(signature).decode())
-
- def verify_typed_data(self, data: Dict[str, Any], signature: bytes) -> bool:
- """
- Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.verify_typed_data`.
- """
- if self._security_module:
- assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
- try:
- msg_hash = encode_typed_data(data)
- signature_vrs = signature_to_v_r_s(signature)
- public_key = ecrecover_to_pub(msg_hash, *signature_vrs)
- address_bytes = sha3(public_key)[-20:]
- address = checksum_encode(address_bytes)
- except Exception as e:
- return txaio.create_future_error(e)
- else:
- return txaio.create_future_success(address == self._address)
-
- @classmethod
- def from_address(cls, address: Union[str, bytes]) -> 'EthereumKey':
- """
- Create a public key from an address, which can be used to verify signatures.
-
- :param address: The Ethereum address (20 octets).
- :return: New instance of :class:`EthereumKey`
- """
- return EthereumKey(key_or_address=address, can_sign=False)
-
- @classmethod
- def from_bytes(cls, key: bytes) -> 'EthereumKey':
- """
- Create a private key from seed bytes, which can be used to sign and create signatures.
-
- :param key: The Ethereum private key seed (32 octets).
- :return: New instance of :class:`EthereumKey`
- """
- if type(key) != bytes:
- raise ValueError("invalid seed type {} (expected binary)".format(type(key)))
-
- if len(key) != 32:
- raise ValueError("invalid seed length {} (expected 32)".format(len(key)))
-
- account: LocalAccount = Account.from_key(key)
- return EthereumKey(key_or_address=account, can_sign=True)
-
- @classmethod
- def from_seedphrase(cls, seedphrase: str, index: int = 0) -> 'EthereumKey':
- """
- Create a private key from the given BIP-39 mnemonic seed phrase and index,
- which can be used to sign and create signatures.
-
- :param seedphrase: The BIP-39 seedphrase ("Mnemonic") from which to derive the account.
- :param index: The account index in account hierarchy defined by the seedphrase.
- :return: New instance of :class:`EthereumKey`
- """
- # Base HD Path: m/44'/60'/0'/0/{account_index}
- derivation_path = "m/44'/60'/0'/0/{}".format(index)
-
- key = mnemonic_to_private_key(seedphrase, str_derivation_path=derivation_path)
- assert type(key) == bytes
- assert len(key) == 32
-
- account: LocalAccount = Account.from_key(key)
- return EthereumKey(key_or_address=account, can_sign=True)
-
- @classmethod
- def from_keyfile(cls, keyfile: str) -> 'EthereumKey':
- """
- Create a public or private key from reading the given public or private key file.
-
- Here is an example key file that includes an Ethereum private key ``private-key-eth``, which
- is loaded in this function, and other fields, which are ignored by this function:
-
- .. code-block::
-
- This is a comment (all lines until the first empty line are comments indeed).
-
- creator: oberstet@intel-nuci7
- created-at: 2022-07-05T12:29:48.832Z
- user-id: oberstet@intel-nuci7
- public-key-ed25519: 7326d9dc0307681cc6940fde0e60eb31a6e4d642a81e55c434462ce31f95deed
- public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
- private-key-ed25519: f750f42b0430e28a2e272c3cedcae4dcc4a1cf33bc345c35099d3322626ab666
- private-key-eth: 4d787714dcb0ae52e1c5d2144648c255d660b9a55eac9deeb80d9f506f501025
-
- :param keyfile: Path (relative or absolute) to a public or private keys file.
- :return: New instance of :class:`EthereumKey`
- """
- if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
- raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
-
- # now load the private or public key file - this returns a dict which should
- # include (for a private key):
- #
- # private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
- #
- # or (for a public key only):
- #
- # public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
- #
- data = parse_keyfile(keyfile)
-
- privkey_eth_hex = data.get('private-key-eth', None)
- if privkey_eth_hex is None:
- pub_adr_eth = data.get('public-adr-eth', None)
- if pub_adr_eth is None:
- raise RuntimeError('neither "private-key-eth" nor "public-adr-eth" found in keyfile {}'.format(keyfile))
- else:
- return EthereumKey.from_address(pub_adr_eth)
- else:
- return EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex))
-
-
- IEthereumKey.register(EthereumKey)
-
-
- class SecurityModuleMemory(MutableMapping):
- """
- A transient, memory-based implementation of :class:`ISecurityModule`.
- """
-
- def __init__(self, keys: Optional[List[Union[CryptosignKey, EthereumKey]]] = None):
- self._mutex = Lock()
- self._is_open = False
- self._is_locked = True
- self._keys: Dict[int, Union[CryptosignKey, EthereumKey]] = {}
- self._counters: Dict[int, int] = {}
- if keys:
- for i, key in enumerate(keys):
- self._keys[i] = key
-
- def __len__(self) -> int:
- """
- Implements :meth:`ISecurityModule.__len__`
- """
- assert self._is_open, 'security module not open'
-
- return len(self._keys)
-
- def __contains__(self, key_no: int) -> bool:
- assert self._is_open, 'security module not open'
-
- return key_no in self._keys
-
- def __iter__(self) -> Iterator[int]:
- """
- Implements :meth:`ISecurityModule.__iter__`
- """
- assert self._is_open, 'security module not open'
-
- yield from self._keys
-
- def __getitem__(self, key_no: int) -> Union[CryptosignKey, EthereumKey]:
- """
- Implements :meth:`ISecurityModule.__getitem__`
- """
- assert self._is_open, 'security module not open'
-
- if key_no in self._keys:
- return self._keys[key_no]
- else:
- raise IndexError('key_no {} not found'.format(key_no))
-
- def __setitem__(self, key_no: int, key: Union[CryptosignKey, EthereumKey]) -> None:
- assert self._is_open, 'security module not open'
-
- assert key_no >= 0
- if key_no in self._keys:
- # FIXME
- pass
- self._keys[key_no] = key
-
- def __delitem__(self, key_no: int) -> None:
- assert self._is_open, 'security module not open'
-
- if key_no in self._keys:
- del self._keys[key_no]
- else:
- raise IndexError()
-
- def open(self):
- """
- Implements :meth:`ISecurityModule.open`
- """
- assert not self._is_open, 'security module already open'
-
- self._is_open = True
- return txaio.create_future_success(None)
-
- def close(self):
- """
- Implements :meth:`ISecurityModule.close`
- """
- assert self._is_open, 'security module not open'
-
- self._is_open = False
- self._is_locked = True
- return txaio.create_future_success(None)
-
- @property
- def is_open(self) -> bool:
- """
- Implements :meth:`ISecurityModule.is_open`
- """
- return self._is_open
-
- @property
- def can_lock(self) -> bool:
- """
- Implements :meth:`ISecurityModule.can_lock`
- """
- return True
-
- @property
- def is_locked(self) -> bool:
- """
- Implements :meth:`ISecurityModule.is_locked`
- """
- return self._is_locked
-
- def lock(self):
- """
- Implements :meth:`ISecurityModule.lock`
- """
- assert self._is_open, 'security module not open'
- assert not self._is_locked
-
- self._is_locked = True
- return txaio.create_future_success(None)
-
- def unlock(self):
- """
- Implements :meth:`ISecurityModule.unlock`
- """
- assert self._is_open, 'security module not open'
- assert self._is_locked
-
- self._is_locked = False
- return txaio.create_future_success(None)
-
- def create_key(self, key_type: str) -> int:
- assert self._is_open, 'security module not open'
-
- key_no = len(self._keys)
- if key_type == 'cryptosign':
- key = CryptosignKey(key=nacl.signing.SigningKey(os.urandom(32)),
- can_sign=True,
- security_module=self,
- key_no=key_no)
- elif key_type == 'ethereum':
- key = EthereumKey(key_or_address=Account.from_key(os.urandom(32)),
- can_sign=True,
- security_module=self,
- key_no=key_no)
- else:
- raise ValueError('invalid key_type "{}"'.format(key_type))
- self._keys[key_no] = key
- return txaio.create_future_success(key_no)
-
- def delete_key(self, key_no: int):
- assert self._is_open, 'security module not open'
-
- if key_no in self._keys:
- del self._keys[key_no]
- return txaio.create_future_success(key_no)
- else:
- return txaio.create_future_success(None)
-
- def get_random(self, octets: int) -> bytes:
- """
- Implements :meth:`ISecurityModule.get_random`
- """
- assert self._is_open, 'security module not open'
-
- data = os.urandom(octets)
- return txaio.create_future_success(data)
-
- def get_counter(self, counter_no: int) -> int:
- """
- Implements :meth:`ISecurityModule.get_counter`
- """
- assert self._is_open, 'security module not open'
-
- self._mutex.acquire()
- res = self._counters.get(counter_no, 0)
- self._mutex.release()
- return txaio.create_future_success(res)
-
- def increment_counter(self, counter_no: int) -> int:
- """
- Implements :meth:`ISecurityModule.increment_counter`
- """
- assert self._is_open, 'security module not open'
-
- self._mutex.acquire()
- if counter_no not in self._counters:
- self._counters[counter_no] = 0
- self._counters[counter_no] += 1
- res = self._counters[counter_no]
- self._mutex.release()
- return txaio.create_future_success(res)
-
- @classmethod
- def from_seedphrase(cls, seedphrase: str, num_eth_keys: int = 1,
- num_cs_keys: int = 1) -> 'SecurityModuleMemory':
- """
- Create a new memory-backed security module with
-
- 1. ``num_eth_keys`` keys of type :class:`EthereumKey`, followed by
- 2. ``num_cs_keys`` keys of type :class:`CryptosignKey`
-
- computed from a (common) BIP44 seedphrase.
-
- :param seedphrase: BIP44 seedphrase to use.
- :param num_eth_keys: Number of Ethereum keys to derive.
- :param num_cs_keys: Number of Cryptosign keys to derive.
- :return: New memory-backed security module instance.
- """
- keys: List[Union[EthereumKey, CryptosignKey]] = []
-
- # first, add num_eth_keys EthereumKey(s), numbering starting at 0
- for i in range(num_eth_keys):
- key = EthereumKey.from_seedphrase(seedphrase, i)
- keys.append(key)
-
- # second, add num_cs_keys CryptosignKey(s), numbering starting at num_eth_keys (!)
- for i in range(num_cs_keys):
- key = CryptosignKey.from_seedphrase(seedphrase, i + num_eth_keys)
- keys.append(key)
-
- # initialize security module from collected keys
- sm = SecurityModuleMemory(keys=keys)
- return sm
-
- @classmethod
- def from_config(cls, config: str, profile: str = 'default') -> 'SecurityModuleMemory':
- """
- Create a new memory-backed security module with keys referred from a profile in
- the given configuration file.
-
- :param config: Path (relative or absolute) to an INI configuration file.
- :param profile: Name of the profile within the given INI configuration file.
- :return: New memory-backed security module instance.
- """
- keys: List[Union[EthereumKey, CryptosignKey]] = []
-
- cfg = configparser.ConfigParser()
- cfg.read(config)
-
- if not cfg.has_section(profile):
- raise RuntimeError('profile "{}" not found in configuration file "{}"'.format(profile, config))
-
- if not cfg.has_option(profile, 'privkey'):
- raise RuntimeError('missing option "privkey" in profile "{}" of configuration file "{}"'.format(profile, config))
-
- privkey = os.path.join(os.path.dirname(config), cfg.get(profile, 'privkey'))
- if not os.path.exists(privkey) or not os.path.isfile(privkey):
- raise RuntimeError('privkey "{}" is not a file in profile "{}" of configuration file "{}"'.format(privkey, profile, config))
-
- # now load the private key file - this returns a dict which should include:
- # private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
- # private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
- data = parse_keyfile(privkey)
-
- # first, add Ethereum key
- privkey_eth_hex = data.get('private-key-eth', None)
- keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))
-
- # second, add Cryptosign key
- privkey_ed25519_hex = data.get('private-key-ed25519', None)
- keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))
-
- # initialize security module from collected keys
- sm = SecurityModuleMemory(keys=keys)
- return sm
-
- @classmethod
- def from_keyfile(cls, keyfile: str) -> 'SecurityModuleMemory':
- """
- Create a new memory-backed security module with keys referred from a profile in
- the given configuration file.
-
- :param keyfile: Path (relative or absolute) to a private keys file.
- :return: New memory-backed security module instance.
- """
- keys: List[Union[EthereumKey, CryptosignKey]] = []
-
- if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
- raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
-
- # now load the private key file - this returns a dict which should include:
- # private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
- # private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
- data = parse_keyfile(keyfile)
-
- # first, add Ethereum key
- privkey_eth_hex = data.get('private-key-eth', None)
- if privkey_eth_hex is None:
- raise RuntimeError('"private-key-eth" not found in keyfile {}'.format(keyfile))
- keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))
-
- # second, add Cryptosign key
- privkey_ed25519_hex = data.get('private-key-ed25519', None)
- if privkey_ed25519_hex is None:
- raise RuntimeError('"private-key-ed25519" not found in keyfile {}'.format(keyfile))
- keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))
-
- # initialize security module from collected keys
- sm = SecurityModuleMemory(keys=keys)
- return sm
-
-
- ISecurityModule.register(SecurityModuleMemory)
|