233 lines
7 KiB
Python
Executable file
233 lines
7 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
#
|
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
|
# Copyright (C)2013-2024 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-wallet
|
|
# https://gitlab.com/mmgen/mmgen-wallet
|
|
|
|
"""
|
|
mmgen-msg: Message signing operations for the MMGen suite
|
|
"""
|
|
|
|
import sys
|
|
from .cfg import Config
|
|
from .base_obj import AsyncInit
|
|
from .util import msg,suf,async_run,die
|
|
from .msg import (
|
|
NewMsg,
|
|
UnsignedMsg,
|
|
SignedMsg,
|
|
SignedOnlineMsg,
|
|
ExportedMsgSigs,
|
|
)
|
|
|
|
class MsgOps:
|
|
ops = ('create','sign','verify')
|
|
|
|
class create:
|
|
|
|
def __init__(self,msg,addr_specs):
|
|
NewMsg(
|
|
cfg = cfg,
|
|
coin = cfg._proto.coin,
|
|
network = cfg._proto.network,
|
|
message = msg,
|
|
addrlists = addr_specs,
|
|
msghash_type = cfg.msghash_type
|
|
).write_to_file( ask_overwrite=False )
|
|
|
|
class sign(metaclass=AsyncInit):
|
|
|
|
async def __init__(self,msgfile,wallet_files):
|
|
|
|
m = UnsignedMsg( cfg, 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'), cfg.data_dir )]
|
|
|
|
await m.sign(wallet_files)
|
|
|
|
m = SignedMsg( cfg, data=m.__dict__ )
|
|
|
|
m.write_to_file( ask_overwrite=False )
|
|
|
|
if m.data.get('failed_sids'):
|
|
sys.exit(1)
|
|
|
|
class verify(sign):
|
|
|
|
async def __init__(self,msgfile,addr=None):
|
|
try:
|
|
m = SignedOnlineMsg( cfg, infile=msgfile )
|
|
except:
|
|
m = ExportedMsgSigs( cfg, infile=msgfile )
|
|
|
|
nSigs = await m.verify(addr)
|
|
|
|
summary = f'{nSigs} signature{suf(nSigs)} verified'
|
|
|
|
if cfg.quiet:
|
|
msg(summary)
|
|
else:
|
|
cfg._util.stdout_or_pager(m.format(addr) + '\n\n' + summary + '\n')
|
|
|
|
if m.data.get('failed_sids'):
|
|
sys.exit(1)
|
|
|
|
class export(sign):
|
|
|
|
async def __init__(self,msgfile,addr=None):
|
|
|
|
from .fileutil import write_data_to_file
|
|
write_data_to_file(
|
|
cfg = cfg,
|
|
outfile = 'signatures.json',
|
|
data = SignedOnlineMsg( cfg, infile=msgfile ).get_json_for_export( addr ),
|
|
desc = 'signature data' )
|
|
|
|
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 [MMGen ID]',
|
|
'[opts] verify <exported JSON dump file> [address]',
|
|
'[opts] export MESSAGE_FILE [MMGen ID]',
|
|
],
|
|
'options': """
|
|
-h, --help Print this help message
|
|
--, --longhelp Print help message for long (global) 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': """
|
|
|
|
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
|
|
export - dump signed MMGen message file to ‘signatures.json’, including only
|
|
data relevant for a third-party verifier
|
|
|
|
|
|
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 supported for Bitcoin, Ethereum and code forks
|
|
thereof.
|
|
|
|
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
|
|
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
|
|
|
|
Export data relevant for a third-party verifier to ‘signatures.json’:
|
|
$ mmgen-msg export <signed message file>
|
|
|
|
Same as above, but export only one signature:
|
|
$ mmgen-msg export <signed message file> DEADBEEF:B:98
|
|
|
|
Verify and display the exported JSON signature data:
|
|
$ mmgen-msg verify signatures.json
|
|
"""
|
|
},
|
|
'code': {
|
|
'notes': lambda help_notes,s: s.format(
|
|
n_at=help_notes('address_types'),
|
|
)
|
|
}
|
|
}
|
|
|
|
cfg = Config( opts_data=opts_data, need_amt=False )
|
|
|
|
cmd_args = cfg._args
|
|
|
|
if len(cmd_args) < 2:
|
|
cfg._usage()
|
|
|
|
op = cmd_args.pop(0)
|
|
|
|
if cfg.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:
|
|
cfg._usage()
|
|
MsgOps.create( cmd_args[0], ' '.join(cmd_args[1:]) )
|
|
elif op == 'sign':
|
|
if len(cmd_args) < 1:
|
|
cfg._usage()
|
|
await MsgOps.sign( cmd_args[0], cmd_args[1:] )
|
|
elif op in ('verify','export'):
|
|
if len(cmd_args) not in (1,2):
|
|
cfg._usage()
|
|
await getattr(MsgOps,op)( cmd_args[0], cmd_args[1] if len(cmd_args) == 2 else None )
|
|
else:
|
|
die(1,f'{op!r}: unrecognized operation')
|
|
|
|
async_run(main())
|