mmgen-msg: support raw (unprefixed) message signing for Ethereum
This commit is contained in:
parent
815e75c602
commit
a81ff33f08
10 changed files with 81 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) )
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
13.1.dev26
|
||||
13.1.dev27
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
18
mmgen/msg.py
18
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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue