Coverage for tests/admin/test_signmigration.py: 100%

305 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 Mock, call, patch, mock_open 

25from signmigration import main 

26from admin.bip32 import BIP32Path 

27import ecdsa 

28import logging 

29 

30logging.disable(logging.CRITICAL) 

31 

32RETURN_SUCCESS = 0 

33RETURN_ERROR = 1 

34 

35 

36@patch("signmigration.SGXMigrationAuthorization") 

37@patch("signmigration.SGXMigrationSpec") 

38@patch("signmigration.info") 

39class TestSignMigrationMessage(TestCase): 

40 def setUp(self): 

41 self.migration_auth = Mock() 

42 self.migration_spec = Mock() 

43 self.migration_spec.get_authorization_msg.return_value = ( 

44 b"the-authorization-message" 

45 ) 

46 self.migration_auth.for_spec.return_value = self.migration_auth 

47 

48 def test_ok_to_console(self, info_mock, migration_spec_mock, migration_auth_mock): 

49 migration_spec_mock.return_value = self.migration_spec 

50 migration_auth_mock.for_spec.return_value = self.migration_auth 

51 

52 with patch("sys.argv", ["signmigration.py", "message", 

53 "-e", "exporter-hash", 

54 "-i", "importer-hash"]): 

55 with self.assertRaises(SystemExit) as exit: 

56 main() 

57 

58 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

59 self.assertEqual( 

60 [call("Computing the SGX migration authorization message..."), 

61 call("the-authorization-message")], info_mock.call_args_list 

62 ) 

63 self.assertEqual( 

64 [call({"exporter": "exporter-hash", "importer": "importer-hash"})], 

65 migration_spec_mock.call_args_list 

66 ) 

67 self.assertEqual( 

68 [call(self.migration_spec)], 

69 migration_auth_mock.for_spec.call_args_list 

70 ) 

71 

72 def test_ok_to_file(self, info_mock, migration_spec_mock, migration_auth_mock): 

73 migration_spec_mock.return_value = self.migration_spec 

74 migration_auth_mock.for_spec.return_value = self.migration_auth 

75 

76 with patch("sys.argv", ["signmigration.py", "message", 

77 "-e", "exporter-hash", 

78 "-i", "importer-hash", 

79 "-o", "an-output-path"]): 

80 with self.assertRaises(SystemExit) as exit: 

81 main() 

82 

83 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

84 self.assertEqual( 

85 [call("Computing the SGX migration authorization message..."), 

86 call("SGX migration authorization saved to an-output-path")], 

87 info_mock.call_args_list 

88 ) 

89 self.assertEqual( 

90 [call({"exporter": "exporter-hash", "importer": "importer-hash"})], 

91 migration_spec_mock.call_args_list 

92 ) 

93 self.assertEqual( 

94 [call(self.migration_spec)], 

95 migration_auth_mock.for_spec.call_args_list 

96 ) 

97 self.assertEqual( 

98 [call("an-output-path")], 

99 self.migration_auth.save_to_jsonfile.call_args_list 

100 ) 

101 

102 def test_missing_exporter(self, info_mock, migration_spec_mock, migration_auth_mock): 

103 with patch("sys.argv", ["signmigration.py", "message", 

104 "-i", "importer-hash"]): 

105 with self.assertRaises(SystemExit) as exit: 

106 main() 

107 

108 self.assertEqual(exit.exception.code, RETURN_ERROR) 

109 self.assertEqual( 

110 [call("Must provide an exporter hash (-e/--exporter)")], 

111 info_mock.call_args_list 

112 ) 

113 migration_spec_mock.assert_not_called() 

114 migration_auth_mock.for_spec.assert_not_called() 

115 

116 def test_missing_importer(self, info_mock, migration_spec_mock, migration_auth_mock): 

117 with patch("sys.argv", ["signmigration.py", "message", 

118 "-e", "exporter-hash"]): 

119 with self.assertRaises(SystemExit) as exit: 

120 main() 

121 

122 self.assertEqual(exit.exception.code, RETURN_ERROR) 

123 self.assertEqual( 

124 [call("Must provide an importer hash (-i/--importer)")], 

125 info_mock.call_args_list 

126 ) 

127 migration_spec_mock.assert_not_called() 

128 migration_auth_mock.for_spec.assert_not_called() 

129 

130 

131@patch("signmigration.isfile") 

