message signing: user-level support

Usage information:

    $ mmgen-msg --help

Testing:

    $ test/test.py -e regtest
This commit is contained in:
The MMGen Project 2022-03-27 13:42:43 +00:00
commit e5cf3b6ec8
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
8 changed files with 283 additions and 11 deletions

16
cmds/mmgen-msg Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen
# https://gitlab.com/mmgen/mmgen
"""
mmgen-msg: Message signing operations for the MMGen suite
"""
from mmgen.main import launch
launch("msg")

View file

@ -1 +1 @@
February 2022 March 2022

View file

@ -1 +1 @@
13.1.dev21 13.1.dev22

173
mmgen/main_msg.py Executable file
View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen
# https://gitlab.com/mmgen/mmgen
"""
mmgen.main_msg: Message signing operations for the MMGen suite
"""
from .base_obj import AsyncInit
from .common import *
from .msg import *
class MsgOps:
ops = ('create','sign','verify')
class create:
def __init__(self,msg,addr_specs):
from .protocol import init_proto_from_opts
proto = init_proto_from_opts()
if proto.base_proto != 'Bitcoin':
die('Message signing operations are supported for Bitcoin and Bitcoin-derived coins only')
NewMsg(
coin = proto.coin,
network = proto.network,
message = msg,
addrlists = addr_specs ).write_to_file( ask_overwrite=False )
class sign(metaclass=AsyncInit):
async def __init__(self,msgfile,wallet_files):
m = UnsignedMsg( infile=msgfile )
if not wallet_files:
from .filename import find_file_in_dir
from .wallet import get_wallet_cls
wallet_files = [find_file_in_dir( get_wallet_cls('mmgen'), g.data_dir )]
await m.sign(wallet_files)
SignedMsg( data=m.__dict__ ).write_to_file( ask_overwrite=False )
class verify(sign):
async def __init__(self,msgfile,addr=None):
m = SignedOnlineMsg( infile=msgfile )
qmsg(m.format(addr) + '\n')
await m.verify(addr,summary=True)
opts_data = {
'text': {
'desc': 'Perform message signing operations for MMGen addresses',
'usage2': [
'[opts] create MESSAGE_TEXT ADDRESS_SPEC [...]',
'[opts] sign MESSAGE_FILE [WALLET_FILE ...]',
'[opts] verify MESSAGE_FILE',
],
'options': """
-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
-q, --quiet Produce quieter output
""",
'notes': """
SUPPORTED OPERATIONS
create - create a raw MMGen message file with specified message text for
signing for addresses specified by ADDRESS_SPEC (see ADDRESS
SPECIFIER below)
sign - perform signing operation on an unsigned MMGen message file
verify - verify and display the contents of a signed MMGen message file
ADDRESS SPECIFIER
The `create` operation takes one or more ADDRESS_SPEC arguments with the
following format:
SEED_ID:ADDR_TYPE:ADDR_IDX_SPEC
where ADDR_TYPE is an address type letter from the list below, and
ADDR_IDX_SPEC is a comma-separated list of address indexes or hyphen-
separated address index ranges.
ADDRESS TYPES
{n_at}
NOTES
Message signing operations are currently supported for Bitcoin and Bitcoin
code fork coins only.
Messages signed for Segwit-P2SH addresses cannot be verified directly using
the Bitcoin Core `verifymessage` RPC call, since such addresses are not hashes
of public keys. As a workaround for this limitation, this utility creates for
each Segwit-P2SH address a non-Segwit address with the same public key to be
used for verification purposes. This non-Segwit verifying address should then
be passed on to the verifying party together with the signature. The verifying
party may then use a tool of their choice (e.g. `mmgen-tool addr2pubhash`) to
assure themselves that the verifying address and Segwit address share the same
public key.
Unfortunately, the aforementioned limitation applies to Segwit-P2PKH (Bech32)
addresses as well, despite the fact that Bech32 addresses are hashes of public
keys (we consider this an implementation shortcoming of `verifymessage`).
Therefore, the above procedure must be followed to verify messages for Bech32
addresses too. `mmgen-tool addr2pubhash` or `bitcoin-cli validateaddress`
may then be used to demonstrate that the two addresses share the same public
key.
EXAMPLES
Create a raw message file for the specified message and specified addresses,
where DEADBEEF is the Seed ID of the users default wallet and BEEFCAFE one
of its subwallets:
$ mmgen-msg create '16/3/2022 Earthquake strikes Fukushima coast' DEADBEEF:B:1-3,10,98 BEEFCAFE:S:3,9
Sign the raw message file created by the previous step:
$ mmgen-msg sign <raw message file>
Sign the raw message file using an explicitly supplied wallet:
$ mmgen-msg sign <raw message file> DEADBEEF.bip39
Verify and display all signatures in the signed message file:
$ mmgen-msg verify <signed message file>
Verify and display a single signature in the signed message file:
$ mmgen-msg verify <signed message file> DEADBEEF:B:98
"""
},
'code': {
'notes': lambda help_notes,s: s.format(
n_at=help_notes('address_types'),
)
}
}
cmd_args = opts.init(opts_data)
if len(cmd_args) < 2:
opts.usage()
op = cmd_args.pop(0)
async def main():
if op == 'create':
if len(cmd_args) < 2:
opts.usage()
MsgOps.create( cmd_args[0], ' '.join(cmd_args[1:]) )
elif op == 'sign':
if len(cmd_args) < 1:
opts.usage()
await MsgOps.sign( cmd_args[0], cmd_args[1:] )
elif op == 'verify':
if len(cmd_args) not in (1,2):
opts.usage()
await MsgOps.verify( cmd_args[0], cmd_args[1] if len(cmd_args) == 2 else None )
else:
die(1,f'{op!r}: unrecognized operation')
run_session(main())

