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

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 

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 

29 

30import ecdsa 

31import json 

32import logging 

33 

34logging.disable(logging.CRITICAL) 

35 

36 

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) 

50 

51 self.dongle = Mock() 

52 self.invalid_public_key = 'aa' * 65 

53 

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

58 

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] 

63 

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 

68 

69 with patch('builtins.open', mock_open()) as file_mock: 

70 do_get_pubkeys(self.default_options) 

71 

72 self.assertEqual([call(self.output_file_path, 'w'), 

73 call(f'{self.output_file_path}.json', 'w')], 

74 file_mock.call_args_list) 

75 

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

92 

93 self.assertEqual(expected_call_list, file_mock.return_value.write.call_args_list) 

94 self.assertTrue(unlock_mock.called) 

95 

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] 

100 

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 

105 

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) 

110 

111 self.assertEqual([call(self.output_file_path, 'w'), 

112 call(f'{self.output_file_path}.json', 'w')], 

113 file_mock.call_args_list) 

114 

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

131 

132 self.assertEqual(expected_call_list, file_mock.return_value.write.call_args_list) 

133 self.assertFalse(unlock_mock.called) 

134 

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 

140 

141 with self.assertRaises(AdminError) as e: 

142 do_get_pubkeys(self.default_options) 

143 

144 self.assertTrue(unlock_mock.called) 

145 self.assertEqual('Failed to unlock device: unlock-error', str(e.exception)) 

146 

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 

152 

153 with patch('builtins.open', mock_open()) as file_mock: 

154 with self.assertRaises(AdminError) as e: 

155 do_get_pubkeys(self.default_options) 

156 

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) 

166 

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 

171 

172 with self.assertRaises(AdminError) as e: 

173 do_get_pubkeys(self.default_options) 

174 

175 self.assertTrue(unlock_mock.called) 

176 self.assertTrue(str(e.exception).startswith('Device not in app mode.'))