main_msg.py 7.1 KB

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