132@patch("signmigration.SGXMigrationAuthorization") 

133@patch("signmigration.info") 

134class TestSignMigrationManual(TestCase): 

135 def test_ok(self, info_mock, migration_auth_mock, isfile_mock): 

136 migration_auth = Mock() 

137 migration_auth_mock.from_jsonfile.return_value = migration_auth 

138 isfile_mock.return_value = True 

139 

140 with patch("sys.argv", ["signmigration.py", "manual", 

141 "-o", "an-output-path", 

142 "-g", "a-signature"]): 

143 with self.assertRaises(SystemExit) as exit: 

144 main() 

145 

146 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

147 self.assertEqual( 

148 [call("an-output-path")], 

149 migration_auth_mock.from_jsonfile.call_args_list 

150 ) 

151 self.assertEqual( 

152 [call("a-signature")], 

153 migration_auth.add_signature.call_args_list 

154 ) 

155 self.assertEqual( 

156 [call("an-output-path")], 

157 migration_auth.save_to_jsonfile.call_args_list 

158 ) 

159 self.assertEqual( 

160 [ 

161 call("Opening SGX migration authorization file an-output-path..."), 

162 call("Adding signature..."), 

163 call("SGX migration authorization saved to an-output-path") 

164 ], 

165 info_mock.call_args_list 

166 ) 

167 

168 def test_file_not_found(self, info_mock, migration_auth_mock, isfile_mock): 

169 isfile_mock.return_value = False 

170 

171 with patch("sys.argv", ["signmigration.py", "manual", 

172 "-o", "an-output-path", 

173 "-g", "a-signature"]): 

174 with self.assertRaises(SystemExit) as exit: 

175 main() 

176 

177 self.assertEqual(exit.exception.code, RETURN_ERROR) 

178 self.assertEqual( 

179 [ 

180 call("Invalid output path: an-output-path") 

181 ], 

182 info_mock.call_args_list 

183 ) 

184 migration_auth_mock.from_jsonfile.assert_not_called() 

185 

186 def test_missing_signature(self, info_mock, migration_auth_mock, isfile_mock): 

187 migration_auth = Mock() 

188 migration_auth_mock.from_jsonfile.return_value = migration_auth 

189 isfile_mock.return_value = True 

190 

191 with patch("sys.argv", ["signmigration.py", "manual", 

192 "-o", "an-output-path"]): 

193 with self.assertRaises(SystemExit) as exit: 

194 main() 

195 

196 self.assertEqual(exit.exception.code, RETURN_ERROR) 

197 self.assertEqual( 

198 [ 

199 call("Must provide a signature (-g/--signature)"), 

200 ], 

201 info_mock.call_args_list 

202 ) 

203 migration_auth_mock.from_jsonfile.assert_not_called() 

204 migration_auth.add_signature.assert_not_called() 

205 migration_auth.save_to_jsonfile.assert_not_called() 

206 

207 def test_missing_output_file(self, info_mock, migration_auth_mock, isfile_mock): 

208 with patch("sys.argv", ["signmigration.py", "manual", 

209 "-g", "a-signature"]): 

210 with self.assertRaises(SystemExit) as exit: 

211 main() 

212 

213 self.assertEqual(exit.exception.code, RETURN_ERROR) 

214 self.assertEqual( 

215 [ 

216 call("Must provide an output path (-o/--output)"), 

217 ], 

218 info_mock.call_args_list 

219 ) 

220 isfile_mock.assert_not_called() 

221 migration_auth_mock.from_jsonfile.assert_not_called() 

222 migration_auth_mock.add_signature.assert_not_called() 

223 migration_auth_mock.save_to_jsonfile.assert_not_called() 

224 

225 def test_non_existent_output_file(self, info_mock, migration_auth_mock, isfile_mock): 

226 isfile_mock.return_value = False 

227 

228 with patch("sys.argv", ["signmigration.py", "manual", 

229 "-o", "an-output-path"]): 

230 with self.assertRaises(SystemExit) as exit: 

231 main() 

232 

233 self.assertEqual(exit.exception.code, RETURN_ERROR) 

234 isfile_mock.assert_called_once_with("an-output-path") 

235 self.assertEqual( 

236 [ 

237 call("Invalid output path: an-output-path"), 

238 ], 

239 info_mock.call_args_list 

240 ) 

241 migration_auth_mock.from_jsonfile.assert_not_called() 

