main_autosign.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. mmgen-autosign: Auto-sign MMGen transactions
  20. """
  21. import sys,os,time,signal,shutil
  22. from subprocess import run,PIPE,DEVNULL
  23. from stat import *
  24. mountpoint = '/mnt/tx'
  25. tx_dir = '/mnt/tx/tx'
  26. part_label = 'MMGEN_TX'
  27. wallet_dir = '/dev/shm/autosign'
  28. key_fn = 'autosign.key'
  29. mn_fmts = {
  30. 'mmgen': 'words',
  31. 'bip39': 'bip39',
  32. }
  33. mn_fmt_dfl = 'mmgen'
  34. from .common import *
  35. opts.UserOpts._set_ok += ('outdir','passwd_file')
  36. prog_name = os.path.basename(sys.argv[0])
  37. opts_data = {
  38. 'sets': [('stealth_led', True, 'led', True)],
  39. 'text': {
  40. 'desc': 'Auto-sign MMGen transactions',
  41. 'usage':'[opts] [command]',
  42. 'options': f"""
  43. -h, --help Print this help message
  44. --, --longhelp Print help message for long options (common options)
  45. -c, --coins=c Coins to sign for (comma-separated list)
  46. -I, --no-insert-check Don’t check for device insertion
  47. -l, --led Use status LED to signal standby, busy and error
  48. -m, --mountpoint=M Specify an alternate mountpoint 'M' (default: {mountpoint!r})
  49. -M, --mnemonic-fmt=F During setup, prompt for mnemonic seed phrase of format
  50. 'F' (choices: {fmt_list(mn_fmts,fmt='no_spc')}; default: {mn_fmt_dfl!r})
  51. -n, --no-summary Don’t print a transaction summary
  52. -s, --stealth-led Stealth LED mode - signal busy and error only, and only
  53. after successful authorization.
  54. -S, --full-summary Print a full summary of each signed transaction after
  55. each autosign run. The default list of non-MMGen outputs
  56. will not be printed.
  57. -q, --quiet Produce quieter output
  58. -v, --verbose Produce more verbose output
  59. """,
  60. 'notes': f"""
  61. COMMANDS
  62. gen_key - generate the wallet encryption key and copy it to '{tx_dir}'
  63. setup - generate the wallet encryption key and wallet
  64. wait - start in loop mode: wait-mount-sign-unmount-wait
  65. USAGE NOTES
  66. If invoked with no command, the program mounts a removable device containing
  67. MMGen transactions, signs any unsigned transactions, unmounts the removable
  68. device and exits.
  69. If invoked with 'wait', the program waits in a loop, mounting, signing and
  70. unmounting every time the removable device is inserted.
  71. On supported platforms (currently Orange Pi, Rock Pi and Raspberry Pi boards),
  72. the status LED indicates whether the program is busy or in standby mode, i.e.
  73. ready for device insertion or removal.
  74. The removable device must have a partition labeled MMGEN_TX with a user-
  75. writable directory '/tx', where unsigned MMGen transactions are placed.
  76. On the signing machine the mount point {mountpoint!r} must exist and /etc/fstab
  77. must contain the following entry:
  78. LABEL='MMGEN_TX' /mnt/tx auto noauto,user 0 0
  79. Transactions are signed with a wallet on the signing machine (in the directory
  80. {wallet_dir!r}) encrypted with a 64-character hexadecimal password on the
  81. removable device.
  82. The password and wallet can be created in one operation by invoking the
  83. command with 'setup' with the removable device inserted. The user will be
  84. prompted for a seed mnemonic.
  85. Alternatively, the password and wallet can be created separately by first
  86. invoking the command with 'gen_key' and then creating and encrypting the
  87. wallet using the -P (--passwd-file) option:
  88. $ mmgen-walletconv -r0 -q -iwords -d{wallet_dir} -p1 -P{tx_dir}/{key_fn} -Llabel
  89. Note that the hash preset must be '1'. Multiple wallets are permissible.
  90. For good security, it's advisable to re-generate a new wallet and key for
  91. each signing session.
  92. This command is currently available only on Linux-based platforms.
  93. """
  94. }
  95. }
  96. cmd_args = opts.init(
  97. opts_data,
  98. add_opts = ['mmgen_keys_from_file','hidden_incog_input_params'],
  99. init_opts = {
  100. 'quiet': True,
  101. 'out_fmt': 'wallet',
  102. 'usr_randchars': 0,
  103. 'hash_preset': '1',
  104. 'label': 'Autosign Wallet',
  105. })
  106. exit_if_mswin('autosigning')
  107. if opt.mnemonic_fmt:
  108. if opt.mnemonic_fmt not in mn_fmts:
  109. die(1,'{!r}: invalid mnemonic format (must be one of: {})'.format(
  110. opt.mnemonic_fmt,
  111. fmt_list(mn_fmts,fmt='no_spc') ))
  112. import mmgen.tx
  113. from .wallet import Wallet
  114. from .txsign import txsign
  115. from .protocol import init_proto
  116. from .rpc import rpc_init
  117. if g.test_suite:
  118. from .daemon import CoinDaemon
  119. if opt.mountpoint:
  120. mountpoint = opt.mountpoint
  121. opt.outdir = tx_dir = os.path.join(mountpoint,'tx')
  122. opt.passwd_file = os.path.join(tx_dir,key_fn)
  123. async def check_daemons_running():
  124. if opt.coin:
  125. die(1,'--coin option not supported with this command. Use --coins instead')
  126. if opt.coins:
  127. coins = opt.coins.upper().split(',')
  128. else:
  129. ymsg('Warning: no coins specified, defaulting to BTC')
  130. coins = ['BTC']
  131. for coin in coins:
  132. proto = init_proto(coin,testnet=g.testnet)
  133. if proto.sign_mode == 'daemon':
  134. vmsg(f'Checking {coin} daemon')
  135. try:
  136. await rpc_init(proto)
  137. except SocketError as e:
  138. ydie(1,f'{coin} daemon not running or not listening on port {proto.rpc_port}')
  139. def get_wallet_files():
  140. try:
  141. dlist = os.listdir(wallet_dir)
  142. except:
  143. die(1,f"Cannot open wallet directory {wallet_dir!r}. Did you run 'mmgen-autosign setup'?")
  144. fns = [x for x in dlist if x.endswith('.mmdat')]
  145. if fns:
  146. return [os.path.join(wallet_dir,w) for w in fns]
  147. else:
  148. die(1,'No wallet files present!')
  149. def do_mount():
  150. if not os.path.ismount(mountpoint):
  151. if run(['mount',mountpoint],stderr=DEVNULL,stdout=DEVNULL).returncode == 0:
  152. msg(f'Mounting {mountpoint}')
  153. try:
  154. ds = os.stat(tx_dir)
  155. assert S_ISDIR(ds.st_mode), f'{tx_dir!r} is not a directory!'
  156. assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR, f'{tx_dir!r} is not read/write for this user!'
  157. except:
  158. die(1,f'{tx_dir!r} missing or not read/writable by user!')
  159. def do_umount():
  160. if os.path.ismount(mountpoint):
  161. run(['sync'],check=True)
  162. msg(f'Unmounting {mountpoint}')
  163. run(['umount',mountpoint],check=True)
  164. async def sign_tx_file(txfile):
  165. from .tx import MMGenTX
  166. try:
  167. tx1 = MMGenTX.Unsigned(filename=txfile)
  168. if tx1.proto.sign_mode == 'daemon':
  169. tx1.rpc = await rpc_init(tx1.proto)
  170. tx2 = await txsign(tx1,wfs,None,None)
  171. if tx2:
  172. tx2.write_to_file(ask_write=False)
  173. return tx2
  174. else:
  175. return False
  176. except Exception as e:
  177. ymsg(f'An error occurred with transaction {txfile!r}:\n {e!s}')
  178. return False
  179. except:
  180. ymsg(f'An error occurred with transaction {txfile!r}')
  181. return False
  182. async def sign():
  183. raw,signed = [tuple(f[:-6] for f in os.listdir(tx_dir) if f.endswith(ext)) for ext in ('.rawtx','.sigtx')]
  184. unsigned = [os.path.join(tx_dir,f+'.rawtx') for f in raw if f not in signed]
  185. if unsigned:
  186. signed_txs,fails = [],[]
  187. for txfile in unsigned:
  188. ret = await sign_tx_file(txfile)
  189. if ret:
  190. signed_txs.append(ret)
  191. else:
  192. fails.append(txfile)
  193. qmsg('')
  194. time.sleep(0.3)
  195. msg(f'{len(signed_txs)} transaction{suf(signed_txs)} signed')
  196. if fails:
  197. rmsg(f'{len(fails)} transaction{suf(fails)} failed to sign')
  198. if signed_txs and not opt.no_summary:
  199. print_summary(signed_txs)
  200. if fails:
  201. rmsg('\nFailed transactions:')
  202. rmsg(' ' + '\n '.join(sorted(fails)) + '\n')
  203. return False if fails else True
  204. else:
  205. msg('No unsigned transactions')
  206. time.sleep(1)
  207. return True
  208. def decrypt_wallets():
  209. msg(f'Unlocking wallet{suf(wfs)} with key from {opt.passwd_file!r}')
  210. fails = 0
  211. for wf in wfs:
  212. try:
  213. Wallet(wf,ignore_in_fmt=True)
  214. except SystemExit as e:
  215. if e.code != 0:
  216. fails += 1
  217. return False if fails else True
  218. def print_summary(signed_txs):
  219. if opt.full_summary:
  220. bmsg('\nAutosign summary:\n')
  221. def gen():
  222. for tx in signed_txs:
  223. yield tx.format_view(terse=True)
  224. msg_r(''.join(gen()))
  225. return
  226. def gen():
  227. for tx in signed_txs:
  228. non_mmgen = [o for o in tx.outputs if not o.mmid]
  229. if non_mmgen:
  230. yield (tx,non_mmgen)
  231. body = list(gen())
  232. if body:
  233. bmsg('\nAutosign summary:')
  234. fs = '{} {} {}'
  235. t_wid,a_wid = 6,44
  236. def gen():
  237. yield fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount')
  238. yield fs.format('-'*t_wid, '-'*a_wid, '-'*7)
  239. for tx,non_mmgen in body:
  240. for nm in non_mmgen:
  241. yield fs.format(
  242. tx.txid.fmt(width=t_wid,color=True) if nm is non_mmgen[0] else ' '*t_wid,
  243. nm.addr.fmt(width=a_wid,color=True),
  244. nm.amt.hl() + ' ' + yellow(tx.coin))
  245. msg('\n'.join(gen()))
  246. else:
  247. msg('No non-MMGen outputs')
  248. async def do_sign():
  249. if not opt.stealth_led:
  250. led.set('busy')
  251. do_mount()
  252. key_ok = decrypt_wallets()
  253. if key_ok:
  254. if opt.stealth_led:
  255. led.set('busy')
  256. ret = await sign()
  257. do_umount()
  258. led.set(('standby','off','error')[(not ret)*2 or bool(opt.stealth_led)])
  259. return ret
  260. else:
  261. msg('Password is incorrect!')
  262. do_umount()
  263. if not opt.stealth_led:
  264. led.set('error')
  265. return False
  266. def wipe_existing_key():
  267. fn = os.path.join(tx_dir,key_fn)
  268. try: os.stat(fn)
  269. except: pass
  270. else:
  271. msg(f'\nShredding existing key {fn!r}')
  272. shred_file( fn, verbose=opt.verbose )
  273. def create_key():
  274. kdata = os.urandom(32).hex()
  275. fn = os.path.join(tx_dir,key_fn)
  276. desc = f'key file {fn!r}'
  277. msg('Creating ' + desc)
  278. try:
  279. open(fn,'w').write(kdata+'\n')
  280. os.chmod(fn,0o400)
  281. msg('Wrote ' + desc)
  282. except:
  283. die(2,'Unable to write ' + desc)
  284. def gen_key(no_unmount=False):
  285. create_wallet_dir()
  286. if not get_insert_status():
  287. die(1,'Removable device not present!')
  288. do_mount()
  289. wipe_existing_key()
  290. create_key()
  291. if not no_unmount:
  292. do_umount()
  293. def remove_wallet_dir():
  294. msg(f'Deleting {wallet_dir!r}')
  295. try: shutil.rmtree(wallet_dir)
  296. except: pass
  297. def create_wallet_dir():
  298. try: os.mkdir(wallet_dir)
  299. except: pass
  300. try: os.stat(wallet_dir)
  301. except: die(2,f'Unable to create wallet directory {wallet_dir!r}')
  302. def setup():
  303. remove_wallet_dir()
  304. gen_key(no_unmount=True)
  305. ss_in = Wallet(in_fmt=mn_fmts[opt.mnemonic_fmt or mn_fmt_dfl])
  306. ss_out = Wallet(ss=ss_in)
  307. ss_out.write_to_file(desc='autosign wallet',outdir=wallet_dir)
  308. def get_insert_status():
  309. if opt.no_insert_check:
  310. return True
  311. try: os.stat(os.path.join('/dev/disk/by-label',part_label))
  312. except: return False
  313. else: return True
  314. async def do_loop():
  315. n,prev_status = 0,False
  316. if not opt.stealth_led:
  317. led.set('standby')
  318. while True:
  319. status = get_insert_status()
  320. if status and not prev_status:
  321. msg('Device insertion detected')
  322. await do_sign()
  323. prev_status = status
  324. if not n % 10:
  325. msg_r(f"\r{' '*17}\rWaiting")
  326. sys.stderr.flush()
  327. time.sleep(1)
  328. msg_r('.')
  329. n += 1
  330. if len(cmd_args) not in (0,1):
  331. opts.usage()
  332. if len(cmd_args) == 1:
  333. cmd = cmd_args[0]
  334. if cmd in ('gen_key','setup'):
  335. globals()[cmd]()
  336. sys.exit(0)
  337. elif cmd != 'wait':
  338. die(1,f'{cmd!r}: unrecognized command')
  339. wfs = get_wallet_files()
  340. def at_exit(exit_val,message='\nCleaning up...'):
  341. if message:
  342. msg(message)
  343. led.stop()
  344. sys.exit(exit_val)
  345. def handler(a,b):
  346. at_exit(1)
  347. signal.signal(signal.SIGTERM,handler)
  348. signal.signal(signal.SIGINT,handler)
  349. from .led import LEDControl
  350. led = LEDControl(enabled=opt.led,simulate=g.test_suite and not os.getenv('MMGEN_TEST_SUITE_AUTOSIGN_LIVE'))
  351. led.set('off')
  352. async def main():
  353. await check_daemons_running()
  354. if len(cmd_args) == 0:
  355. ret = await do_sign()
  356. at_exit(int(not ret),message='')
  357. elif cmd_args[0] == 'wait':
  358. await do_loop()
  359. run_session(main())