Browse Source

mmgen-autosign: support signing of TXs with non-MMGen inputs

The MMGen Project 5 months ago
parent
commit
b12fd879bf

+ 37 - 0
mmgen/autosign.py

@@ -275,6 +275,7 @@ class Signable:
 					self.cfg,
 					self.cfg,
 					tx1,
 					tx1,
 					seedfiles = self.parent.wallet_files[:],
 					seedfiles = self.parent.wallet_files[:],
+					keylist = self.parent.keylist,
 					passwdfile = str(self.parent.keyfile),
 					passwdfile = str(self.parent.keyfile),
 					autosign = True).keys)
 					autosign = True).keys)
 			if tx2:
 			if tx2:
@@ -424,6 +425,7 @@ class Autosign:
 	macOS_ramdisk_name = 'AutosignRamDisk'
 	macOS_ramdisk_name = 'AutosignRamDisk'
 	wallet_subdir = 'autosign'
 	wallet_subdir = 'autosign'
 	linux_blkid_cmd = 'sudo blkid -s LABEL -o value'
 	linux_blkid_cmd = 'sudo blkid -s LABEL -o value'
+	keylist_fn = 'keylist.mmenc'
 
 
 	cmds = ('setup', 'xmr_setup', 'sign', 'wait')
 	cmds = ('setup', 'xmr_setup', 'sign', 'wait')
 
 
@@ -672,6 +674,7 @@ class Autosign:
 			self.led.set('busy')
 			self.led.set('busy')
 		self.do_mount()
 		self.do_mount()
 		key_ok = self.decrypt_wallets()
 		key_ok = self.decrypt_wallets()
+		self.init_non_mmgen_keys()
 		if key_ok:
 		if key_ok:
 			if self.cfg.stealth_led:
 			if self.cfg.stealth_led:
 				self.led.set('busy')
 				self.led.set('busy')
@@ -780,6 +783,9 @@ class Autosign:
 		ss_out = Wallet(self.cfg, ss=ss_in, passwd_file=str(self.keyfile))
 		ss_out = Wallet(self.cfg, ss=ss_in, passwd_file=str(self.keyfile))
 		ss_out.write_to_file(desc='autosign wallet', outdir=self.wallet_dir)
 		ss_out.write_to_file(desc='autosign wallet', outdir=self.wallet_dir)
 
 
+		if self.cfg.keys_from_file:
+			self.setup_non_mmgen_keys()
+
 	@property
 	@property
 	def xmrwallet_cfg(self):
 	def xmrwallet_cfg(self):
 		if not hasattr(self, '_xmrwallet_cfg'):
 		if not hasattr(self, '_xmrwallet_cfg'):
@@ -908,3 +914,34 @@ class Autosign:
 			enabled = self.cfg.led,
 			enabled = self.cfg.led,
 			simulate = self.cfg.test_suite_autosign_led_simulate)
 			simulate = self.cfg.test_suite_autosign_led_simulate)
 		self.led.set('off')
 		self.led.set('off')
+
+	def setup_non_mmgen_keys(self):
+		from .fileutil import get_lines_from_file, write_data_to_file
+		from .crypto import Crypto
+		lines = get_lines_from_file(self.cfg, self.cfg.keys_from_file, desc='keylist data')
+		write_data_to_file(
+			self.cfg,
+			str(self.wallet_dir / self.keylist_fn),
+			Crypto(self.cfg).mmgen_encrypt(
+				data = '\n'.join(lines).encode(),
+				passwd = self.keyfile.read_text()),
+			desc = 'encrypted keylist data',
+			binary = True)
+		if keypress_confirm(self.cfg, 'Securely delete original keylist file?'):
+			shred_file(self.cfg, self.cfg.keys_from_file)
+
+	def init_non_mmgen_keys(self):
+		if not hasattr(self, 'keylist'):
+			path = self.wallet_dir / self.keylist_fn
+			if path.exists():
+				from .crypto import Crypto
+				from .fileutil import get_data_from_file
+				self.keylist = Crypto(self.cfg).mmgen_decrypt(
+					get_data_from_file(
+						self.cfg,
+						path,
+						desc = 'encrypted keylist data',
+						binary = True),
+					passwd = self.keyfile.read_text()).decode().split()
+			else:
+				self.keylist = None

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev48
+15.1.dev49

+ 6 - 0
mmgen/main_autosign.py

