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

184 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 

26from admin.misc import AdminError 

27from admin.pubkeys import PATHS 

28from admin.verify_ledger_attestation import do_verify_attestation 

29import ecdsa 

30import secp256k1 as ec 

31import hashlib 

32import logging 

33 

34logging.disable(logging.CRITICAL) 

35 

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

37LEGACY_SIGNER_HEADER = b"HSM:SIGNER:5.3" 

38POWHSM_HEADER = b"POWHSM:5.5::" 

39UI_HEADER = b"HSM:UI:5.5" 

40 

41 

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

43class TestVerifyLedgerAttestation(TestCase): 

44 def setUp(self): 

45 self.certification_path = 'certification-path' 

46 self.pubkeys_path = 'pubkeys-path' 

47 options = { 

48 'attestation_certificate_file_path': self.certification_path, 

49 'pubkeys_file_path': self.pubkeys_path, 

50 'root_authority': None 

51 } 

52 self.default_options = SimpleNamespace(**options) 

53 

54 paths = [] 

55 for path in PATHS.values(): 

56 paths.append(str(path)) 

57 

58 self.public_keys = {} 

59 self.expected_pubkeys_output = [] 

60 pubkeys_hash = hashlib.sha256() 

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

62 for path in sorted(paths): 

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

64 self.public_keys[path] = ec.PublicKey( 

65 pubkey.to_string('compressed'), raw=True) 

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

67 self.expected_pubkeys_output.append( 

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

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

70 ) 

71 self.pubkeys_hash = pubkeys_hash.digest() 

72 self.expected_ui_pubkey = self.public_keys[EXPECTED_UI_DERIVATION_PATH]\ 

73 .serialize(compressed=True).hex() 

74 

75 self.ui_msg = UI_HEADER + \ 

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

77 bytes.fromhex(self.expected_ui_pubkey) + \ 

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

79 bytes.fromhex("0123") 

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

81 

82 self.signer_msg = POWHSM_HEADER + \ 

83 b'plf' + \ 

84 bytes.fromhex('aa'*32) + \ 

85 bytes.fromhex(self.pubkeys_hash.hex()) + \ 

86 bytes.fromhex('bb'*32) + \ 

87 bytes.fromhex('cc'*8) + \ 

88 bytes.fromhex('00'*7 + 'ab') 

89 

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

91 

92 self.result = {} 

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

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

95 

96 @patch("admin.verify_ledger_attestation.head") 

97 @patch("admin.verify_ledger_attestation.HSMCertificate") 

98 @patch("admin.verify_ledger_attestation.load_pubkeys") 

99 def test_verify_attestation_legacy(self, 

100 load_pubkeys_mock, 

101 certificate_mock, 

102 head_mock, _): 

103 self.signer_msg = LEGACY_SIGNER_HEADER + \ 

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

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

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

107 

108 load_pubkeys_mock.return_value = self.public_keys 

109 att_cert = Mock() 

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

111 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

112 

113 do_verify_attestation(self.default_options) 

114 

115 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

117 certificate_mock.from_jsonfile.call_args_list) 

118 

119 expected_call_ui = call( 

120 [ 

121 "UI verified with:", 

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

123 f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): " 

124 f"{self.expected_ui_pubkey}", 

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

126 "Authorized signer iteration: 291", 

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

128 "Installed UI version: 5.5", 

129 ], 

130 fill="-", 

131 ) 

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

133 

134 expected_call_signer = call( 

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

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

137 "", 

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

139 "Installed Signer version: 5.3", 

140 ], 

141 fill="-", 

142 ) 

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

144 

145 @patch("admin.verify_ledger_attestation.head") 

146 @patch("admin.verify_ledger_attestation.HSMCertificate") 

147 @patch("admin.verify_ledger_attestation.load_pubkeys") 

148 def test_verify_attestation(self, 

149 load_pubkeys_mock, 

150 certificate_mock, 

151 head_mock, _): 

152 load_pubkeys_mock.return_value = self.public_keys 

153 att_cert = Mock() 

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

155 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

156 

157 do_verify_attestation(self.default_options) 

158 

159 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

161 certificate_mock.from_jsonfile.call_args_list) 

162 

163 expected_call_ui = call( 

164 [ 

165 "UI verified with:", 

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

167 f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): " 

168 f"{self.expected_ui_pubkey}", 

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

170 "Authorized signer iteration: 291", 

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

172 "Installed UI version: 5.5", 

173 ], 

174 fill="-", 

175 ) 

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

177 

178 expected_call_signer = call( 

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

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

181 "", 

182 "Installed Signer hash: ffffffffffffffffffffffffffffffffffffffffffff" + 

183 "ffffffffffffffffffff", 

184 "Installed Signer version: 5.5", 

185 "Platform: plf", 

186 "UD value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 

187 "aaaaaaa", 

188 "Best block: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + 

189 "bbbbbbbbb", 

190 "Last transaction signed: cccccccccccccccc", 

191 "Timestamp: 171", 

192 ], 

193 fill="-", 

194 ) 

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

196 

197 def test_verify_attestation_no_certificate(self, _): 

198 options = self.default_options 

199 options.attestation_certificate_file_path = None 

200 with self.assertRaises(AdminError) as e: 

201 do_verify_attestation(options) 

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

203 

204 def test_verify_attestation_no_pubkey(self, _): 

205 options = self.default_options 

206 options.pubkeys_file_path = None 

