main_msg.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. mmgen-msg: Message signing operations for the MMGen suite
  12. """
  13. import sys
  14. from .cfg import Config
  15. from .base_obj import AsyncInit
  16. from .util import msg, suf, async_run, die
  17. from .msg import (
  18. NewMsg,
  19. UnsignedMsg,
  20. SignedMsg,
  21. SignedOnlineMsg,
  22. ExportedMsgSigs,
  23. )
  24. class MsgOps:
  25. ops = ('create', 'sign', 'verify')
  26. class create:
  27. def __init__(self, msg, addr_specs):
  28. NewMsg(
  29. cfg = cfg,
  30. coin = cfg._proto.coin,
  31. network = cfg._proto.network,
  32. message = msg,
  33. addrlists = addr_specs,
  34. msghash_type = cfg.msghash_type
  35. ).write_to_file(ask_overwrite=False)
  36. class sign(metaclass=AsyncInit):
  37. async def __init__(self, msgfile, wallet_files):
  38. m = UnsignedMsg(cfg, infile=msgfile)
  39. if not wallet_files:
  40. from .filename import find_file_in_dir
  41. from .wallet import get_wallet_cls
  42. wallet_files = [find_file_in_dir(get_wallet_cls('mmgen'), cfg.data_dir)]
  43. await m.sign(wallet_files)
  44. m = SignedMsg(cfg, data=m.__dict__)
  45. m.write_to_file(ask_overwrite=False)
  46. if m.data.get('failed_sids'):
  47. sys.exit(1)
  48. class verify(sign):
  49. async def __init__(self, msgfile, *, addr=None):
  50. try:
  51. m = SignedOnlineMsg(cfg, infile=msgfile)
  52. except:
  53. m = ExportedMsgSigs(cfg, infile=msgfile)
  54. nSigs = await m.verify(addr=addr)
  55. summary = f'{nSigs} signature{suf(nSigs)} verified'
  56. if cfg.quiet:
  57. msg(summary)
  58. else:
  59. cfg._util.stdout_or_pager(m.format(addr) + '\n\n' + summary + '\n')
  60. if m.data.get('failed_sids'):
  61. sys.exit(1)
  62. class export(sign):
  63. async def __init__(self, msgfile, *, addr=None):
  64. from .fileutil import write_data_to_file
  65. write_data_to_file(
  66. cfg = cfg,
  67. outfile = 'signatures.json',
  68. data = SignedOnlineMsg(cfg, infile=msgfile).get_json_for_export(addr=addr),
  69. desc = 'signature data')
  70. opts_data = {
  71. 'text': {
  72. 'desc': 'Perform message signing operations for MMGen addresses',
  73. 'usage2': [
  74. '[opts] create MESSAGE_TEXT ADDRESS_SPEC [...]',
  75. '[opts] sign MESSAGE_FILE [WALLET_FILE ...]',
  76. '[opts] verify MESSAGE_FILE [MMGen ID]',
  77. '[opts] verify <exported JSON dump file> [address]',
  78. '[opts] export MESSAGE_FILE [MMGen ID]',
  79. ],
  80. 'options': """
  81. -h, --help Print this help message
  82. --, --longhelp Print help message for long (global) options
  83. -d, --outdir=d Output file to directory 'd' instead of working dir
  84. -t, --msghash-type=T Specify the message hash type. Supported values:
  85. 'eth_sign' (ETH default), 'raw' (non-ETH default)
  86. -q, --quiet Produce quieter output
  87. """,
  88. 'notes': """
  89. SUPPORTED OPERATIONS
  90. create - create a raw MMGen message file with specified message text for
  91. signing for addresses specified by ADDRESS_SPEC (see ADDRESS
  92. SPECIFIER below)
  93. sign - perform signing operation on an unsigned MMGen message file
  94. verify - verify and display the contents of a signed MMGen message file
  95. export - dump signed MMGen message file to ‘signatures.json’, including only
  96. data relevant for a third-party verifier
  97. ADDRESS SPECIFIER
  98. The `create` operation takes one or more ADDRESS_SPEC arguments with the
  99. following format:
  100. SEED_ID:ADDRTYPE_CODE:ADDR_IDX_SPEC
  101. where ADDRTYPE_CODE is a one-letter address type code from the list below, and
  102. ADDR_IDX_SPEC is a comma-separated list of address indexes or hyphen-separated
  103. address index ranges.
  104. {n_at}
  105. NOTES
  106. Message signing operations are supported for Bitcoin, Ethereum and code forks
  107. thereof.
  108. By default, Ethereum messages are prefixed before hashing in conformity with
  109. the standard defined by the Geth ‘eth_sign’ JSON-RPC call. This behavior may
  110. be overridden with the --msghash-type option.
  111. Messages signed for Segwit-P2SH addresses cannot be verified directly using
  112. the Bitcoin Core `verifymessage` RPC call, since such addresses are not hashes
  113. of public keys. As a workaround for this limitation, this utility creates for
  114. each Segwit-P2SH address a non-Segwit address with the same public key to be
  115. used for verification purposes. This non-Segwit verifying address should then
  116. be passed on to the verifying party together with the signature. The verifying
  117. party may then use a tool of their choice (e.g. `mmgen-tool addr2pubhash`) to
  118. assure themselves that the verifying address and Segwit address share the same
  119. public key.
  120. Unfortunately, the aforementioned limitation applies to Segwit-P2PKH (Bech32)
  121. addresses as well, despite the fact that Bech32 addresses are hashes of public
  122. keys (we consider this an implementation shortcoming of `verifymessage`).
  123. Therefore, the above procedure must be followed to verify messages for Bech32
  124. addresses too. `mmgen-tool addr2pubhash` or `bitcoin-cli validateaddress`
  125. may then be used to demonstrate that the two addresses share the same public
  126. key.
  127. EXAMPLES
  128. Create a raw message file for the specified message and specified addresses,
  129. where DEADBEEF is the Seed ID of the user’s default wallet and BEEFCAFE one
  130. of its subwallets:
  131. $ mmgen-msg create '16/3/2022 Earthquake strikes Fukushima coast' DEADBEEF:B:1-3,10,98 BEEFCAFE:S:3,9
  132. Sign the raw message file created by the previous step:
  133. $ mmgen-msg sign <raw message file>
  134. Sign the raw message file using an explicitly supplied wallet:
  135. $ mmgen-msg sign <raw message file> DEADBEEF.bip39
  136. Verify and display all signatures in the signed message file:
  137. $ mmgen-msg verify <signed message file>
  138. Verify and display a single signature in the signed message file:
  139. $ mmgen-msg verify <signed message file> DEADBEEF:B:98
  140. Export data relevant for a third-party verifier to ‘signatures.json’:
  141. $ mmgen-msg export <signed message file>
  142. Same as above, but export only one signature:
  143. $ mmgen-msg export <signed message file> DEADBEEF:B:98
  144. Verify and display the exported JSON signature data:
  145. $ mmgen-msg verify signatures.json
  146. """
  147. },
  148. 'code': {
  149. 'notes': lambda help_notes, s: s.format(
  150. n_at = help_notes('address_types'),
  151. )
  152. }
  153. }
  154. cfg = Config(opts_data=opts_data, need_amt=False)
  155. cmd_args = cfg._args
  156. if len(cmd_args) < 2:
  157. cfg._usage()
  158. op = cmd_args.pop(0)
  159. arg1 = cmd_args.pop(0)
  160. if cfg.msghash_type and op != 'create':
  161. die(1, '--msghash-type option may only be used with the "create" command')
  162. async def main():
  163. match op:
  164. case 'create':
  165. if not cmd_args:
  166. cfg._usage()
  167. MsgOps.create(arg1, ' '.join(cmd_args))
  168. case 'sign':
  169. await MsgOps.sign(arg1, cmd_args[:])
  170. case 'verify' | 'export':
  171. if len(cmd_args) not in (0, 1):
  172. cfg._usage()
  173. await getattr(MsgOps, op)(arg1, addr=cmd_args[0] if cmd_args else None)
  174. case _:
  175. die(1, f'{op!r}: unrecognized operation')
  176. async_run(cfg, main)