main_autosign.py 11 KB

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