Browse Source

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

The MMGen Project 3 years ago
parent
commit
a81ff33f08

+ 2 - 2
mmgen/base_proto/bitcoin/msg.py

@@ -24,14 +24,14 @@ class coin_msg(coin_msg):
 
 
 	class unsigned(completed,coin_msg.unsigned):
 	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 )
 			return await self.rpc.call( 'signmessagewithprivkey', wif, message )
 
 
 	class signed(completed,coin_msg.signed): pass
 	class signed(completed,coin_msg.signed): pass
 
 
 	class signed_online(signed,coin_msg.signed_online):
 	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 )
 			return await self.rpc.call( 'verifymessage', addr, sig, message )
 
 
 	class exported_sigs(coin_msg.exported_sigs,signed_online): pass
 	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
 	return key
 
 
-def hash_message(message):
+def hash_message(message,msghash_type):
 	return get_keccak()(
 	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()
 	).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
 	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
 	Conforms to the standard defined by the Geth `eth_sign` JSON-RPC call
 	"""
 	"""
 	from py_ecc.secp256k1 import ecdsa_raw_sign
 	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)
 	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
 	Given a message and signature, recover the public key associated with the private key
 	used to make the signature
 	used to make the signature
@@ -91,5 +94,5 @@ def ec_recover_pubkey(message,sig):
 	from py_ecc.secp256k1 import ecdsa_raw_recover
 	from py_ecc.secp256k1 import ecdsa_raw_recover
 	r,s,v = ( sig[:64], sig[64:128], sig[128:] )
 	r,s,v = ( sig[:64], sig[64:128], sig[128:] )
 	return '{:064x}{:064x}'.format(
 	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):
 	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
 			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(completed,coin_msg.signed): pass
 
 
 	class signed_online(signed,coin_msg.signed_online):
 	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 ...tool.coin import tool_cmd
 			from .misc import ec_recover_pubkey
 			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
 	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,
 				coin      = proto.coin,
 				network   = proto.network,
 				network   = proto.network,
 				message   = msg,
 				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):
 	class sign(metaclass=AsyncInit):
 
 
@@ -89,6 +91,8 @@ opts_data = {
 -h, --help           Print this help message
 -h, --help           Print this help message
 --, --longhelp       Print help message for long options (common options)
 --, --longhelp       Print help message for long options (common options)
 -d, --outdir=d       Output file to directory 'd' instead of working dir
 -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
 -q, --quiet          Produce quieter output
 """,
 """,
 	'notes': """
 	'notes': """
@@ -126,8 +130,9 @@ separated address index ranges.
 Message signing operations are supported for Bitcoin, Ethereum and code forks
 Message signing operations are supported for Bitcoin, Ethereum and code forks
 thereof.
 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
 Messages signed for Segwit-P2SH addresses cannot be verified directly using
 the Bitcoin Core `verifymessage` RPC call, since such addresses are not hashes
 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)
 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():
 async def main():
 	if op == 'create':
 	if op == 'create':
 		if len(cmd_args) < 2:
 		if len(cmd_args) < 2:

+ 14 - 4
mmgen/msg.py

@@ -64,7 +64,7 @@ class coin_msg:
 		def chksum(self):
 		def chksum(self):
 			return make_chksum_6(
 			return make_chksum_6(
 				json.dumps(
 				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,
 					sort_keys = True,
 					separators = (',', ':')
 					separators = (',', ':')
 			))
 			))
@@ -106,11 +106,14 @@ class coin_msg:
 
 
 	class new(base):
 	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 = {
 			self.data = {
 				'network': '{}_{}'.format( self.proto.coin.lower(), self.proto.network ),
 				'network': '{}_{}'.format( self.proto.coin.lower(), self.proto.network ),
 				'addrlists': [MMGenIDRange(self.proto,i) for i in addrlists.split()],
 				'addrlists': [MMGenIDRange(self.proto,i) for i in addrlists.split()],
 				'message': message,
 				'message': message,
+				'msghash_type': msghash_type,
 			}
 			}
 			self.sigs = {}
 			self.sigs = {}
 
 
