diff --git a/MANIFEST.in b/MANIFEST.in index c8ac5f80..ce214517 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,5 +8,6 @@ include scripts/compute-file-chksum.py include scripts/deinstall.sh include scripts/tx-old2new.py include scripts/test-release.sh +include scripts/mmgen-autosign prune test/ref/__db* diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 60058eb3..3131dc93 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -38,7 +38,7 @@ class g(object): sys.exit(ev) # Variables - these might be altered at runtime: - version = '0.9.4a' + version = '0.9.499' release_date = 'October 2017' proj_name = 'MMGen' diff --git a/scripts/mmgen-autosign b/scripts/mmgen-autosign new file mode 100755 index 00000000..742427d8 --- /dev/null +++ b/scripts/mmgen-autosign @@ -0,0 +1,330 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2017 Philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +mmgen-autosign: Auto-sign MMGen transactions +""" + +import sys,os,subprocess,time,signal +from stat import * + +from mmgen.common import * +prog_name = os.path.basename(sys.argv[0]) +opts_data = lambda: { + 'desc': 'Auto-sign MMGen transactions', + 'usage':'[opts] [command]', + 'options': """ +-h, --help Print this help message +--, --longhelp Print help message for long options (common options) +-l, --led Use status LED to signal standby, busy and error +-s, --stealth-led Stealth LED mode - signal busy and error only, and only + after successful authorization. +""", + 'notes': """ + + + COMMANDS + +gen_secret - generate the shared secret and copy to /dev/shm and USB stick +wait - start in loop mode: wait - mount - sign - unmount - wait + + + USAGE NOTES + +If invoked with no command, the program mounts the USB stick, signs any +unsigned transactions, unmounts the USB stick and exits. + +If invoked with 'wait', the program waits in a loop, mounting, signing +and unmounting every time the USB stick is inserted. The status LED +indicates whether the program is busy or in standby mode, i.e. ready for +USB stick insertion or removal. + +The USB stick must have a partition with the label MMGEN_TX and a user- +writable directory '/tx', where unsigned MMGen transactions are placed. + +On the signing machine the directory /mnt/tx must exist and /etc/fstab must +contain the following entry: + + LABEL='MMGEN_TX' /mnt/tx auto noauto,user 0 0 + +The signing wallet or wallets must be in MMGen mnemonic format and +present in /dev/shm. The wallet(s) can be created interactively with +the following command: + + $ mmgen-walletconv -i words -o words -d /dev/shm + +{} checks that a shared secret is present on the USB stick +before signing transactions. The shared secret is generated by invoking +the command with 'gen_secret' with the USB stick inserted. For good +security, it's advisable to re-generate a new shared secret before each +signing session. + +Status LED functionality is supported on Orange Pi and Raspberry Pi boards. + +This program is a helper script and is not installed by default. You +may copy it to your executable path if you wish, or just run it in place +in the scripts directory of the MMGen repository root where it resides. +""".format(prog_name) +} + +cmd_args = opts.init(opts_data) +if opt.stealth_led: opt.led = True + +mountpoint = '/mnt/tx' +tx_dir = os.path.join(mountpoint,'tx') +part_label = 'MMGEN_TX' +shm_dir = '/dev/shm' +secret_fn = 'txsign-secret' +tn_arg = ([],['--testnet=1'])[bool(opt.testnet)] + +def check_daemon_running(): + try: subprocess.check_output(['mmgen-tool'] + tn_arg + ['getbalance']) + except: die(1,'Daemon not running') + +def get_wallet_files(): + wfs = [f for f in os.listdir(shm_dir) if f[-8:] == '.mmwords'] + if not wfs: + die(1,'No mnemonic files present!') + return [os.path.join(shm_dir,w) for w in wfs] + +def get_secret_in_dir(d,on_fail='die'): + try: + with open(os.path.join(d,secret_fn)) as f: + return f.read().rstrip() + except: + msg('Unable to read secret file!') + if on_fail == 'die': sys.exit(1) + else: return None + +def do_mount(): + if not os.path.ismount(mountpoint): + msg('Mounting '+mountpoint) + subprocess.call(['mount',mountpoint]) + try: + ds = os.stat(tx_dir) + assert S_ISDIR(ds.st_mode) + assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR + except: + die(1,'{} missing, or not read/writable by user!'.format(tx_dir)) + +def do_umount(): + if os.path.ismount(mountpoint): + subprocess.call(['sync']) + msg('Unmounting '+mountpoint) + subprocess.call(['umount',mountpoint]) + +def sign(): + dirlist = os.listdir(tx_dir) + raw = [f for f in dirlist if f[-6:] == '.rawtx'] + signed = [f[:-6] for f in dirlist if f[-6:] == '.sigtx'] + unsigned = [os.path.join(tx_dir,f) for f in raw if f[:-6] not in signed] + + if unsigned: + cmd = ['mmgen-txsign','--yes','--outdir='+tx_dir] + tn_arg + unsigned + wfs + ret = subprocess.call(cmd) + msg('') + time.sleep(0.3) + return (1,0)[ret==0] + else: + msg('No unsigned transactions') + time.sleep(1) + return 0 + +def wipe_existing_secret_files(): + for d in (tx_dir,shm_dir): + fn = os.path.join(d,secret_fn) + try: + os.stat(fn) + except: + pass + else: + msg('\nWiping existing key {}'.format(fn)) + subprocess.call(['wipe','-c',fn]) + +def create_secret_files(): + from binascii import hexlify + secret = hexlify(os.urandom(16)) + for d in (tx_dir,shm_dir): + fn = os.path.join(d,secret_fn) + desc = 'secret file in {}'.format(d) + msg('Creating ' + desc) + try: + with open(fn,'w') as f: f.write(secret+'\n') + os.chmod(fn,0400) + msg('Wrote ' + desc) + except: + die(2,'Unable to write ' + desc) + +def do_create_secret_files(): + if not get_insert_status(): + die(2,'USB stick not present!') + do_mount() + wipe_existing_secret_files() + create_secret_files() + do_umount() + +def ev_sleep(secs): + ev.wait(secs) + return (False,True)[ev.isSet()] + +def do_led(on,off): + if not on: + with open(status_ctl,'w') as f: f.write('0\n') + while True: + if ev_sleep(3600): return + + while True: + with open(status_ctl,'w') as f: f.write('255\n') + if ev_sleep(on): return + with open(status_ctl,'w') as f: f.write('0\n') + if ev_sleep(off): return + +def set_led(cmd): + if not opt.led: return + timings = { + 'off': ( 0, 0 ), + 'standby': ( 2.2, 0.2 ), + 'busy': ( 0.06, 0.06 ), + 'error': ( 0.5, 0.5 )}[cmd] + vmsg("Executing command '{}'".format(cmd)) + global led_thread + if led_thread: + ev.set(); led_thread.join(); ev.clear() + led_thread = threading.Thread(target=do_led,name='LED loop',args=timings) + led_thread.start() + +def do_sign(): + if not opt.stealth_led: set_led('busy') + do_mount() + ret = get_secret_in_dir(tx_dir,on_fail='return') + if ret == secret: + if opt.stealth_led: set_led('busy') + exit_val = sign() + do_umount() + set_led(('standby','off','error')[bool(exit_val)*2 or bool(opt.stealth_led)]) + else: + if ret != None: + msg('Secret is incorrect!') + do_umount() + if not opt.stealth_led: set_led('error') + +def get_insert_status(): + try: os.stat(os.path.join('/dev/disk/by-label/',part_label)) + except: return False + else: return True + +def do_loop(): + n,prev_status = 0,False + if not opt.stealth_led: + set_led('standby') + while True: + status = get_insert_status() + if status and not prev_status: + msg('Running command...') + do_sign() + prev_status = status + if not n % 10: + msg_r('\r{}\rwaiting'.format(' '*17)) + time.sleep(1) + msg_r('.') + n += 1 + +def check_access(fn,desc='status LED control',init_val=None): + try: + with open(fn) as f: b = f.read().strip() + with open(fn,'w') as f: + f.write('{}\n'.format(init_val or b)) + return True + except: + m1 = "You do not have access to the {} file\n".format(desc) + m2 = "To allow access, run 'sudo chmod 0666 {}'".format(fn) + msg(m1+m2) + return False + +def check_wipe_present(): + try: + subprocess.Popen(['wipe','-v'],stdout=subprocess.PIPE,stderr=subprocess.PIPE) + except: + die(2,"The 'wipe' utility must be installed before running this program") + +if opt.led: + import threading + status = { + 'opi': '/sys/class/leds/orangepi:red:status/brightness', + 'rpi': '/sys/class/leds/led0/brightness' + } + trigger = { + 'rpi': '/sys/class/leds/led0/trigger', # mmc,none + } + for k in ('opi','rpi'): + try: os.stat(status[k]) + except: pass + else: + board = k + status_ctl = status[board] + break + else: + die(2,'Control files not found! LED option not supported') + + if not check_access(status_ctl) or ( + board == 'rpi' and not check_access(trigger[board],desc='LED trigger',init_val='none')): + sys.exit(1) + + ev = threading.Event() + led_thread = None + + if board == 'rpi': + with open(trigger[board],'w') as f: f.write('none\n') + +# main() +if len(cmd_args) == 1 and cmd_args[0] == 'gen_secret': + do_create_secret_files() + sys.exit() + +check_wipe_present() +wfs = get_wallet_files() +secret = get_secret_in_dir(shm_dir,on_fail='die') +check_daemon_running() + +def at_exit(nl=True): + if opt.led: + set_led('off') + ev.set() + led_thread.join() + if board == 'rpi': + with open(trigger[board],'w') as f: f.write('mmc0\n') + if nl: msg('') + raise SystemExit + +def handler(a,b): at_exit() + +signal.signal(signal.SIGTERM,handler) +signal.signal(signal.SIGINT,handler) + +try: + if len(cmd_args) == 1 and cmd_args[0] == 'wait': + do_loop() + elif len(cmd_args) == 0: + do_sign() + at_exit(nl=False) + else: + msg('Invalid invocation') +except IOError: + at_exit() +except KeyboardInterrupt: + at_exit() diff --git a/setup.py b/setup.py index 39f184d5..9836f095 100755 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ module1 = Extension( libraries = ['secp256k1'], library_dirs = ['/usr/local/lib',r'c:\msys\local\lib'], # mingw32 needs this, Linux can use it, but it breaks mingw64 - extra_link_args = (['-lgmp'],[])[have_mingw64 or have_arm], + extra_link_args = (['-lgmp'],[])[have_mingw64], include_dirs = ['/usr/local/include',r'c:\msys\local\include'], )