Coverage for tests/admin/test_sgx_migration_authorization.py: 100%
122 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 unittest import TestCase
24from unittest.mock import patch, call, mock_open
25from admin.sgx_migration_authorization import SGXMigrationAuthorization, SGXMigrationSpec
26from comm.utils import keccak_256
27import json
29import logging
31logging.disable(logging.CRITICAL)
34class TestSGXMigrationAuthorization(TestCase):
35 def setUp(self):
36 # Sample valid DER signatures
37 self.sigs = [
38 "3045022100f31ee73e3b10c5d610d9f5501e12ce1f2fd31182d0630c8e0db75fba3f35bbe3022056d0703a27937aec36a0a05bd5b85de6144279ab3a66faf266378ce42a838831", # noqa E501
39 "3046022100d2e039915b4decd3d32d613bdcfc84090560e0e714284ff4c3b454b563d81c7c022100ff0de20f22f75a87cf546a6e3dd9dada082b0bdd01862e1c566e5bdd67f0c3b1", # noqa E501
40 ]
42 # Sample mrenclave values (32-byte hex strings)
43 self.exporter_mrenclave = "aa" * 32
44 self.importer_mrenclave = "bb" * 32
46 # Create migration spec
47 self.migration_spec = SGXMigrationSpec({
48 "exporter": self.exporter_mrenclave,
49 "importer": self.importer_mrenclave
50 })
52 # Create SGX authorization instance
53 self.sa = SGXMigrationAuthorization(self.migration_spec, self.sigs)
55 def test_migration_spec_n_signatures(self):
56 # Test basic property getters and verify signatures list is copied
57 self.assertEqual(self.sa.migration_spec.exporter, self.exporter_mrenclave)
58 self.assertEqual(self.sa.migration_spec.importer, self.importer_mrenclave)
59 self.assertEqual(self.sa.signatures, self.sigs)
60 self.assertIsNot(self.sa.signatures, self.sigs) # Verify list is copied
62 def test_invalid_migration_spec(self):
63 # Test constructor with invalid migration spec
64 with self.assertRaises(ValueError):
65 SGXMigrationAuthorization("not-a-migration-spec", self.sigs)
67 def test_invalid_signatures(self):
68 # Test constructor with invalid signatures (non-list)
69 with self.assertRaises(ValueError):
70 SGXMigrationAuthorization(self.migration_spec, "not-an-array")
72 def test_invalid_signature(self):
73 # Test constructor with invalid signature format
74 with self.assertRaises(ValueError):
75 SGXMigrationAuthorization(
76 self.migration_spec,
77 [self.sigs[0], "not-a-valid-signature"]
78 )
80 def test_to_dict(self):
81 # Test dictionary conversion
82 expected_dict = {
83 "version": 1,
84 "hashes": {
85 "exporter": self.exporter_mrenclave,
86 "importer": self.importer_mrenclave,
87 },
88 "signatures": self.sigs
89 }
90 self.assertEqual(expected_dict, self.sa.to_dict())
92 def test_add_signature(self):
93 # Test adding a valid signature
94 new_sig = "3045022100d2dac5b641d6a454cacdff045ab428bfc4c86e"\
95 "004ff69728050a33788f6e9e7602207e8b3536a7a50185e2"\
96 "7219237358823b98678fe32aa7a30b31155cbffad3747d"
97 self.sa.add_signature(new_sig)
98 self.assertEqual(self.sa.signatures, self.sigs + [new_sig])
100 def test_add_duplicate_signature_not_allowed(self):
101 # Adding the same signature should not be allowed
102 with self.assertRaises(ValueError) as e:
103 self.sa.add_signature(self.sigs[0])
104 self.assertEqual(
105 str(e.exception),
106 "Signature already exists"
107 )
109 def test_add_invalid_signature(self):
110 # Test signature validation with various invalid formats
111 invalid_signatures = [
112 "not-a-signature",
113 "0x1234", # Too short
114 "0x" + "1" * 100, # Too long
115 ]
116 for sig in invalid_signatures:
117 with self.assertRaises(ValueError):
118 self.sa.add_signature(sig)
120 def test_save_to_jsonfile(self):
121 # Test saving authorization to JSON file
122 with patch("builtins.open", mock_open()) as open_mock:
123 self.sa.save_to_jsonfile("/a/file/path.json")
125 self.assertEqual([call("/a/file/path.json", "w")], open_mock.call_args_list)
126 self.assertEqual([call(json.dumps(self.sa.to_dict(), indent=2))],
127 open_mock.return_value.write.call_args_list)
129 def test_from_jsonfile(self):
130 # Test loading authorization from valid JSON file
131 jsonsample = """
132 {
133 "version": 1,
134 "hashes": {
135 "exporter": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
136 "importer": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
137 },
138 "signatures": [
139 "3045022100f31ee73e3b10c5d610d9f5501e12ce1f2fd31182d0630c8e0db75fba3f35bbe3022056d0703a27937aec36a0a05bd5b85de6144279ab3a66faf266378ce42a838831",
140 "3046022100d2e039915b4decd3d32d613bdcfc84090560e0e714284ff4c3b454b563d81c7c022100ff0de20f22f75a87cf546a6e3dd9dada082b0bdd01862e1c566e5bdd67f0c3b1"
141 ]
142 }
143 """ # noqa E501
145 with patch("builtins.open", mock_open()) as open_mock:
146 open_mock.return_value.read.return_value = jsonsample
147 sa = SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json")
149 self.assertEqual([call("/an/existing/file.json", "r")], open_mock.call_args_list)
150 self.assertEqual("aa" * 32, sa.migration_spec.exporter)
151 self.assertEqual("bb" * 32, sa.migration_spec.importer)
152 self.assertEqual(
153 [
154 "3045022100f31ee73e3b10c5d610d9f5501e12ce1f2fd31182d0630c8e0db75fba3f35bbe3022056d0703a27937aec36a0a05bd5b85de6144279ab3a66faf266378ce42a838831", # noqa E501
155 "3046022100d2e039915b4decd3d32d613bdcfc84090560e0e714284ff4c3b454b563d81c7c022100ff0de20f22f75a87cf546a6e3dd9dada082b0bdd01862e1c566e5bdd67f0c3b1" # noqa E501
156 ],
157 sa.signatures)
159 def test_from_jsonfile_invalid_json(self):
160 # Test loading authorization from invalid JSON file
161 jsonsample = """
162 { THISISNOTJSON
163 "version": 1,
164 "hashes": {
165 "exporter": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
166 "importer": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
167 },
168 "signatures": []
169 }
170 """ # noqa E501
172 with patch("builtins.open", mock_open()) as open_mock:
173 open_mock.return_value.read.return_value = jsonsample
175 with self.assertRaises(ValueError):
176 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json")
178 def test_from_jsonfile_invalid_version(self):
179 # Test loading authorization with invalid version
180 jsonsample = """
181 {
182 "version": 2,
183 "hashes": {
184 "exporter": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
185 "importer": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
186 },
187 "signatures": []
188 }
189 """ # noqa E501
191 with patch("builtins.open", mock_open()) as open_mock:
192 open_mock.return_value.read.return_value = jsonsample
194 with self.assertRaises(ValueError):
195 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json")
197 def test_from_jsonfile_invalid_migration_spec(self):
198 # Test loading authorization with invalid migration spec
199 jsonsample = """
200 {
201 "version": 1,
202 "hashes": {
203 "exporter": "not-a-mrenclave",
204 "importer": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
205 },
206 "signatures": []
207 }
208 """
210 with patch("builtins.open", mock_open()) as open_mock:
211 open_mock.return_value.read.return_value = jsonsample
213 with self.assertRaises(ValueError):
214 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json")
216 def test_from_jsonfile_invalid_signatures(self):
217 # Test loading authorization with invalid signatures format
218 jsonsample = """
219 {
220 "version": 1,
221 "hashes": {
222 "exporter": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
223 "importer": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
224 },
225 "signatures": "not-signatures"
226 }
227 """ # noqa E501
229 with patch("builtins.open", mock_open()) as open_mock:
230 open_mock.return_value.read.return_value = jsonsample
232 with self.assertRaises(ValueError):
233 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json")
235 def test_from_jsonfile_invalid_signature(self):
236 # Test loading authorization with invalid signature format
237 jsonsample = """
238 {
239 "version": 1,
240 "hashes": {
241 "exporter": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
242 "importer": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
243 },
244 "signatures": [
245 "not-a-signature",
246 "304402201ef9d2a728e86aa3e8a0cf27a1f6afeba84af90f89ea50ea14483c4bd0c17fcd02201b6130ab0aed38128a4637b93ac90484aa2361c014e89c915d061fd27cab6aa6"
247 ]
248 }
249 """ # noqa E501
251 with patch("builtins.open", mock_open()) as open_mock:
252 open_mock.return_value.read.return_value = jsonsample
254 with self.assertRaises(ValueError):
255 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json")
257 def test_authorization_message(self):
258 # Test authorization message format
259 expected_msg = (b"\x19Ethereum Signed Message:\n" +
260 b"160" +
261 b"RSK_powHSM_SGX_upgrade_from_" +
262 self.exporter_mrenclave.encode("ASCII") +
263 b"_to_" + self.importer_mrenclave.encode("ASCII"))
264 self.assertEqual(expected_msg, self.sa.migration_spec.get_authorization_msg())
267class TestSGXMigrationSpec(TestCase):
268 def setUp(self):
269 # Sample mrenclave values (32-byte hex strings)
270 self.exporter_mrenclave = "aa" * 32
271 self.importer_mrenclave = "bb" * 32
272 self.migration_spec = SGXMigrationSpec({
273 "exporter": self.exporter_mrenclave,
274 "importer": self.importer_mrenclave
275 })
277 def test_mrenclave_getters(self):
278 # Test mrenclave getters and normalization
279 self.assertEqual(self.migration_spec.exporter, self.exporter_mrenclave.lower())
280 self.assertEqual(self.migration_spec.importer, self.importer_mrenclave.lower())
282 def test_invalid_exporter_mrenclave(self):
283 # Test constructor with invalid exporter mrenclave
284 with self.assertRaises(ValueError):
285 SGXMigrationSpec({
286 "exporter": "not-a-mrenclave",
287 "importer": self.importer_mrenclave
288 })
290 def test_invalid_importer_mrenclave(self):
291 # Test constructor with invalid importer mrenclave
292 with self.assertRaises(ValueError):
293 SGXMigrationSpec({
294 "exporter": self.exporter_mrenclave,
295 "importer": "not-a-mrenclave"
296 })
298 def test_mrenclave_normalization(self):
299 # Test mrenclave hex string normalization
300 spec = SGXMigrationSpec({
301 "exporter": "0x" + "aa" * 32,
302 "importer": "0x" + "bb" * 32
303 })
304 self.assertEqual(spec.exporter, "aa" * 32)
305 self.assertEqual(spec.importer, "bb" * 32)
307 def test_to_dict(self):
308 # Test dictionary conversion
309 expected_dict = {
310 "exporter": self.exporter_mrenclave,
311 "importer": self.importer_mrenclave,
312 }
313 self.assertEqual(expected_dict, self.migration_spec.to_dict())
315 def test_msg_generation(self):
316 # Test non-prefixed message generation
317 expected_msg = (f"RSK_powHSM_SGX_upgrade_from_"
318 f"{'aa' * 32}"
319 f"_to_{'bb' * 32}")
320 self.assertEqual(expected_msg, self.migration_spec.msg)
322 def test_authorization_message(self):
323 # Test authorization message format
324 expected_msg = (b"\x19Ethereum Signed Message:\n" +
325 b"160" +
326 b"RSK_powHSM_SGX_upgrade_from_" +
327 self.exporter_mrenclave.encode("ASCII") +
328 b"_to_" + self.importer_mrenclave.encode("ASCII"))
329 self.assertEqual(expected_msg, self.migration_spec.get_authorization_msg())
331 def test_authorization_digest(self):
332 # Test authorization digest generation
333 msg = self.migration_spec.get_authorization_msg()
334 expected_digest = keccak_256(msg)
335 self.assertEqual(expected_digest, self.migration_spec.get_authorization_digest())