Skip to content

Commit

Permalink
Add error handling for Ledger and get_address function
Browse files Browse the repository at this point in the history
  • Loading branch information
sammchardy committed Apr 27, 2019
1 parent e5c7347 commit dcba816
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 28 deletions.
13 changes: 11 additions & 2 deletions README.rst
Expand Up @@ -859,11 +859,20 @@ Uses the `btchip-python library <https://github.com/LedgerHQ/btchip-python>`_ if
print(app.get_version())
# Show your address on the Ledger
print(app.get_version())
print(app.show_address())
# Show your address on the Ledger
# Get your address and public key from the Ledger
print(app.get_address())
# Get your public key from the Ledger
print(app.get_public_key())
Create a Wallet to use with the HTTP and Node RPC clients

.. code:: python
# this will prompt you on your Ledger to confirm the address you want to use
wallet = LedgerWallet(app, env=testnet_env)
Expand Down
140 changes: 119 additions & 21 deletions binance_chain/ledger/client.py
@@ -1,9 +1,11 @@
import re
import binascii
from typing import Optional

from btchip.btchip import writeUint32LE, BTChipException

from binance_chain.environment import BinanceEnvironment
from binance_chain.ledger.exceptions import LedgerRequestException


class LedgerApp:
Expand All @@ -13,7 +15,8 @@ class LedgerApp:
BNC_INS_PUBLIC_KEY_SECP256K1 = 0x01
BNC_INS_SIGN_SECP256K1 = 0x02
BNC_INS_SHOW_ADDR_SECP256K1 = 0x03
ACCEPT_STATUSES = 0x9000
BNC_INS_GET_ADDR_SECP256K1 = 0x04
SUCCESS_CODE = 0x9000

CHUNK_SIZE = 250
HD_PATH = "44'/714'/0'/0/0"
Expand All @@ -24,52 +27,147 @@ def __init__(self, dongle, env: Optional[BinanceEnvironment] = None):
self._env = env or BinanceEnvironment.get_production_env()
self._hrp = self._env.hrp

def get_version(self):
def _exchange(self, apdu):
apdu_data = bytearray(apdu)
try:
response = self._dongle.exchange(apdu_data)
except BTChipException as e:
if e.message.startswith('Invalid status'):
raise LedgerRequestException(e.sw, binascii.hexlify(apdu_data))
else:
raise e

return response

def get_version(self) -> dict:
"""Gets the version of the Ledger app that is currently open on the device.
.. code:: python
version = client.get_version()
:return: API Response
.. code:: python
{'testMode': False, 'version': '1.1.3', 'locked': False}
"""
result = {}
apdu = [self.BNC_CLA, self.BNC_INS_GET_VERSION, 0x00, 0x00, 0x00]
response = self._dongle.exchange(bytearray(apdu))
response = self._exchange(apdu)

result['compressedKeys'] = (response[0] == 0x01)
result['version'] = "%d.%d.%d" % (response[2], response[3], response[4])
result['specialVersion'] = response[1]
result['testMode'] = (response[0] == 0xFF)
result['version'] = "%d.%d.%d" % (response[1], response[2], response[3])
result['locked'] = bool(response[4])
return result

def get_public_key(self):
result = {}
def get_public_key(self) -> str:
"""Gets the public key from the Ledger app that is currently open on the device.
.. code:: python
public_key = client.get_public_key()
:return: API Response
.. code:: python
'<public_key>'
"""
dongle_path = self._parse_hd_path(self._path)
apdu = [self.BNC_CLA, self.BNC_INS_PUBLIC_KEY_SECP256K1, 0x00, 0x00, len(dongle_path)]
apdu.extend(dongle_path)
response = self._dongle.exchange(bytearray(apdu))
response = self._exchange(apdu)

return_code = response[:-2]
result['pk'] = response[0: 1 + 64]
result['return_code'] = return_code[0] * 256 + return_code[1]
return response[0: 1 + 64]

