From 9b394bea7fab7b893db0ce0f6aba9e549ae0cc64 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 29 Jun 2019 11:35:49 +0200 Subject: [PATCH] Add support for sapling in Ledger plugin. Precursor to Ledger change. Also add Ledger X Support --- .../deterministic-build/requirements-hw.txt | 4 +- contrib/requirements/requirements-hw.txt | 2 + plugins/ledger/btchip_zcash.py | 268 ++++++++++++++++++ plugins/ledger/ledger.py | 106 +++++-- 4 files changed, 358 insertions(+), 22 deletions(-) create mode 100644 plugins/ledger/btchip_zcash.py diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 88cd12e7..a1270325 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -1,5 +1,5 @@ -btchip-python==0.1.26 \ - --hash=sha256:427d67c5b4f4709605c51dd91d5d44a2ad8f541693673817765271e4b3a1461e +btchip-python==0.1.28 \ + --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83 certifi==2018.1.18 \ --hash=sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296 \ --hash=sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 5647f3ca..e242a2aa 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -2,3 +2,5 @@ Cython>=0.27 trezor>=0.9.0 keepkey btchip-python +libusb1 + diff --git a/plugins/ledger/btchip_zcash.py b/plugins/ledger/btchip_zcash.py new file mode 100644 index 00000000..b17918be --- /dev/null +++ b/plugins/ledger/btchip_zcash.py @@ -0,0 +1,268 @@ +from binascii import hexlify, unhexlify +from struct import pack, unpack + +from btchip.btchip import btchip +from btchip.bitcoinTransaction import bitcoinInput, bitcoinOutput +from btchip.bitcoinVarint import readVarint, writeVarint +from btchip.btchipHelpers import parse_bip32_path, writeUint32BE + +from electrum_zclassic.transaction import (OVERWINTERED_VERSION_GROUP_ID, + SAPLING_VERSION_GROUP_ID) + + +class zcashTransaction: + + def __init__(self, data=None): + self.version = '' + self.version_group_id = '' + self.inputs = [] + self.outputs = [] + self.lockTime = '' + self.expiry_height = '' + self.value_balance = '' + self.overwintered = False + self.n_version = 0 + if data is not None: + offset = 0 + self.version = data[offset:offset + 4] + offset += 4 + header = unpack('= 4: + offset += 4 + self.value_balance = data[offset:offset + 8] + + def serializeOutputs(self): + result = [] + writeVarint(len(self.outputs), result) + for troutput in self.outputs: + result.extend(troutput.serialize()) + return result + + +class btchip_zcash(btchip): + + def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, + redeemScript, version=0x02, + overwintered=False): + # Start building a fake transaction with the passed inputs + if newTransaction: + if overwintered: + p2 = 0x05 if version == 4 else 0x04 + else: + p2 = 0x00 + else: + p2 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ] + if overwintered and version == 3: + params = bytearray([version, 0x00, 0x00, 0x80, 0x70, 0x82, 0xc4, 0x03]) + elif overwintered and version == 4: + params = bytearray([version, 0x00, 0x00, 0x80, 0x85, 0x20, 0x2f, 0x89]) + else: + params = bytearray([version, 0x00, 0x00, 0x00]) + writeVarint(len(outputList), params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + # Loop for each input + currentIndex = 0 + for passedOutput in outputList: + if ('sequence' in passedOutput) and passedOutput['sequence']: + sequence = bytearray(unhexlify(passedOutput['sequence'])) + else: + sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) # default sequence + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ] + params = [] + script = bytearray(redeemScript) + if overwintered: + params.append(0x02) + elif ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + params.append(0x01) + else: + params.append(0x00) + if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + params.append(len(passedOutput['value'])) + params.extend(passedOutput['value']) + if currentIndex != inputIndex: + script = bytearray() + writeVarint(len(script), params) + if len(script) == 0: + params.extend(sequence) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset = 0 + while(offset < len(script)): + blockLength = 255 + if ((offset + blockLength) < len(script)): + dataLength = blockLength + else: + dataLength = len(script) - offset + params = script[offset : offset + dataLength] + if ((offset + dataLength) == len(script)): + params.extend(sequence) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(params) ] + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset += blockLength + currentIndex += 1 + + def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): + alternateEncoding = False + donglePath = parse_bip32_path(changePath) + if self.needKeyCache: + self.resolvePublicKeysInPath(changePath) + result = {} + outputs = None + if rawTx is not None: + try: + fullTx = zcashTransaction(bytearray(rawTx)) + outputs = fullTx.serializeOutputs() + if len(donglePath) != 0: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00 ] + params = [] + params.extend(donglePath) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + offset = 0 + while (offset < len(outputs)): + blockLength = self.scriptBlockLength + if ((offset + blockLength) < len(outputs)): + dataLength = blockLength + p1 = 0x00 + else: + dataLength = len(outputs) - offset + p1 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ + p1, 0x00, dataLength ] + apdu.extend(outputs[offset : offset + dataLength]) + response = self.dongle.exchange(bytearray(apdu)) + offset += dataLength + alternateEncoding = True + except: + pass + if not alternateEncoding: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ] + params = [] + params.append(len(outputAddress)) + params.extend(bytearray(outputAddress)) + writeHexAmountBE(btc_to_satoshi(str(amount)), params) + writeHexAmountBE(btc_to_satoshi(str(fees)), params) + params.extend(donglePath) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1 + response[0] + 1:] + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + offset = offset + 1 + result['keycardData'] = response[offset : offset + keycardDataLength] + offset = offset + keycardDataLength + result['secureScreenData'] = response[offset:] + if result['confirmationType'] == 0x04: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] + if outputs == None: + result['outputData'] = response[1 : 1 + response[0]] + else: + result['outputData'] = outputs + return result + + def finalizeInputFull(self, outputData): + result = {} + offset = 0 + encryptedOutputData = b"" + while (offset < len(outputData)): + blockLength = self.scriptBlockLength + if ((offset + blockLength) < len(outputData)): + dataLength = blockLength + p1 = 0x00 + else: + dataLength = len(outputData) - offset + p1 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ + p1, 0x00, dataLength ] + apdu.extend(outputData[offset : offset + dataLength]) + response = self.dongle.exchange(bytearray(apdu)) + encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] + offset += dataLength + if len(response) > 1: + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1 + response[0] + 1:] # legacy + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + offset = offset + 1 + result['keycardData'] = response[offset : offset + keycardDataLength] + offset = offset + keycardDataLength + result['secureScreenData'] = response[offset:] + result['encryptedOutputData'] = encryptedOutputData + if result['confirmationType'] == 0x04: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] + return result + + def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01, + version=0x02, overwintered=False): + if isinstance(pin, str): + pin = pin.encode('utf-8') + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_SIGN, 0x00, 0x00 ] + params = [] + params.extend(donglePath) + params.append(len(pin)) + params.extend(bytearray(pin)) + writeUint32BE(lockTime, params) + params.append(sighashType) + if overwintered: + params.extend(bytearray([0]*4)) + apdu.append(len(params)) + apdu.extend(params) + result = self.dongle.exchange(bytearray(apdu)) + if not result: + return + result[0] = 0x30 + return result diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index a60b3ecb..af843fa3 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -28,24 +28,29 @@ try: from btchip.btchipComm import HIDDongleHIDAPI, DongleWait from btchip.btchip import btchip from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script - from btchip.bitcoinTransaction import bitcoinTransaction from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware from btchip.btchipException import BTChipException + from .btchip_zcash import btchip_zcash, zcashTransaction btchip.setAlternateCoinVersions = setAlternateCoinVersions BTCHIP = True BTCHIP_DEBUG = is_verbose -except ImportError: +except ImportError as e: BTCHIP = False MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \ ' https://www.ledgerwallet.com' +MSG_NEEDS_FW_UPDATE_OVERWINTER = (_('Firmware version too old for ' + 'Overwinter/Sapling support. ' + 'Please update at') + + ' https://www.ledgerwallet.com') MULTI_OUTPUT_SUPPORT = '1.1.4' ALTERNATIVE_COIN_VERSION = '1.0.1' +OVERWINTER_SUPPORT = '1.3.3' class Ledger_Client(): def __init__(self, hidDevice): - self.dongleObject = btchip(hidDevice) + self.dongleObject = btchip_zcash(hidDevice) self.preflightDone = False def is_pairable(self): @@ -90,7 +95,7 @@ class Ledger_Client(): @test_pin_unlocked def get_xpub(self, bip32_path, xtype): self.checkDevice() - # bip32_path is of the form 44'/147'/0' + # bip32_path is of the form 44'/133'/1' # S-L-O-W - we don't handle the fingerprint directly, so compute # it manually from the previous node # This only happens once so it's bearable @@ -139,11 +144,15 @@ class Ledger_Client(): def supports_multi_output(self): return self.multiOutputSupported + def supports_overwinter(self): + return self.overwinterSupported + def perform_hw1_preflight(self): try: firmwareInfo = self.dongleObject.getFirmwareVersion() firmware = firmwareInfo['version'] self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT) + self.overwinterSupported = versiontuple(firmware) >= versiontuple(OVERWINTER_SUPPORT) self.canAlternateCoinVersions = (versiontuple(firmware) >= versiontuple(ALTERNATIVE_COIN_VERSION) and firmwareInfo['specialVersion'] >= 0x20) @@ -190,7 +199,7 @@ class Ledger_Client(): self.perform_hw1_preflight() except BTChipException as e: if (e.sw == 0x6d00 or e.sw == 0x6700): - raise Exception(_("Device not in Zclassic mode")) from e + raise Exception(_("Device not in ZClassic mode")) from e raise e self.preflightDone = True @@ -389,8 +398,15 @@ class Ledger_KeyStore(Hardware_KeyStore): # Get trusted inputs from the original transactions for utxo in inputs: sequence = int_to_hex(utxo[5], 4) - if not p2shTransaction: - txtmp = bitcoinTransaction(bfh(utxo[0])) + if tx.overwintered: + txtmp = zcashTransaction(bfh(utxo[0])) + tmp = bfh(utxo[3])[::-1] + tmp += bfh(int_to_hex(utxo[1], 4)) + tmp += txtmp.outputs[utxo[1]].amount + chipInputs.append({'value' : tmp, 'sequence' : sequence}) + redeemScripts.append(bfh(utxo[2])) + elif not p2shTransaction: + txtmp = zcashTransaction(bfh(utxo[0])) trustedInput = self.get_client().getTrustedInput(txtmp, utxo[1]) trustedInput['sequence'] = sequence chipInputs.append(trustedInput) @@ -400,24 +416,30 @@ class Ledger_KeyStore(Hardware_KeyStore): tmp += bfh(int_to_hex(utxo[1], 4)) chipInputs.append({'value' : tmp, 'sequence' : sequence}) redeemScripts.append(bfh(utxo[2])) - # Sign all inputs firstTransaction = True inputIndex = 0 rawTx = tx.serialize() self.get_client().enableAlternate2fa(False) - while inputIndex < len(inputs): - self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - chipInputs, redeemScripts[inputIndex]) + if tx.overwintered: + self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs, + redeemScripts[inputIndex], + version=tx.version, + overwintered=tx.overwintered) if changePath: # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) else: outputData = self.get_client().finalizeInputFull(txOutput) + + if tx.overwintered: + inputSignature = self.get_client().untrustedHashSign('', + '', lockTime=tx.locktime, + version=tx.version, + overwintered=tx.overwintered) outputData['outputData'] = txOutput - if firstTransaction: - transactionOutput = outputData['outputData'] + transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() @@ -426,14 +448,51 @@ class Ledger_KeyStore(Hardware_KeyStore): raise UserWarning() if pin != 'paired': self.handler.show_message(_("Confirmed. Signing Transaction...")) - else: - # Sign input with the provided PIN - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + while inputIndex < len(inputs): + singleInput = [ chipInputs[inputIndex] ] + self.get_client().startUntrustedTransaction(False, 0, singleInput, + redeemScripts[inputIndex], + version=tx.version, + overwintered=tx.overwintered) + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], + pin, lockTime=tx.locktime, + version=tx.version, + overwintered=tx.overwintered) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 - if pin != 'paired': - firstTransaction = False + else: + while inputIndex < len(inputs): + self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, + chipInputs, redeemScripts[inputIndex]) + if changePath: + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) + else: + outputData = self.get_client().finalizeInputFull(txOutput) + outputData['outputData'] = txOutput + if firstTransaction: + transactionOutput = outputData['outputData'] + if outputData['confirmationNeeded']: + outputData['address'] = output + self.handler.finished() + pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin + if not pin: + raise UserWarning() + if pin != 'paired': + self.handler.show_message(_("Confirmed. Signing Transaction...")) + else: + # Sign input with the provided PIN + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], + pin, lockTime=tx.locktime, + version=tx.version, + overwintered=tx.overwintered) + inputSignature[0] = 0x30 # force for 1.4.9+ + signatures.append(inputSignature) + inputIndex = inputIndex + 1 + if pin != 'paired': + firstTransaction = False except UserWarning: self.handler.show_error(_('Cancelled by user')) return @@ -483,7 +542,14 @@ class LedgerPlugin(HW_PluginBase): (0x2581, 0x3b7c), # HW.1 ledger production (0x2581, 0x4b7c), # HW.1 ledger test (0x2c97, 0x0000), # Blue - (0x2c97, 0x0001) # Nano-S + (0x2c97, 0x0001), # Nano-S + (0x2c97, 0x0004), # Nano-X + (0x2c97, 0x0005), # RFU + (0x2c97, 0x0006), # RFU + (0x2c97, 0x0007), # RFU + (0x2c97, 0x0008), # RFU + (0x2c97, 0x0009), # RFU + (0x2c97, 0x000a) # RFU ] def __init__(self, parent, config, name): @@ -521,7 +587,7 @@ class LedgerPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) - client.get_xpub("m/44'/147'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 + client.get_xpub("m/44'/133'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 def get_xpub(self, device_id, derivation, xtype, wizard): devmgr = self.device_manager()