@@ -36,6 +36,12 @@ opts_data = {
 --, --longhelp        Print help message for long (global) options
 --, --longhelp        Print help message for long (global) options
 -c, --coins=c         Coins to sign for (comma-separated list)
 -c, --coins=c         Coins to sign for (comma-separated list)
 -I, --no-insert-check Don’t check for device insertion
 -I, --no-insert-check Don’t check for device insertion
+-k, --keys-from-file=F Use wif keys listed in file ‘F’ for signing non-MMGen
+                      inputs. The file may be MMGen encrypted if desired. The
+                      ‘setup’ operation creates a temporary encrypted copy of
+                      the file in volatile memory for use during the signing
+                      session, thus permitting the deletion of the original
+                      file for increased security.
 -l, --seed-len=N      Specify wallet seed length of ‘N’ bits (for setup only)
 -l, --seed-len=N      Specify wallet seed length of ‘N’ bits (for setup only)
 -L, --led             Use status LED to signal standby, busy and error
 -L, --led             Use status LED to signal standby, busy and error
 -m, --mountpoint=M    Specify an alternate mountpoint 'M'
 -m, --mountpoint=M    Specify an alternate mountpoint 'M'

+ 3 - 3
mmgen/tx/base.py

@@ -95,8 +95,7 @@ class Base(MMGenObject):
 		This transaction includes inputs with non-{gc.proj_name} addresses.  When
 		This transaction includes inputs with non-{gc.proj_name} addresses.  When
 		signing the transaction, private keys for the addresses listed below must
 		signing the transaction, private keys for the addresses listed below must
 		be supplied using the --keys-from-file option.  The key file must contain
 		be supplied using the --keys-from-file option.  The key file must contain
-		one key per line.  Please note that this transaction cannot be autosigned,
-		as autosigning does not support the use of key files.
+		one key per line.
 
 
 		Non-{gc.proj_name} addresses found in inputs:
 		Non-{gc.proj_name} addresses found in inputs:
 		  {{}}
 		  {{}}
@@ -213,6 +212,7 @@ class Base(MMGenObject):
 			quiet = True)
 			quiet = True)
 
 
 	def check_non_mmgen_inputs(self, *, caller, non_mmaddrs=None):
 	def check_non_mmgen_inputs(self, *, caller, non_mmaddrs=None):
+		assert caller in ('txcreate', 'txdo', 'txsign', 'autosign')
 		non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
 		non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
 		if non_mmaddrs:
 		if non_mmaddrs:
 			indent = '  '
 			indent = '  '
@@ -223,7 +223,7 @@ class Base(MMGenObject):
 					die('UserOptError', f'\n{indent}ERROR: {m}\n')
 					die('UserOptError', f'\n{indent}ERROR: {m}\n')
 			else:
 			else:
 				msg(f'\n{indent}WARNING: {m}\n')
 				msg(f'\n{indent}WARNING: {m}\n')
-				if not self.cfg.yes:
+				if not (caller == 'autosign' or self.cfg.yes):
 					from ..ui import keypress_confirm
 					from ..ui import keypress_confirm
 					keypress_confirm(self.cfg, 'Continue?', default_yes=True, do_exit=True)
 					keypress_confirm(self.cfg, 'Continue?', default_yes=True, do_exit=True)
 
 

+ 2 - 1
mmgen/tx/keys.py

@@ -89,6 +89,7 @@ class TxKeys:
 		self.keylist     = keylist if autosign else keylist or get_keylist(cfg)
 		self.keylist     = keylist if autosign else keylist or get_keylist(cfg)
 		self.keyaddrlist = keyaddrlist if autosign else keyaddrlist or get_keyaddrlist(cfg, tx.proto)
 		self.keyaddrlist = keyaddrlist if autosign else keyaddrlist or get_keyaddrlist(cfg, tx.proto)
 		self.passwdfile  = passwdfile
 		self.passwdfile  = passwdfile
+		self.autosign    = autosign
 		self.saved_seeds = {}
 		self.saved_seeds = {}
 
 
 	def get_keys_for_non_mmgen_inputs(self):
 	def get_keys_for_non_mmgen_inputs(self):
