Add support for sapling in Ledger plugin. Precursor to Ledger change. Also add Ledger X Support

This commit is contained in:
James 2019-06-29 11:35:49 +02:00
parent 8a7a390f76
commit 9b394bea7f
4 changed files with 358 additions and 22 deletions

View File

@ -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

View File

@ -2,3 +2,5 @@ Cython>=0.27
trezor>=0.9.0
keepkey
btchip-python
libusb1

View File

@ -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('<I', self.version)[0]
if header & 0x80000000:
self.n_version = header & 0x7FFFFFFF
self.version_group_id = data[offset:offset + 4]
offset += 4
version_group_id = unpack('<I', self.version_group_id)[0]
if (self.n_version == 3
and version_group_id == OVERWINTERED_VERSION_GROUP_ID):
self.overwintered = True
elif (self.n_version == 4
and version_group_id == SAPLING_VERSION_GROUP_ID):
self.overwintered = True
else:
offset -= 4
self.version_group_id = ''
self.n_version = header
inputSize = readVarint(data, offset)
offset += inputSize['size']
numInputs = inputSize['value']
for i in range(numInputs):
tmp = { 'buffer': data, 'offset' : offset}
self.inputs.append(bitcoinInput(tmp))
offset = tmp['offset']
outputSize = readVarint(data, offset)
offset += outputSize['size']
numOutputs = outputSize['value']
for i in range(numOutputs):
tmp = { 'buffer': data, 'offset' : offset}
self.outputs.append(bitcoinOutput(tmp))
offset = tmp['offset']
self.lockTime = data[offset:offset + 4]
if self.overwintered:
offset += 4
self.expiry_height = data[offset:offset + 4]
if self.n_version >= 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

View File

@ -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()