[Cmd] msg-file
This commit is contained in:
parent
6d9f7193b3
commit
81a3f5e552
123
ckcc/cli.py
123
ckcc/cli.py
@ -463,6 +463,129 @@ def sign_message(message, path, verbose, just_sig, wrap, segwit):
|
||||
click.echo('%s\n%s\n%s' % (message.decode('ascii'), addr, sig))
|
||||
|
||||
|
||||
@main.command('msg-file')
|
||||
@click.argument('json_file', type=click.Path(exists=True, dir_okay=False), metavar="FILE_PATH")
|
||||
@click.option('--outfile', '-o', type=click.Path(), default=None,
|
||||
help="Output file path for signed result (default: <basename>-signed.<ext>)")
|
||||
@click.option('--outdir', '-d', type=click.Path(exists=True, dir_okay=True, file_okay=False),
|
||||
default=None, help="Directory to save signed file (default: same as input)")
|
||||
def sign_msg_file(json_file, outfile, outdir):
|
||||
"""
|
||||
Sign a text file containing a message (TXT or JSON format).
|
||||
|
||||
Reads a JSON or TXT file, extracts the message to sign, sends it to the Coldcard
|
||||
for signing, and writes the result as an RFC2440-like signed message file.
|
||||
|
||||
For JSON files, the file must contain a 'msg' field. The 'subpath' and 'addr_fmt'
|
||||
fields are optional:
|
||||
|
||||
{"msg":"this address belongs to me","subpath":"m/84h/0h/0h/0/120"}
|
||||
|
||||
For TXT files, the first line is the message. The optional second line specifies
|
||||
the subkey derivation path. The optional third line specifies the address format
|
||||
(p2pkh, p2sh-p2wpkh, or p2wpkh).
|
||||
|
||||
The signed output file is saved with '-signed' inserted before the file extension.
|
||||
"""
|
||||
|
||||
basename = os.path.basename(json_file)
|
||||
name, ext = os.path.splitext(basename)
|
||||
ext_lower = ext.lower()
|
||||
|
||||
if ext_lower not in ('.txt', '.json'):
|
||||
click.echo("Error: file must have .txt or .json extension")
|
||||
sys.exit(1)
|
||||
|
||||
if '-signed' in name:
|
||||
click.echo("Error: filename cannot contain '-signed'")
|
||||
sys.exit(1)
|
||||
|
||||
raw = open(json_file, 'r').read()
|
||||
if len(raw.encode('utf-8')) > 500: #CC Spec: "Be less than 500 bytes in size"
|
||||
click.echo("Error: file must be less than 500 bytes")
|
||||
sys.exit(1)
|
||||
|
||||
# parse file contents
|
||||
message = None
|
||||
subpath = None
|
||||
addr_fmt_str = None
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
if isinstance(parsed, dict):
|
||||
if 'msg' not in parsed:
|
||||
click.echo("Error: JSON must contain 'msg' field")
|
||||
sys.exit(1)
|
||||
message = parsed['msg']
|
||||
subpath = parsed.get('subpath', None)
|
||||
addr_fmt_str = parsed.get('addr_fmt', None)
|
||||
else:
|
||||
raise ValueError("not a JSON object")
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
lines = raw.strip().split('\n')
|
||||
if not lines or not lines[0].strip():
|
||||
click.echo("Error: file is empty or has no message")
|
||||
sys.exit(1)
|
||||
message = lines[0].strip()
|
||||
if len(lines) >= 2 and lines[1].strip():
|
||||
subpath = lines[1].strip()
|
||||
if len(lines) >= 3 and lines[2].strip():
|
||||
addr_fmt_str = lines[2].strip()
|
||||
|
||||
wrap = False
|
||||
segwit = False
|
||||
if addr_fmt_str:
|
||||
af = addr_fmt_str.lower().replace('-', '').replace('_', '')
|
||||
if af in ('p2shp2wpkh', 'p2sh_p2wpkh', 'p2shp2wpkh'):
|
||||
wrap = True
|
||||
elif af in ('p2wpkh', 'segwit', 'bech32'):
|
||||
segwit = True
|
||||
elif af not in ('p2pkh', 'classic', ''):
|
||||
click.echo("Warning: unknown addr_fmt '%s', using default (p2pkh)" % addr_fmt_str,
|
||||
err=True)
|
||||
|
||||
with get_device() as dev:
|
||||
|
||||
addr_fmt, af_path = addr_fmt_help(dev, wrap, segwit)
|
||||
signing_path = subpath or af_path
|
||||
msg_bytes = message.encode('ascii') if not isinstance(message, bytes) else message
|
||||
|
||||
ok = dev.send_recv(CCProtocolPacker.sign_message(msg_bytes, signing_path, addr_fmt),
|
||||
timeout=None)
|
||||
assert ok == None
|
||||
|
||||
print("Waiting for OK on the Coldcard...", end='', file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
while 1:
|
||||
time.sleep(0.250)
|
||||
done = dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
|
||||
if done == None:
|
||||
continue
|
||||
break
|
||||
|
||||
print("\r \r", end='', file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
if len(done) != 2:
|
||||
click.echo('Failed: %r' % done)
|
||||
sys.exit(1)
|
||||
|
||||
addr, raw_sig = done
|
||||
|
||||
sig = str(b64encode(raw_sig), 'ascii').replace('\n', '')
|
||||
|
||||
# format as RFC2440-like armoured output
|
||||
result = RFC_SIGNATURE_TEMPLATE.format(msg=message, addr=addr, sig=sig)
|
||||
|
||||
if not outfile:
|
||||
out_dir = outdir or os.path.dirname(os.path.abspath(json_file))
|
||||
signed_name = '%s-signed%s' % (name, ext)
|
||||
outfile = os.path.join(out_dir, signed_name)
|
||||
|
||||
open(outfile, 'w').write(result)
|
||||
click.echo("Wrote signed message to: %s" % outfile)
|
||||
|
||||
def wait_and_download(dev, req, fn):
|
||||
# Wait for user action on the device... by polling w/ indicated request
|
||||
# - also download resulting file
|
||||
|
||||
Loading…
Reference in New Issue
Block a user