@@ -96,7 +97,7 @@ class TxKeys:
 		sep = '\n    '
 		sep = '\n    '
 		if addrs := self.tx.get_non_mmaddrs('inputs'):
 		if addrs := self.tx.get_non_mmaddrs('inputs'):
 			self.tx.check_non_mmgen_inputs(
 			self.tx.check_non_mmgen_inputs(
-				caller = 'txsign',
+				caller = 'autosign' if self.autosign and self.keylist else 'txsign',
 				non_mmaddrs = addrs)
 				non_mmaddrs = addrs)
 			kal = KeyAddrList(
 			kal = KeyAddrList(
 				cfg         = self.cfg,
 				cfg         = self.cfg,

+ 45 - 5
test/cmdtest_d/automount.py

@@ -15,7 +15,7 @@ import time
 
 
 from .autosign import CmdTestAutosignThreaded
 from .autosign import CmdTestAutosignThreaded
 from .regtest import CmdTestRegtest, rt_pw
 from .regtest import CmdTestRegtest, rt_pw
-from ..include.common import gr_uc
+from ..include.common import gr_uc, create_addrpairs
 
 
 class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 	'automounted transacting operations via regtest mode'
 	'automounted transacting operations via regtest mode'
@@ -23,21 +23,29 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 	networks = ('btc', 'bch', 'ltc')
 	networks = ('btc', 'bch', 'ltc')
 	tmpdir_nums = [49]
 	tmpdir_nums = [49]
 	bdb_wallet = True
 	bdb_wallet = True
+	keylist_passwd = 'abc'
 
 
 	rt_data = {
 	rt_data = {
 		'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'},
 		'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'},
 	}
 	}
+	bal1_chk = {
+		'btc': '502.46',
+		'bch': '502.46',
+		'ltc': '5502.46'}
 	bal2_chk = {
 	bal2_chk = {
-		'btc': '491.11002204',
-		'bch': '498.7653392',
-		'ltc': '5491.11002204'}
+		'btc': '493.56992828',
+		'bch': '501.22524576',
+		'ltc': '5493.56992828'}
 
 
 	cmd_group = (
 	cmd_group = (
 		('setup',                            'regtest mode setup'),
 		('setup',                            'regtest mode setup'),
 		('walletgen_alice',                  'wallet generation (Alice)'),
 		('walletgen_alice',                  'wallet generation (Alice)'),
 		('addrgen_alice',                    'address generation (Alice)'),
 		('addrgen_alice',                    'address generation (Alice)'),
 		('addrimport_alice',                 'importing Alice’s addresses'),
 		('addrimport_alice',                 'importing Alice’s addresses'),
+		('addrimport_alice_non_mmgen',       'importing Alice’s non-MMGen addresses'),
 		('fund_alice',                       'funding Alice’s wallet'),
 		('fund_alice',                       'funding Alice’s wallet'),
+		('fund_alice_non_mmgen1',            'funding Alice’s wallet (non-MMGen addr #1)'),
+		('fund_alice_non_mmgen2',            'funding Alice’s wallet (non-MMGen addr #2)'),
 		('generate',                         'mining a block'),
 		('generate',                         'mining a block'),
 		('alice_bal1',                       'checking Alice’s balance'),
 		('alice_bal1',                       'checking Alice’s balance'),
 		('alice_txcreate1',                  'creating a transaction'),
 		('alice_txcreate1',                  'creating a transaction'),
@@ -92,9 +100,30 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 
 
 		self.opts.append('--alice')
 		self.opts.append('--alice')
 
 
+		self.non_mmgen_addrs = create_addrpairs(self.proto, 'C', 2)
+
+	def addrimport_alice_non_mmgen(self):
+		self.write_to_tmpfile(
+			'non_mmgen_addrs',
+			'\n'.join(e.addr for e in self.non_mmgen_addrs))
+		return self.spawn(
+			'mmgen-addrimport',
+			['--alice', '--quiet', '--addrlist', f'{self.tmpdir}/non_mmgen_addrs'])
+
+	def fund_alice_non_mmgen1(self):
+		return self.fund_wallet('alice', '1.23', addr=self.non_mmgen_addrs[0].addr)
+
+	def fund_alice_non_mmgen2(self):
+		return self.fund_wallet('alice', '1.23', addr=self.non_mmgen_addrs[1].addr)
+
+	def alice_bal1(self):
+		return self._user_bal_cli('alice', chk=self.bal1_chk[self.coin])
+
 	def alice_txcreate1(self):
 	def alice_txcreate1(self):
 		return self._user_txcreate(
 		return self._user_txcreate(
 			'alice',
 			'alice',
+			inputs = '1-3',
+			tweaks = ['confirm_non_mmgen'],
 			chg_addr = 'C:5',
 			chg_addr = 'C:5',
 			data_arg = 'data:'+gr_uc[:24])
 			data_arg = 'data:'+gr_uc[:24])
 
 
@@ -147,7 +176,18 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		return self._user_txcreate('alice', chg_addr='C:5', exit_val=2, expect_str='unsent transaction')
 		return self._user_txcreate('alice', chg_addr='C:5', exit_val=2, expect_str='unsent transaction')
 
 
 	def alice_run_autosign_setup(self):
 	def alice_run_autosign_setup(self):
-		return self.run_setup(mn_type='default', use_dfl_wallet=True, passwd=rt_pw)
+		from mmgen.crypto import Crypto
+		from mmgen.cfg import Config
+		new_cfg = Config({'_clone': self.cfg, 'usr_randchars': 0, 'hash_preset': '1'})
+		enc_data = Crypto(new_cfg).mmgen_encrypt(
+			'\n'.join(e.wif for e in self.non_mmgen_addrs).encode(), passwd=self.keylist_passwd)
+		self.write_to_tmpfile('non_mmgen_keys.mmenc', enc_data, binary=True)
+		return self.run_setup(
+			mn_type = 'default',
+			use_dfl_wallet = True,
+			wallet_passwd = rt_pw,
+			add_opts = [f'--keys-from-file={self.tmpdir}/non_mmgen_keys.mmenc'],
+			keylist_passwd = self.keylist_passwd)
 
 
 	def alice_txsend1(self):
 	def alice_txsend1(self):
 		return self._user_txsend('alice', comment='This one’s worth a comment', no_wait=True)
 		return self._user_txsend('alice', comment='This one’s worth a comment', no_wait=True)

+ 5 - 0
test/cmdtest_d/autosign.py

@@ -197,6 +197,7 @@ class CmdTestAutosignBase(CmdTestBase):
 			seed_len       = None,
 			seed_len       = None,
 			usr_entry_modes = False,
 			usr_entry_modes = False,
 			wallet_passwd  = None,
 			wallet_passwd  = None,
+			keylist_passwd = None,
 			add_opts       = [],
 			add_opts       = [],
 			expect_args    = []):
 			expect_args    = []):
 
 
