From b4d75b09f26078194d0ab4b0c8c7c1319e18a3d1 Mon Sep 17 00:00:00 2001 From: philemon Date: Tue, 10 Oct 2017 23:43:27 +0300 Subject: [PATCH] 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. --- mmgen/globalvars.py | 2 +- mmgen/tx.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index ad4a9da1..60058eb3 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.399' + version = '0.9.4a' release_date = 'October 2017' proj_name = 'MMGen' diff --git a/mmgen/tx.py b/mmgen/tx.py index 6e293e0d..42c3d327 100755 --- a/mmgen/tx.py +++ b/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: