From a81ff33f08865be5e85265dd8cd5920f4f13fbf4 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 29 Apr 2022 16:53:52 +0000 Subject: [PATCH] mmgen-msg: support raw (unprefixed) message signing for Ethereum --- mmgen/base_proto/bitcoin/msg.py | 4 ++-- mmgen/base_proto/ethereum/misc.py | 15 +++++++++------ mmgen/base_proto/ethereum/msg.py | 8 ++++---- mmgen/data/version | 2 +- mmgen/main_msg.py | 14 +++++++++++--- mmgen/msg.py | 18 ++++++++++++++---- mmgen/proto/btc.py | 1 + mmgen/proto/eth.py | 1 + test/test_py_d/ts_ethdev.py | 30 +++++++++++++++++++++++++++--- test/unit_tests_d/ut_msg.py | 18 +++++++++++------- 10 files changed, 81 insertions(+), 30 deletions(-) diff --git a/mmgen/base_proto/bitcoin/msg.py b/mmgen/base_proto/bitcoin/msg.py index 0b0a2332..b8e4fc17 100755 --- a/mmgen/base_proto/bitcoin/msg.py +++ b/mmgen/base_proto/bitcoin/msg.py @@ -24,14 +24,14 @@ class coin_msg(coin_msg): class unsigned(completed,coin_msg.unsigned): - async def do_sign(self,wif,message): + async def do_sign(self,wif,message,msghash_type): return await self.rpc.call( 'signmessagewithprivkey', wif, message ) class signed(completed,coin_msg.signed): pass class signed_online(signed,coin_msg.signed_online): - async def do_verify(self,addr,sig,message): + async def do_verify(self,addr,sig,message,msghash_type): return await self.rpc.call( 'verifymessage', addr, sig, message ) class exported_sigs(coin_msg.exported_sigs,signed_online): pass diff --git a/mmgen/base_proto/ethereum/misc.py b/mmgen/base_proto/ethereum/misc.py index eb9a4fc2..73d28f99 100755 --- a/mmgen/base_proto/ethereum/misc.py +++ b/mmgen/base_proto/ethereum/misc.py @@ -66,22 +66,25 @@ def extract_key_from_geth_keystore_wallet(wallet_fn,passwd,check_addr=True): return key -def hash_message(message): +def hash_message(message,msghash_type): return get_keccak()( - '\x19Ethereum Signed Message:\n{}{}'.format( len(message), message ).encode() + { + 'raw': message, + 'eth_sign': '\x19Ethereum Signed Message:\n{}{}'.format( len(message), message ), + }[msghash_type].encode() ).digest() -def ec_sign_message_with_privkey(message,key): +def ec_sign_message_with_privkey(message,key,msghash_type): """ Sign an arbitrary string with an Ethereum private key, returning the signature Conforms to the standard defined by the Geth `eth_sign` JSON-RPC call """ from py_ecc.secp256k1 import ecdsa_raw_sign - v,r,s = ecdsa_raw_sign( hash_message(message), key ) + v,r,s = ecdsa_raw_sign( hash_message(message,msghash_type), key ) return '{:064x}{:064x}{:02x}'.format(r,s,v) -def ec_recover_pubkey(message,sig): +def ec_recover_pubkey(message,sig,msghash_type): """ Given a message and signature, recover the public key associated with the private key used to make the signature @@ -91,5 +94,5 @@ def ec_recover_pubkey(message,sig): from py_ecc.secp256k1 import ecdsa_raw_recover r,s,v = ( sig[:64], sig[64:128], sig[128:] ) return '{:064x}{:064x}'.format( - *ecdsa_raw_recover( hash_message(message), tuple(int(hexstr,16) for hexstr in (v,r,s)) ) + *ecdsa_raw_recover( hash_message(message,msghash_type), tuple(int(hexstr,16) for hexstr in (v,r,s)) ) ) diff --git a/mmgen/base_proto/ethereum/msg.py b/mmgen/base_proto/ethereum/msg.py index a630a78d..29e1533a 100755 --- a/mmgen/base_proto/ethereum/msg.py +++ b/mmgen/base_proto/ethereum/msg.py @@ -24,17 +24,17 @@ class coin_msg(coin_msg): class unsigned(completed,coin_msg.unsigned): - async def do_sign(self,wif,message): + async def do_sign(self,wif,message,msghash_type): from .misc import ec_sign_message_with_privkey - return ec_sign_message_with_privkey( message, bytes.fromhex(wif) ) + return ec_sign_message_with_privkey( message, bytes.fromhex(wif), msghash_type ) class signed(completed,coin_msg.signed): pass class signed_online(signed,coin_msg.signed_online): - async def do_verify(self,addr,sig,message): + async def do_verify(self,addr,sig,message,msghash_type): from ...tool.coin import tool_cmd from .misc import ec_recover_pubkey - return tool_cmd(proto=self.proto).pubhex2addr(ec_recover_pubkey( message, sig )) == addr + return tool_cmd(proto=self.proto).pubhex2addr(ec_recover_pubkey( message, sig, msghash_type )) == addr class exported_sigs(coin_msg.exported_sigs,signed_online): pass diff --git a/mmgen/data/version b/mmgen/data/version index 5c02943a..effe573e 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.dev26 +13.1.dev27 diff --git a/mmgen/main_msg.py b/mmgen/main_msg.py index e717040f..1a23ea5b 100755 --- a/mmgen/main_msg.py +++ b/mmgen/main_msg.py @@ -28,7 +28,9 @@ class MsgOps: coin = proto.coin, network = proto.network, message = msg, - addrlists = addr_specs ).write_to_file( ask_overwrite=False ) + addrlists = addr_specs, + msghash_type = opt.msghash_type or proto.msghash_types[0] + ).write_to_file( ask_overwrite=False ) class sign(metaclass=AsyncInit): @@ -89,6 +91,8 @@ opts_data = { -h, --help Print this help message --, --longhelp Print help message for long options (common options) -d, --outdir=d Output file to directory 'd' instead of working dir +-t, --msghash-type=T Specify the message hash type. Supported values: + 'eth_sign' (ETH default), 'raw' (non-ETH default) -q, --quiet Produce quieter output """, 'notes': """ @@ -126,8 +130,9 @@ separated address index ranges. Message signing operations are supported for Bitcoin, Ethereum and code forks thereof. -Ethereum signatures conform to the standard defined by the Geth ‘eth_sign’ -JSON-RPC call. +By default, Ethereum messages are prefixed before hashing in conformity with +the standard defined by the Geth ‘eth_sign’ JSON-RPC call. This behavior may +be overridden with the --msghash-type option. Messages signed for Segwit-P2SH addresses cannot be verified directly using the Bitcoin Core `verifymessage` RPC call, since such addresses are not hashes @@ -191,6 +196,9 @@ if len(cmd_args) < 2: op = cmd_args.pop(0) +if opt.msghash_type and op != 'create': + die(1,'--msghash-type option may only be used with the "create" command') + async def main(): if op == 'create': if len(cmd_args) < 2: diff --git a/mmgen/msg.py b/mmgen/msg.py index 4fd51142..d952ddcc 100755 --- a/mmgen/msg.py +++ b/mmgen/msg.py @@ -64,7 +64,7 @@ class coin_msg: def chksum(self): return make_chksum_6( json.dumps( - {k:v for k,v in self.data.items() if k != 'failed_sids'}, + {k:v for k,v in self.data.items() if k in ('addrlists','message','network','msghash_type')}, sort_keys = True, separators = (',', ':') )) @@ -106,11 +106,14 @@ class coin_msg: class new(base): - def __init__(self,message,addrlists,*args,**kwargs): + def __init__(self,message,addrlists,msghash_type,*args,**kwargs): + if msghash_type not in self.proto.msghash_types: + die(2,f'msghash_type {msghash_type!r} not supported for {self.proto.base_proto} protocol') self.data = { 'network': '{}_{}'.format( self.proto.coin.lower(), self.proto.network ), 'addrlists': [MMGenIDRange(self.proto,i) for i in addrlists.split()], 'message': message, + 'msghash_type': msghash_type, } self.sigs = {} @@ -173,10 +176,14 @@ class coin_msg: hdr_data = { 'message': ('Message:', lambda v: grnbg(v) ), 'network': ('Network:', lambda v: v.replace('_',' ').upper() ), + 'msghash_type': ('Message Hash Type:', lambda v: v ), 'addrlists': ('Address Ranges:', lambda v: fmt_list(v,fmt='bare') ), 'failed_sids': ('Failed Seed IDs:', lambda v: red(fmt_list(v,fmt='bare')) ), } + if len(self.proto.msghash_types) == 1: + del hdr_data['msghash_type'] + if req_addr or type(self).__name__ == 'exported_sigs': del hdr_data['addrlists'] @@ -214,7 +221,8 @@ class coin_msg: for e in al.data: sig = await self.do_sign( wif = e.sec.wif, - message = self.data['message'] ) + message = self.data['message'], + msghash_type = self.data['msghash_type'] ) mmid = '{}:{}:{}'.format( al_in.sid, al_in.mmtype, e.idx ) data = { @@ -301,7 +309,8 @@ class coin_msg: ret = await self.do_verify( addr = v.get('addr_p2pkh') or v['addr'], sig = v['sig'], - message = self.data['message'] ) + message = self.data['message'], + msghash_type = self.data['msghash_type'] ) if not ret: die(3,f'Invalid signature for address {k} ({v["addr"]})') @@ -315,6 +324,7 @@ class coin_msg: return json.dumps( { 'message': self.data['message'], + 'msghash_type': self.data['msghash_type'], 'network': self.data['network'].upper(), 'signatures': sigs, }, diff --git a/mmgen/proto/btc.py b/mmgen/proto/btc.py index e370a578..f94eafd0 100755 --- a/mmgen/proto/btc.py +++ b/mmgen/proto/btc.py @@ -43,6 +43,7 @@ class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp witness_vernum = int(witness_vernum_hex,16) bech32_hrp = 'bc' sign_mode = 'daemon' + msghash_types = ('raw',) # first-listed is the default avg_bdi = int(9.7 * 60) # average block discovery interval (historical) halving_interval = 210000 max_halvings = 64 diff --git a/mmgen/proto/eth.py b/mmgen/proto/eth.py index c4798b71..dc46fa53 100755 --- a/mmgen/proto/eth.py +++ b/mmgen/proto/eth.py @@ -30,6 +30,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1): max_tx_fee = '0.005' chain_names = ['ethereum','foundation'] sign_mode = 'standalone' + msghash_types = ('eth_sign','raw') # first-listed is the default caps = ('token',) mmcaps = ('key','addr','rpc','tx') base_proto = 'Ethereum' diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index f504e959..971c5c60 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -149,6 +149,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ('msgexport', 'exporting the message file data to JSON for third-party verifier'), ('msgverify_export', 'verifying the exported JSON data'), + ('msgcreate_raw', 'creating a message file (--msghash-type=raw)'), + ('msgsign_raw', 'signing the message file (msghash_type=raw)'), + ('msgverify_raw', 'verifying the message file (msghash_type=raw)'), + ('msgexport_raw', 'exporting the message file data to JSON (msghash_type=raw)'), + ('msgverify_export_raw', 'verifying the exported JSON data (msghash_type=raw)'), + ('txcreate1', 'creating a transaction (spend from dev address to address :1)'), ('txview1_raw', 'viewing the raw transaction'), ('txsign1', 'signing the transaction'), @@ -667,7 +673,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): imsg(f'Key: {key.hex()}') from mmgen.base_proto.ethereum.misc import ec_sign_message_with_privkey - return ec_sign_message_with_privkey(self.message,key) + return ec_sign_message_with_privkey(self.message,key,'eth_sign') async def create_signature_rpc(): from mmgen.rpc import rpc_init @@ -695,8 +701,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): return 'ok' - def msgcreate(self): - t = self.spawn('mmgen-msg', self.eth_args_noquiet + [ 'create', self.message, '98831F3A:E:1' ]) + def msgcreate(self,add_args=[]): + t = self.spawn('mmgen-msg', self.eth_args_noquiet + add_args + [ 'create', self.message, '98831F3A:E:1' ]) t.written_to_file('Unsigned message data') return t @@ -721,6 +727,24 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def msgverify_export(self): return self.msgverify( fn=os.path.join(self.tmpdir,'signatures.json') ) + def msgcreate_raw(self): + get_file_with_ext(self.tmpdir,'rawmsg.json',delete_all=True) + return self.msgcreate(add_args=['--msghash-type=raw']) + + def msgsign_raw(self,*args,**kwargs): + get_file_with_ext(self.tmpdir,'sigmsg.json',delete_all=True) + return self.msgsign(*args,**kwargs) + + def msgverify_raw(self,*args,**kwargs): + return self.msgverify(*args,**kwargs) + + def msgexport_raw(self,*args,**kwargs): + get_file_with_ext(self.tmpdir,'signatures.json',no_dot=True,delete_all=True) + return self.msgexport(*args,**kwargs) + + def msgverify_export_raw(self,*args,**kwargs): + return self.msgverify_export(*args,**kwargs) + def txcreate4(self): return self.txcreate( args = ['98831F3A:E:2,23.45495'], diff --git a/test/unit_tests_d/ut_msg.py b/test/unit_tests_d/ut_msg.py index 010dea03..aed65d15 100755 --- a/test/unit_tests_d/ut_msg.py +++ b/test/unit_tests_d/ut_msg.py @@ -12,7 +12,7 @@ from mmgen.protocol import CoinProtocol from mmgen.msg import NewMsg,UnsignedMsg,SignedMsg,SignedOnlineMsg,ExportedMsgSigs from mmgen.addr import MMGenID -def get_obj(coin,network): +def get_obj(coin,network,msghash_type): if coin == 'bch': addrlists = 'DEADBEEF:C:1-20 98831F3A:C:8,2 A091ABAA:L:111 A091ABAA:C:1' @@ -26,9 +26,10 @@ def get_obj(coin,network): coin = coin, network = network, message = '08/Jun/2021 Bitcoin Law Enacted by El Salvador Legislative Assembly', - addrlists = addrlists ) + addrlists = addrlists, + msghash_type = msghash_type ) -async def run_test(network_id): +async def run_test(network_id,msghash_type='raw'): coin,network = CoinProtocol.Base.parse_network_id(network_id) @@ -41,7 +42,7 @@ async def run_test(network_id): pumsg('\nTesting data creation:\n') - m = get_obj(coin,network) + m = get_obj(coin,network,msghash_type) tmpdir = os.path.join('test','trash2') @@ -53,7 +54,7 @@ async def run_test(network_id): pumsg('\nTesting signing:\n') - m = UnsignedMsg( infile = os.path.join(tmpdir,get_obj(coin,network).filename) ) + m = UnsignedMsg( infile = os.path.join(tmpdir,get_obj(coin,network,msghash_type).filename) ) await m.sign(wallet_files=['test/ref/98831F3A.mmwords']) m = SignedMsg( data=m.__dict__ ) @@ -63,7 +64,7 @@ async def run_test(network_id): pumsg('\nTesting display:\n') - m = SignedOnlineMsg( infile = os.path.join(tmpdir,get_obj(coin,network).signed_filename) ) + m = SignedOnlineMsg( infile = os.path.join(tmpdir,get_obj(coin,network,msghash_type).signed_filename) ) msg(m.format()) @@ -118,7 +119,7 @@ async def run_test(network_id): class unit_tests: - altcoin_deps = ('ltc','bch','eth') + altcoin_deps = ('ltc','bch','eth','eth_raw') def btc(self,name,ut): return run_test('btc') @@ -136,4 +137,7 @@ class unit_tests: return run_test('bch') def eth(self,name,ut): + return run_test('eth',msghash_type='eth_sign') + + def eth_raw(self,name,ut): return run_test('eth')