@@ -173,10 +176,14 @@ class coin_msg:
 			hdr_data = {
 			hdr_data = {
 				'message':      ('Message:',           lambda v: grnbg(v) ),
 				'message':      ('Message:',           lambda v: grnbg(v) ),
 				'network':      ('Network:',           lambda v: v.replace('_',' ').upper() ),
 				'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') ),
 				'addrlists':    ('Address Ranges:',    lambda v: fmt_list(v,fmt='bare') ),
 				'failed_sids':  ('Failed Seed IDs:',   lambda v: red(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':
 			if req_addr or type(self).__name__ == 'exported_sigs':
 				del hdr_data['addrlists']
 				del hdr_data['addrlists']
 
 
@@ -214,7 +221,8 @@ class coin_msg:
 				for e in al.data:
 				for e in al.data:
 					sig = await self.do_sign(
 					sig = await self.do_sign(
 						wif     = e.sec.wif,
 						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 )
 					mmid = '{}:{}:{}'.format( al_in.sid, al_in.mmtype, e.idx )
 					data = {
 					data = {
@@ -301,7 +309,8 @@ class coin_msg:
 				ret = await self.do_verify(
 				ret = await self.do_verify(
 					addr    = v.get('addr_p2pkh') or v['addr'],
 					addr    = v.get('addr_p2pkh') or v['addr'],
 					sig     = v['sig'],
 					sig     = v['sig'],
-					message = self.data['message'] )
+					message = self.data['message'],
+					msghash_type = self.data['msghash_type'] )
 				if not ret:
 				if not ret:
 					die(3,f'Invalid signature for address {k} ({v["addr"]})')
 					die(3,f'Invalid signature for address {k} ({v["addr"]})')
 
 
@@ -315,6 +324,7 @@ class coin_msg:
 			return json.dumps(
 			return json.dumps(
 				{
 				{
 					'message': self.data['message'],
 					'message': self.data['message'],
+					'msghash_type': self.data['msghash_type'],
 					'network': self.data['network'].upper(),
 					'network': self.data['network'].upper(),
 					'signatures': sigs,
 					'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)
 	witness_vernum  = int(witness_vernum_hex,16)
 	bech32_hrp      = 'bc'
 	bech32_hrp      = 'bc'
 	sign_mode       = 'daemon'
 	sign_mode       = 'daemon'
+	msghash_types   = ('raw',) # first-listed is the default
 	avg_bdi         = int(9.7 * 60) # average block discovery interval (historical)
 	avg_bdi         = int(9.7 * 60) # average block discovery interval (historical)
 	halving_interval = 210000
 	halving_interval = 210000
 	max_halvings    = 64
 	max_halvings    = 64

+ 1 - 0
mmgen/proto/eth.py

@@ -30,6 +30,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
 	max_tx_fee    = '0.005'
 	max_tx_fee    = '0.005'
 	chain_names   = ['ethereum','foundation']
 	chain_names   = ['ethereum','foundation']
 	sign_mode     = 'standalone'
 	sign_mode     = 'standalone'
+	msghash_types = ('eth_sign','raw') # first-listed is the default
 	caps          = ('token',)
 	caps          = ('token',)
 	mmcaps        = ('key','addr','rpc','tx')
 	mmcaps        = ('key','addr','rpc','tx')
 	base_proto    = 'Ethereum'
 	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'),
 		('msgexport',                       'exporting the message file data to JSON for third-party verifier'),
 		('msgverify_export',                'verifying the exported JSON data'),
 		('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)'),
 		('txcreate1',                       'creating a transaction (spend from dev address to address :1)'),
 		('txview1_raw',                     'viewing the raw transaction'),
 		('txview1_raw',                     'viewing the raw transaction'),
 		('txsign1',                         'signing the transaction'),
 		('txsign1',                         'signing the transaction'),
@@ -667,7 +673,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 			imsg(f'Key:       {key.hex()}')
 			imsg(f'Key:       {key.hex()}')
 
 
 			from mmgen.base_proto.ethereum.misc import ec_sign_message_with_privkey
 			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():
 		async def create_signature_rpc():
 			from mmgen.rpc import rpc_init
 			from mmgen.rpc import rpc_init
@@ -695,8 +701,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 
 
 		return 'ok'
 		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')
 		t.written_to_file('Unsigned message data')
 		return t
 		return t
 
 
@@ -721,6 +727,24 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	def msgverify_export(self):
 	def msgverify_export(self):
 		return self.msgverify( fn=os.path.join(self.tmpdir,'signatures.json') )
 		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):
 	def txcreate4(self):
 		return self.txcreate(
 		return self.txcreate(
 			args             = ['98831F3A:E:2,23.45495'],
 			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.msg import NewMsg,UnsignedMsg,SignedMsg,SignedOnlineMsg,ExportedMsgSigs
 from mmgen.addr import MMGenID
 from mmgen.addr import MMGenID
 
 
-def get_obj(coin,network):
+def get_obj(coin,network,msghash_type):
 
 
 	if coin == 'bch':
 	if coin == 'bch':
 		addrlists = 'DEADBEEF:C:1-20 98831F3A:C:8,2 A091ABAA:L:111 A091ABAA:C:1'
 		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,
 		coin      = coin,
 		network   = network,
 		network   = network,
 		message   = '08/Jun/2021 Bitcoin Law Enacted by El Salvador Legislative Assembly',
 		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)
 	coin,network = CoinProtocol.Base.parse_network_id(network_id)
 
 
@@ -41,7 +42,7 @@ async def run_test(network_id):
 
 
 	pumsg('\nTesting data creation:\n')
 	pumsg('\nTesting data creation:\n')
 
 
-	m = get_obj(coin,network)
+	m = get_obj(coin,network,msghash_type)
 
 
 	tmpdir = os.path.join('test','trash2')
 	tmpdir = os.path.join('test','trash2')
 
 
@@ -53,7 +54,7 @@ async def run_test(network_id):
 
 
 	pumsg('\nTesting signing:\n')
 	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'])
 	await m.sign(wallet_files=['test/ref/98831F3A.mmwords'])
 
 
 	m = SignedMsg( data=m.__dict__ )
 	m = SignedMsg( data=m.__dict__ )
@@ -63,7 +64,7 @@ async def run_test(network_id):
 
 
 	pumsg('\nTesting display:\n')
 	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())
 	msg(m.format())
 
 
@@ -118,7 +119,7 @@ async def run_test(network_id):
 
 
 class unit_tests:
 class unit_tests:
 
 
-	altcoin_deps = ('ltc','bch','eth')
+	altcoin_deps = ('ltc','bch','eth','eth_raw')
 
 
 	def btc(self,name,ut):
 	def btc(self,name,ut):
 		return run_test('btc')
 		return run_test('btc')
@@ -136,4 +137,7 @@ class unit_tests:
 		return run_test('bch')
 		return run_test('bch')
 
 
 	def eth(self,name,ut):
 	def eth(self,name,ut):
+		return run_test('eth',msghash_type='eth_sign')
+
+	def eth_raw(self,name,ut):
 		return run_test('eth')
 		return run_test('eth')