242 migration_auth_mock.add_signature.assert_not_called() 

243 migration_auth_mock.save_to_jsonfile.assert_not_called() 

244 

245 

246@patch("signmigration.isfile") 

247@patch("signmigration.SGXMigrationAuthorization") 

248@patch("signmigration.info") 

249class TestSignMigrationKey(TestCase): 

250 def test_ok(self, info_mock, migration_auth_mock, isfile_mock): 

251 migration_auth = Mock() 

252 migration_auth_mock.from_jsonfile.return_value = migration_auth 

253 migration_auth.add_signature.return_value = None 

254 isfile_mock.return_value = True 

255 migration_auth.migration_spec.get_authorization_digest.return_value = ( 

256 bytes.fromhex("bb"*32) 

257 ) 

258 

259 with patch("sys.argv", ["signmigration.py", "key", 

260 "-o", "an-output-path", 

261 "-k", "aa"*32]): 

262 with self.assertRaises(SystemExit) as exit: 

263 main() 

264 

265 privkey = ecdsa.SigningKey.from_string( 

266 bytes.fromhex("aa"*32), 

267 curve=ecdsa.SECP256k1 

268 ) 

269 pubkey = privkey.get_verifying_key() 

270 signature = migration_auth.add_signature.call_args_list[0][0][0] 

271 pubkey.verify_digest(bytes.fromhex(signature), bytes.fromhex("bb"*32), 

272 sigdecode=ecdsa.util.sigdecode_der) 

273 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

274 self.assertEqual( 

275 [call("an-output-path")], 

276 migration_auth_mock.from_jsonfile.call_args_list 

277 ) 

278 self.assertEqual( 

279 [ 

280 call("Opening SGX migration authorization file an-output-path..."), 

281 call("Signing with key..."), 

282 call("SGX migration authorization saved to an-output-path") 

283 ], 

284 info_mock.call_args_list 

285 ) 

286 

287 def test_missing_key(self, info_mock, migration_auth_mock, isfile_mock): 

288 isfile_mock.return_value = True 

289 

290 with patch("sys.argv", ["signmigration.py", "key", 

291 "-o", "an-output-path"]): 

292 with self.assertRaises(SystemExit) as exit: 

293 main() 

294 

295 self.assertEqual(exit.exception.code, RETURN_ERROR) 

296 self.assertEqual( 

297 [ 

298 call("Must provide a signing key (-k/--key)"), 

299 ], 

300 info_mock.call_args_list 

301 ) 

302 migration_auth_mock.from_jsonfile.assert_not_called() 

303 migration_auth_mock.add_signature.assert_not_called() 

304 migration_auth_mock.save_to_jsonfile.assert_not_called() 

305 

306 def test_invalid_key(self, info_mock, migration_auth_mock, isfile_mock): 

307 isfile_mock.return_value = True 

308 

309 with patch("sys.argv", ["signmigration.py", "key", 

310 "-o", "an-output-path", 

311 "-k", "invalid-key"]): 

312 with self.assertRaises(SystemExit) as exit: 

313 main() 

314 

315 self.assertEqual(exit.exception.code, RETURN_ERROR) 

316 self.assertEqual( 

317 [ 

318 call("Invalid key 'invalid-key'"), 

319 ], 

320 info_mock.call_args_list 

321 ) 

322 migration_auth_mock.from_jsonfile.assert_not_called() 

323 migration_auth_mock.add_signature.assert_not_called() 

324 migration_auth_mock.save_to_jsonfile.assert_not_called() 

325 

326 def test_missing_output_file(self, info_mock, migration_auth_mock, isfile_mock): 

327 with patch("sys.argv", ["signmigration.py", "key", 

328 "-k", "aa"*32]): 

329 with self.assertRaises(SystemExit) as exit: 

330 main() 

331 

332 self.assertEqual(exit.exception.code, RETURN_ERROR) 

333 self.assertEqual( 

334 [ 

335 call("Must provide an output path (-o/--output)"), 

336 ], 

337 info_mock.call_args_list 

338 ) 

339 isfile_mock.assert_not_called() 

340 migration_auth_mock.from_jsonfile.assert_not_called() 

341 

342 def test_non_existent_output_file(self, info_mock, migration_auth_mock, isfile_mock): 

343 isfile_mock.return_value = False 

344 