View file

@ -16,7 +16,7 @@ import os,importlib,json
from .globalvars import g from .globalvars import g
from .objmethods import MMGenObject,Hilite,InitErrors from .objmethods import MMGenObject,Hilite,InitErrors
from .util import msg,vmsg,die,suf,make_chksum_6,fmt_list,remove_dups from .util import msg,vmsg,die,suf,make_chksum_6,fmt_list,remove_dups
from .color import orange from .color import orange,grnbg
from .protocol import init_proto from .protocol import init_proto
from .fileutil import get_data_from_file,write_data_to_file from .fileutil import get_data_from_file,write_data_to_file
from .addr import MMGenID from .addr import MMGenID
@ -116,9 +116,8 @@ class coin_msg:
if data: if data:
self.__dict__ = data self.__dict__ = data
return return
elif infile:
self.infile = infile
self.infile = infile
self.data = get_data_from_file( self.data = get_data_from_file(
infile = self.infile, infile = self.infile,
desc = f'{self.desc} data' ) desc = f'{self.desc} data' )
@ -168,7 +167,7 @@ class coin_msg:
yield res yield res
disp_data = { disp_data = {
'message': ('Message', lambda v: repr(v) ), 'message': ('Message', lambda v: grnbg(v) ),
'network': ('Network', lambda v: v.replace('_',' ').upper() ), 'network': ('Network', lambda v: v.replace('_',' ').upper() ),
'addrlists': ('Address Ranges', lambda v: fmt_list(v,fmt='bare') ), 'addrlists': ('Address Ranges', lambda v: fmt_list(v,fmt='bare') ),
} }
@ -255,12 +254,18 @@ class coin_msg:
class signed_online(signed): class signed_online(signed):
async def verify(self): async def verify(self,addr=None,summary=False):
from .rpc import rpc_init from .rpc import rpc_init
self.rpc = await rpc_init(self.proto) self.rpc = await rpc_init(self.proto)
for k,v in self.sigs.items(): if addr:
mmaddr = MMGenID(self.proto,addr)
sigs = {k:v for k,v in self.sigs.items() if k == mmaddr}
else:
sigs = self.sigs
for k,v in sigs.items():
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'],
@ -268,7 +273,8 @@ class coin_msg:
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"]})')
vmsg('{} signature{} verified'.format( len(self.sigs), suf(self.sigs) )) if summary:
msg('{} signature{} verified'.format( len(sigs), suf(sigs) ))
def _get_obj(clsname,coin=None,network='mainnet',infile=None,data=None,*args,**kwargs): def _get_obj(clsname,coin=None,network='mainnet',infile=None,data=None,*args,**kwargs):

View file

@ -66,6 +66,7 @@ scripts =
cmds/mmgen-addrimport cmds/mmgen-addrimport
cmds/mmgen-autosign cmds/mmgen-autosign
cmds/mmgen-keygen cmds/mmgen-keygen
cmds/mmgen-msg
cmds/mmgen-passchg cmds/mmgen-passchg
cmds/mmgen-passgen cmds/mmgen-passgen
cmds/mmgen-regtest cmds/mmgen-regtest

View file

