Coverage for tests/admin/test_verify_attestation.py: 100%

147 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-05 20:41 +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 PATHS 

28from admin.verify_attestation import do_verify_attestation 

29import ecdsa 

30import hashlib 

31import logging 

32 

33logging.disable(logging.CRITICAL) 

34 

35EXPECTED_UI_DERIVATION_PATH = "m/44'/0'/0'/0/0" 

36 

37 

38@patch("sys.stdout.write") 

39class TestVerifyAttestation(TestCase): 

40 def setUp(self): 

41 self.certification_path = 'certification-path' 

42 self.pubkeys_path = 'pubkeys-path' 

43 options = { 

44 'attestation_certificate_file_path': self.certification_path, 

45 'pubkeys_file_path': self.pubkeys_path, 

46 'root_authority': None 

47 } 

48 self.default_options = SimpleNamespace(**options) 

49 

50 paths = [] 

51 for path in PATHS.values(): 

52 paths.append(str(path)) 

53 

54 self.public_keys = {} 

55 self.expected_pubkeys_output = [] 

56 pubkeys_hash = hashlib.sha256() 

57 path_name_padding = max(map(len, paths)) 

58 for path in sorted(paths): 

59 pubkey = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1).get_verifying_key() 

60 self.public_keys[path] = pubkey.to_string('compressed').hex() 

61 pubkeys_hash.update(pubkey.to_string('uncompressed')) 

62 self.expected_pubkeys_output.append( 

63 f"{(path + ':').ljust(path_name_padding+1)} " 

64 f"{pubkey.to_string('compressed').hex()}" 

65 ) 

66 self.pubkeys_hash = pubkeys_hash.digest() 

67 

68 self.ui_msg = b"HSM:UI:4.0" + \ 

69 bytes.fromhex("aa"*32) + \ 

70 bytes.fromhex("bb"*33) + \ 

71 bytes.fromhex("cc"*32) + \ 

72 bytes.fromhex("0123") 

73 self.ui_hash = bytes.fromhex("ee" * 32) 

74 

75 self.signer_msg = b"HSM:SIGNER:4.0" + \ 

76 bytes.fromhex(self.pubkeys_hash.hex()) 

77 self.signer_hash = bytes.fromhex("ff" * 32) 

78 

79 self.result = {} 

80 self.result['ui'] = (True, self.ui_msg.hex(), self.ui_hash.hex()) 

81 self.result['signer'] = (True, self.signer_msg.hex(), self.signer_hash.hex()) 

82 

83 @patch("admin.verify_attestation.head") 

84 @patch("admin.verify_attestation.HSMCertificate") 

85 @patch("json.loads") 

86 def test_verify_attestation(self, 

87 loads_mock, 

88 certificate_mock, 

89 head_mock, 

90 _): 

91 loads_mock.return_value = self.public_keys 

92 att_cert = Mock() 

93 att_cert.validate_and_get_values = Mock(return_value=self.result) 

94 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

95 

96 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

97 do_verify_attestation(self.default_options) 

98 

99 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

100 self.assertEqual([call(self.certification_path)], 

101 certificate_mock.from_jsonfile.call_args_list) 

102 

103 expected_call_ui = call( 

104 [ 

105 "UI verified with:", 

106 f"UD value: {'aa'*32}", 

107 f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): {'bb'*33}", 

108 f"Authorized signer hash: {'cc'*32}", 

109 "Authorized signer iteration: 291", 

110 f"Installed UI hash: {'ee'*32}", 

111 ], 

112 fill="-", 

113 ) 

114 self.assertEqual(expected_call_ui, head_mock.call_args_list[1]) 

115 

116 expected_call_signer = call( 

117 ["Signer verified with public keys:"] + self.expected_pubkeys_output + [ 

118 "", 

119 f"Hash: {self.pubkeys_hash.hex()}", 

120 f"Installed Signer hash: {'ff'*32}", 

121 ], 

122 fill="-", 

123 ) 

124 self.assertEqual(expected_call_signer, head_mock.call_args_list[2]) 

125 

126 def test_verify_attestation_no_certificate(self, _): 

127 options = self.default_options 

128 options.attestation_certificate_file_path = None 

129 with self.assertRaises(AdminError) as e: 

130 do_verify_attestation(options) 

131 self.assertEqual('No attestation certificate file given', str(e.exception)) 

132 

133 def test_verify_attestation_no_pubkey(self, _): 

134 options = self.default_options 

135 options.pubkeys_file_path = None 

136 

137 with self.assertRaises(AdminError) as e: 

138 do_verify_attestation(options) 

139 self.assertEqual('No public keys file given', str(e.exception)) 

140 

141 @patch("json.loads") 

142 def test_verify_attestation_invalid_pubkeys_map(self, loads_mock, _): 

143 loads_mock.return_value = 'invalid-json' 

144 with patch('builtins.open', mock_open(read_data='')): 

