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

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 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 

28 

29import logging 

30 

31logging.disable(logging.CRITICAL) 

32 

33 

34class TestSGXMigrationAuthorization(TestCase): 

35 def setUp(self): 

36 # Sample valid DER signatures 

37 self.sigs = [ 

38 "3045022100f31ee73e3b10c5d610d9f5501e12ce1f2fd31182d0630c8e0db75fba3f35bbe3022056d0703a27937aec36a0a05bd5b85de6144279ab3a66faf266378ce42a838831", # noqa E501 

39 "3046022100d2e039915b4decd3d32d613bdcfc84090560e0e714284ff4c3b454b563d81c7c022100ff0de20f22f75a87cf546a6e3dd9dada082b0bdd01862e1c566e5bdd67f0c3b1", # noqa E501 

40 ] 

41 

42 # Sample mrenclave values (32-byte hex strings) 

43 self.exporter_mrenclave = "aa" * 32 

44 self.importer_mrenclave = "bb" * 32 

45 

46 # Create migration spec 

47 self.migration_spec = SGXMigrationSpec({ 

48 "exporter": self.exporter_mrenclave, 

49 "importer": self.importer_mrenclave 

50 }) 

51 

52 # Create SGX authorization instance 

53 self.sa = SGXMigrationAuthorization(self.migration_spec, self.sigs) 

54 

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 

61 

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) 

66 

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") 

71 

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 ) 

79 

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()) 

91 

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]) 

99 

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 ) 

108 

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) 

119 

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") 

124 

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) 

128 

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 

144 

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") 

148 

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) 

158 

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 

171 

172 with patch("builtins.open", mock_open()) as open_mock: 

173 open_mock.return_value.read.return_value = jsonsample 

174 

175 with self.assertRaises(ValueError): 

176 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json") 

177 

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 

190 

191 with patch("builtins.open", mock_open()) as open_mock: 

192 open_mock.return_value.read.return_value = jsonsample 

193 

194 with self.assertRaises(ValueError): 

195 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json") 

196 

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 """ 

209 

210 with patch("builtins.open", mock_open()) as open_mock: 

211 open_mock.return_value.read.return_value = jsonsample 

212 

213 with self.assertRaises(ValueError): 

214 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json") 

215 

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 

228 

229 with patch("builtins.open", mock_open()) as open_mock: 

230 open_mock.return_value.read.return_value = jsonsample 

231 

232 with self.assertRaises(ValueError): 

233 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json") 

234 

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 

250 

251 with patch("builtins.open", mock_open()) as open_mock: 

252 open_mock.return_value.read.return_value = jsonsample 

253 

254 with self.assertRaises(ValueError): 

255 SGXMigrationAuthorization.from_jsonfile("/an/existing/file.json") 

256 

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()) 

265 

266 

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 }) 

276 

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()) 

281 

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 }) 

289 

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 }) 

297 

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) 

306 

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()) 

314 

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) 

321 

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()) 

330 

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())