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

184 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-10-30 06:22 +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.6::" 

39UI_HEADER = b"HSM:UI:5.6" 

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'] = { 

94 "valid": True, 

95 "value": self.ui_msg.hex(), 

96 "tweak": self.ui_hash.hex(), 

97 "collateral": {}, 

98 } 

99 self.result['signer'] = { 

100 "valid": True, 

101 "value": self.signer_msg.hex(), 

102 "tweak": self.signer_hash.hex(), 

103 "collateral": {}, 

104 } 

105 

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

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

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

109 def test_verify_attestation_legacy(self, 

110 load_pubkeys_mock, 

111 certificate_mock, 

112 head_mock, _): 

113 self.signer_msg = LEGACY_SIGNER_HEADER + \ 

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

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

116 self.result['signer'] = { 

117 "valid": True, 

118 "value": self.signer_msg.hex(), 

119 "tweak": self.signer_hash.hex(), 

120 "collateral": {}, 

121 } 

122 

123 load_pubkeys_mock.return_value = self.public_keys 

124 att_cert = Mock() 

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

126 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

127 

128 do_verify_attestation(self.default_options) 

129 

130 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

132 certificate_mock.from_jsonfile.call_args_list) 

133 

134 expected_call_ui = call( 

135 [ 

136 "UI verified with:", 

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

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

139 f"{self.expected_ui_pubkey}", 

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

141 "Authorized signer iteration: 291", 

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

143 "Installed UI version: 5.6", 

144 ], 

145 fill="-", 

146 ) 

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

148 

149 expected_call_signer = call( 

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

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

152 "", 

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

154 "Installed Signer version: 5.3", 

155 ], 

156 fill="-", 

157 ) 

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

159 

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

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

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

163 def test_verify_attestation(self, 

164 load_pubkeys_mock, 

165 certificate_mock, 

166 head_mock, _): 

167 load_pubkeys_mock.return_value = self.public_keys 

168 att_cert = Mock() 

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

170 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

171 

172 do_verify_attestation(self.default_options) 

173 

174 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

176 certificate_mock.from_jsonfile.call_args_list) 

177 

178 expected_call_ui = call( 

179 [ 

180 "UI verified with:", 

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

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

183 f"{self.expected_ui_pubkey}", 

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

185 "Authorized signer iteration: 291", 

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

187 "Installed UI version: 5.6", 

188 ], 

189 fill="-", 

190 ) 

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

192 

193 expected_call_signer = call( 

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

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

196 "", 

197 "Installed Signer hash: ffffffffffffffffffffffffffffffffffffffffffff" + 

198 "ffffffffffffffffffff", 

199 "Installed Signer version: 5.6", 

200 "Platform: plf", 

201 "UD value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + 

202 "aaaaaaa", 

203 "Best block: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + 

204 "bbbbbbbbb", 

205 "Last transaction signed: cccccccccccccccc", 

206 "Timestamp: 171", 

207 ], 

208 fill="-", 

209 ) 

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

211 

212 def test_verify_attestation_no_certificate(self, _): 

213 options = self.default_options 

214 options.attestation_certificate_file_path = None 

215 with self.assertRaises(AdminError) as e: 

216 do_verify_attestation(options) 

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

218 

219 def test_verify_attestation_no_pubkey(self, _): 

220 options = self.default_options 

221 options.pubkeys_file_path = None 

222 

223 with self.assertRaises(AdminError) as e: 

224 do_verify_attestation(options) 

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

226 

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

228 def test_verify_attestation_no_ui_derivation_key(self, load_pubkeys_mock, _): 

229 incomplete_pubkeys = self.public_keys 

230 incomplete_pubkeys.pop(EXPECTED_UI_DERIVATION_PATH, None) 

231 load_pubkeys_mock.return_value = incomplete_pubkeys 

232 

233 with self.assertRaises(AdminError) as e: 

234 do_verify_attestation(self.default_options) 

235 

236 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

238 'not present in public key file'), 

