Browse Source

Version 0.9.4a - security release

TX hexdata switch attack protection:

  - Protect against an exotic but theoretically possible attack where a
    malicious, compromised or malfunctioning coin daemon could alter hex
    transaction data.  An integrity check is performed during both signing and
    sending.  Needless to say, a user should always view a transaction and
    double-check its outputs before broadcasting.
philemon 7 years ago
parent
commit
b4d75b09f2
2 changed files with 37 additions and 1 deletions
  1. 1 1
      mmgen/globalvars.py
  2. 36 0
      mmgen/tx.py

+ 1 - 1
mmgen/globalvars.py

@@ -38,7 +38,7 @@ class g(object):
 		sys.exit(ev)
 	# Variables - these might be altered at runtime:
 
-	version      = '0.9.399'
+	version      = '0.9.4a'
 	release_date = 'October 2017'
 
 	proj_name = 'MMGen'

+ 36 - 0
mmgen/tx.py

@@ -48,6 +48,12 @@ def bytes2int(hex_bytes):
 def bytes2btc(hex_bytes):
 	return bytes2int(hex_bytes) * g.satoshi
 
+def scriptPubKey2addr(s):
+	if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac': addr_hex,p2sh = s[6:-4],False
+	elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':   addr_hex,p2sh = s[4:-2],True
+	else: raise NotImplementedError,'Unknown scriptPubKey'
+	return g.proto.hexaddr2addr(addr_hex,p2sh)
+
 from collections import OrderedDict
 class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
 	def __init__(self,txhex):
@@ -91,6 +97,9 @@ class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
 			('scriptPubKey', hshift(tx,readVInt(tx)))
 		)) for i in range(d['num_txouts'])])
 
+		for o in d['txouts']:
+			o['address'] = scriptPubKey2addr(o['scriptPubKey'])
+
 		d['witness_size'] = 0
 		if has_witness:
 			# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
@@ -475,6 +484,7 @@ class MMGenTX(MMGenObject):
 				self.hex = ret['hex']
 				vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
 				dt = DeserializedTX(self.hex)
+				self.check_hex_tx_matches_mmgen_tx(dt)
 				txid = dt['txid']
 				self.check_sigs(dt)
 				assert txid == c.decoderawtransaction(self.hex)['txid'], 'txid mismatch (after signing)'
@@ -498,6 +508,30 @@ class MMGenTX(MMGenObject):
 		ret = self.desc == 'signed transaction'
 		return (red,green)[ret](str(ret)) if color else ret
 
+	# protect against an attack where a malicious, compromised or malfunctioning daemon could switch
+	# hex transaction data.
+	def check_hex_tx_matches_mmgen_tx(self,deserial_tx):
+		m = 'Fatal error: a malicious or malfunctioning coin daemon or other program has altered your data!'
+
+		if deserial_tx['lock_time'] != 0:
+			rdie(3,'\nLock time is not zero!\n' + m)
+
+		def do_io_err(desc,mmio,hexio):
+			msg('\nMMGen {}:\n{}'.format(desc,pformat(mmio)))
+			msg('Hex {}:\n{}'.format(desc,pformat(hexio)))
+			m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + m
+			rdie(3,m2.format(desc.capitalize()))
+
+		i_hex   = sorted((i['txid'],i['vout']) for i in deserial_tx['txins'])
+		i_mmgen = sorted((i.txid,i.vout) for i in self.inputs)
+		if i_hex != i_mmgen:
+			do_io_err('inputs',i_hex,i_mmgen)
+
+		o_hex   = sorted((o['address'],BTCAmt(o['amount'])) for o in deserial_tx['txouts'])
+		o_mmgen = sorted((o.addr,o.amt) for o in self.outputs)
+		if o_hex != o_mmgen:
+			do_io_err('outputs',o_hex,o_mmgen)
+
 	def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error
 		txins = (deserial_tx or DeserializedTX(self.hex))['txins']
 		has_ss = any(ti['scriptSig'] for ti in txins)
@@ -557,6 +591,8 @@ class MMGenTX(MMGenObject):
 
 		self.die_if_incorrect_chain()
 
+		self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.hex))
+
 		bogus_send = os.getenv('MMGEN_BOGUS_SEND')
 
 		if self.has_segwit_outputs() and not segwit_is_active() and not bogus_send: