Coverage for admin/dongle_eth.py: 91%
66 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-07-10 13:43 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-07-10 13:43 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2021 RSK Labs Ltd
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy of
6# this software and associated documentation files (the "Software"), to deal in
7# the Software without restriction, including without limitation the rights to
8# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9# of the Software, and to permit persons to whom the Software is furnished to do
10# so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
23import ecdsa
24import struct
26from enum import IntEnum
27from ledgerblue.comm import getDongle
28from ledgerblue.commException import CommException
31class _ErrorCode(IntEnum):
32 WRONG_APP = 0x6511
33 INVALID_PATH = 0x6a15
34 DONGLE_LOCKED = 0x6b0c
37class DongleEthError(RuntimeError):
38 ERR = _ErrorCode
40 ERROR_MESSAGES = {
41 ERR.WRONG_APP: "Ethereum app not open",
42 ERR.INVALID_PATH: "Invalid path for Ethereum app",
43 ERR.DONGLE_LOCKED: "Device locked"
44 }
46 @staticmethod
47 def from_error_code(code):
48 message = DongleEthError.ERROR_MESSAGES.get(code, "Unknown error")
49 return DongleEthError("Error sending command: %s" % message)
52# Dongle commands
53class _Command(IntEnum):
54 GET_PUBLIC_ADDRESS = 0x02,
55 SIGN_PERSONAL_MSG = 0x08
58class _Offset(IntEnum):
59 PUBKEY = 1
60 SIG_R = 1
61 SIG_S = 33
62 SIG_S_END = 65
65# Handles low-level communication with an ledger device running Ethereum App
66class DongleEth:
67 # APDU prefix
68 CLA = 0xE0
70 # Enumeration shorthands
71 CMD = _Command
72 OFF = _Offset
74 # Maximum size of msg allowed by sign command
75 MAX_MSG_LEN = 255
77 def __init__(self, debug):
78 self.debug = debug
80 # Connect to the dongle
81 def connect(self):
82 try:
83 self.dongle = getDongle(self.debug)
84 except CommException as e:
85 msg = "Error connecting: %s" % e.message
86 raise DongleEthError(msg)
88 # Disconnect from dongle
89 def disconnect(self):
90 try:
91 if self.dongle and self.dongle.opened:
92 self.dongle.close()
93 except CommException as e:
94 msg = "Error disconnecting: %s" % e.message
95 raise DongleEthError(msg)
97 def get_pubkey(self, path):
98 # Skip length byte
99 dongle_path = path.to_binary("big")[1:]
100 result = self._send_command(self.CMD.GET_PUBLIC_ADDRESS,
101 bytes([0x00, 0x00, len(dongle_path) + 1,
102 len(path.elements)]) + dongle_path)
103 pubkey = result[self.OFF.PUBKEY:self.OFF.PUBKEY + result[0]]
104 return bytes(pubkey)
106 def sign(self, path, msg):
107 if len(msg) > self.MAX_MSG_LEN:
108 raise DongleEthError("Message greater than maximum supported size of "
109 f"{self.MAX_MSG_LEN} bytes")
111 # Skip length byte
112 dongle_path = path.to_binary("big")[1:]
113 encoded_tx = struct.pack(">I", len(msg)) + msg
114 result = self._send_command(self.CMD.SIGN_PERSONAL_MSG,
115 bytes([0x00, 0x00,
116 len(dongle_path) + 1 + len(encoded_tx),
117 len(path.elements)])
118 + dongle_path + encoded_tx)
120 r = result[self.OFF.SIG_R:self.OFF.SIG_S].hex()
121 s = result[self.OFF.SIG_S:self.OFF.SIG_S_END].hex()
123 return ecdsa.util.sigencode_der(int(r, 16), int(s, 16), 0)
125 def _send_command(self, cmd, data):
126 try:
127 apdu = bytes([self.CLA, cmd]) + data
128 return self.dongle.exchange(apdu)
129 except CommException as e:
130 raise DongleEthError.from_error_code(e.sw)
131 except BaseException as e:
132 raise DongleEthError("Error sending command: %s" % str(e))