Coverage for tests/admin/test_dongle_eth.py: 100%
109 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
24from admin.bip32 import BIP32Path
25from admin.dongle_eth import DongleEth, DongleEthError
26from ledgerblue.commException import CommException
27import struct
28from unittest import TestCase
29from unittest.mock import call, Mock, patch
32class TestDongleEth(TestCase):
33 @classmethod
34 def setUpClass(cls):
35 privkey = ecdsa.SigningKey.from_string(
36 bytes.fromhex("aa"*32), curve=ecdsa.SECP256k1)
37 cls.pubkey = privkey.get_verifying_key().to_string("uncompressed")
39 @patch("admin.dongle_eth.getDongle")
40 def setUp(self, getDongleMock):
41 dongle_mock = Mock()
42 getDongleMock.return_value = dongle_mock
43 self.exchange_mock = dongle_mock.exchange
44 self.eth = DongleEth(True)
45 self.eth.connect()
47 def tearDown(self):
48 self.eth.disconnect()
50 def test_get_pubkey_ok(self):
51 self.exchange_mock.side_effect = [bytes([0x41]) + self.pubkey]
53 eth_path = BIP32Path("m/44'/60'/0'/0/0")
54 self.assertEqual(self.pubkey, self.eth.get_pubkey(eth_path))
56 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
57 self.assertEqual([call(bytes([0xE0, 0x02, 0x00, 0x00, len(encoded_path) + 1,
58 len(eth_path.elements)]) + encoded_path)],
59 self.exchange_mock.call_args_list)
61 def test_get_pubkey_invalid_path(self):
62 self.exchange_mock.side_effect = CommException("msg", 0x6a15)
64 eth_path = BIP32Path("m/44'/137'/0'/0/0")
65 with self.assertRaises(DongleEthError):
66 self.eth.get_pubkey(eth_path)
68 encoded_path = bytes.fromhex('8000002c80000089800000000000000000000000')
69 self.assertEqual([call(bytes([0xE0, 0x02, 0x00, 0x00, len(encoded_path) + 1,
70 len(eth_path.elements)]) + encoded_path)],
71 self.exchange_mock.call_args_list)
73 def test_get_pubkey_wrong_app(self):
74 self.exchange_mock.side_effect = CommException("msg", 0x6511)
76 eth_path = BIP32Path("m/44'/60'/0'/0/0")
77 with self.assertRaises(DongleEthError):
78 self.eth.get_pubkey(eth_path)
80 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
81 self.assertEqual([call(bytes([0xE0, 0x02, 0x00, 0x00, len(encoded_path) + 1,
82 len(eth_path.elements)]) + encoded_path)],
83 self.exchange_mock.call_args_list)
85 def test_get_pubkey_device_locked(self):
86 self.exchange_mock.side_effect = CommException("msg", 0x6b0c)
88 eth_path = BIP32Path("m/44'/60'/0'/0/0")
89 with self.assertRaises(DongleEthError):
90 self.eth.get_pubkey(eth_path)
92 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
93 self.assertEqual([call(bytes([0xE0, 0x02, 0x00, 0x00, len(encoded_path) + 1,
94 len(eth_path.elements)]) + encoded_path)],
95 self.exchange_mock.call_args_list)
97 def test_get_pubkey_dongle_error(self):
98 self.exchange_mock.side_effect = Exception('error-msg')
100 eth_path = BIP32Path("m/44'/60'/0'/0/0")
101 with self.assertRaises(DongleEthError):
102 self.eth.get_pubkey(eth_path)
104 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
105 self.assertEqual([call(bytes([0xE0, 0x02, 0x00, 0x00, len(encoded_path) + 1,
106 len(eth_path.elements)]) + encoded_path)],
107 self.exchange_mock.call_args_list)
109 def test_sign_message_ok(self):
110 v = 'aa'
111 r = 'bb' * 32
112 s = 'cc' * 32
113 self.exchange_mock.side_effect = [bytes.fromhex(v + r + s)]
115 expected_signature = ecdsa.util.sigencode_der(int(r, 16), int(s, 16), 0)
116 eth_path = BIP32Path("m/44'/60'/0'/0/0")
117 msg = ('aa' * 72).encode()
118 self.assertEqual(expected_signature, self.eth.sign(eth_path, msg))
120 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
121 encoded_tx = struct.pack(">I", len(msg)) + msg
122 self.assertEqual([call(bytes([0xE0, 0x08, 0x00, 0x00,
123 len(encoded_path) + 1 + len(encoded_tx),
124 len(eth_path.elements)]) + encoded_path + encoded_tx)],
125 self.exchange_mock.call_args_list)
127 def test_sign_message_invalid_path(self):
128 self.exchange_mock.side_effect = CommException("msg", 0x6a15)
130 eth_path = BIP32Path("m/44'/137'/0'/0/0")
131 msg = ('aa' * 72).encode()
132 with self.assertRaises(DongleEthError):
133 self.eth.sign(eth_path, msg)
135 encoded_path = bytes.fromhex('8000002c80000089800000000000000000000000')
136 encoded_tx = struct.pack(">I", len(msg)) + msg
137 self.assertEqual([call(bytes([0xE0, 0x08, 0x00, 0x00,
138 len(encoded_path) + 1 + len(encoded_tx),
139 len(eth_path.elements)]) + encoded_path + encoded_tx)],
140 self.exchange_mock.call_args_list)
142 def test_sign_message_wrong_app(self):
143 self.exchange_mock.side_effect = CommException("msg", 0x6511)
145 eth_path = BIP32Path("m/44'/60'/0'/0/0")
146 msg = ('aa' * 72).encode()
147 with self.assertRaises(DongleEthError):
148 self.eth.sign(eth_path, msg)
150 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
151 encoded_tx = struct.pack(">I", len(msg)) + msg
152 self.assertEqual([call(bytes([0xE0, 0x08, 0x00, 0x00,
153 len(encoded_path) + 1 + len(encoded_tx),
154 len(eth_path.elements)]) + encoded_path + encoded_tx)],
155 self.exchange_mock.call_args_list)
157 def test_sign_message_device_locked(self):
158 self.exchange_mock.side_effect = CommException("msg", 0x6b0c)
160 eth_path = BIP32Path("m/44'/60'/0'/0/0")
161 msg = ('aa' * 72).encode()
162 with self.assertRaises(DongleEthError):
163 self.eth.sign(eth_path, msg)
165 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
166 encoded_tx = struct.pack(">I", len(msg)) + msg
167 self.assertEqual([call(bytes([0xE0, 0x08, 0x00, 0x00,
168 len(encoded_path) + 1 + len(encoded_tx),
169 len(eth_path.elements)]) + encoded_path + encoded_tx)],
170 self.exchange_mock.call_args_list)
172 def test_sign_message_dongle_error(self):
173 self.exchange_mock.side_effect = Exception('error-msg')
175 eth_path = BIP32Path("m/44'/60'/0'/0/0")
176 msg = ('aa' * 72).encode()
177 with self.assertRaises(DongleEthError):
178 self.eth.sign(eth_path, msg)
180 encoded_path = bytes.fromhex('8000002c8000003c800000000000000000000000')
181 encoded_tx = struct.pack(">I", len(msg)) + msg
182 self.assertEqual([call(bytes([0xE0, 0x08, 0x00, 0x00,
183 len(encoded_path) + 1 + len(encoded_tx),
184 len(eth_path.elements)]) + encoded_path + encoded_tx)],
185 self.exchange_mock.call_args_list)
187 def test_sign_msg_too_big(self):
188 ethpath = BIP32Path("m/44'/60'/0'/0/0")
189 msg = ('aa' * 300).encode()
190 with self.assertRaises(DongleEthError):
191 self.eth.sign(ethpath, msg)
193 self.assertFalse(self.exchange_mock.called)