# TODO: handle error response
def show_address(self):
"""Shows the user's address for the given HD path on the device display.
return result['pk']
.. code:: python
def show_address(self):
client.show_address()
:return: None
"""
dongle_path = self._parse_hrp(self._hrp) + self._parse_hd_path(self._path)
apdu = [self.BNC_CLA, self.BNC_INS_SHOW_ADDR_SECP256K1, 0x00, 0x00, len(dongle_path)]
apdu.extend(dongle_path)
response = self._dongle.exchange(bytearray(apdu))
# TODO: check 0x9000 response
return response
self._exchange(apdu)

def get_address(self) -> dict:
"""Gets the address and public key from the Ledger app that is currently open on the device.
.. code:: python
address = client.get_address()
:return: API Response
.. code:: python
{'pk': '<public_key>', 'address': '<address>'}
"""
dongle_path = self._parse_hrp(self._hrp) + self._parse_hd_path(self._path)
apdu = [self.BNC_CLA, self.BNC_INS_GET_ADDR_SECP256K1, 0x00, 0x00, len(dongle_path)]
apdu.extend(dongle_path)
response = self._exchange(apdu)
return {
'pk': response[0: 1 + 32],
'address': response[1 + 32:].decode()
}

def _get_sign_chunks(self, msg: bytes):
chunks = [self._parse_hd_path(self._path)]
chunks += [msg[i:i + self.CHUNK_SIZE] for i in range(0, len(msg), self.CHUNK_SIZE)]
return chunks

def sign(self, msg: bytes):
def sign(self, msg: bytes) -> str:
"""Sends a transaction sign doc to the Ledger app to be signed.
.. code:: python
address = client.get_address()
:return: str
.. code:: python
'<signed message hash>'
"""
chunks = self._get_sign_chunks(msg)
response = ''
for idx, chunk in enumerate(chunks):
apdu = [self.BNC_CLA, self.BNC_INS_SIGN_SECP256K1, idx + 1, len(chunks), len(chunk)]
apdu.extend(chunk)
response = self._dongle.exchange(bytearray(apdu))
return response
response = self._exchange(apdu)

if response[0] != 0x30:
raise Exception("Ledger assertion failed: Expected a signature header of 0x30")

# decode DER format
r_offset = 4
r_len = response[3]
s_len = response[4 + r_len + 1]
s_offset = len(response) - s_len

if r_len == 33:
r_offset += 1
r_len -= 1

if s_len == 3:
s_offset += 1

sig_r = response[r_offset: r_offset + r_len]
sig_s = response[s_offset:]

return sig_r + sig_s

def _parse_hd_path(self, path):
if len(path) == 0:
Expand Down
22 changes: 22 additions & 0 deletions binance_chain/ledger/exceptions.py
@@ -0,0 +1,22 @@
LEDGER_RESPONSE_CODES = {
0x6400: 'Execution Error',
0x6982: 'Empty buffer',
0x6983: 'Output buffer too small',
0x6986: 'Command not allowed',
0x6A80: 'Incorrect tx data',
0x6D00: 'INS not supported',
0x6E00: 'CLA not supported',
0x6F00: 'Unknown',
}


class LedgerRequestException(Exception):

def __init__(self, response_code, request):
self._response_code = response_code
self._response_msg = LEDGER_RESPONSE_CODES.get(response_code, 'Unknown')

self._request = request

def __str__(self): # pragma: no cover
return f'LedgerRequestException(code={self._response_code}): {self._response_msg} - request {self._request}'
8 changes: 3 additions & 5 deletions binance_chain/ledger/wallet.py
Expand Up @@ -3,18 +3,16 @@
from binance_chain.environment import BinanceEnvironment
from binance_chain.wallet import BaseWallet
from binance_chain.ledger.client import LedgerApp
from binance_chain.utils.segwit_addr import address_from_public_key


class LedgerWallet(BaseWallet):

def __init__(self, app: LedgerApp, env: Optional[BinanceEnvironment] = None):
super().__init__(env)
self._app = app
self._public_key = self._app.get_public_key()
print(self._public_key)
self._address = address_from_public_key(self._public_key, self._env.hrp)
print(self._address)
pk_address = self._app.get_address()
self._public_key = pk_address['pk']
self._address = pk_address['address']

def sign_message(self, msg_bytes):
return self._app.sign(msg_bytes)

0 comments on commit dcba816

Please sign in to comment.