239 str(e.exception)) 

240 

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

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

243 def test_verify_attestation_invalid_certificate(self, 

244 load_pubkeys_mock, 

245 certificate_mock, 

246 _): 

247 load_pubkeys_mock.return_value = self.public_keys 

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

249 

250 with self.assertRaises(AdminError) as e: 

251 do_verify_attestation(self.default_options) 

252 

253 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

255 str(e.exception)) 

256 

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

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

259 def test_verify_attestation_no_ui_att(self, 

260 load_pubkeys_mock, 

261 certificate_mock, 

262 _): 

263 load_pubkeys_mock.return_value = self.public_keys 

264 

265 result = self.result 

266 result.pop('ui', None) 

267 att_cert = Mock() 

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

269 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

270 

271 with self.assertRaises(AdminError) as e: 

272 do_verify_attestation(self.default_options) 

273 

274 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

276 str(e.exception)) 

277 

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

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

280 def test_verify_attestation_invalid_ui_att(self, 

281 load_pubkeys_mock, 

282 certificate_mock, 

283 _): 

284 load_pubkeys_mock.return_value = self.public_keys 

285 result = self.result 

286 result['ui'] = { 

287 "valid": False, 

288 "failed_element": "ui", 

289 } 

290 att_cert = Mock() 

291 att_cert.validate_and_get_values = Mock(return_value=result) 

292 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

293 

294 with self.assertRaises(AdminError) as e: 

295 do_verify_attestation(self.default_options) 

296 

297 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

299 str(e.exception)) 

300 

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

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

303 def test_verify_attestation_no_signer_att(self, 

304 load_pubkeys_mock, 

305 certificate_mock, 

306 _): 

307 load_pubkeys_mock.return_value = self.public_keys 

308 result = self.result 

309 result.pop('signer', None) 

310 att_cert = Mock() 

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

312 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

313 

314 with self.assertRaises(AdminError) as e: 

315 do_verify_attestation(self.default_options) 

316 

317 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

319 str(e.exception)) 

320 

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

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

323 def test_verify_attestation_invalid_signer_att(self, 

324 load_pubkeys_mock, 

325 certificate_mock, 

326 _): 

327 load_pubkeys_mock.return_value = self.public_keys 

328 result = self.result 

329 result['signer'] = { 

330 "valid": False, 

331 "failed_element": "signer", 

332 } 

333 att_cert = Mock() 

334 att_cert.validate_and_get_values = Mock(return_value=result) 

335 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

336 

337 with self.assertRaises(AdminError) as e: 

338 do_verify_attestation(self.default_options) 

339 

340 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

342 str(e.exception)) 

343 

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

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

346 def test_verify_attestation_invalid_signer_att_header(self, 

347 load_pubkeys_mock, 

348 certificate_mock, _): 

349 load_pubkeys_mock.return_value = self.public_keys 

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

351 self.result["signer"] = { 

352 "valid": True, 

353 "value": signer_header, 

354 "tweak": self.signer_hash.hex(), 

355 "collateral": {}, 

356 } 

357 att_cert = Mock() 

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

359 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

360 

361 with self.assertRaises(AdminError) as e: 

362 do_verify_attestation(self.default_options) 

363 

364 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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

366 str(e.exception)) 

367 

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

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

370 def test_verify_attestation_invalid_signer_att_msg_too_long(self, 

371 load_pubkeys_mock, 

372 certificate_mock, _): 

373 load_pubkeys_mock.return_value = self.public_keys 

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

375 self.result["signer"] = { 

376 "valid": True, 

377 "value": signer_header, 

378 "tweak": self.signer_hash.hex(), 

379 "collateral": {}, 

380 } 

381 att_cert = Mock() 

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

383 certificate_mock.from_jsonfile = Mock(return_value=att_cert) 

384 

385 with self.assertRaises(AdminError) as e: 

386 do_verify_attestation(self.default_options) 

387 

388 load_pubkeys_mock.assert_called_with(self.pubkeys_path) 

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