Browse Source

message signing: user-level support

Usage information:

    $ mmgen-msg --help

Testing:

    $ test/test.py -e regtest
The MMGen Project 3 years ago
parent
commit
e5cf3b6ec
8 changed files with 283 additions and 11 deletions
  1. 16 0
      cmds/mmgen-msg
  2. 1 1
      mmgen/data/release_date
  3. 1 1
      mmgen/data/version
  4. 173 0
      mmgen/main_msg.py
  5. 13 7
      mmgen/msg.py
  6. 1 0
      setup.cfg
  7. 74 1
      test/test_py_d/ts_regtest.py
  8. 4 1
      test/unit_tests_d/ut_msg.py

+ 16 - 0
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 <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")

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-February 2022
+March 2022

+ 1 - 1
mmgen/data/version

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

+ 173 - 0
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 <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 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 <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())

+ 13 - 7
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):
 

+ 1 - 0
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

+ 74 - 1
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)

+ 4 - 1
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)