2020-10-17 02:44:25 +02:00
# standard imports
2021-03-17 15:34:51 +01:00
import re
2020-10-17 02:44:25 +02:00
import os
import sys
import stat
2020-08-08 11:33:15 +02:00
import socket
import json
import logging
2020-10-17 02:44:25 +02:00
import argparse
2021-03-17 15:34:51 +01:00
from urllib . parse import urlparse
2021-09-06 15:47:03 +02:00
from http . server import (
HTTPServer ,
BaseHTTPRequestHandler ,
)
2020-08-08 11:33:15 +02:00
2021-03-31 12:13:04 +02:00
# external imports
2020-10-17 02:44:25 +02:00
import confini
2020-08-08 11:33:15 +02:00
from jsonrpc . exceptions import *
2021-03-31 12:13:04 +02:00
from hexathon import add_0x
2020-08-08 11:33:15 +02:00
2020-10-17 02:44:25 +02:00
# local imports
2020-08-08 11:33:15 +02:00
from crypto_dev_signer . eth . signer import ReferenceSigner
from crypto_dev_signer . eth . transaction import EIP155Transaction
2021-03-25 19:14:55 +01:00
from crypto_dev_signer . keystore . reference import ReferenceKeystore
2020-12-25 09:21:09 +01:00
from crypto_dev_signer . error import UnknownAccountError
2020-08-08 11:33:15 +02:00
2021-01-25 18:22:06 +01:00
logging . basicConfig ( level = logging . WARNING )
2020-08-08 11:33:15 +02:00
logg = logging . getLogger ( )
2021-01-25 18:22:06 +01:00
config_dir = ' . '
2020-10-17 02:44:25 +02:00
2020-08-08 11:33:15 +02:00
db = None
signer = None
2021-02-28 08:39:15 +01:00
session = None
2020-08-08 11:33:15 +02:00
chainId = 8995
2020-10-17 02:44:25 +02:00
socket_path = ' /run/crypto-dev-signer/jsonrpc.ipc '
argparser = argparse . ArgumentParser ( )
argparser . add_argument ( ' -c ' , type = str , default = config_dir , help = ' config file ' )
2020-10-17 14:47:01 +02:00
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 ' )
2020-10-17 02:44:25 +02:00
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 )
2020-10-17 14:47:01 +02:00
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 ) )
2020-10-17 02:44:25 +02:00
if args . i :
chainId = args . i
if args . s :
2021-03-17 15:34:51 +01:00
socket_url = urlparse ( args . s )
2020-10-17 02:44:25 +02:00
elif config . get ( ' SIGNER_SOCKET_PATH ' ) :
2021-03-17 15:34:51 +01:00
socket_url = urlparse ( config . get ( ' SIGNER_SOCKET_PATH ' ) )
2020-08-08 11:33:15 +02:00
2020-10-17 14:47:01 +02:00
2020-10-17 11:06:52 +02:00
# 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 ' ) ,
)
2020-08-08 11:33:15 +02:00
2020-10-17 14:47:01 +02:00
logg . info ( ' using dsn {} ' . format ( dsn ) )
2021-05-02 18:06:27 +02:00
logg . info ( ' using socket {} ' . format ( config . get ( ' SIGNER_SOCKET_PATH ' ) ) )
2020-10-17 14:47:01 +02:00
2021-03-17 15:34:51 +01:00
re_http = r ' ^http '
2021-09-06 15:47:03 +02:00
re_tcp = r ' ^tcp '
2021-03-17 15:34:51 +01:00
re_unix = r ' ^ipc '
2020-10-17 14:47:01 +02:00
2020-08-08 12:27:05 +02:00
class MissingSecretError ( BaseException ) :
def __init__ ( self , message ) :
super ( MissingSecretError , self ) . __init__ ( message )
2020-08-08 11:33:15 +02:00
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 )
2021-03-31 12:13:04 +02:00
return add_0x ( r )
2020-08-08 11:33:15 +02:00
2021-03-31 12:13:04 +02:00
# TODO: move to translation module ("personal" rpc namespace is node-specific)
2021-03-27 15:32:05 +01:00
def personal_signTransaction ( p ) :
2020-10-26 09:02:29 +01:00
logg . debug ( ' got {} to sign ' . format ( p [ 0 ] ) )
2021-01-14 11:11:25 +01:00
#t = EIP155Transaction(p[0], p[0]['nonce'], 8995)
2021-03-27 16:28:55 +01:00
t = EIP155Transaction ( p [ 0 ] , p [ 0 ] [ ' nonce ' ] , p [ 0 ] [ ' chainId ' ] )
2021-03-27 15:32:05 +01:00
# z = signer.sign_transaction(t, p[1])
# raw_signed_tx = t.rlp_serialize()
raw_signed_tx = signer . sign_transaction_to_rlp ( t , p [ 1 ] )
2020-08-08 11:33:15 +02:00
o = {
' raw ' : ' 0x ' + raw_signed_tx . hex ( ) ,
' tx ' : t . serialize ( ) ,
}
2020-09-22 11:26:48 +02:00
logg . debug ( ' signed {} ' . format ( o ) )
2020-08-08 11:33:15 +02:00
return o
2021-03-27 15:32:05 +01:00
def eth_signTransaction ( tx ) :
2021-03-31 12:13:04 +02:00
o = personal_signTransaction ( [ tx [ 0 ] , ' ' ] )
return o [ ' raw ' ]
2020-08-08 11:33:15 +02:00
2020-12-25 12:00:58 +01:00
def eth_sign ( p ) :
logg . debug ( ' got message {} to sign ' . format ( p [ 1 ] ) )
message_type = type ( p [ 1 ] ) . __name__
2020-12-25 11:56:33 +01:00
if message_type != ' str ' :
raise ValueError ( ' invalid message format, must be {} , not {} ' . format ( message_type ) )
2021-03-26 13:08:36 +01:00
z = signer . sign_ethereum_message ( p [ 0 ] , p [ 1 ] [ 2 : ] )
2021-01-09 20:25:47 +01:00
return str ( z )
2020-12-22 14:50:00 +01:00
2020-08-08 11:33:15 +02:00
methods = {
' personal_newAccount ' : personal_new_account ,
2021-03-27 15:32:05 +01:00
' personal_signTransaction ' : personal_signTransaction ,
' eth_signTransaction ' : eth_signTransaction ,
2020-12-19 08:47:21 +01:00
' eth_sign ' : eth_sign ,
2020-08-08 11:33:15 +02:00
}
def jsonrpc_error ( rpc_id , err ) :
return {
2021-04-23 22:02:47 +02:00
' jsonrpc ' : ' 2.0 ' ,
2020-08-08 11:33:15 +02:00
' id ' : rpc_id ,
' error ' : {
' code ' : err . CODE ,
' message ' : err . MESSAGE ,
} ,
}
def jsonrpc_ok ( rpc_id , response ) :
return {
2021-04-23 22:02:47 +02:00
' jsonrpc ' : ' 2.0 ' ,
2020-08-08 11:33:15 +02:00
' 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 ) )
2021-01-27 13:33:52 +01:00
def start_server_tcp ( spec ) :
2021-01-25 18:22:06 +01:00
s = socket . socket ( family = socket . AF_INET , type = socket . SOCK_STREAM )
s . bind ( spec )
logg . debug ( ' created tcp socket {} ' . format ( spec ) )
2021-09-06 15:47:03 +02:00
start_server_socket ( s )
2021-01-25 18:22:06 +01:00
2021-01-27 13:33:52 +01:00
def start_server_unix ( socket_path ) :
2020-10-17 02:44:25 +02:00
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 )
2020-08-08 11:33:15 +02:00
try :
2020-10-17 02:44:25 +02:00
os . unlink ( socket_path )
2020-08-08 11:33:15 +02:00
except FileNotFoundError :
pass
s = socket . socket ( family = socket . AF_UNIX , type = socket . SOCK_STREAM )
2020-10-17 02:44:25 +02:00
s . bind ( socket_path )
2021-01-25 18:22:06 +01:00
logg . debug ( ' created unix ipc socket {} ' . format ( socket_path ) )
2021-09-06 15:47:03 +02:00
start_server_socket ( s )
2021-01-25 18:22:06 +01:00
2021-01-27 13:33:52 +01:00
2021-09-06 15:47:03 +02:00
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 ) :
2020-08-08 11:33:15 +02:00
j = None
try :
j = json . loads ( d )
is_valid_json ( j )
logg . debug ( ' {} ' . format ( d . decode ( ' utf-8 ' ) ) )
2020-09-22 11:26:48 +02:00
except Exception as e :
2021-03-27 16:28:55 +01:00
logg . exception ( ' input error {} ' . format ( e ) )
2021-09-06 15:47:03 +02:00
j = json . dumps ( jsonrpc_error ( None , JSONRPCParseError ) ) . encode ( ' utf-8 ' )
raise SignerError ( j )
2020-08-08 11:33:15 +02:00
try :
( rpc_id , r ) = process_input ( j )
2020-10-17 02:44:25 +02:00
r = jsonrpc_ok ( rpc_id , r )
j = json . dumps ( r ) . encode ( ' utf-8 ' )
except ValueError as e :
2020-08-08 11:33:15 +02:00
# TODO: handle cases to give better error context to caller
2021-03-27 16:28:55 +01:00
logg . exception ( ' process error {} ' . format ( e ) )
2021-09-06 15:47:03 +02:00
j = json . dumps ( jsonrpc_error ( j [ ' id ' ] , JSONRPCServerError ) ) . encode ( ' utf-8 ' )
raise SignerError ( j )
2020-12-25 09:21:09 +01:00
except UnknownAccountError as e :
2021-03-27 16:28:55 +01:00
logg . exception ( ' process unknown account error {} ' . format ( e ) )
2021-09-06 15:47:03 +02:00
j = json . dumps ( jsonrpc_error ( j [ ' id ' ] , JSONRPCServerError ) ) . encode ( ' utf-8 ' )
raise SignerError ( j )
return j
class HTTPSignRequestHandler ( BaseHTTPRequestHandler , SignRequestHandler ) :
2020-08-08 11:33:15 +02:00
2021-09-06 15:47:03 +02:00
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 )
2020-08-08 11:33:15 +02:00
csock . close ( )
s . close ( )
2020-10-17 02:44:25 +02:00
os . unlink ( socket_path )
2020-08-08 11:33:15 +02:00
def init ( ) :
global db , signer
2020-10-17 02:44:25 +02:00
secret_hex = config . get ( ' SIGNER_SECRET ' )
if secret_hex == None :
raise MissingSecretError ( ' please provide a valid hex value for the SIGNER_SECRET configuration variable ' )
2020-08-08 12:27:05 +02:00
2020-08-08 11:33:15 +02:00
secret = bytes . fromhex ( secret_hex )
kw = {
' symmetric_key ' : secret ,
}
2020-10-17 11:06:52 +02:00
db = ReferenceKeystore ( dsn , * * kw )
2020-08-08 11:33:15 +02:00
signer = ReferenceSigner ( db )
2020-10-18 10:32:23 +02:00
def main ( ) :
2020-08-08 11:33:15 +02:00
init ( )
arg = None
try :
arg = json . loads ( sys . argv [ 1 ] )
except :
2021-09-06 15:47:03 +02:00
logg . info ( ' no json rpc command detected, starting socket server {} ' . format ( socket_url ) )
2021-03-17 15:34:51 +01:00
scheme = ' ipc '
if socket_url . scheme != ' ' :
scheme = socket_url . scheme
2021-09-06 15:47:03 +02:00
if re . match ( re_tcp , socket_url . scheme ) :
2021-03-17 15:34:51 +01:00
socket_spec = socket_url . netloc . split ( ' : ' )
2021-01-25 18:22:06 +01:00
host = socket_spec [ 0 ]
port = int ( socket_spec [ 1 ] )
2021-01-27 13:33:52 +01:00
start_server_tcp ( ( host , port ) )
2021-09-06 15:47:03 +02:00
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 ) )
2021-01-25 18:22:06 +01:00
else :
2021-03-17 15:34:51 +01:00
start_server_unix ( socket_url . path )
2020-08-08 11:33:15 +02:00
sys . exit ( 0 )
( rpc_id , response ) = process_input ( arg )
r = jsonrpc_ok ( rpc_id , response )
sys . stdout . write ( json . dumps ( r ) )
2020-10-26 09:02:29 +01:00
if __name__ == ' __main__ ' :
main ( )