345 with patch("sys.argv", ["signmigration.py", "key", 

346 "-o", "an-output-path", 

347 "-k", "aa"*32]): 

348 with self.assertRaises(SystemExit) as exit: 

349 main() 

350 

351 self.assertEqual(exit.exception.code, RETURN_ERROR) 

352 isfile_mock.assert_called_once_with("an-output-path") 

353 self.assertEqual( 

354 [ 

355 call("Invalid output path: an-output-path"), 

356 ], 

357 info_mock.call_args_list 

358 ) 

359 migration_auth_mock.from_jsonfile.assert_not_called() 

360 

361 def test_canonical_signature_encoding(self, _, migration_auth_mock, isfile_mock): 

362 migration_auth = Mock() 

363 migration_auth_mock.from_jsonfile.return_value = migration_auth 

364 migration_auth.add_signature.return_value = None 

365 isfile_mock.return_value = True 

366 test_digest = bytes.fromhex("bb"*32) 

367 migration_auth.migration_spec.get_authorization_digest.return_value = test_digest 

368 

369 with patch("signmigration.ecdsa.util.sigencode_der_canonize") as sigencode_mock: 

370 known_signature = bytes.fromhex( 

371 "30440220" + "11" * 32 + "0220" + "22" * 32 

372 ) 

373 sigencode_mock.return_value = known_signature 

374 

375 with patch("sys.argv", ["signmigration.py", "key", 

376 "-o", "an-output-path", 

377 "-k", "aa"*32]): 

378 with self.assertRaises(SystemExit) as exit: 

379 main() 

380 

381 self.assertEqual(sigencode_mock.call_count, 1) 

382 signature_hex = migration_auth.add_signature.call_args_list[0][0][0] 

383 self.assertEqual(signature_hex, known_signature.hex()) 

384 

385 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

386 

387 

388@patch("signmigration.isfile") 

389@patch("signmigration.dispose_eth_dongle") 

390@patch("signmigration.get_eth_dongle") 

391@patch("signmigration.BIP32Path") 

392@patch("signmigration.SGXMigrationSpec") 

393@patch("signmigration.SGXMigrationAuthorization") 

394@patch("signmigration.info") 

395class TestSignMigrationEth(TestCase): 

396 def test_ok_pubkey( 

397 self, 

398 info_mock, 

399 migration_auth_mock, 

400 migration_spec_mock, 

401 bip32path_mock, 

402 get_eth_mock, 

403 dispose_eth_mock, 

404 isfile_mock): 

405 migration_auth = Mock() 

406 migration_auth_mock.from_jsonfile.return_value = migration_auth 

407 bip32path_mock.return_value = "bip32-path" 

408 get_eth_mock.return_value = Mock() 

409 eth_mock = Mock() 

410 eth_mock.get_pubkey.return_value = bytes.fromhex("aa"*32) 

411 eth_mock.sign.return_value = bytes.fromhex("bb"*32) 

412 get_eth_mock.return_value = eth_mock 

413 isfile_mock.return_value = True 

414 

415 mock_file = mock_open() 

416 with patch("builtins.open", mock_file) as open_mock: 

417 with patch("sys.argv", ["signmigration.py", "eth", 

418 "-o", "an-output-path", "-b"]): 

419 with self.assertRaises(SystemExit) as exit: 

420 main() 

421 

422 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

423 get_eth_mock.assert_called_once() 

424 eth_mock.get_pubkey.assert_called_once_with("bip32-path") 

425 self.assertEqual( 

426 [ 

427 call("Retrieving public key for path 'bip32-path'..."), 

428 call("Public key: " + "aa"*32), 

429 call("Opening public key file an-output-path..."), 

430 call("Adding public key..."), 

431 call("Public key saved to an-output-path") 

432 ], 

433 info_mock.call_args_list 

434 ) 

435 # Verify the file was opened in write mode 

436 open_mock.assert_called_once_with("an-output-path", "w") 

437 # Verify the correct content was written 

438 mock_file.return_value.write.assert_called_once_with("aa"*32 + "\n") 

439 

440 def test_existingfile_ok( 

441 self, 

442 info_mock, 

443 migration_auth_mock, 

444 migration_spec_mock, 

445 bip32path_mock, 

446 get_eth_mock, 

447 dispose_eth_mock, 

448 isfile_mock): 

449 migration_spec_mock = Mock() 

450 migration_spec_mock.get_authorization_digest.return_value = bytes.fromhex("aa"*32) 