@@ -244,6 +245,10 @@ class CmdTestAutosignBase(CmdTestBase):
 		if expect_args:
 		if expect_args:
 			t.expect(*expect_args)
 			t.expect(*expect_args)
 
 
+		if keylist_passwd:
+			t.passphrase('keylist data', keylist_passwd)
+			t.expect('(y/N): ', 'y')
+
 		t.read()
 		t.read()
 		self.remove_device()
 		self.remove_device()
 
 

+ 12 - 0
test/include/common.py

@@ -23,6 +23,7 @@ test.include.common: Shared routines and data for the MMGen test suites
 import sys, os, re, atexit
 import sys, os, re, atexit
 from subprocess import run, PIPE, DEVNULL
 from subprocess import run, PIPE, DEVNULL
 from pathlib import Path
 from pathlib import Path
+from collections import namedtuple
 
 
 from mmgen.cfg import gv
 from mmgen.cfg import gv
 from mmgen.color import yellow, green, orange
 from mmgen.color import yellow, green, orange
@@ -362,6 +363,17 @@ def make_burn_addr(proto, mmtype='compressed', hexdata=None):
 		proto   = proto,
 		proto   = proto,
 		mmtype  = mmtype).pubhash2addr(hexdata or '00'*20)
 		mmtype  = mmtype).pubhash2addr(hexdata or '00'*20)
 
 
+def create_addrpairs(proto, mmtype, num):
+	ap = namedtuple('addrpair', ['wif', 'addr'])
+	from mmgen.tool.coin import tool_cmd
+	n = 123456789123456789
+	return [ap(*tool_cmd(
+		cfg     = cfg,
+		cmdname = 'privhex2pair',
+		proto   = proto,
+		mmtype  = mmtype).privhex2pair(f'{n+m:064x}'))
+			for m in range(num)]
+
 def VirtBlockDevice(img_path, size):
 def VirtBlockDevice(img_path, size):
 	if sys.platform == 'linux':
 	if sys.platform == 'linux':
 		return VirtBlockDeviceLinux(img_path, size)
 		return VirtBlockDeviceLinux(img_path, size)