207 

208 with self.assertRaises(AdminError) as e: 

209 do_verify_attestation(options) 

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

211 

212 @patch("admin.verify_ledger_attestation.load_pubkeys") 

213 def test_verify_attestation_no_ui_derivation_key(self, load_pubkeys_mock, _): 

214 incomplete_pubkeys = self.public_keys 

215 incomplete_pubkeys.pop(EXPECTED_UI_DERIVATION_PATH, None) 

216 load_pubkeys_mock.return_value = incomplete_pubkeys 

217 

218 with self.assertRaises(AdminError) as e: 

219 do_verify_attestation(self.default_options) 

220 

221 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

223 'not present in public key file'), 

224 str(e.exception)) 

225 

226 @patch("admin.verify_ledger_attestation.HSMCertificate") 

227 @patch("admin.verify_ledger_attestation.load_pubkeys") 

228 def test_verify_attestation_invalid_certificate(self, 

229 load_pubkeys_mock, 

230 certificate_mock, 

231 _): 

232 load_pubkeys_mock.return_value = self.public_keys 

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

234 

235 with self.assertRaises(AdminError) as e: 

236 do_verify_attestation(self.default_options) 

237 

238 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

240 str(e.exception)) 

241 

242 @patch("admin.verify_ledger_attestation.HSMCertificate") 

243 @patch("admin.verify_ledger_attestation.load_pubkeys") 

244 def test_verify_attestation_no_ui_att(self, 

245 load_pubkeys_mock, 

246 certificate_mock, 

247 _): 

248 load_pubkeys_mock.return_value = self.public_keys 

249 

250 result = self.result 

251 result.pop('ui', None) 

252 att_cert = Mock() 

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

254 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

255 

256 with self.assertRaises(AdminError) as e: 

257 do_verify_attestation(self.default_options) 

258 

259 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

261 str(e.exception)) 

262 

263 @patch("admin.verify_ledger_attestation.HSMCertificate") 

264 @patch("admin.verify_ledger_attestation.load_pubkeys") 

265 def test_verify_attestation_invalid_ui_att(self, 

266 load_pubkeys_mock, 

267 certificate_mock, 

268 _): 

269 load_pubkeys_mock.return_value = self.public_keys 

270 result = self.result 

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

272 att_cert = Mock() 

273 att_cert.validate_and_get_values = Mock(return_value=result) 

274 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

275 

276 with self.assertRaises(AdminError) as e: 

277 do_verify_attestation(self.default_options) 

278 

279 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

281 str(e.exception)) 

282 

283 @patch("admin.verify_ledger_attestation.HSMCertificate") 

284 @patch("admin.verify_ledger_attestation.load_pubkeys") 

285 def test_verify_attestation_no_signer_att(self, 

286 load_pubkeys_mock, 

287 certificate_mock, 

288 _): 

289 load_pubkeys_mock.return_value = self.public_keys 

290 result = self.result 

291 result.pop('signer', None) 

292 att_cert = Mock() 

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

294 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

295 

296 with self.assertRaises(AdminError) as e: 

297 do_verify_attestation(self.default_options) 

298 

299 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

301 str(e.exception)) 

302 

303 @patch("admin.verify_ledger_attestation.HSMCertificate") 

304 @patch("admin.verify_ledger_attestation.load_pubkeys") 

305 def test_verify_attestation_invalid_signer_att(self, 

306 load_pubkeys_mock, 

307 certificate_mock, 

308 _): 

309 load_pubkeys_mock.return_value = self.public_keys 

310 result = self.result 

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

312 att_cert = Mock() 

313 att_cert.validate_and_get_values = Mock(return_value=result) 

314 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

315 

316 with self.assertRaises(AdminError) as e: 

317 do_verify_attestation(self.default_options) 

318 

319 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

321 str(e.exception)) 

322 

323 @patch("admin.verify_ledger_attestation.HSMCertificate") 

324 @patch("admin.verify_ledger_attestation.load_pubkeys") 

325 def test_verify_attestation_invalid_signer_att_header(self, 

326 load_pubkeys_mock, 

327 certificate_mock, _): 

328 load_pubkeys_mock.return_value = self.public_keys 

329 signer_header = b"POWHSM:AAA::somerandomstuff".hex() 

330 self.result["signer"] = (True, signer_header, self.signer_hash.hex()) 

331 att_cert = Mock() 

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

333 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

334 

335 with self.assertRaises(AdminError) as e: 

336 do_verify_attestation(self.default_options) 

337 

338 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

339 self.assertEqual((f"Invalid Signer attestation message header: {signer_header}"), 

340 str(e.exception)) 

341 

342 @patch("admin.verify_ledger_attestation.HSMCertificate") 

343 @patch("admin.verify_ledger_attestation.load_pubkeys") 

344 def test_verify_attestation_invalid_signer_att_msg_too_long(self, 

345 load_pubkeys_mock, 

346 certificate_mock, _): 

347 load_pubkeys_mock.return_value = self.public_keys 

348 signer_header = (b"POWHSM:5.9::" + b"aa"*300).hex() 

349 self.result["signer"] = (True, signer_header, self.signer_hash.hex()) 

350 att_cert = Mock() 

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

352 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

353 

354 with self.assertRaises(AdminError) as e: 

355 do_verify_attestation(self.default_options) 

356 

357 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

358 self.assertIn("Signer attestation message length mismatch", str(e.exception))