message signing: user-level support
Usage information:
$ mmgen-msg --help
Testing:
$ test/test.py -e regtest
This commit is contained in:
parent
69cff97201
commit
e5cf3b6ec8
8 changed files with 283 additions and 11 deletions
16
cmds/mmgen-msg
Executable file
16
cmds/mmgen-msg
Executable 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")
|
||||
|
|
@ -1 +1 @@
|
|||
February 2022
|
||||
March 2022
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
13.1.dev21
|
||||
13.1.dev22
|
||||
|
|
|
|||
173
mmgen/main_msg.py
Executable file
173
mmgen/main_msg.py
Executable 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 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())
|
||||
20
mmgen/msg.py
20
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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue