Coverage for signapp.py: 82%

108 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.signer_authorization import SignerAuthorization, SignerVersion 

36from admin.ledger_utils import eth_message_to_printable, compute_app_hash 

37 

38# Default signing path 

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

40 

41# Legacy dongle constants 

42COMMAND_SIGN = 0x02 

43COMMAND_PUBKEY = 0x04 

44OP_SIGN_MSG_PATH = bytes.fromhex("70") 

45OP_SIGN_MSG_HASH = bytes.fromhex("800000") 

46 

47 

48def main(): 

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

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

51 parser.add_argument( 

52 "-a", 

53 "--app", 

54 dest="app_path", 

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

56 ) 

57 parser.add_argument( 

58 "-i", 

59 "--iteration", 

60 dest="iteration", 

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

62 ) 

63 parser.add_argument( 

64 "-o", 

65 "--output", 

66 dest="output_path", 

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

68 ) 

69 parser.add_argument( 

70 "-k", 

71 "--key", 

72 dest="key", 

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

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

75 ) 

76 parser.add_argument( 

77 "-p", 

78 "--path", 

79 dest="path", 

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

81 f"Default \"{DEFAULT_ETH_PATH}\"" 

82 ) 

83 parser.add_argument( 

84 "-g", 

85 "--signature", 

86 dest="signature", 

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

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

89 ) 

90 parser.add_argument( 

91 "-b", 

92 "--pubkey", 

93 dest="pubkey", 

94 action="store_true", 

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

96 ) 

97 parser.add_argument( 

98 "-v", 

99 "--verbose", 

100 dest="verbose", 

101 action="store_const", 

102 help="Enable verbose mode", 

103 default=False, 

104 const=True, 

105 ) 

106 options = parser.parse_args() 

107 

108 try: 

109 eth = None 

110 

111 if options.path is None: 

112 options.path = DEFAULT_ETH_PATH 

113 

114 # Require an output path for certain operations 

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

116 options.output_path is None: 

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

118 

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

120 if options.operation == "manual": 

121 if options.signature is None: 

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

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

124 signer_authorization = SignerAuthorization.from_jsonfile(options.output_path) 

125 info("Adding signature...") 

126 signer_authorization.add_signature(options.signature) 

127 signer_authorization.save_to_jsonfile(options.output_path) 

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

129 sys.exit(0) 

130 

131 if options.operation == "key": 

132 # Validate key 

133 if options.key is None: 

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

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

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

137 elif options.operation == "eth": 

138 # Parse path 

139 path = BIP32Path(options.path) 

140 

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

142 eth = get_eth_dongle(options.verbose) 

143 

144 # Retrieve public key 

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

146 pubkey = eth.get_pubkey(path) 

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

148 

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

150 if options.pubkey: 

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

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

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

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

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

156 sys.exit(0) 

157 

158 # Is there an existing signer authorization? Read it 

159 signer_authorization = None 

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

161 options.output_path is not None and \ 

162 isfile(options.output_path): 

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

164 signer_authorization = SignerAuthorization.from_jsonfile(options.output_path) 

165 signer_version = signer_authorization.signer_version 

166 else: 

167 if options.app_path is None: 

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

169 

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

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

172 

173 info("Computing hash...") 

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

175 if options.operation == "hash": 

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

177 sys.exit(0) 

178 

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

180 signer_version = SignerVersion(app_hash, options.iteration) 

181 signer_authorization = SignerAuthorization.for_signer_version(signer_version) 

182 

183 if options.operation == "message": 

184 signer_authorization_msg = signer_version.get_authorization_msg() 

185 if options.output_path is None: 

186 info(eth_message_to_printable(signer_authorization_msg)) 

187 else: 

188 signer_authorization.save_to_jsonfile(options.output_path) 

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

190 sys.exit(0) 

191 

192 # Sign the app hash 

193 if options.operation == "key": 

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

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

196 curve=ecdsa.SECP256k1) 

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

198 sigencode=ecdsa.util.sigencode_der) 

199 elif options.operation == "eth": 

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

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

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

203 

204 try: 

205 if not vkey.verify_digest( 

206 signature, signer_version.get_authorization_digest(), 

207 sigdecode=ecdsa.util.sigdecode_der): 

208 raise Exception() 

209 except Exception: 

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

211 else: 

212 raise AdminError("Unexpected state reached! " 

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

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

215 

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

217 signer_authorization.add_signature(signature.hex()) 

218 signer_authorization.save_to_jsonfile(options.output_path) 

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

220 sys.exit(0) 

221 except Exception as e: 

222 info(str(e)) 

223 sys.exit(1) 

224 finally: 

225 dispose_eth_dongle(eth) 

226 

227 

228if __name__ == "__main__": 

229 main()