Coverage for signmigration.py: 97%

119 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 

23import sys 

24from os.path import isfile 

25from argparse import ArgumentParser 

26import ecdsa 

27from admin.misc import ( 

28 get_eth_dongle, 

29 dispose_eth_dongle, 

30 info, 

31 AdminError 

32) 

33from comm.utils import is_hex_string_of_length 

34from comm.bip32 import BIP32Path 

35from admin.sgx_migration_authorization import SGXMigrationAuthorization, SGXMigrationSpec 

36from admin.ledger_utils import eth_message_to_printable 

37 

38# Default signing path 

39DEFAULT_ETH_PATH = "m/44'/60'/0'/0/0" 

40 

41 

42def _require_output_path(options, require_existing=False): 

43 if options.output_path is None: 

44 raise AdminError("Must provide an output path (-o/--output)") 

45 if require_existing and not isfile(options.output_path): 

46 raise AdminError(f"Invalid output path: {options.output_path}") 

47 

48 

49def do_message(options): 

50 if options.exporter_hash is None: 

51 raise AdminError("Must provide an exporter hash (-e/--exporter)") 

52 if options.importer_hash is None: 

53 raise AdminError("Must provide an importer hash (-i/--importer)") 

54 

55 info("Computing the SGX migration authorization message...") 

56 migration_spec = SGXMigrationSpec({ 

57 "exporter": options.exporter_hash, 

58 "importer": options.importer_hash 

59 }) 

60 sgx_authorization = SGXMigrationAuthorization.for_spec(migration_spec) 

61 if options.output_path is None: 

62 info(eth_message_to_printable(migration_spec.get_authorization_msg())) 

63 else: 

64 sgx_authorization.save_to_jsonfile(options.output_path) 

65 info(f"SGX migration authorization saved to {options.output_path}") 

66 

67 

68def do_manual_sign(options): 

69 _require_output_path(options, require_existing=True) 

70 if options.signature is None: 

71 raise AdminError("Must provide a signature (-g/--signature)") 

72 

73 info(f"Opening SGX migration authorization file {options.output_path}...") 

74 sgx_authorization = SGXMigrationAuthorization.from_jsonfile(options.output_path) 

75 info("Adding signature...") 

76 sgx_authorization.add_signature(options.signature) 

77 sgx_authorization.save_to_jsonfile(options.output_path) 

78 info(f"SGX migration authorization saved to {options.output_path}") 

79 

80 

81def do_key(options): 

82 _require_output_path(options, require_existing=True) 

83 if options.key is None: 

84 raise AdminError("Must provide a signing key (-k/--key)") 

85 if not is_hex_string_of_length(options.key, 32, allow_prefix=True): 

86 raise AdminError(f"Invalid key '{options.key}'") 

87 

88 info(f"Opening SGX migration authorization file {options.output_path}...") 

89 sgx_authorization = SGXMigrationAuthorization.from_jsonfile(options.output_path) 

90 migration_spec = sgx_authorization.migration_spec 

91 info("Signing with key...") 

92 sk = ecdsa.SigningKey.from_string( 

93 bytes.fromhex(options.key), 

94 curve=ecdsa.SECP256k1 

95 ) 

96 signature = sk.sign_digest( 

97 migration_spec.get_authorization_digest(), 

98 sigencode=ecdsa.util.sigencode_der_canonize 

99 ) 

100 # Add the signature to the authorization and save it to disk 

101 sgx_authorization.add_signature(signature.hex()) 

102 sgx_authorization.save_to_jsonfile(options.output_path) 

103 info(f"SGX migration authorization saved to {options.output_path}") 

104 

105 

106def do_eth(options): 

107 _require_output_path(options) 

108 if options.path is None: 

109 options.path = DEFAULT_ETH_PATH 

110 # Parse path 

111 path = BIP32Path(options.path) 

112 eth = None 

113 try: 

114 # Get dongle access (must have ethereum app open) 

115 eth = get_eth_dongle(options.verbose) 

116 # Retrieve public key 

117 info(f"Retrieving public key for path '{str(path)}'...") 

118 pubkey = eth.get_pubkey(path) 

119 info(f"Public key: {pubkey.hex()}") 

120 