145 with self.assertRaises(ValueError) as e: 

146 do_verify_attestation(self.default_options) 

147 

148 self.assertEqual(('Unable to read public keys from "pubkeys-path": Public keys ' 

149 'file must contain an object as a top level element'), 

150 str(e.exception)) 

151 

152 @patch("json.loads") 

153 def test_verify_attestation_invalid_pubkey(self, loads_mock, _): 

154 loads_mock.return_value = {'invalid-path': 'invalid-key'} 

155 with patch('builtins.open', mock_open(read_data='')): 

156 with self.assertRaises(AdminError) as e: 

157 do_verify_attestation(self.default_options) 

158 

159 self.assertEqual('Invalid public key for path invalid-path: invalid-key', 

160 str(e.exception)) 

161 

162 @patch("json.loads") 

163 def test_verify_attestation_no_ui_derivation_key(self, loads_mock, _): 

164 incomplete_pubkeys = self.public_keys 

165 incomplete_pubkeys.pop(EXPECTED_UI_DERIVATION_PATH, None) 

166 loads_mock.return_value = incomplete_pubkeys 

167 

168 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

169 with self.assertRaises(AdminError) as e: 

170 do_verify_attestation(self.default_options) 

171 

172 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

173 self.assertEqual((f'Public key with path {EXPECTED_UI_DERIVATION_PATH} ' 

174 'not present in public key file'), 

175 str(e.exception)) 

176 

177 @patch("admin.verify_attestation.HSMCertificate") 

178 @patch("json.loads") 

179 def test_verify_attestation_invalid_certificate(self, 

180 loads_mock, 

181 certificate_mock, 

182 _): 

183 loads_mock.return_value = self.public_keys 

184 certificate_mock.from_jsonfile = Mock(side_effect=Exception('error-msg')) 

185 

186 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

187 with self.assertRaises(AdminError) as e: 

188 do_verify_attestation(self.default_options) 

189 

190 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

191 self.assertEqual('While loading the attestation certificate file: error-msg', 

192 str(e.exception)) 

193 

194 @patch("admin.verify_attestation.HSMCertificate") 

195 @patch("json.loads") 

196 def test_verify_attestation_no_ui_att(self, 

197 loads_mock, 

198 certificate_mock, 

199 _): 

200 loads_mock.return_value = self.public_keys 

201 

202 result = self.result 

203 result.pop('ui', None) 

204 att_cert = Mock() 

205 att_cert.validate_and_get_values = Mock(return_value=self.result) 

206 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

207 

208 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

209 with self.assertRaises(AdminError) as e: 

210 do_verify_attestation(self.default_options) 

211 

212 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

213 self.assertEqual('Certificate does not contain a UI attestation', 

214 str(e.exception)) 

215 

216 @patch("admin.verify_attestation.HSMCertificate") 

217 @patch("json.loads") 

218 def test_verify_attestation_invalid_ui_att(self, 

219 loads_mock, 

220 certificate_mock, 

221 _): 

222 loads_mock.return_value = self.public_keys 

223 result = self.result 

224 result['ui'] = (False, 'ui') 

225 att_cert = Mock() 

226 att_cert.validate_and_get_values = Mock(return_value=result) 

227 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

228 

229 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

230 with self.assertRaises(AdminError) as e: 

231 do_verify_attestation(self.default_options) 

232 

233 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

234 self.assertEqual("Invalid UI attestation: error validating 'ui'", 

235 str(e.exception)) 

236 

237 @patch("admin.verify_attestation.HSMCertificate") 

238 @patch("json.loads") 

239 def test_verify_attestation_no_signer_att(self, 

240 loads_mock, 

241 certificate_mock, 

242 _): 

243 loads_mock.return_value = self.public_keys 

244 

245 result = self.result 

246 result.pop('signer', None) 

247 att_cert = Mock() 

248 att_cert.validate_and_get_values = Mock(return_value=self.result) 

249 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

250 

251 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

252 with self.assertRaises(AdminError) as e: 

253 do_verify_attestation(self.default_options) 

254 

255 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

256 self.assertEqual('Certificate does not contain a Signer attestation', 

257 str(e.exception)) 

258 

259 @patch("admin.verify_attestation.HSMCertificate") 

260 @patch("json.loads") 

261 def test_verify_attestation_invalid_signer_att(self, 

262 loads_mock, 

263 certificate_mock, 

264 _): 

265 loads_mock.return_value = self.public_keys 

266 result = self.result 

267 result['signer'] = (False, 'signer') 

268 att_cert = Mock() 

269 att_cert.validate_and_get_values = Mock(return_value=result) 

270 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

271 

272 with patch('builtins.open', mock_open(read_data='')) as file_mock: 

273 with self.assertRaises(AdminError) as e: 

274 do_verify_attestation(self.default_options) 

275 

276 self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) 

277 self.assertEqual(("Invalid Signer attestation: error validating 'signer'"), 

278 str(e.exception))