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,
 					tx1,
 					seedfiles = self.parent.wallet_files[:],
+					keylist = self.parent.keylist,
 					passwdfile = str(self.parent.keyfile),
 					autosign = True).keys)
 			if tx2:
@@ -424,6 +425,7 @@ class Autosign:
 	macOS_ramdisk_name = 'AutosignRamDisk'
 	wallet_subdir = 'autosign'
 	linux_blkid_cmd = 'sudo blkid -s LABEL -o value'
+	keylist_fn = 'keylist.mmenc'
 
 	cmds = ('setup', 'xmr_setup', 'sign', 'wait')
 
@@ -672,6 +674,7 @@ class Autosign:
 			self.led.set('busy')
 		self.do_mount()
 		key_ok = self.decrypt_wallets()
+		self.init_non_mmgen_keys()
 		if key_ok:
 			if self.cfg.stealth_led:
 				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.write_to_file(desc='autosign wallet', outdir=self.wallet_dir)
 
+		if self.cfg.keys_from_file:
+			self.setup_non_mmgen_keys()
+
 	@property
 	def xmrwallet_cfg(self):
 		if not hasattr(self, '_xmrwallet_cfg'):
@@ -908,3 +914,34 @@ class Autosign:
 			enabled = self.cfg.led,
 			simulate = self.cfg.test_suite_autosign_led_simulate)
 		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
 -c, --coins=c         Coins to sign for (comma-separated list)
 -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, --led             Use status LED to signal standby, busy and error
 -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
 		signing the transaction, private keys for the addresses listed below must
 		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:
 		  {{}}
@@ -213,6 +212,7 @@ class Base(MMGenObject):
 			quiet = True)
 
 	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')
 		if non_mmaddrs:
 			indent = '  '
@@ -223,7 +223,7 @@ class Base(MMGenObject):
 					die('UserOptError', f'\n{indent}ERROR: {m}\n')
 			else:
 				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
 					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.keyaddrlist = keyaddrlist if autosign else keyaddrlist or get_keyaddrlist(cfg, tx.proto)
 		self.passwdfile  = passwdfile
+		self.autosign    = autosign
 		self.saved_seeds = {}
 
 	def get_keys_for_non_mmgen_inputs(self):
@@ -96,7 +97,7 @@ class TxKeys:
 		sep = '\n    '
 		if addrs := self.tx.get_non_mmaddrs('inputs'):
 			self.tx.check_non_mmgen_inputs(
-				caller = 'txsign',
+				caller = 'autosign' if self.autosign and self.keylist else 'txsign',
 				non_mmaddrs = addrs)
 			kal = KeyAddrList(
 				cfg         = self.cfg,

+ 45 - 5
test/cmdtest_d/automount.py

@@ -15,7 +15,7 @@ import time
 
 from .autosign import CmdTestAutosignThreaded
 from .regtest import CmdTestRegtest, rt_pw
-from ..include.common import gr_uc
+from ..include.common import gr_uc, create_addrpairs
 
 class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 	'automounted transacting operations via regtest mode'
@@ -23,21 +23,29 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 	networks = ('btc', 'bch', 'ltc')
 	tmpdir_nums = [49]
 	bdb_wallet = True
+	keylist_passwd = 'abc'
 
 	rt_data = {
 		'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'},
 	}
+	bal1_chk = {
+		'btc': '502.46',
+		'bch': '502.46',
+		'ltc': '5502.46'}
 	bal2_chk = {
-		'btc': '491.11002204',
-		'bch': '498.7653392',
-		'ltc': '5491.11002204'}
+		'btc': '493.56992828',
+		'bch': '501.22524576',
+		'ltc': '5493.56992828'}
 
 	cmd_group = (
 		('setup',                            'regtest mode setup'),
 		('walletgen_alice',                  'wallet generation (Alice)'),
 		('addrgen_alice',                    'address generation (Alice)'),
 		('addrimport_alice',                 'importing Alice’s addresses'),
+		('addrimport_alice_non_mmgen',       'importing Alice’s non-MMGen addresses'),
 		('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'),
 		('alice_bal1',                       'checking Alice’s balance'),
 		('alice_txcreate1',                  'creating a transaction'),
@@ -92,9 +100,30 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 
 		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):
 		return self._user_txcreate(
 			'alice',
+			inputs = '1-3',
+			tweaks = ['confirm_non_mmgen'],
 			chg_addr = 'C:5',
 			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')
 
 	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):
 		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,
 			usr_entry_modes = False,
 			wallet_passwd  = None,
+			keylist_passwd = None,
 			add_opts       = [],
 			expect_args    = []):
 
@@ -244,6 +245,10 @@ class CmdTestAutosignBase(CmdTestBase):
 		if expect_args:
 			t.expect(*expect_args)
 
+		if keylist_passwd:
+			t.passphrase('keylist data', keylist_passwd)
+			t.expect('(y/N): ', 'y')
+
 		t.read()
 		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
 from subprocess import run, PIPE, DEVNULL
 from pathlib import Path
+from collections import namedtuple
 
 from mmgen.cfg import gv
 from mmgen.color import yellow, green, orange
@@ -362,6 +363,17 @@ def make_burn_addr(proto, mmtype='compressed', hexdata=None):
 		proto   = proto,
 		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):
 	if sys.platform == 'linux':
 		return VirtBlockDeviceLinux(img_path, size)