Browse Source

mmgen-msg: support raw (unprefixed) message signing for Ethereum

The MMGen Project 2 years ago
parent
commit
a81ff33f0

+ 2 - 2
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

+ 9 - 6
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)) )
 	)

+ 4 - 4
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

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.1.dev26
+13.1.dev27

+ 11 - 3
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:

+ 14 - 4
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,
 				},

+ 1 - 0
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

+ 1 - 0
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'

+ 27 - 3
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'],

+ 11 - 7
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')