451 migration_spec_mock.msg = "RSK_powHSM_SGX_upgrade_from_exporter_to_importer" 

452 migration_auth = Mock() 

453 migration_auth.migration_spec = migration_spec_mock 

454 migration_auth_mock.from_jsonfile.return_value = migration_auth 

455 bip32path_mock.return_value = BIP32Path("m/44'/60'/0'/0/0") 

456 privkey = ecdsa.SigningKey.from_string(bytes.fromhex("dd"*32), 

457 curve=ecdsa.SECP256k1) 

458 pubkey = privkey.get_verifying_key() 

459 eth_mock = Mock() 

460 eth_mock.get_pubkey.return_value = pubkey.to_string("uncompressed") 

461 eth_mock.sign.return_value = privkey.sign_digest( 

462 bytes.fromhex("aa"*32), sigencode=ecdsa.util.sigencode_der) 

463 get_eth_mock.return_value = eth_mock 

464 isfile_mock.return_value = True 

465 

466 with patch("sys.argv", ["signmigration.py", "eth", 

467 "-o", "an-output-path"]): 

468 with self.assertRaises(SystemExit) as exit: 

469 main() 

470 

471 self.assertEqual(exit.exception.code, RETURN_SUCCESS) 

472 

473 self.assertEqual([call("an-output-path")], isfile_mock.call_args_list) 

474 self.assertFalse(migration_spec_mock.called) 

475 self.assertEqual([call("an-output-path")], 

476 migration_auth_mock.from_jsonfile.call_args_list) 

477 self.assertEqual([call(BIP32Path("m/44'/60'/0'/0/0"))], 

478 eth_mock.get_pubkey.call_args_list) 

479 self.assertEqual([call(BIP32Path("m/44'/60'/0'/0/0"), 

480 b"RSK_powHSM_SGX_upgrade_from_exporter_to_importer")], 

481 eth_mock.sign.call_args_list) 

482 self.assertEqual(1, migration_auth.add_signature.call_count) 

483 signature = migration_auth.add_signature.call_args_list[0][0][0] 

484 pubkey.verify_digest(bytes.fromhex(signature), bytes.fromhex("aa"*32), 

485 sigdecode=ecdsa.util.sigdecode_der) 

486 self.assertEqual([call("an-output-path")], 

487 migration_auth.save_to_jsonfile.call_args_list) 

488 

489 def test_missing_output_file( 

490 self, 

491 info_mock, 

492 migration_auth_mock, 

493 migration_spec_mock, 

494 bip32path_mock, 

495 get_eth_mock, 

496 dispose_eth_mock, 

497 isfile_mock): 

498 

499 with patch("sys.argv", ["signmigration.py", "eth"]): 

500 with self.assertRaises(SystemExit) as exit: 

501 main() 

502 

503 self.assertEqual(exit.exception.code, RETURN_ERROR) 

504 self.assertEqual( 

505 [call("Must provide an output path (-o/--output)")], 

506 info_mock.call_args_list 

507 ) 

508 get_eth_mock.assert_not_called() 

509 dispose_eth_mock.assert_not_called() 

510 

511 def test_get_eth_dongle_exception( 

512 self, 

513 info_mock, 

514 migration_auth_mock, 

515 migration_spec_mock, 

516 bip32path_mock, 

517 get_eth_mock, 

518 dispose_eth_mock, 

519 isfile_mock): 

520 

521 bip32path_mock.return_value = BIP32Path("m/44'/60'/0'/0/0") 

522 get_eth_mock.side_effect = Exception("Dongle connection error") 

523 

524 with patch("sys.argv", ["signmigration.py", "eth", 

525 "-o", "an-output-path"]): 

526 with self.assertRaises(SystemExit) as exit: 

527 main() 

528 

529 self.assertEqual(exit.exception.code, RETURN_ERROR) 

530 bip32path_mock.assert_called_once_with("m/44'/60'/0'/0/0") 

531 get_eth_mock.assert_called_once() 

532 self.assertEqual( 

533 [call("Error signing with dongle: Dongle connection error")], 

534 info_mock.call_args_list 

535 ) 

536 # dispose_eth_dongle should be called even if get_eth_dongle fails 

537 dispose_eth_mock.assert_called_once_with(None) 

538 

