Coverage for signapp.py: 82%
108 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-07-10 13:43 +0000
« 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.
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
38# Default signing path
39DEFAULT_ETH_PATH = "m/44'/60'/0'/0/0"
41# Legacy dongle constants
42COMMAND_SIGN = 0x02
43COMMAND_PUBKEY = 0x04
44OP_SIGN_MSG_PATH = bytes.fromhex("70")
45OP_SIGN_MSG_HASH = bytes.fromhex("800000")
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()
108 try:
109 eth = None
111 if options.path is None:
112 options.path = DEFAULT_ETH_PATH
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)")
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)
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)
141 # Get dongle access (must have ethereum app open)
142 eth = get_eth_dongle(options.verbose)
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()}")
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)
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'")
170 if options.operation != "hash" and options.iteration is None:
171 raise AdminError("Must provide a signer iteration with '-i/--iteration'")
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)
179 info("Computing signer authorization message...")
180 signer_version = SignerVersion(app_hash, options.iteration)
181 signer_authorization = SignerAuthorization.for_signer_version(signer_version)
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)
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)
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}")
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)
228if __name__ == "__main__":
229 main()