Coverage for signapp.py: 83%
110 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 20:41 +0000
« 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.
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
39# Default signing path
40DEFAULT_ETH_PATH = "m/44'/60'/0'/0/0"
42# Legacy dongle constants
43COMMAND_SIGN = 0x02
44COMMAND_PUBKEY = 0x04
45OP_SIGN_MSG_PATH = bytes.fromhex("70")
46OP_SIGN_MSG_HASH = bytes.fromhex("800000")
49def main():
50 logging.disable(logging.CRITICAL)
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()
111 try:
112 eth = None
114 if options.path is None:
115 options.path = DEFAULT_ETH_PATH
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)")
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)
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)
144 # Get dongle access (must have ethereum app open)
145 eth = get_eth_dongle(options.verbose)
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()}")
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)
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'")
173 if options.operation != "hash" and options.iteration is None:
174 raise AdminError("Must provide a signer iteration with '-i/--iteration'")
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)
182 info("Computing signer authorization message...")
183 signer_version = SignerVersion(app_hash, options.iteration)
184 signer_authorization = SignerAuthorization.for_signer_version(signer_version)
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)
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)
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}")
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)
231if __name__ == "__main__":
232 main()