Coverage for signapp.py: 83%

110 statements  

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

22 

23import sys 

24from os.path import isfile 

25from argparse import ArgumentParser 

26import logging 

27import ecdsa 

28from admin.misc import ( 

29 get_eth_dongle, 

30 dispose_eth_dongle, 

31 info, 

32 AdminError 

33) 

34from comm.utils import is_hex_string_of_length 

35from comm.bip32 import BIP32Path 

36from admin.signer_authorization import SignerAuthorization, SignerVersion 

37from admin.ledger_utils import eth_message_to_printable, compute_app_hash 

38 

39# Default signing path 

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

41 

42# Legacy dongle constants 

43COMMAND_SIGN = 0x02 

44COMMAND_PUBKEY = 0x04 

45OP_SIGN_MSG_PATH = bytes.fromhex("70") 

46OP_SIGN_MSG_HASH = bytes.fromhex("800000") 

47 

48 

49def main(): 

50 logging.disable(logging.CRITICAL) 

51 

52 parser = ArgumentParser(description="powHSM Signer Authorization Generator") 

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

54 parser.add_argument( 

55 "-a", 

56 "--app", 

57 dest="app_path", 

58 help="App path (used to compute the app hash and authorization message).", 

59 ) 

60 parser.add_argument( 

61 "-i", 

62 "--iteration", 

63 dest="iteration", 

64 help="Signer iteration (used to compute the authorization message).", 

65 ) 

66 parser.add_argument( 

67 "-o", 

68 "--output", 

69 dest="output_path", 

70 help="Destination file for the authorization.", 

71 ) 

72 parser.add_argument( 

73 "-k", 

74 "--key", 

75 dest="key", 

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

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

78 ) 

79 parser.add_argument( 

80 "-p", 

81 "--path", 

82 dest="path", 

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

84 f"Default \"{DEFAULT_ETH_PATH}\"" 

85 ) 

86 parser.add_argument( 

87 "-g", 

88 "--signature", 

89 dest="signature", 

90 help="Signature to add to signer authorization (only for 'manual' option)." 

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

92 ) 

93 parser.add_argument( 

94 "-b", 

95 "--pubkey", 

96 dest="pubkey", 

97 action="store_true", 

98 help="Retrieve pubkic key (only for 'eth' option)." 

99 ) 

100 parser.add_argument( 

101 "-v", 

102 "--verbose", 

103 dest="verbose", 

104 action="store_const", 

105 help="Enable verbose mode", 

106 default=False, 

107 const=True, 

108 ) 

109 options = parser.parse_args() 

110 

111 try: 

112 eth = None 

113 

114 if options.path is None: 

115 options.path = DEFAULT_ETH_PATH 

116 

117 # Require an output path for certain operations 

118 if options.operation not in ["hash", "message"] and \ 

119 options.output_path is None: 

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

121 

122 # Manual addition of signatures is radically different from the rest 

123 if options.operation == "manual": 

124 if options.signature is None: 

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

126 info(f"Opening signer authorization file {options.output_path}...") 

127 signer_authorization = SignerAuthorization.from_jsonfile(options.output_path) 

128 info("Adding signature...") 

129 signer_authorization.add_signature(options.signature) 

130 signer_authorization.save_to_jsonfile(options.output_path) 

131 info(f"Signer authorization saved to {options.output_path}") 

132 sys.exit(0) 

133 

134 if options.operation == "key": 

135 # Validate key 

136 if options.key is None: 

137 raise AdminError("Must provide a signing key with '-k/--key'") 

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

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

140 elif options.operation == "eth": 

141 # Parse path 

142 path = BIP32Path(options.path) 

143 

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

145 eth = get_eth_dongle(options.verbose) 

146 

147 # Retrieve public key 

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

149 pubkey = eth.get_pubkey(path) 

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

151 

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

153 if options.pubkey: 

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

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

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

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

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

159 sys.exit(0) 

160 

161 # Is there an existing signer authorization? Read it 

162 signer_authorization = None 

163 if options.operation not in ["message", "hash"] and \ 

164 options.output_path is not None and \ 

165 isfile(options.output_path): 

166 info(f"Opening signer authorization file {options.output_path}...") 

167 signer_authorization = SignerAuthorization.from_jsonfile(options.output_path) 

168 signer_version = signer_authorization.signer_version 

169 else: 

170 if options.app_path is None: 

171 raise AdminError("Must provide an app path with '-a/--app'") 

172 

173 if options.operation != "hash" and options.iteration is None: 

174 raise AdminError("Must provide a signer iteration with '-i/--iteration'") 

175 

176 info("Computing hash...") 

177 app_hash = compute_app_hash(options.app_path).hex() 

178 if options.operation == "hash": 

179 info(f"Computed hash: {app_hash}") 

180 sys.exit(0) 

181 

182 info("Computing signer authorization message...") 

183 signer_version = SignerVersion(app_hash, options.iteration) 

184 signer_authorization = SignerAuthorization.for_signer_version(signer_version) 

185 

186 if options.operation == "message": 

187 signer_authorization_msg = signer_version.get_authorization_msg() 

188 if options.output_path is None: 

189 info(eth_message_to_printable(signer_authorization_msg)) 

190 else: 

191 signer_authorization.save_to_jsonfile(options.output_path) 

192 info(f"Signer authorization saved to {options.output_path}") 

193 sys.exit(0) 

194 

195 # Sign the app hash 

196 if options.operation == "key": 

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

198 sk = ecdsa.SigningKey.from_string(bytes.fromhex(options.key), 

199 curve=ecdsa.SECP256k1) 

200 signature = sk.sign_digest(signer_version.get_authorization_digest(), 

201 sigencode=ecdsa.util.sigencode_der) 

202 elif options.operation == "eth": 

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

204 signature = eth.sign(path, signer_version.msg.encode('ascii')) 

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

206 

207 try: 

208 if not vkey.verify_digest( 

209 signature, signer_version.get_authorization_digest(), 

210 sigdecode=ecdsa.util.sigdecode_der): 

211 raise Exception() 

212 except Exception: 

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

214 else: 

215 raise AdminError("Unexpected state reached! " 

216 "Expected operation to be either 'eth' or 'key', " 

217 f"but was {options.operation}") 

218 

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

220 signer_authorization.add_signature(signature.hex()) 

221 signer_authorization.save_to_jsonfile(options.output_path) 

222 info(f"Signer authorization saved to {options.output_path}") 

223 sys.exit(0) 

224 except Exception as e: 

225 info(str(e)) 

226 sys.exit(1) 

227 finally: 

228 dispose_eth_dongle(eth) 

229 

230 

231if __name__ == "__main__": 

232 main()