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
« 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.
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
33logging.disable(logging.CRITICAL)
35EXPECTED_UI_DERIVATION_PATH = "m/44'/0'/0'/0/0"
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)
50 paths = []
51 for path in PATHS.values():
52 paths.append(str(path))
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()
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)
75 self.signer_msg = b"HSM:SIGNER:4.0" + \
76 bytes.fromhex(self.pubkeys_hash.hex())
77 self.signer_hash = bytes.fromhex("ff" * 32)
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())
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)
96 with patch('builtins.open', mock_open(read_data='')) as file_mock:
97 do_verify_attestation(self.default_options)
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)
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])
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])
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))
133 def test_verify_attestation_no_pubkey(self, _):
134 options = self.default_options
135 options.pubkeys_file_path = None
137 with self.assertRaises(AdminError) as e:
138 do_verify_attestation(options)
139 self.assertEqual('No public keys file given', str(e.exception))
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)
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))
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)
159 self.assertEqual('Invalid public key for path invalid-path: invalid-key',
160 str(e.exception))
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
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)
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))
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'))
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)
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))
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
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)
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)
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))
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)
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)
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))
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
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)
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)
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))
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)
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)
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))