diff --git a/cmds/mmgen-msg b/cmds/mmgen-msg new file mode 100755 index 00000000..ed380cec --- /dev/null +++ b/cmds/mmgen-msg @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# 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") diff --git a/mmgen/data/release_date b/mmgen/data/release_date index a23b2fd9..fbf89c0d 100644 --- a/mmgen/data/release_date +++ b/mmgen/data/release_date @@ -1 +1 @@ -February 2022 +March 2022 diff --git a/mmgen/data/version b/mmgen/data/version index 5234c8ba..3d89af9e 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.dev21 +13.1.dev22 diff --git a/mmgen/main_msg.py b/mmgen/main_msg.py new file mode 100755 index 00000000..1cc3f41c --- /dev/null +++ b/mmgen/main_msg.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# 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 user’s 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 + +Sign the raw message file using an explicitly supplied wallet: +$ mmgen-msg sign DEADBEEF.bip39 + +Verify and display all signatures in the signed message file: +$ mmgen-msg verify + +Verify and display a single signature in the signed message file: +$ mmgen-msg verify 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()) diff --git a/mmgen/msg.py b/mmgen/msg.py index 4400e062..050d4996 100755 --- a/mmgen/msg.py +++ b/mmgen/msg.py @@ -16,7 +16,7 @@ import os,importlib,json from .globalvars import g from .objmethods import MMGenObject,Hilite,InitErrors 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 .fileutil import get_data_from_file,write_data_to_file from .addr import MMGenID @@ -116,9 +116,8 @@ class coin_msg: if data: self.__dict__ = data return - elif infile: - self.infile = infile + self.infile = infile self.data = get_data_from_file( infile = self.infile, desc = f'{self.desc} data' ) @@ -168,7 +167,7 @@ class coin_msg: yield res disp_data = { - 'message': ('Message', lambda v: repr(v) ), + 'message': ('Message', lambda v: grnbg(v) ), 'network': ('Network', lambda v: v.replace('_',' ').upper() ), 'addrlists': ('Address Ranges', lambda v: fmt_list(v,fmt='bare') ), } @@ -255,12 +254,18 @@ class coin_msg: class signed_online(signed): - async def verify(self): + async def verify(self,addr=None,summary=False): from .rpc import rpc_init 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( addr = v.get('addr_p2pkh') or v['addr'], sig = v['sig'], @@ -268,7 +273,8 @@ class coin_msg: if not ret: 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): diff --git a/setup.cfg b/setup.cfg index 02023bf0..b6bd5176 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,6 +66,7 @@ scripts = cmds/mmgen-addrimport cmds/mmgen-autosign cmds/mmgen-keygen + cmds/mmgen-msg cmds/mmgen-passchg cmds/mmgen-passgen cmds/mmgen-regtest diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 90cf211f..54f42532 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -257,7 +257,16 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): ('alice_twview_date_time', 'twview (age_fmt=date_time)'), ('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): @@ -284,6 +293,8 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): os.environ['MMGEN_BOGUS_SEND'] = '' self.write_to_tmpfile('wallet_password',rt_pw) + self.dfl_mmtype = 'C' if self.proto.coin == 'BCH' else 'B' + def __del__(self): os.environ['MMGEN_BOGUS_SEND'] = '1' @@ -1022,6 +1033,68 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): regex=True ) 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): if opt.no_daemon_stop: self.spawn('',msg_only=True) diff --git a/test/unit_tests_d/ut_msg.py b/test/unit_tests_d/ut_msg.py index 6eb5d6e5..83952895 100755 --- a/test/unit_tests_d/ut_msg.py +++ b/test/unit_tests_d/ut_msg.py @@ -68,7 +68,10 @@ async def run_test(network_id): msg(m.format('A091ABAA:111')) 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)