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
« 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.
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.5::"
39UI_HEADER = b"HSM:UI:5.5"
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'] = (True, self.ui_msg.hex(), self.ui_hash.hex())
94 self.result['signer'] = (True, self.signer_msg.hex(), self.signer_hash.hex())
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())
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)
113 do_verify_attestation(self.default_options)
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)
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])
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])
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)
157 do_verify_attestation(self.default_options)
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)
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])
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])
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))
204 def test_verify_attestation_no_pubkey(self, _):
205 options = self.default_options
206 options.pubkeys_file_path = None
208 with self.assertRaises(AdminError) as e:
209 do_verify_attestation(options)
210 self.assertEqual('No public keys file given', str(e.exception))
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
218 with self.assertRaises(AdminError) as e:
219 do_verify_attestation(self.default_options)
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))
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'))
235 with self.assertRaises(AdminError) as e:
236 do_verify_attestation(self.default_options)
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))
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
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)
256 with self.assertRaises(AdminError) as e:
257 do_verify_attestation(self.default_options)
259 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
260 self.assertEqual('Certificate does not contain a UI attestation',
261 str(e.exception))
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)
276 with self.assertRaises(AdminError) as e:
277 do_verify_attestation(self.default_options)
279 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
280 self.assertEqual("Invalid UI attestation: error validating 'ui'",
281 str(e.exception))
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)
296 with self.assertRaises(AdminError) as e:
297 do_verify_attestation(self.default_options)
299 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
300 self.assertEqual('Certificate does not contain a Signer attestation',
301 str(e.exception))
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)
316 with self.assertRaises(AdminError) as e:
317 do_verify_attestation(self.default_options)
319 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
320 self.assertEqual(("Invalid Signer attestation: error validating 'signer'"),
321 str(e.exception))
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)
335 with self.assertRaises(AdminError) as e:
336 do_verify_attestation(self.default_options)
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))
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)
354 with self.assertRaises(AdminError) as e:
355 do_verify_attestation(self.default_options)
357 load_pubkeys_mock.assert_called_with(self.pubkeys_path)
358 self.assertIn("Signer attestation message length mismatch", str(e.exception))