121 # If options.pubkey is True, we just want to retrieve the public key 

122 if options.pubkey: 

123 info(f"Opening public key file {options.output_path}...") 

124 info("Adding public key...") 

125 with open(options.output_path, "w") as file: 

126 file.write("%s\n" % pubkey.hex()) 

127 info(f"Public key saved to {options.output_path}") 

128 return 

129 

130 # Is there an existing migration authorization? Read it 

131 sgx_authorization = None 

132 _require_output_path(options, require_existing=True) 

133 

134 info(f"Opening SGX migration authorization file {options.output_path}...") 

135 sgx_authorization = SGXMigrationAuthorization.from_jsonfile(options.output_path) 

136 migration_spec = sgx_authorization.migration_spec 

137 info("Signing with dongle...") 

138 try: 

139 signature = eth.sign(path, migration_spec.msg.encode('ascii')) 

140 vkey = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.SECP256k1) 

141 

142 if not vkey.verify_digest( 

143 signature, migration_spec.get_authorization_digest(), 

144 sigdecode=ecdsa.util.sigdecode_der 

145 ): 

146 raise Exception() 

147 except Exception: 

148 raise AdminError(f"Bad signature from dongle! (got '{signature.hex()}')") 

149 # Add the signature to the authorization and save it to disk 

150 sgx_authorization.add_signature(signature.hex()) 

151 sgx_authorization.save_to_jsonfile(options.output_path) 

152 info(f"SGX migration authorization saved to {options.output_path}") 

153 except AdminError: 

154 raise 

155 except Exception as e: 

156 raise AdminError(f"Error signing with dongle: {e}") 

157 finally: 

158 dispose_eth_dongle(eth) 

159 

160 

161def main(): 

162 parser = ArgumentParser( 

163 description="powHSM SGX migration authorization generation and signing tool" 

164 ) 

165 parser.add_argument("operation", choices=["message", "key", "eth", "manual"]) 

166 parser.add_argument( 

167 "-o", 

168 "--output", 

169 dest="output_path", 

170 help="Destination file for SGX migration authorization.", 

171 ) 

172 parser.add_argument( 

173 "-k", 

174 "--key", 

175 dest="key", 

176 help="Private key used for signing (only for 'key' option)." 

177 "Must be a 32-byte hex-encoded string.", 

178 ) 

179 parser.add_argument( 

180 "-p", 

181 "--path", 

182 dest="path", 

183 help="Path used for signing (only for 'eth' option). " 

184 f"Default \"{DEFAULT_ETH_PATH}\"" 

185 ) 

186 parser.add_argument( 

187 "-g", 

188 "--signature", 

189 dest="signature", 

190 help="Signature to add to SGX migration authorization (only for 'manual' option)." 

191 "Must be a hex-encoded, der-encoded SECP256k1 signature.", 

192 ) 

193 parser.add_argument( 

194 "-b", 

195 "--pubkey", 

196 dest="pubkey", 

197 action="store_true", 

198 help="Retrieve public key (only for 'eth' option)." 

199 ) 

200 parser.add_argument( 

201 "-e", 

202 "--exporter", 

203 dest="exporter_hash", 

204 help="The hash of the exporter enclave (only for 'message' option)." 

205 ) 

206 parser.add_argument( 

207 "-i", 

208 "--importer", 

209 dest="importer_hash", 

210 help="The hash of the importer enclave (only for 'message' option)." 

211 ) 

212 parser.add_argument( 

213 "-v", 

214 "--verbose", 

215 dest="verbose", 

216 action="store_const", 

217 help="Enable verbose mode", 

218 default=False, 

219 const=True, 

220 ) 

221 options = parser.parse_args() 

222 

223 try: 

224 if options.operation == "message": 

225 do_message(options) 

226 elif options.operation == "key": 

227 do_key(options) 

228 elif options.operation == "eth": 

229 do_eth(options) 

230 elif options.operation == "manual": 

231 do_manual_sign(options) 

232 else: 

233 raise AdminError(f"Invalid operation: {options.operation}") 

234 sys.exit(0) 

235 except Exception as e: 

236 info(str(e)) 

237 sys.exit(1) 

238 

239 

240if __name__ == "__main__": 

241 main()