mmgen-autosign 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
  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,subprocess,time,signal
  22. from stat import *
  23. from mmgen.common import *
  24. prog_name = os.path.basename(sys.argv[0])
  25. opts_data = lambda: {
  26. 'desc': 'Auto-sign MMGen transactions',
  27. 'usage':'[opts] [command]',
  28. 'options': """
  29. -h, --help Print this help message
  30. --, --longhelp Print help message for long options (common options)
  31. -l, --led Use status LED to signal standby, busy and error
  32. -s, --stealth-led Stealth LED mode - signal busy and error only, and only
  33. after successful authorization.
  34. -v, --verbose Produce more verbose output
  35. """,
  36. 'notes': """
  37. COMMANDS
  38. gen_secret - generate the shared secret and copy to /dev/shm and USB stick
  39. wait - start in loop mode: wait - mount - sign - unmount - wait
  40. USAGE NOTES
  41. If invoked with no command, the program mounts the USB stick, signs any
  42. unsigned transactions, unmounts the USB stick and exits.
  43. If invoked with 'wait', the program waits in a loop, mounting, signing
  44. and unmounting every time the USB stick is inserted. The status LED
  45. indicates whether the program is busy or in standby mode, i.e. ready for
  46. USB stick insertion or removal.
  47. The USB stick must have a partition with the label MMGEN_TX and a user-
  48. writable directory '/tx', where unsigned MMGen transactions are placed.
  49. On the signing machine the directory /mnt/tx must exist and /etc/fstab must
  50. contain the following entry:
  51. LABEL='MMGEN_TX' /mnt/tx auto noauto,user 0 0
  52. The signing wallet or wallets must be in MMGen mnemonic format and
  53. present in /dev/shm. The wallet(s) can be created interactively with
  54. the following command:
  55. $ mmgen-walletconv -i words -o words -d /dev/shm
  56. {} checks that a shared secret is present on the USB stick
  57. before signing transactions. The shared secret is generated by invoking
  58. the command with 'gen_secret' with the USB stick inserted. For good
  59. security, it's advisable to re-generate a new shared secret before each
  60. signing session.
  61. Status LED functionality is supported on Orange Pi and Raspberry Pi boards.
  62. This program is a helper script and is not installed by default. You
  63. may copy it to your executable path if you wish, or just run it in place
  64. in the scripts directory of the MMGen repository root where it resides.
  65. """.format(prog_name)
  66. }
  67. cmd_args = opts.init(opts_data)
  68. if opt.stealth_led: opt.led = True
  69. mountpoint = '/mnt/tx'
  70. tx_dir = os.path.join(mountpoint,'tx')
  71. part_label = 'MMGEN_TX'
  72. shm_dir = '/dev/shm'
  73. secret_fn = 'txsign-secret'
  74. tn_arg = ['--testnet={}'.format(opt.testnet or '0')]
  75. coin_arg = ['--coin={}'.format(opt.coin or 'btc')]
  76. def check_daemon_running():
  77. cmd = ['mmgen-tool'] + tn_arg + coin_arg + ['getbalance']
  78. vmsg('Executing: {}'.format(' '.join(cmd)))
  79. try: subprocess.check_output(cmd)
  80. except: die(1,'Daemon not running')
  81. def get_wallet_files():
  82. wfs = [f for f in os.listdir(shm_dir) if f[-8:] == '.mmwords']
  83. if not wfs:
  84. die(1,'No mnemonic files present!')
  85. return [os.path.join(shm_dir,w) for w in wfs]
  86. def get_secret_in_dir(d,on_fail='die'):
  87. fn = os.path.join(d,secret_fn)
  88. try:
  89. with open(fn) as f: ret = f.read().rstrip()
  90. assert is_hex_str(ret) and len(ret) == 32
  91. return ret
  92. except:
  93. msg("Secret file '{}' non-existent, unreadable or in incorrect format!".format(fn))
  94. if on_fail == 'die': sys.exit(1)
  95. else: return None
  96. def do_mount():
  97. if not os.path.ismount(mountpoint):
  98. msg('Mounting '+mountpoint)
  99. subprocess.call(['mount',mountpoint])
  100. try:
  101. ds = os.stat(tx_dir)
  102. assert S_ISDIR(ds.st_mode)
  103. assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR
  104. except:
  105. die(1,'{} missing, or not read/writable by user!'.format(tx_dir))
  106. def do_umount():
  107. if os.path.ismount(mountpoint):
  108. subprocess.call(['sync'])
  109. msg('Unmounting '+mountpoint)
  110. subprocess.call(['umount',mountpoint])
  111. def sign():
  112. dirlist = os.listdir(tx_dir)
  113. raw = [f for f in dirlist if f[-6:] == '.rawtx']
  114. signed = [f[:-6] for f in dirlist if f[-6:] == '.sigtx']
  115. unsigned = [os.path.join(tx_dir,f) for f in raw if f[:-6] not in signed]
  116. if unsigned:
  117. cmd = ['mmgen-txsign','--yes','--outdir='+tx_dir] + tn_arg + coin_arg + unsigned + wfs
  118. vmsg('Executing: {}'.format(' '.join(cmd)))
  119. ret = subprocess.call(cmd)
  120. msg('')
  121. time.sleep(0.3)
  122. return (1,0)[ret==0]
  123. else:
  124. msg('No unsigned transactions')
  125. time.sleep(1)
  126. return 0
  127. def wipe_existing_secret_files():
  128. for d in (tx_dir,shm_dir):
  129. fn = os.path.join(d,secret_fn)
  130. try:
  131. os.stat(fn)
  132. except:
  133. pass
  134. else:
  135. msg('\nWiping existing key {}'.format(fn))
  136. subprocess.call(['wipe','-c',fn])
  137. def create_secret_files():
  138. from binascii import hexlify
  139. secret = hexlify(os.urandom(16))
  140. for d in (tx_dir,shm_dir):
  141. fn = os.path.join(d,secret_fn)
  142. desc = 'secret file in {}'.format(d)
  143. msg('Creating ' + desc)
  144. try:
  145. with open(fn,'w') as f: f.write(secret+'\n')
  146. os.chmod(fn,0400)
  147. msg('Wrote ' + desc)
  148. except:
  149. die(2,'Unable to write ' + desc)
  150. def do_create_secret_files():
  151. if not get_insert_status():
  152. die(2,'USB stick not present!')
  153. do_mount()
  154. wipe_existing_secret_files()
  155. create_secret_files()
  156. do_umount()
  157. def ev_sleep(secs):
  158. ev.wait(secs)
  159. return (False,True)[ev.isSet()]
  160. def do_led(on,off):
  161. if not on:
  162. with open(status_ctl,'w') as f: f.write('0\n')
  163. while True:
  164. if ev_sleep(3600): return
  165. while True:
  166. with open(status_ctl,'w') as f: f.write('255\n')
  167. if ev_sleep(on): return
  168. with open(status_ctl,'w') as f: f.write('0\n')
  169. if ev_sleep(off): return
  170. def set_led(cmd):
  171. if not opt.led: return
  172. timings = {
  173. 'off': ( 0, 0 ),
  174. 'standby': ( 2.2, 0.2 ),
  175. 'busy': ( 0.06, 0.06 ),
  176. 'error': ( 0.5, 0.5 )}[cmd]
  177. vmsg("Executing command '{}'".format(cmd))
  178. global led_thread
  179. if led_thread:
  180. ev.set(); led_thread.join(); ev.clear()
  181. led_thread = threading.Thread(target=do_led,name='LED loop',args=timings)
  182. led_thread.start()
  183. def do_sign():
  184. if not opt.stealth_led: set_led('busy')
  185. do_mount()
  186. ret = get_secret_in_dir(tx_dir,on_fail='return')
  187. if ret == secret:
  188. if opt.stealth_led: set_led('busy')
  189. exit_val = sign()
  190. do_umount()
  191. set_led(('standby','off','error')[bool(exit_val)*2 or bool(opt.stealth_led)])
  192. else:
  193. if ret != None:
  194. msg('Secret is incorrect!')
  195. do_umount()
  196. if not opt.stealth_led: set_led('error')
  197. def get_insert_status():
  198. try: os.stat(os.path.join('/dev/disk/by-label/',part_label))
  199. except: return False
  200. else: return True
  201. def do_loop():
  202. n,prev_status = 0,False
  203. if not opt.stealth_led:
  204. set_led('standby')
  205. while True:
  206. status = get_insert_status()
  207. if status and not prev_status:
  208. msg('Running command...')
  209. do_sign()
  210. prev_status = status
  211. if not n % 10:
  212. msg_r('\r{}\rWaiting'.format(' '*17))
  213. time.sleep(1)
  214. msg_r('.')
  215. n += 1
  216. def check_access(fn,desc='status LED control',init_val=None):
  217. try:
  218. with open(fn) as f: b = f.read().strip()
  219. with open(fn,'w') as f:
  220. f.write('{}\n'.format(init_val or b))
  221. return True
  222. except:
  223. m1 = "You do not have access to the {} file\n".format(desc)
  224. m2 = "To allow access, run 'sudo chmod 0666 {}'".format(fn)
  225. msg(m1+m2)
  226. return False
  227. def check_wipe_present():
  228. try:
  229. subprocess.Popen(['wipe','-v'],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
  230. except:
  231. die(2,"The 'wipe' utility must be installed before running this program")
  232. def at_exit(nl=True):
  233. if opt.led:
  234. set_led('off')
  235. ev.set()
  236. led_thread.join()
  237. if board == 'rpi':
  238. with open(trigger[board],'w') as f: f.write('mmc0\n')
  239. if nl: msg('')
  240. raise SystemExit
  241. def handler(a,b): at_exit()
  242. # main()
  243. if len(cmd_args) == 1 and cmd_args[0] == 'gen_secret':
  244. do_create_secret_files()
  245. sys.exit()
  246. if opt.led:
  247. import threading
  248. status = {
  249. 'opi': '/sys/class/leds/orangepi:red:status/brightness',
  250. 'rpi': '/sys/class/leds/led0/brightness'
  251. }
  252. trigger = {
  253. 'rpi': '/sys/class/leds/led0/trigger', # mmc,none
  254. }
  255. for k in ('opi','rpi'):
  256. try: os.stat(status[k])
  257. except: pass
  258. else:
  259. board = k
  260. status_ctl = status[board]
  261. break
  262. else:
  263. die(2,'Control files not found! LED option not supported')
  264. if not check_access(status_ctl) or (
  265. board == 'rpi' and not check_access(trigger[board],desc='LED trigger',init_val='none')):
  266. sys.exit(1)
  267. ev = threading.Event()
  268. led_thread = None
  269. if board == 'rpi':
  270. with open(trigger[board],'w') as f: f.write('none\n')
  271. check_wipe_present()
  272. wfs = get_wallet_files()
  273. secret = get_secret_in_dir(shm_dir,on_fail='die')
  274. check_daemon_running()
  275. signal.signal(signal.SIGTERM,handler)
  276. signal.signal(signal.SIGINT,handler)
  277. try:
  278. if len(cmd_args) == 1 and cmd_args[0] == 'wait':
  279. do_loop()
  280. elif len(cmd_args) == 0:
  281. do_sign()
  282. at_exit(nl=False)
  283. else:
  284. msg('Invalid invocation')
  285. except IOError:
  286. at_exit()
  287. except KeyboardInterrupt:
  288. at_exit()