funga

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

commit b59f52d77d182796553689f210dc093479b88138
parent 3fb1344371706501932b32a2f0c58b88c775a11f
Author: nolash <dev@holbrook.no>
Date:   Wed, 15 Sep 2021 17:36:52 +0200

Merge branch 'lash/http-real'

Diffstat:
AMANIFEST.in | 1+
ATODO | 6++++++
Acrypto_dev_signer/cli/__init__.py | 0
Acrypto_dev_signer/cli/handle.py | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrypto_dev_signer/cli/http.py | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrypto_dev_signer/cli/jsonrpc.py | 30++++++++++++++++++++++++++++++
Acrypto_dev_signer/cli/socket.py | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrypto_dev_signer/error.py | 14++++++++++++++
Mcrypto_dev_signer/runnable/signer.py | 189++++++++-----------------------------------------------------------------------
Arun_tests.sh | 10++++++++++
Msetup.py | 9++-------
Atests/test_cli.py | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_keystore_dict.py | 1-
Atests/test_socket.py | 15+++++++++++++++
14 files changed, 450 insertions(+), 180 deletions(-)

diff --git a/MANIFEST.in b/MANIFEST.in @@ -0,0 +1 @@ +include *requirements* diff --git a/TODO b/TODO @@ -0,0 +1,6 @@ +Missing tests for: + +- crypto_dev_signer/cli/http.py +- crypto_dev_signer/cli/socket.py + +tests/test_keystore_reference.py is dependent on postgres, should use sqlite for tests so that it can run independently diff --git a/crypto_dev_signer/cli/__init__.py b/crypto_dev_signer/cli/__init__.py diff --git a/crypto_dev_signer/cli/handle.py b/crypto_dev_signer/cli/handle.py @@ -0,0 +1,115 @@ +# standard imports +import json +import logging + +# external imports +from jsonrpc.exceptions import ( + JSONRPCServerError, + JSONRPCParseError, + JSONRPCInvalidParams, + ) +from hexathon import add_0x + +# local imports +from crypto_dev_signer.eth.transaction import EIP155Transaction +from crypto_dev_signer.error import ( + UnknownAccountError, + SignerError, + ) +from crypto_dev_signer.cli.jsonrpc import jsonrpc_ok +from .jsonrpc import ( + jsonrpc_error, + is_valid_json, + ) + +logg = logging.getLogger(__name__) + + +class SignRequestHandler: + + keystore = None + signer = None + + def process_input(self, j): + rpc_id = j['id'] + m = j['method'] + p = j['params'] + return (rpc_id, getattr(self, m)(p)) + + + def handle_jsonrpc(self, d): + j = None + try: + j = json.loads(d) + is_valid_json(j) + logg.debug('{}'.format(d.decode('utf-8'))) + except Exception as e: + logg.exception('input error {}'.format(e)) + j = json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8') + raise SignerError(j) + + try: + (rpc_id, r) = self.process_input(j) + r = jsonrpc_ok(rpc_id, r) + j = json.dumps(r).encode('utf-8') + except ValueError as e: + # TODO: handle cases to give better error context to caller + logg.exception('process error {}'.format(e)) + j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8') + raise SignerError(j) + except UnknownAccountError as e: + logg.exception('process unknown account error {}'.format(e)) + j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8') + raise SignerError(j) + + return j + + + def personal_newAccount(self, 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 = self.keystore.new(password) + + return add_0x(r) + + + # TODO: move to translation module ("personal" rpc namespace is node-specific) + def personal_signTransaction(self, p): + logg.debug('got {} to sign'.format(p[0])) + t = EIP155Transaction(p[0], p[0]['nonce'], p[0]['chainId']) + raw_signed_tx = self.signer.sign_transaction_to_rlp(t, p[1]) + o = { + 'raw': '0x' + raw_signed_tx.hex(), + 'tx': t.serialize(), + } + return o + + + def eth_signTransaction(self, tx): + o = self.personal_signTransaction([tx[0], '']) + return o['raw'] + + + def eth_sign(self, p): + logg.debug('got message {} to sign'.format(p[1])) + message_type = type(p[1]).__name__ + if message_type != 'str': + raise ValueError('invalid message format, must be {}, not {}'.format(message_type)) + z = self.signer.sign_ethereum_message(p[0], p[1][2:]) + return add_0x(z.hex()) + diff --git a/crypto_dev_signer/cli/http.py b/crypto_dev_signer/cli/http.py @@ -0,0 +1,85 @@ +# standard imports +import logging + +# external imports +from http.server import ( + HTTPServer, + BaseHTTPRequestHandler, + ) + +# local imports +from .handle import SignRequestHandler +from crypto_dev_signer.error import SignerError + +logg = logging.getLogger(__name__) + + +def start_server_http(spec): + httpd = HTTPServer(spec, HTTPSignRequestHandler) + logg.debug('starting http server {}'.format(spec)) + httpd.serve_forever() + + +class HTTPSignRequestHandler(SignRequestHandler, BaseHTTPRequestHandler): + + def do_POST(self): + if self.headers.get('Content-Type') != 'application/json': + self.send_response(400, 'me read json only') + self.end_headers() + return + + try: + if 'application/json' not in self.headers.get('Accept').split(','): + self.send_response(400, 'me json only speak') + self.end_headers() + return + except AttributeError: + pass + + l = self.headers.get('Content-Length') + try: + l = int(l) + except ValueError: + self.send_response(400, 'content length must be integer') + self.end_headers() + return + if l > 4096: + self.send_response(400, 'too much information') + self.end_headers() + return + if l < 0: + self.send_response(400, 'you are too negative') + self.end_headers() + return + + b = b'' + c = 0 + while c < l: + d = self.rfile.read(l-c) + if d == None: + break + b += d + c += len(d) + if c > 4096: + self.send_response(413, 'i should slap you around for lying about your size') + self.end_headers() + return + + try: + r = self.handle_jsonrpc(d) + except SignerError as e: + r = e.to_jsonrpc() + + l = len(r) + self.send_response(200, 'You are the Keymaster') + self.send_header('Content-Length', str(l)) + self.send_header('Cache-Control', 'no-cache') + self.send_header('Content-Type', 'application/json') + self.end_headers() + + c = 0 + while c < l: + n = self.wfile.write(r[c:]) + c += n + + diff --git a/crypto_dev_signer/cli/jsonrpc.py b/crypto_dev_signer/cli/jsonrpc.py @@ -0,0 +1,30 @@ +# local imports +from crypto_dev_signer.error import UnknownAccountError + + +def jsonrpc_error(rpc_id, err): + return { + 'jsonrpc': '2.0', + 'id': rpc_id, + 'error': { + 'code': err.CODE, + 'message': err.MESSAGE, + }, + } + + +def jsonrpc_ok(rpc_id, response): + return { + 'jsonrpc': '2.0', + 'id': rpc_id, + 'result': response, + } + + +def is_valid_json(j): + if j.get('id') == 'None': + raise ValueError('id missing') + return True + + + diff --git a/crypto_dev_signer/cli/socket.py b/crypto_dev_signer/cli/socket.py @@ -0,0 +1,67 @@ +# standard imports +import os +import logging +import socket +import stat + +# local imports +from crypto_dev_signer.error import SignerError +from .handle import SignRequestHandler + +logg = logging.getLogger(__name__) + + +class SocketHandler: + + def __init__(self): + self.handler = SignRequestHandler() + + + def process(self, csock): + d = csock.recv(4096) + + r = None + try: + r = self.handler.handle_jsonrpc(d) + except SignerError as e: + r = e.to_jsonrpc() + + csock.send(r) + + +def start_server_socket(s): + s.listen(10) + logg.debug('server started') + handler = SocketHandler() + while True: + (csock, caddr) = s.accept() + handler.process(csock) + csock.close() + s.close() + os.unlink(socket_path) + + +def start_server_tcp(spec): + s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) + s.bind(spec) + logg.debug('created tcp socket {}'.format(spec)) + start_server_socket(s) + + +def start_server_unix(socket_path): + 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) + logg.debug('created unix ipc socket {}'.format(socket_path)) + start_server_socket(s) diff --git a/crypto_dev_signer/error.py b/crypto_dev_signer/error.py @@ -8,3 +8,17 @@ class TransactionRevertError(Exception): class NetworkError(Exception): pass + + +class SignerError(Exception): + + def __init__(self, s): + super(SignerError, self).__init__(s) + self.jsonrpc_error = s + + + def to_jsonrpc(self): + return self.jsonrpc_error + + + diff --git a/crypto_dev_signer/runnable/signer.py b/crypto_dev_signer/runnable/signer.py @@ -2,8 +2,6 @@ import re import os import sys -import stat -import socket import json import logging import argparse @@ -12,13 +10,11 @@ from urllib.parse import urlparse # external imports import confini from jsonrpc.exceptions import * -from hexathon import add_0x # local imports from crypto_dev_signer.eth.signer import ReferenceSigner -from crypto_dev_signer.eth.transaction import EIP155Transaction from crypto_dev_signer.keystore.reference import ReferenceKeystore -from crypto_dev_signer.error import UnknownAccountError +from crypto_dev_signer.cli.handle import SignRequestHandler logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -72,171 +68,15 @@ logg.info('using dsn {}'.format(dsn)) logg.info('using socket {}'.format(config.get('SIGNER_SOCKET_PATH'))) re_http = r'^http' +re_tcp = r'^tcp' re_unix = r'^ipc' -class MissingSecretError(BaseException): +class MissingSecretError(Exception): + pass - 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 add_0x(r) - - -# TODO: move to translation module ("personal" rpc namespace is node-specific) -def personal_signTransaction(p): - logg.debug('got {} to sign'.format(p[0])) - #t = EIP155Transaction(p[0], p[0]['nonce'], 8995) - t = EIP155Transaction(p[0], p[0]['nonce'], p[0]['chainId']) - # z = signer.sign_transaction(t, p[1]) - # raw_signed_tx = t.rlp_serialize() - raw_signed_tx = signer.sign_transaction_to_rlp(t, p[1]) - o = { - 'raw': '0x' + raw_signed_tx.hex(), - 'tx': t.serialize(), - } - logg.debug('signed {}'.format(o)) - return o - - -def eth_signTransaction(tx): - o = personal_signTransaction([tx[0], '']) - return o['raw'] - - -def eth_sign(p): - logg.debug('got message {} to sign'.format(p[1])) - message_type = type(p[1]).__name__ - if message_type != 'str': - raise ValueError('invalid message format, must be {}, not {}'.format(message_type)) - z = signer.sign_ethereum_message(p[0], p[1][2:]) - return str(z) - - -methods = { - 'personal_newAccount': personal_new_account, - 'personal_signTransaction': personal_signTransaction, - 'eth_signTransaction': eth_signTransaction, - 'eth_sign': eth_sign, - } - - -def jsonrpc_error(rpc_id, err): - return { - 'jsonrpc': '2.0', - 'id': rpc_id, - 'error': { - 'code': err.CODE, - 'message': err.MESSAGE, - }, - } - - -def jsonrpc_ok(rpc_id, response): - return { - 'jsonrpc': '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_tcp(spec): - s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) - s.bind(spec) - logg.debug('created tcp socket {}'.format(spec)) - start_server(s) - - -def start_server_unix(socket_path): - 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) - logg.debug('created unix ipc socket {}'.format(socket_path)) - start_server(s) - - -def start_server(s): - s.listen(10) - logg.debug('server started') - 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.exception('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.exception('process error {}'.format(e)) - csock.send(json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')) - except UnknownAccountError as e: - logg.exception('process unknown account error {}'.format(e)) - csock.send(json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')) - - csock.close() - s.close() - - os.unlink(socket_path) +def main(): -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') @@ -245,26 +85,31 @@ def init(): kw = { 'symmetric_key': secret, } - db = ReferenceKeystore(dsn, **kw) - signer = ReferenceSigner(db) - + SignRequestHandler.keystore = ReferenceKeystore(dsn, **kw) + SignRequestHandler.signer = ReferenceSigner(SignRequestHandler.keystore) -def main(): - init() arg = None try: arg = json.loads(sys.argv[1]) except: - logg.info('no json rpc command detected, starting socket server') + logg.info('no json rpc command detected, starting socket server {}'.format(socket_url)) scheme = 'ipc' if socket_url.scheme != '': scheme = socket_url.scheme - if re.match(re_http, socket_url.scheme): + if re.match(re_tcp, socket_url.scheme): + from crypto_dev_signer.cli.socket import start_server_tcp socket_spec = socket_url.netloc.split(':') host = socket_spec[0] port = int(socket_spec[1]) start_server_tcp((host, port)) + elif re.match(re_http, socket_url.scheme): + from crypto_dev_signer.cli.http import start_server_http + socket_spec = socket_url.netloc.split(':') + host = socket_spec[0] + port = int(socket_spec[1]) + start_server_http((host, port)) else: + from crypto_dev_signer.cli.socket import start_server_unix start_server_unix(socket_url.path) sys.exit(0) diff --git a/run_tests.sh b/run_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e +set -x +#export PYTHONPATH=${PYTHONPATH:.} +for f in `ls tests/*.py`; do + python $f +done +set +x +set +e diff --git a/setup.py b/setup.py @@ -33,18 +33,16 @@ f.close() setup( name="crypto-dev-signer", - version="0.4.15a1", + version="0.4.15a4", description="A signer and keystore daemon and library for cryptocurrency software development", author="Louis Holbrook", author_email="dev@holbrook.no", packages=[ 'crypto_dev_signer.eth.signer', - #'crypto_dev_signer.eth.web3ext', - #'crypto_dev_signer.eth.helper', 'crypto_dev_signer.eth', + 'crypto_dev_signer.cli', 'crypto_dev_signer.keystore', 'crypto_dev_signer.runnable', - #'crypto_dev_signer.helper', 'crypto_dev_signer', ], install_requires=requirements, @@ -54,9 +52,6 @@ setup( tests_require=test_requirements, 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', diff --git a/tests/test_cli.py b/tests/test_cli.py @@ -0,0 +1,88 @@ +# standard imports +import unittest +import logging +import os + +# external imports +from hexathon import strip_0x + +# local imports +from crypto_dev_signer.eth.signer import ReferenceSigner +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.cli.handle import SignRequestHandler +from crypto_dev_signer.eth.transaction import EIP155Transaction + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +script_dir = os.path.dirname(os.path.realpath(__file__)) +data_dir = os.path.join(script_dir, 'testdata') + +class TestCli(unittest.TestCase): + + def setUp(self): + #pk = bytes.fromhex('5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6') + #pk_getter = pkGetter(pk) + self.keystore = DictKeystore() + SignRequestHandler.keystore = self.keystore + self.signer = ReferenceSigner(self.keystore) + SignRequestHandler.signer = self.signer + self.handler = SignRequestHandler() + + + def test_new_account(self): + q = { + 'id': 0, + 'method': 'personal_newAccount', + 'params': [''], + } + (rpc_id, result) = self.handler.process_input(q) + self.assertTrue(self.keystore.get(result)) + + + def test_sign_tx(self): + keystore_file = os.path.join(data_dir, 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72') + sender = self.keystore.import_keystore_file(keystore_file) + tx_hexs = { + 'nonce': '0x', + 'from': sender, + 'gasPrice': "0x04a817c800", + 'gas': "0x5208", + 'to': '0x3535353535353535353535353535353535353535', + 'value': "0x03e8", + 'data': "0xdeadbeef", + 'chainId': 8995, + } + tx = EIP155Transaction(tx_hexs, 42, 8995) + tx_s = tx.serialize() + + # TODO: move to serialization wrapper for tests + tx_s['chainId'] = tx_s['v'] + tx_s['from'] = sender + + # eth_signTransaction wraps personal_signTransaction, so here we test both already + q = { + 'id': 0, + 'method': 'eth_signTransaction', + 'params': [tx_s], + } + (rpc_id, result) = self.handler.process_input(q) + logg.debug('result {}'.format(result)) + + self.assertEqual(strip_0x(result), 'f86c2a8504a817c8008252089435353535353535353535353535353535353535358203e884deadbeef82466aa0b7c1bbf52f736ada30fe253c7484176f44d6fd097a9720dc85ae5bbc7f060e54a07afee2563b0cf6d00333df51cc62b0d13c63108b2bce54ce2ad24e26ce7b4f25') + + def test_sign_msg(self): + keystore_file = os.path.join(data_dir, 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72') + sender = self.keystore.import_keystore_file(keystore_file) + q = { + 'id': 0, + 'method': 'eth_sign', + 'params': [sender, '0xdeadbeef'], + } + (rpc_id, result) = self.handler.process_input(q) + logg.debug('result msg {}'.format(result)) + self.assertEqual(strip_0x(result), '50320dda75190a121b7b5979de66edadafd02bdfbe4f6d49552e79c01410d2464aae35e385c0e5b61663ff7b44ef65fa0ac7ad8a57472cf405db399b9dba3e1600') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_keystore_dict.py b/tests/test_keystore_dict.py @@ -29,7 +29,6 @@ class TestDict(unittest.TestCase): db = None def setUp(self): - logg.debug('setup') self.db = DictKeystore() keystore_filepath = os.path.join(script_dir, 'testdata', 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72') diff --git a/tests/test_socket.py b/tests/test_socket.py @@ -0,0 +1,15 @@ +# standard imports +import unittest +import logging + +logg = logging.getLogger(__name__) + + +class SocketTest(unittest.TestCase): + + def test_placeholder_warning(self): + logg.warning('socket tests are missing! :/') + + +if __name__ == '__main__': + unittest.main()