funga

Signer and keystore daemon and library for cryptocurrency software development
Log | Files | Refs | README | LICENSE

commit 86c018033536c2608da78befd4cc69737908a573
parent cdd1c58c514b0d0f6227b479d7574fd29ec90fa3
Author: nolash <dev@holbrook.no>
Date:   Sun, 18 Oct 2020 10:32:23 +0200

Upgrade confini

Diffstat:
M.gitignore | 3+++
MCHANGELOG | 2++
MREADME.md | 18+++++-------------
Acrypto_dev_signer/runnable/signer.py | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dscripts/crypto-dev-daemon | 227-------------------------------------------------------------------------------
Dscripts/crypto-dev-daemon.py | 2--
Msetup.cfg | 10+++++++++-
Msetup.py | 23+++++++++++++++++------
8 files changed, 264 insertions(+), 249 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -2,3 +2,6 @@ __pycache__ *.pyc venv .venv +build +dist +*.egg-info diff --git a/CHANGELOG b/CHANGELOG @@ -1,3 +1,5 @@ +* 0.2.6 + - Upgrade confini * 0.2.5 - Add default env override - Do not create middleware if ipc no set diff --git a/README.md b/README.md @@ -1,9 +1,9 @@ -# CIC PLATFORM SIGNER +# CRYPTO DEV SIGNER This package is written because at the time no good solution seemed to exist for solving the following combined requirements and issues: * A service has custody of its users' private keys. -* The are a large number of private keys involved (tens of thousands minimum). +* The are a large number of private keys involved (hundreds of thousands and up). * Need to sign transactions conforming to EIP-155, with the ability to arbitrarily specify the "chain id". * Do not want to store the keys inside an ethereum node, especially not the one connected to the network. * Want to use the "standard" web3 JSON-RPC interface, so that the component can be easily replaced later. @@ -14,20 +14,12 @@ This package is written because at the time no good solution seemed to exist for ### Scripts -Two scripts are currently available: - -### `crypto-dev-daemon.py` - -An Unix socket IPC server implementing the following web3 json-rpc methods: +When installed with pip/setuptools, this package provides a Unix socket IPC server as `crypto-dev-daemon` implementing the following web3 json-rpc methods: * web3.eth.personal.newAccount * web3.eth.personal.signTransaction * web3.eth.signTransaction -### `web3_middleware.py` - -Demonstrates use of the IPC server as middleware for handling calls to the web3 json-rpc methods provided by the daemon. - ### Classes The classes and packages provided are: @@ -49,9 +41,9 @@ The classes and packages provided are: ## VERSION -This software is in alpha state and very brittle. +This software is in alpha state. -Current version is 0.1.0 +Current version is 0.2.5 ## LICENSE diff --git a/crypto_dev_signer/runnable/signer.py b/crypto_dev_signer/runnable/signer.py @@ -0,0 +1,228 @@ +# standard imports +import os +import sys +import stat +import socket +import json +import logging +import argparse + +# third-party imports +import confini +from jsonrpc.exceptions import * + +# local imports +from crypto_dev_signer.eth.signer import ReferenceSigner +from crypto_dev_signer.eth.transaction import EIP155Transaction +from crypto_dev_signer.keystore import ReferenceKeystore + +#logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +config_dir = os.path.join('/usr/local/etc/cic-eth') + +db = None +signer = None +chainId = 8995 +socket_path = '/run/crypto-dev-signer/jsonrpc.ipc' + +argparser = argparse.ArgumentParser() +argparser.add_argument('-c', type=str, default=config_dir, help='config file') +argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') +argparser.add_argument('-i', type=int, help='default chain id for EIP155') +argparser.add_argument('-s', type=str, help='socket path') +argparser.add_argument('-v', action='store_true', help='be verbose') +argparser.add_argument('-vv', action='store_true', help='be more verbose') +args = argparser.parse_args() + +if args.vv: + logging.getLogger().setLevel(logging.DEBUG) +elif args.v: + logging.getLogger().setLevel(logging.INFO) + +config = confini.Config(args.c, args.env_prefix) +config.process() +config.censor('PASSWORD', 'DATABASE') +config.censor('SECRET', 'SIGNER') +logg.debug('config loaded from {}:\n{}'.format(config_dir, config)) + +if args.i: + chainId = args.i +if args.s: + socket_path = args.s +elif config.get('SIGNER_SOCKET_PATH'): + socket_path = config.get('SIGNER_SOCKET_PATH') + + +# connect to database +dsn = 'postgresql://{}:{}@{}:{}/{}'.format( + config.get('DATABASE_USER'), + config.get('DATABASE_PASSWORD'), + config.get('DATABASE_HOST'), + config.get('DATABASE_PORT'), + config.get('DATABASE_NAME'), + ) + +logg.info('using dsn {}'.format(dsn)) +logg.info('using socket {}'.format(socket_path)) + + +class MissingSecretError(BaseException): + + def __init__(self, message): + super(MissingSecretError, self).__init__(message) + + +def personal_new_account(p): + password = p + if p.__class__.__name__ != 'str': + if p.__class__.__name__ != 'list': + e = JSONRPCInvalidParams() + e.data = 'parameter must be list containing one string' + raise ValueError(e) + logg.error('foo {}'.format(p)) + if len(p) != 1: + e = JSONRPCInvalidParams() + e.data = 'parameter must be list containing one string' + raise ValueError(e) + if p[0].__class__.__name__ != 'str': + e = JSONRPCInvalidParams() + e.data = 'parameter must be list containing one string' + raise ValueError(e) + password = p[0] + + r = db.new(password) + + return r + + +def personal_sign_transaction(p): + t = EIP155Transaction(p[0], p[0]['nonce'], 8995) + z = signer.signTransaction(t, p[1]) + raw_signed_tx = t.rlp_serialize() + o = { + 'raw': '0x' + raw_signed_tx.hex(), + 'tx': t.serialize(), + } + logg.debug('signed {}'.format(o)) + return o + + +# TODO: temporary workaround for platform, since personal_signTransaction is missing from web3.py +def eth_signTransaction(tx): + return personal_sign_transaction([tx, '']) + + +methods = { + 'personal_newAccount': personal_new_account, + 'personal_signTransaction': personal_sign_transaction, + 'eth_signTransaction': eth_signTransaction, + } + + +def jsonrpc_error(rpc_id, err): + return { + 'json-rpc': '2.0', + 'id': rpc_id, + 'error': { + 'code': err.CODE, + 'message': err.MESSAGE, + }, + } + + +def jsonrpc_ok(rpc_id, response): + return { + 'json-rpc': '2.0', + 'id': rpc_id, + 'result': response, + } + + +def is_valid_json(j): + if j.get('id') == 'None': + raise ValueError('id missing') + return True + + +def process_input(j): + rpc_id = j['id'] + m = j['method'] + p = j['params'] + return (rpc_id, methods[m](p)) + + +def start_server(): + socket_dir = os.path.dirname(socket_path) + try: + fi = os.stat(socket_dir) + if not stat.S_ISDIR: + RuntimeError('socket path {} is not a directory'.format(socket_dir)) + except FileNotFoundError: + os.mkdir(socket_dir) + + try: + os.unlink(socket_path) + except FileNotFoundError: + pass + s = socket.socket(family = socket.AF_UNIX, type = socket.SOCK_STREAM) + s.bind(socket_path) + s.listen(10) + while True: + (csock, caddr) = s.accept() + d = csock.recv(4096) + j = None + try: + j = json.loads(d) + is_valid_json(j) + logg.debug('{}'.format(d.decode('utf-8'))) + except Exception as e: + logg.error('input error {}'.format(e)) + csock.send(json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8')) + csock.close() + continue + + try: + (rpc_id, r) = process_input(j) + r = jsonrpc_ok(rpc_id, r) + j = json.dumps(r).encode('utf-8') + csock.send(j) + except ValueError as e: + # TODO: handle cases to give better error context to caller + logg.error('process error {}'.format(e)) + csock.send(json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')) + + csock.close() + s.close() + + os.unlink(socket_path) + + +def init(): + global db, signer + secret_hex = config.get('SIGNER_SECRET') + if secret_hex == None: + raise MissingSecretError('please provide a valid hex value for the SIGNER_SECRET configuration variable') + + secret = bytes.fromhex(secret_hex) + kw = { + 'symmetric_key': secret, + } + db = ReferenceKeystore(dsn, **kw) + signer = ReferenceSigner(db) + + +#if __name__ == '__main__': +def main(): + init() + arg = None + try: + arg = json.loads(sys.argv[1]) + except: + logg.info('no json rpc command detected, starting socket server') + start_server() + sys.exit(0) + + (rpc_id, response) = process_input(arg) + r = jsonrpc_ok(rpc_id, response) + sys.stdout.write(json.dumps(r)) diff --git a/scripts/crypto-dev-daemon b/scripts/crypto-dev-daemon @@ -1,227 +0,0 @@ -# standard imports -import os -import sys -import stat -import socket -import json -import logging -import argparse - -# third-party imports -import confini -from jsonrpc.exceptions import * - -# local imports -from crypto_dev_signer.eth.signer import ReferenceSigner -from crypto_dev_signer.eth.transaction import EIP155Transaction -from crypto_dev_signer.keystore import ReferenceKeystore - -#logging.basicConfig(level=logging.DEBUG) -logg = logging.getLogger() - -config_dir = os.path.join('/usr/local/etc/cic-eth') - -db = None -signer = None -chainId = 8995 -socket_path = '/run/crypto-dev-signer/jsonrpc.ipc' - -argparser = argparse.ArgumentParser() -argparser.add_argument('-c', type=str, default=config_dir, help='config file') -argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') -argparser.add_argument('-i', type=int, help='default chain id for EIP155') -argparser.add_argument('-s', type=str, help='socket path') -argparser.add_argument('-v', action='store_true', help='be verbose') -argparser.add_argument('-vv', action='store_true', help='be more verbose') -args = argparser.parse_args() - -if args.vv: - logging.getLogger().setLevel(logging.DEBUG) -elif args.v: - logging.getLogger().setLevel(logging.INFO) - -config = confini.Config(args.c, args.env_prefix) -config.process() -config.censor('PASSWORD', 'DATABASE') -config.censor('SECRET', 'SIGNER') -logg.debug('config loaded from {}:\n{}'.format(config_dir, config)) - -if args.i: - chainId = args.i -if args.s: - socket_path = args.s -elif config.get('SIGNER_SOCKET_PATH'): - socket_path = config.get('SIGNER_SOCKET_PATH') - - -# connect to database -dsn = 'postgresql://{}:{}@{}:{}/{}'.format( - config.get('DATABASE_USER'), - config.get('DATABASE_PASSWORD'), - config.get('DATABASE_HOST'), - config.get('DATABASE_PORT'), - config.get('DATABASE_NAME'), - ) - -logg.info('using dsn {}'.format(dsn)) -logg.info('using socket {}'.format(socket_path)) - - -class MissingSecretError(BaseException): - - def __init__(self, message): - super(MissingSecretError, self).__init__(message) - - -def personal_new_account(p): - password = p - if p.__class__.__name__ != 'str': - if p.__class__.__name__ != 'list': - e = JSONRPCInvalidParams() - e.data = 'parameter must be list containing one string' - raise ValueError(e) - logg.error('foo {}'.format(p)) - if len(p) != 1: - e = JSONRPCInvalidParams() - e.data = 'parameter must be list containing one string' - raise ValueError(e) - if p[0].__class__.__name__ != 'str': - e = JSONRPCInvalidParams() - e.data = 'parameter must be list containing one string' - raise ValueError(e) - password = p[0] - - r = db.new(password) - - return r - - -def personal_sign_transaction(p): - t = EIP155Transaction(p[0], p[0]['nonce'], 8995) - z = signer.signTransaction(t, p[1]) - raw_signed_tx = t.rlp_serialize() - o = { - 'raw': '0x' + raw_signed_tx.hex(), - 'tx': t.serialize(), - } - logg.debug('signed {}'.format(o)) - return o - - -# TODO: temporary workaround for platform, since personal_signTransaction is missing from web3.py -def eth_signTransaction(tx): - return personal_sign_transaction([tx, '']) - - -methods = { - 'personal_newAccount': personal_new_account, - 'personal_signTransaction': personal_sign_transaction, - 'eth_signTransaction': eth_signTransaction, - } - - -def jsonrpc_error(rpc_id, err): - return { - 'json-rpc': '2.0', - 'id': rpc_id, - 'error': { - 'code': err.CODE, - 'message': err.MESSAGE, - }, - } - - -def jsonrpc_ok(rpc_id, response): - return { - 'json-rpc': '2.0', - 'id': rpc_id, - 'result': response, - } - - -def is_valid_json(j): - if j.get('id') == 'None': - raise ValueError('id missing') - return True - - -def process_input(j): - rpc_id = j['id'] - m = j['method'] - p = j['params'] - return (rpc_id, methods[m](p)) - - -def start_server(): - socket_dir = os.path.dirname(socket_path) - try: - fi = os.stat(socket_dir) - if not stat.S_ISDIR: - RuntimeError('socket path {} is not a directory'.format(socket_dir)) - except FileNotFoundError: - os.mkdir(socket_dir) - - try: - os.unlink(socket_path) - except FileNotFoundError: - pass - s = socket.socket(family = socket.AF_UNIX, type = socket.SOCK_STREAM) - s.bind(socket_path) - s.listen(10) - while True: - (csock, caddr) = s.accept() - d = csock.recv(4096) - j = None - try: - j = json.loads(d) - is_valid_json(j) - logg.debug('{}'.format(d.decode('utf-8'))) - except Exception as e: - logg.error('input error {}'.format(e)) - csock.send(json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8')) - csock.close() - continue - - try: - (rpc_id, r) = process_input(j) - r = jsonrpc_ok(rpc_id, r) - j = json.dumps(r).encode('utf-8') - csock.send(j) - except ValueError as e: - # TODO: handle cases to give better error context to caller - logg.error('process error {}'.format(e)) - csock.send(json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')) - - csock.close() - s.close() - - os.unlink(socket_path) - - -def init(): - global db, signer - secret_hex = config.get('SIGNER_SECRET') - if secret_hex == None: - raise MissingSecretError('please provide a valid hex value for the SIGNER_SECRET configuration variable') - - secret = bytes.fromhex(secret_hex) - kw = { - 'symmetric_key': secret, - } - db = ReferenceKeystore(dsn, **kw) - signer = ReferenceSigner(db) - - -if __name__ == '__main__': - init() - arg = None - try: - arg = json.loads(sys.argv[1]) - except: - logg.info('no json rpc command detected, starting socket server') - start_server() - sys.exit(0) - - (rpc_id, response) = process_input(arg) - r = jsonrpc_ok(rpc_id, response) - sys.stdout.write(json.dumps(r)) diff --git a/scripts/crypto-dev-daemon.py b/scripts/crypto-dev-daemon.py @@ -1 +0,0 @@ -crypto-dev-daemon -\ No newline at end of file diff --git a/setup.cfg b/setup.cfg @@ -1,3 +1,11 @@ [metadata] +classifiers = + Programming Language :: Python :: 3 + Operating System :: OS Independent + Development Status :: 3 - Alpha + Intended Audience :: Developers + Topic :: Software Development :: Libraries + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) license = GPLv3 -license_file = LICENSE.txt +license_files = + LICENSE.txt diff --git a/setup.py b/setup.py @@ -1,8 +1,12 @@ from setuptools import setup +f = open('README.md', 'r') +long_description = f.read() +f.close() + setup( name="crypto-dev-signer", - version="0.2.5", + version="0.2.6", description="A signer and keystore daemon and library for cryptocurrency software development", author="Louis Holbrook", author_email="dev@holbrook.no", @@ -11,6 +15,7 @@ setup( 'crypto_dev_signer.eth.web3ext', 'crypto_dev_signer.eth', 'crypto_dev_signer.keystore', + 'crypto_dev_signer.runnable', 'crypto_dev_signer', ], install_requires=[ @@ -21,11 +26,17 @@ setup( 'pysha3', 'rlp', 'json-rpc', - 'confini==0.2.1', + 'confini==0.2.3', ], - scripts = [ - 'scripts/crypto-dev-daemon', - ], - data_files = [('', ['LICENSE.txt'])], + long_description=long_description, + long_description_content_type='text/markdown', + #scripts = [ + # 'scripts/crypto-dev-daemon', + # ], + entry_points = { + 'console_scripts': [ + 'crypto-dev-daemon=crypto_dev_signer.runnable.signer:main', + ], + }, url='https://gitlab.com/nolash/crypto-dev-signer', )