Coverage for tests/admin/test_pubkeys.py: 100%
104 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.
23from types import SimpleNamespace
24from unittest import TestCase
25from unittest.mock import Mock, call, patch, mock_open
26from admin.misc import AdminError
27from admin.pubkeys import do_get_pubkeys, PATHS
28from ledger.hsm2dongle import HSM2Dongle
30import ecdsa
31import json
32import logging
34logging.disable(logging.CRITICAL)
37@patch("sys.stdout.write")
38@patch("time.sleep")
39@patch("admin.pubkeys.get_hsm")
40@patch("admin.pubkeys.do_unlock")
41class TestPubkeys(TestCase):
42 def setUp(self):
43 self.output_file_path = 'outfile'
44 options = {
45 'no_unlock': False,
46 'output_file_path': self.output_file_path,
47 'verbose': False
48 }
49 self.default_options = SimpleNamespace(**options)
51 self.dongle = Mock()
52 self.invalid_public_key = 'aa' * 65
54 self.public_keys = {}
55 for path in PATHS:
56 pubkey = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1).get_verifying_key()
57 self.public_keys[path] = pubkey.to_string().hex()
59 def test_pubkeys(self, unlock_mock, get_hsm, *_):
60 def get_pubkey_mock(path):
61 path_name = list(PATHS.keys())[list(PATHS.values()).index(path)]
62 return self.public_keys[path_name]
64 self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER)
65 self.dongle.get_public_key = Mock(side_effect=get_pubkey_mock)
66 self.dongle.is_onboarded = Mock(return_value=True)
67 get_hsm.return_value = self.dongle
69 with patch('builtins.open', mock_open()) as file_mock:
70 do_get_pubkeys(self.default_options)
72 self.assertEqual([call(self.output_file_path, 'w'),
73 call(f'{self.output_file_path}.json', 'w')],
74 file_mock.call_args_list)
76 json_dict = {}
77 path_name_padding = max(map(len, PATHS))
78 expected_call_list = [call(f"{'*' * 80}\n"),
79 call('Name \t\t\t Path \t\t\t\t Pubkey\n'),
80 call('==== \t\t\t ==== \t\t\t\t ======\n')]
81 for path_name in PATHS.keys():
82 pubkey = ecdsa.VerifyingKey.from_string(bytes.fromhex(
83 self.public_keys[path_name]), curve=ecdsa.SECP256k1)
84 pubkey_compressed = pubkey.to_string("compressed").hex()
85 pubkey_uncompressed = pubkey.to_string("uncompressed").hex()
86 expected_call_list.append(call(f'{path_name.ljust(path_name_padding)} '
87 f'\t\t {PATHS[path_name]} '
88 f'\t\t {pubkey_compressed}\n'))
89 json_dict[str(PATHS[path_name])] = pubkey_uncompressed
90 expected_call_list.append(call('*' * 80 + '\n'))
91 expected_call_list.append(call('%s\n' % json.dumps(json_dict, indent=2)))
93 self.assertEqual(expected_call_list, file_mock.return_value.write.call_args_list)
94 self.assertTrue(unlock_mock.called)
96 def test_pubkeys_no_unlock(self, unlock_mock, get_hsm, *_):
97 def get_pubkey_mock(path):
98 path_name = list(PATHS.keys())[list(PATHS.values()).index(path)]
99 return self.public_keys[path_name]
101 self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER)
102 self.dongle.get_public_key = Mock(side_effect=get_pubkey_mock)
103 self.dongle.is_onboarded = Mock(return_value=True)
104 get_hsm.return_value = self.dongle
106 options = self.default_options
107 options.no_unlock = True
108 with patch('builtins.open', mock_open()) as file_mock:
109 do_get_pubkeys(options)
111 self.assertEqual([call(self.output_file_path, 'w'),
112 call(f'{self.output_file_path}.json', 'w')],
113 file_mock.call_args_list)
115 json_dict = {}
116 path_name_padding = max(map(len, PATHS))
117 expected_call_list = [call(f"{'*' * 80}\n"),
118 call('Name \t\t\t Path \t\t\t\t Pubkey\n'),
119 call('==== \t\t\t ==== \t\t\t\t ======\n')]
120 for path_name in PATHS.keys():
121 pubkey = ecdsa.VerifyingKey.from_string(bytes.fromhex(
122 self.public_keys[path_name]), curve=ecdsa.SECP256k1)
123 pubkey_compressed = pubkey.to_string("compressed").hex()
124 pubkey_uncompressed = pubkey.to_string("uncompressed").hex()
125 expected_call_list.append(call(f'{path_name.ljust(path_name_padding)} '
126 f'\t\t {PATHS[path_name]} '
127 f'\t\t {pubkey_compressed}\n'))
128 json_dict[str(PATHS[path_name])] = pubkey_uncompressed
129 expected_call_list.append(call('*' * 80 + '\n'))
130 expected_call_list.append(call('%s\n' % json.dumps(json_dict, indent=2)))
132 self.assertEqual(expected_call_list, file_mock.return_value.write.call_args_list)
133 self.assertFalse(unlock_mock.called)
135 def test_pubkeys_unlock_error(self, unlock_mock, get_hsm, *_):
136 unlock_mock.side_effect = Exception("unlock-error")
137 self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER)
138 self.dongle.is_onboarded = Mock(return_value=True)
139 get_hsm.return_value = self.dongle
141 with self.assertRaises(AdminError) as e:
142 do_get_pubkeys(self.default_options)
144 self.assertTrue(unlock_mock.called)
145 self.assertEqual('Failed to unlock device: unlock-error', str(e.exception))
147 def test_pubkeys_invalid_pubkey(self, unlock_mock, get_hsm, *_):
148 self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER)
149 self.dongle.get_public_key = Mock(return_value=self.invalid_public_key)
150 self.dongle.is_onboarded = Mock(return_value=True)
151 get_hsm.return_value = self.dongle
153 with patch('builtins.open', mock_open()) as file_mock:
154 with self.assertRaises(AdminError) as e:
155 do_get_pubkeys(self.default_options)
157 self.assertTrue(unlock_mock.called)
158 self.assertEqual('Error writing output: '
159 'Invalid X9.62 encoding of the public point',
160 str(e.exception))
161 self.assertTrue(file_mock.return_value.write.called)
162 self.assertEqual([call(f"{'*' * 80}\n"),
163 call('Name \t\t\t Path \t\t\t\t Pubkey\n'),
164 call('==== \t\t\t ==== \t\t\t\t ======\n')],
165 file_mock.return_value.write.call_args_list)
167 def test_pubkeys_invalid_mode(self, unlock_mock, get_hsm, *_):
168 self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.BOOTLOADER)
169 self.dongle.is_onboarded = Mock(return_value=True)
170 get_hsm.return_value = self.dongle
172 with self.assertRaises(AdminError) as e:
173 do_get_pubkeys(self.default_options)
175 self.assertTrue(unlock_mock.called)
176 self.assertTrue(str(e.exception).startswith('Device not in app mode.'))