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

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. 

22 

23import ecdsa 

24import struct 

25 

26from enum import IntEnum 

27from ledgerblue.comm import getDongle 

28from ledgerblue.commException import CommException 

29 

30 

31class _ErrorCode(IntEnum): 

32 WRONG_APP = 0x6511 

33 INVALID_PATH = 0x6a15 

34 DONGLE_LOCKED = 0x6b0c 

35 

36 

37class DongleEthError(RuntimeError): 

38 ERR = _ErrorCode 

39 

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 } 

45 

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) 

50 

51 

52# Dongle commands 

53class _Command(IntEnum): 

54 GET_PUBLIC_ADDRESS = 0x02, 

55 SIGN_PERSONAL_MSG = 0x08 

56 

57 

58class _Offset(IntEnum): 

59 PUBKEY = 1 

60 SIG_R = 1 

61 SIG_S = 33 

62 SIG_S_END = 65 

63 

64 

65# Handles low-level communication with an ledger device running Ethereum App 

66class DongleEth: 

67 # APDU prefix 

68 CLA = 0xE0 

69 

70 # Enumeration shorthands 

71 CMD = _Command 

72 OFF = _Offset 

73 

74 # Maximum size of msg allowed by sign command 

75 MAX_MSG_LEN = 255 

76 

77 def __init__(self, debug): 

78 self.debug = debug 

79 

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) 

87 

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) 

96 

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) 

105 

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") 

110 

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) 

119 

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

122 

123 return ecdsa.util.sigencode_der(int(r, 16), int(s, 16), 0) 

124 

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