539 def test_get_pubkey_exception( 

540 self, 

541 info_mock, 

542 migration_auth_mock, 

543 migration_spec_mock, 

544 bip32path_mock, 

545 get_eth_mock, 

546 dispose_eth_mock, 

547 isfile_mock): 

548 

549 bip32path_mock.return_value = BIP32Path("m/44'/60'/0'/0/0") 

550 eth_mock = Mock() 

551 eth_mock.get_pubkey.side_effect = Exception("Could not get pubkey") 

552 get_eth_mock.return_value = eth_mock 

553 

554 with patch("sys.argv", ["signmigration.py", "eth", 

555 "-o", "an-output-path"]): 

556 with self.assertRaises(SystemExit) as exit: 

557 main() 

558 

559 self.assertEqual(exit.exception.code, RETURN_ERROR) 

560 bip32path_mock.assert_called_once_with("m/44'/60'/0'/0/0") 

561 get_eth_mock.assert_called_once() 

562 eth_mock.get_pubkey.assert_called_once_with(BIP32Path("m/44'/60'/0'/0/0")) 

563 self.assertEqual( 

564 [ 

565 call("Retrieving public key for path 'm/44\'/60\'/0\'/0/0'..."), 

566 call("Error signing with dongle: Could not get pubkey") 

567 ], 

568 info_mock.call_args_list 

569 ) 

570 dispose_eth_mock.assert_called_once_with(eth_mock) 

571 

572 def test_bad_signature( 

573 self, 

574 info_mock, 

575 migration_auth_mock, 

576 migration_spec_mock, 

577 bip32path_mock, 

578 get_eth_mock, 

579 dispose_eth_mock, 

580 isfile_mock): 

581 

582 migration_spec = Mock() 

583 migration_spec.get_authorization_digest.return_value = bytes.fromhex("aa"*32) 

584 migration_spec.msg = "RSK_powHSM_SGX_upgrade_from_exporter_to_importer" 

585 migration_auth = Mock() 

586 migration_auth.migration_spec = migration_spec 

587 migration_auth_mock.from_jsonfile.return_value = migration_auth 

588 bip32path_mock.return_value = BIP32Path("m/44'/60'/0'/0/0") 

589 

590 # Generate a valid key pair 

591 privkey = ecdsa.SigningKey.from_string(bytes.fromhex("dd"*32), 

592 curve=ecdsa.SECP256k1) 

593 pubkey = privkey.get_verifying_key() 

594 

595 eth_mock = Mock() 

596 eth_mock.get_pubkey.return_value = pubkey.to_string("uncompressed") 

597 # Sign a DIFFERENT digest to create a bad signature for the expected digest 

598 bad_signature = privkey.sign_digest( 

599 bytes.fromhex("cc"*32), sigencode=ecdsa.util.sigencode_der) 

600 eth_mock.sign.return_value = bad_signature 

601 get_eth_mock.return_value = eth_mock 

602 isfile_mock.return_value = True 

603 

604 with patch("sys.argv", ["signmigration.py", "eth", 

605 "-o", "an-output-path"]): 

606 with self.assertRaises(SystemExit) as exit: 

607 main() 

608 

609 self.assertEqual(exit.exception.code, RETURN_ERROR) 

610 isfile_mock.assert_called_once_with("an-output-path") 

611 migration_auth_mock.from_jsonfile.assert_called_once_with("an-output-path") 

612 get_eth_mock.assert_called_once() 

613 eth_mock.get_pubkey.assert_called_once_with(BIP32Path("m/44'/60'/0'/0/0")) 

614 eth_mock.sign.assert_called_once_with( 

615 BIP32Path("m/44'/60'/0'/0/0"), 

616 b"RSK_powHSM_SGX_upgrade_from_exporter_to_importer" 

617 ) 

618 self.assertEqual( 

619 [ 

620 call("Retrieving public key for path 'm/44\'/60\'/0\'/0/0'..."), 

621 call(f"Public key: {pubkey.to_string('uncompressed').hex()}"), 

622 call("Opening SGX migration authorization file an-output-path..."), 

623 call("Signing with dongle..."), 

624 call(f"Bad signature from dongle! (got '{bad_signature.hex()}')") 

625 ], 

626 info_mock.call_args_list 

627 ) 

628 migration_auth.add_signature.assert_not_called() 

629 migration_auth.save_to_jsonfile.assert_not_called() 

630 dispose_eth_mock.assert_called_once_with(eth_mock)