@ -257,7 +257,16 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
('alice_twview_date_time', 'twview (age_fmt=date_time)'), ('alice_twview_date_time', 'twview (age_fmt=date_time)'),
('alice_txcreate_info', 'txcreate -i'), ('alice_txcreate_info', 'txcreate -i'),
('stop', 'stopping regtest daemon'), ('bob_msgcreate', 'creating a message file for signing'),
('bob_msgsign', 'signing the message file (default wallet)'),
('bob_walletconv_words', 'creating an MMGen mnemonic wallet'),
('bob_subwalletgen_bip39', 'creating a BIP39 mnemonic subwallet'),
('bob_msgsign_userwallet', 'signing the message file (user-specified wallet)'),
('bob_msgsign_userwallets', 'signing the message file (user-specified wallets)'),
('bob_msgverify', 'verifying the message file (all addresses)'),
('bob_msgverify_single', 'verifying the message file (single address)'),
('stop', 'stopping regtest daemon'),
) )
def __init__(self,trunner,cfgs,spawn): def __init__(self,trunner,cfgs,spawn):
@ -284,6 +293,8 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
os.environ['MMGEN_BOGUS_SEND'] = '' os.environ['MMGEN_BOGUS_SEND'] = ''
self.write_to_tmpfile('wallet_password',rt_pw) self.write_to_tmpfile('wallet_password',rt_pw)
self.dfl_mmtype = 'C' if self.proto.coin == 'BCH' else 'B'
def __del__(self): def __del__(self):
os.environ['MMGEN_BOGUS_SEND'] = '1' os.environ['MMGEN_BOGUS_SEND'] = '1'
@ -1022,6 +1033,68 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
regex=True ) regex=True )
return t return t
def bob_msgcreate(self):
sid1 = self._user_sid('bob')
sid2 = self._get_user_subsid('bob','29L')
return self.spawn(
'mmgen-msg', [
'--bob',
f'--outdir={self.tmpdir}',
'create',
'16/3/2022 Earthquake strikes Fukushima coast',
f'{sid1}:{self.dfl_mmtype}:1-4',
f'{sid2}:C:3-7,87,98'
])
def bob_msgsign(self,wallets=[]):
fn = get_file_with_ext(self.tmpdir,'rawmsg.json')
t = self.spawn(
'mmgen-msg', [
'--bob',
f'--outdir={self.tmpdir}',
'sign',
fn
] + wallets )
if not wallets:
t.passphrase(dfl_wcls.desc,rt_pw)
return t
def bob_walletconv_words(self):
t = self.spawn(
'mmgen-walletconv', [ '--bob', f'--outdir={self.tmpdir}', '--out-fmt=words' ] )
t.passphrase(dfl_wcls.desc,rt_pw)
t.written_to_file('data')
return t
def bob_subwalletgen_bip39(self):
t = self.spawn(
'mmgen-subwalletgen', [ '--bob', f'--outdir={self.tmpdir}', '--out-fmt=bip39', '29L' ] )
t.passphrase(dfl_wcls.desc,rt_pw)
t.written_to_file('data')
return t
def bob_msgsign_userwallet(self):
fn1 = get_file_with_ext(self.tmpdir,'mmwords')
return self.bob_msgsign([fn1])
def bob_msgsign_userwallets(self):
fn1 = get_file_with_ext(self.tmpdir,'mmwords')
fn2 = get_file_with_ext(self.tmpdir,'bip39')
return self.bob_msgsign([fn2,fn1])
def bob_msgverify(self,addr=None):
return self.spawn(
'mmgen-msg', [
'--bob',
f'--outdir={self.tmpdir}',
'verify',
get_file_with_ext(self.tmpdir,'sigmsg.json'),
] + ([addr] if addr else []) )
def bob_msgverify_single(self):
sid = self._user_sid('bob')
return self.bob_msgverify(addr=f'{sid}:{self.dfl_mmtype}:1')
def stop(self): def stop(self):
if opt.no_daemon_stop: if opt.no_daemon_stop:
self.spawn('',msg_only=True) self.spawn('',msg_only=True)

View file

@ -68,7 +68,10 @@ async def run_test(network_id):
msg(m.format('A091ABAA:111')) msg(m.format('A091ABAA:111'))
pumsg('\nTesting verification:\n') pumsg('\nTesting verification:\n')
await m.verify() await m.verify(summary=opt.verbose)
pumsg('\nTesting single address verification:\n')
await m.verify('A091ABAA:111',summary=opt.verbose)
stop_test_daemons(network_id) stop_test_daemons(network_id)