# standard imports import re import os import sys import stat import socket import json import logging import argparse from urllib.parse import urlparse from http.server import ( HTTPServer, BaseHTTPRequestHandler, ) # 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 logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() config_dir = '.' db = None signer = None session = 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_url = urlparse(args.s) elif config.get('SIGNER_SOCKET_PATH'): socket_url = urlparse(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(config.get('SIGNER_SOCKET_PATH'))) re_http = r'^http' re_tcp = r'^tcp' re_unix = r'^ipc' 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 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_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) def start_server_http(spec): httpd = HTTPServer(spec, HTTPSignRequestHandler) logg.debug('starting http server {}'.format(spec)) httpd.serve_forever() class SignerError(Exception): def __init__(self, s): super(SignerError, self).__init__(s) self.jsonrpc_error = s def to_jsonrpc(self): return self.jsonrpc_error class SignRequestHandler: 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) = 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 class HTTPSignRequestHandler(BaseHTTPRequestHandler, SignRequestHandler): 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() #b = json.dumps(r).encode('utf-8') 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 def start_server_socket(s): s.listen(10) logg.debug('server started') handler = SignRequestHandler() while True: (csock, caddr) = s.accept() d = csock.recv(4096) r = None try: r = handler.handle_jsonrpc(d) except SignerError as e: r = e.to_jsonrpc() csock.send(r) 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) def main(): init() arg = None try: arg = json.loads(sys.argv[1]) except: 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_tcp, socket_url.scheme): 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): socket_spec = socket_url.netloc.split(':') host = socket_spec[0] port = int(socket_spec[1]) start_server_http((host, port)) else: start_server_unix(socket_url.path) sys.exit(0) (rpc_id, response) = process_input(arg) r = jsonrpc_ok(rpc_id, response) sys.stdout.write(json.dumps(r)) if __name__ == '__main__': main()