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

This commit is contained in:
The MMGen Project 2022-04-29 16:53:52 +00:00
commit a81ff33f08
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
10 changed files with 81 additions and 30 deletions

View file

@ -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

View file

@ -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)) )
)

View file

@ -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

View file

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

View file

@ -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:

View file

@ -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,
},

View file

@ -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

View file

@ -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'

View file

@ -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'],

View file

@ -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')