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
« 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.
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
34logging.disable(logging.CRITICAL)
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"
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)
54 paths = []
55 for path in PATHS.values():
56 paths.append(str(path))
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()
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)
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')
90 self.signer_hash = bytes.fromhex("ff" * 32)
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 }
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 }
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)
128 do_verify_attestation(self.default_options)
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)
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])
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])
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)
172 do_verify_attestation(self.default_options)
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)
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])
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])
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))
219 def test_verify_attestation_no_pubkey(self, _):
220 options = self.default_options
221 options.pubkeys_file_path = None
223 with self.assertRaises(AdminError) as e:
224 do_verify_attestation(options)
225 self.assertEqual('No public keys file given', str(e.exception))
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
233 with self.assertRaises(AdminError) as e:
234 do_verify_attestation(self.default_options)
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))
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'))
250 with self.assertRaises(AdminError) as e:
251 do_verify_attestation(self.default_options)
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))
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
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)
271 with self.assertRaises(AdminError) as e:
272 do_verify_attestation(self.default_options)
274 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
275 self.assertEqual('Certificate does not contain a UI attestation',
276 str(e.exception))
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)
294 with self.assertRaises(AdminError) as e:
295 do_verify_attestation(self.default_options)
297 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
298 self.assertEqual("Invalid UI attestation: error validating 'ui'",
299 str(e.exception))
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)
314 with self.assertRaises(AdminError) as e:
315 do_verify_attestation(self.default_options)
317 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
318 self.assertEqual('Certificate does not contain a Signer attestation',
319 str(e.exception))
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)
337 with self.assertRaises(AdminError) as e:
338 do_verify_attestation(self.default_options)
340 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
341 self.assertEqual(("Invalid Signer attestation: error validating 'signer'"),
342 str(e.exception))
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)
361 with self.assertRaises(AdminError) as e:
362 do_verify_attestation(self.default_options)
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))
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)
385 with self.assertRaises(AdminError) as e:
386 do_verify_attestation(self.default_options)
388 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
389 self.assertIn("Signer attestation message length mismatch", str(e.exception))