Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1dfa858b | ||
|
|
5460f276bf | ||
|
|
81a3f5e552 | ||
|
|
6d9f7193b3 | ||
|
|
abf040789b | ||
|
|
0bd92d4d6d | ||
|
|
8ffb706ad2 | ||
|
|
2afc7d34d2 | ||
|
|
3ae7ae9cf2 | ||
|
|
ab317b00c7 | ||
|
|
8642e5bc47 | ||
|
|
0246f35af3 | ||
|
|
aee51fdc2c | ||
|
|
51de25089e | ||
|
|
f1ce63812c | ||
|
|
4f4d08c73f | ||
|
|
c9ddf5d2a8 | ||
|
|
f87d30f220 | ||
|
|
c52657fe61 | ||
|
|
c14b6b2807 | ||
|
|
695e0f1eb6 | ||
|
|
3d2749db33 | ||
|
|
0e686dbda6 | ||
|
|
00e862d8df | ||
|
|
cad2722d14 | ||
|
|
a9fb98fd93 | ||
|
|
a6d901f9fc | ||
|
|
f924f6d35c | ||
|
|
2abb47844a | ||
|
|
f7999f4288 | ||
|
|
cf270d922b | ||
|
|
11c711e929 | ||
|
|
1efa93c72c | ||
|
|
1ff205f668 | ||
|
|
52b5950105 | ||
|
|
196fd0c436 | ||
|
|
aa75d04465 | ||
|
|
78ce292c4d | ||
|
|
7887bd21b5 | ||
|
|
6a3a3f9234 | ||
|
|
e35e2d204e | ||
|
|
71afe5c9a8 | ||
|
|
38332ff2d0 | ||
|
|
2216e4db0e | ||
|
|
d696bac051 | ||
|
|
d9efc0baef | ||
|
|
59c2e52876 | ||
|
|
757209900a |
@ -5,12 +5,7 @@
|
||||
#
|
||||
# - Copy this file into /etc/udev/rules.d and unplug and re-plug your Coldcard.
|
||||
# - Udev does not have to be restarted.
|
||||
#
|
||||
|
||||
# probably not needed:
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"
|
||||
|
||||
# required:
|
||||
# from <https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules>
|
||||
KERNEL=="hidraw*", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"
|
||||
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", TAG+="uaccess"
|
||||
KERNEL=="hidraw*", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", TAG+="uaccess"
|
||||
|
||||
@ -35,4 +35,4 @@ To build to release for Pypi:
|
||||
- `python3 setup.py sdist bdist_wheel`
|
||||
- maybe delete old version from `./dist`
|
||||
- tag source code with new version (at this point)
|
||||
- `twine upload dist/*` when ready.
|
||||
- `twine upload dist/*1.x.y*` when ready, use `__token__` as username, and API token as password
|
||||
|
||||
122
README.md
122
README.md
@ -1,8 +1,10 @@
|
||||
# Coldcard CLI and Python Interface Library
|
||||
|
||||
Coldcard is a Cheap, Ultra-secure & Opensource Hardware Wallet for #Bitcoin.
|
||||
Coldcard is an affordable, ultra-secure and open-source hardware
|
||||
wallet for Bitcoin. Built for hardcore Bitcoin users who demand
|
||||
maximum security.
|
||||
|
||||
Get yours at [ColdcardWallet.com](http://coldcard.com)
|
||||
Learn more and get yours at: [Coldcard.com](http://coldcard.com)
|
||||
|
||||
This is the python code and command-line utilities you need to communicate with it over USB.
|
||||
|
||||
@ -44,41 +46,46 @@ pip install --editable '.[cli]'
|
||||
Usage: ckcc [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
-s, --serial HEX Operate on specific unit (default: first found)
|
||||
-x, --simulator Connect to the simulator via Unix socket
|
||||
-P, --plaintext Disable USB link-layer encryption
|
||||
--help Show this message and exit.
|
||||
-s, --serial HEX Operate on specific unit (default: first
|
||||
found)
|
||||
-c, --socket ckcc-simulator-<pid>.sock
|
||||
Operate on specific simulator
|
||||
-x, --simulator Connect to the simulator via Unix socket
|
||||
-P, --plaintext Disable USB link-layer encryption
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
addr Show the human version of an address
|
||||
auth Indicate specific user is present (for HSM).
|
||||
backup Creates 7z encrypted backup file after...
|
||||
bag Factory: set or read bag number -- single use...
|
||||
chain Get which blockchain (Bitcoin/Testnet) is...
|
||||
convert2cc Convert existing Electrum wallet file into...
|
||||
backup Creates 7z encrypted backup file after prompting user to...
|
||||
bag Factory: set or read bag number -- single use only!
|
||||
chain Get which blockchain (Bitcoin/Testnet) is configured.
|
||||
convert2cc Convert existing Electrum wallet file into COLDCARD wallet...
|
||||
debug Start interactive (local) debug session
|
||||
eval Simulator only: eval a python statement
|
||||
exec Simulator only: exec a python script
|
||||
get-locker Get the value held in the Storage Locker (not...
|
||||
get-locker Get the value held in the Storage Locker (not Bitcoin...
|
||||
hsm Get current status of HSM feature.
|
||||
hsm-start Enable Hardware Security Module (HSM) mode.
|
||||
list List all attached Coldcard devices
|
||||
local-conf Generate the 6-digit code needed for a...
|
||||
logout Securely logout of device (will require...
|
||||
local-conf Generate the 6-digit code needed for a specific PSBT file...
|
||||
logout Securely logout of device (will require replug to start over)
|
||||
miniscript Miniscript related commands
|
||||
msg Sign a short text message
|
||||
multisig Create a skeleton file which defines a...
|
||||
multisig Create a skeleton file which defines a multisig wallet.
|
||||
p2sh Show a multisig payment address on-screen.
|
||||
pass Provide a BIP39 passphrase
|
||||
pubkey Get the public key for a derivation path Dump...
|
||||
pubkey Get the public key for a derivation path
|
||||
reboot Reboot coldcard, force relogin and start over
|
||||
sign Approve a spending transaction by signing it...
|
||||
restore Uploads 7z encrypted backup file & starts backup restore...
|
||||
sign Approve a spending transaction by signing it on Coldcard
|
||||
test Test USB connection (debug/dev)
|
||||
upgrade Send firmware file (.dfu) and trigger upgrade...
|
||||
upload Send file to Coldcard (PSBT transaction or...
|
||||
user Create a new user on the Coldcard for HSM...
|
||||
upgrade Send firmware file (.dfu) and trigger upgrade process
|
||||
upload Send file to Coldcard (PSBT transaction or firmware)
|
||||
user Create a new user on the Coldcard for HSM policy (also...
|
||||
version Get the version of the firmware installed
|
||||
xfp Get the fingerprint for this wallet (master...
|
||||
xpub Get the XPUB for this wallet (master level,...
|
||||
xfp Get the fingerprint for this wallet (master level)
|
||||
xpub Get the XPUB for this wallet (master level, or any derivation)
|
||||
```
|
||||
|
||||
|
||||
@ -105,6 +112,7 @@ Hello Coldcard
|
||||
H4mTuwMUdnu3MyMA+6aJ3hiAF4L0WBDZFseTEno511hNN8/THIeM4GW4SnrcJJhS3WxMZEWFdEIZDSP+H5aIcao=
|
||||
```
|
||||
|
||||
|
||||
## Transaction Signing
|
||||
|
||||
```
|
||||
@ -114,14 +122,15 @@ Usage: ckcc sign [OPTIONS] PSBT_IN [PSBT_OUT]
|
||||
Approve a spending transaction by signing it on Coldcard
|
||||
|
||||
Options:
|
||||
-v, --verbose Show more details
|
||||
-f, --finalize Show final signed transaction, ready for transmission
|
||||
-z, --visualize Show text of Coldcard's interpretation of the transaction
|
||||
(does not create transaction, no interaction needed)
|
||||
-s, --signed Include a signature over visualization text
|
||||
-x, --hex Write out (signed) PSBT in hexidecimal
|
||||
-6, --base64 Write out (signed) PSBT encoded in base64
|
||||
--help Show this message and exit.
|
||||
-f, --finalize Show final signed transaction, ready for transmission
|
||||
-z, --visualize Show text of Coldcard's interpretation of the transaction
|
||||
(does not create transaction, no interaction needed)
|
||||
-p, --pushtx URL Broadcast transaction via provided PushTx URL. Shortcut
|
||||
options: coldcard, mempool
|
||||
-s, --signed Include a signature over visualization text
|
||||
-x, --hex Write out (signed) PSBT in hexidecimal
|
||||
-6, --base64 Write out (signed) PSBT encoded in base64
|
||||
--help Show this message and exit.
|
||||
|
||||
% (... acquire PSBT file for what you want to do ...)
|
||||
|
||||
@ -137,6 +146,61 @@ Ok! Downloading result (5119 bytes)
|
||||
00000020 c2 23 e1 06 22 1b 02 0e bd c8 1c 71 79 7d 3c 02 |.#.."......qy}<.|
|
||||
00000030 00 00 00 00 fe ff ff ff 4c 85 a0 2c 80 cb 2c 01 |........L..,..,.|
|
||||
|
||||
# sign PSBT provided via stdin
|
||||
% echo 'cHNidP8BAHcBAAAAASGhv...fX4RgPBWlDLAAAgAEAAIAAAACAAQAAAAUAAAAA' | ckcc sign -
|
||||
|
||||
```
|
||||
|
||||
## Miniscript
|
||||
|
||||
```
|
||||
% ckcc miniscript --help
|
||||
Usage: ckcc miniscript [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Miniscript related commands
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
addr Get miniscript internal/external chain address by index with on...
|
||||
del Delete registered miniscript wallet by name with on device...
|
||||
enroll Enroll miniscript wallet
|
||||
get Get registered miniscript wallet by name.
|
||||
ls List registered miniscript wallet names.
|
||||
```
|
||||
|
||||
## Backup/Restore
|
||||
|
||||
```
|
||||
% ckcc backup --help
|
||||
Usage: ckcc backup [OPTIONS]
|
||||
|
||||
Creates 7z encrypted backup file after prompting user to remember a massive
|
||||
passphrase. Downloads the AES-encrypted data backup and by default, saves
|
||||
into current directory using a filename based on today's date.
|
||||
|
||||
Options:
|
||||
-d, --outdir DIRECTORY Save into indicated directory (auto filename)
|
||||
-o, --outfile filename.7z Name for backup file
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
```
|
||||
% ckcc restore --help
|
||||
Usage: ckcc restore [OPTIONS] backup.7z
|
||||
|
||||
Uploads 7z encrypted backup file & starts backup restore process. User needs
|
||||
to specify what kind of backup is being uploaded. Default is 7z encrypted
|
||||
file with word-based password. Use -p/--password flag if your backup has
|
||||
custom not word-based password. User is prompted to enter backup password on
|
||||
the device.
|
||||
|
||||
Options:
|
||||
-c, --plaintext Force plaintext restore. No need to use if file has proper
|
||||
'.txt' suffix
|
||||
-p, --password This backup has custom password. Not words.
|
||||
-t, --tmp Force restoring backup as temporary seed. Only works for
|
||||
seedless Coldcard.
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
# (c) Copyright 2021-2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
|
||||
__version__ = '1.3.2'
|
||||
__version__ = '1.5.0'
|
||||
|
||||
__all__ = [ "client", "protocol", "constants" ]
|
||||
|
||||
|
||||
499
ckcc/cli.py
499
ckcc/cli.py
@ -12,11 +12,10 @@
|
||||
# - see <https://github.com/trezor/cython-hidapi/blob/master/hid.pyx> for HID api
|
||||
#
|
||||
#
|
||||
import hid, click, sys, os, pdb, struct, time, io, re, json, contextlib
|
||||
import hid, click, sys, os, pdb, struct, time, io, re, json, contextlib, tempfile
|
||||
from pprint import pformat
|
||||
from binascii import b2a_hex, a2b_hex
|
||||
from hashlib import sha256
|
||||
from base64 import b64encode
|
||||
from functools import wraps
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
@ -24,13 +23,14 @@ from ecdsa import VerifyingKey, SECP256k1
|
||||
|
||||
from ckcc.protocol import CCProtocolPacker, CCProtocolUnpacker
|
||||
from ckcc.protocol import CCProtoError, CCUserRefused, CCBusyError
|
||||
from ckcc.constants import MAX_MSG_LEN, MAX_BLK_LEN, MAX_USERNAME_LEN
|
||||
from ckcc.constants import MAX_MSG_LEN, MAX_BLK_LEN, MAX_USERNAME_LEN, MAX_SIGNERS
|
||||
from ckcc.constants import USER_AUTH_HMAC, USER_AUTH_TOTP, USER_AUTH_HOTP, USER_AUTH_SHOW_QR
|
||||
from ckcc.constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
|
||||
from ckcc.constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
|
||||
from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID
|
||||
from ckcc.constants import AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH
|
||||
from ckcc.constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, RFC_SIGNATURE_TEMPLATE
|
||||
from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID, DEFAULT_SIM_SOCKET
|
||||
from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC
|
||||
from ckcc.utils import dfu_parse, calc_local_pincode, xfp2str, B2A, decode_xpub, get_pubkey_string
|
||||
from ckcc.utils import dfu_parse, calc_local_pincode, xfp2str, B2A, decode_xpub
|
||||
from ckcc.utils import get_pubkey_string, descriptor_template, addr_fmt_help, txn_to_pushtx_url
|
||||
from ckcc.electrum import filepath_append_cc, convert2cc
|
||||
|
||||
global force_serial
|
||||
@ -70,19 +70,21 @@ def get_device(optional=False):
|
||||
# Options we want for all commands
|
||||
@click.group()
|
||||
@click.option('--serial', '-s', default=None, metavar="HEX",
|
||||
help="Operate on specific unit (default: first found)")
|
||||
help="Operate on specific unit (default: first found)")
|
||||
@click.option('--socket', '-c', type=click.Path(exists=True,dir_okay=False),
|
||||
metavar="ckcc-simulator-<pid>.sock", required=False, default=None,
|
||||
help="Operate on specific simulator")
|
||||
@click.option('--simulator', '-x', default=False, is_flag=True,
|
||||
help="Connect to the simulator via Unix socket")
|
||||
help="Connect to the simulator via Unix socket")
|
||||
@click.option('--plaintext', '-P', default=False, is_flag=True,
|
||||
help="Disable USB link-layer encryption")
|
||||
def main(serial, simulator, plaintext):
|
||||
help="Disable USB link-layer encryption")
|
||||
def main(serial, simulator, plaintext, socket):
|
||||
global force_serial, force_plaintext
|
||||
force_serial = serial
|
||||
force_plaintext = plaintext
|
||||
|
||||
if simulator:
|
||||
force_serial = '/tmp/ckcc-simulator.sock'
|
||||
|
||||
if simulator or socket:
|
||||
force_serial = socket or DEFAULT_SIM_SOCKET
|
||||
|
||||
def display_errors(f):
|
||||
# clean-up display of errors from Coldcard
|
||||
@ -241,7 +243,7 @@ def real_file_upload(fd, dev, blksize=MAX_BLK_LEN, do_upgrade=False, do_reboot=T
|
||||
if not here: break
|
||||
left -= len(here)
|
||||
result = dev.send_recv(CCProtocolPacker.upload(pos, sz, here))
|
||||
assert result == pos, "Got back: %r" % result
|
||||
assert result == pos, f"Got back: {result}"
|
||||
chk.update(here)
|
||||
|
||||
# do a verify
|
||||
@ -274,13 +276,22 @@ def real_file_upload(fd, dev, blksize=MAX_BLK_LEN, do_upgrade=False, do_reboot=T
|
||||
|
||||
@main.command('upload')
|
||||
@click.argument('filename', type=click.File('rb'))
|
||||
@click.option('--blksize', default=MAX_BLK_LEN,
|
||||
type=click.IntRange(256, MAX_BLK_LEN), help='Block size to use (testing)')
|
||||
@click.option('--multisig', '-m', default=False, is_flag=True,
|
||||
help='Attempt multisig enroll using file')
|
||||
def file_upload(filename, blksize, multisig=False):
|
||||
@click.option('--blksize', default=MAX_BLK_LEN, type=click.IntRange(256, MAX_BLK_LEN),
|
||||
help='Block size to use (testing)')
|
||||
@click.option('--multisig', '-m', is_flag=True,
|
||||
help='Attempt multisig enroll using file')
|
||||
@click.option('--miniscript', is_flag=True,
|
||||
help='Attempt miniscript enroll using file')
|
||||
@click.option('--backup', is_flag=True,
|
||||
help='Upload encrypted backup')
|
||||
def file_upload(filename, blksize, multisig, miniscript, backup):
|
||||
"""Send file to Coldcard (PSBT transaction or firmware)"""
|
||||
|
||||
if sum([multisig, miniscript, backup]) > 1:
|
||||
# only 1 or None can be True
|
||||
click.echo("Failed: Only one can be specified from miniscript/multisig/backup")
|
||||
sys.exit(1)
|
||||
|
||||
# NOTE: mostly for debug/dev usage.
|
||||
with get_device() as dev:
|
||||
|
||||
@ -288,6 +299,11 @@ def file_upload(filename, blksize, multisig=False):
|
||||
|
||||
if multisig:
|
||||
dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha))
|
||||
elif miniscript:
|
||||
dev.send_recv(CCProtocolPacker.miniscript_enroll(file_len, sha), timeout=None)
|
||||
elif backup:
|
||||
dev.send_recv(CCProtocolPacker.restore_backup(file_len, sha), timeout=None)
|
||||
|
||||
|
||||
|
||||
@main.command('upgrade')
|
||||
@ -302,7 +318,9 @@ def firmware_upgrade(filename, stop_early):
|
||||
|
||||
@main.command('xpub')
|
||||
@click.argument('subpath', default='m')
|
||||
def get_xpub(subpath):
|
||||
@click.option('--verbose', '-v', is_flag=True,
|
||||
help='Show extended key with master fingerprint and derivation')
|
||||
def get_xpub(subpath, verbose):
|
||||
"""Get the XPUB for this wallet (master level, or any derivation)"""
|
||||
|
||||
with get_device() as dev:
|
||||
@ -312,7 +330,12 @@ def get_xpub(subpath):
|
||||
|
||||
xpub = dev.send_recv(CCProtocolPacker.get_xpub(subpath), timeout=None)
|
||||
|
||||
click.echo(xpub)
|
||||
if verbose:
|
||||
# return key expression with BIP-32 extended key as defined in BIP-380
|
||||
sp = subpath.replace("m/", "").replace("'", "h")
|
||||
click.echo(f"[{xfp2str(dev.master_fingerprint).lower()}/{sp}]{xpub}")
|
||||
else:
|
||||
click.echo(xpub)
|
||||
|
||||
|
||||
@main.command('pubkey')
|
||||
@ -389,22 +412,16 @@ def run_eval(stmt):
|
||||
|
||||
@main.command('msg')
|
||||
@click.argument('message')
|
||||
@click.option('--path', '-p', default=BIP44_FIRST,
|
||||
help=f'Derivation for key to use [default: {BIP44_FIRST}]', metavar="DERIVATION")
|
||||
@click.option('--path', '-p', default=None, help=f'Derivation for key to use', metavar="DERIVATION")
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Include fancy ascii armour')
|
||||
@click.option('--just-sig', '-j', is_flag=True, help='Just the signature itself, nothing more')
|
||||
@click.option('--segwit', '-s', is_flag=True, help='Address in segwit native (p2wpkh, bech32)')
|
||||
@click.option('--wrap', '-w', is_flag=True, help='Address in segwit wrapped in P2SH (p2wpkh)')
|
||||
def sign_message(message, path, verbose=True, just_sig=False, wrap=False, segwit=False):
|
||||
@click.option('--wrap', '-w', is_flag=True, help='Address in segwit wrapped in P2SH (p2sh-p2wpkh)')
|
||||
def sign_message(message, path, verbose, just_sig, wrap, segwit):
|
||||
"""Sign a short text message"""
|
||||
with get_device() as dev:
|
||||
|
||||
if wrap:
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
elif segwit:
|
||||
addr_fmt = AF_P2WPKH
|
||||
else:
|
||||
addr_fmt = AF_CLASSIC
|
||||
addr_fmt, af_path = addr_fmt_help(dev, wrap, segwit)
|
||||
|
||||
# NOTE: initial version of firmware not expected to do segwit stuff right, since
|
||||
# standard very much still in flux, see: <https://github.com/bitcoin/bitcoin/issues/10542>
|
||||
@ -412,7 +429,7 @@ def sign_message(message, path, verbose=True, just_sig=False, wrap=False, segwit
|
||||
# not enforcing policy here on msg contents, so we can define that on product
|
||||
message = message.encode('ascii') if not isinstance(message, bytes) else message
|
||||
|
||||
ok = dev.send_recv(CCProtocolPacker.sign_message(message, path, addr_fmt), timeout=None)
|
||||
ok = dev.send_recv(CCProtocolPacker.sign_message(message, path or af_path, addr_fmt), timeout=None)
|
||||
assert ok == None
|
||||
|
||||
print("Waiting for OK on the Coldcard...", end='', file=sys.stderr)
|
||||
@ -440,13 +457,130 @@ def sign_message(message, path, verbose=True, just_sig=False, wrap=False, segwit
|
||||
if just_sig:
|
||||
click.echo(str(sig))
|
||||
elif verbose:
|
||||
click.echo('-----BEGIN SIGNED MESSAGE-----\n{msg}\n-----BEGIN '
|
||||
'SIGNATURE-----\n{addr}\n{sig}\n-----END SIGNED MESSAGE-----'.format(
|
||||
msg=message.decode('ascii'), addr=addr, sig=sig))
|
||||
click.echo(RFC_SIGNATURE_TEMPLATE.format(msg=message.decode('ascii'),
|
||||
addr=addr, sig=sig))
|
||||
else:
|
||||
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)
|
||||
|
||||
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
|
||||
@ -480,34 +614,40 @@ def wait_and_download(dev, req, fn):
|
||||
@main.command('sign')
|
||||
@click.argument('psbt_in', type=click.File('rb'))
|
||||
@click.argument('psbt_out', type=click.File('wb'), required=False)
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Show more details')
|
||||
@click.option('--finalize', '-f', is_flag=True, help='Show final signed transaction, ready for transmission')
|
||||
@click.option('--visualize', '-z', is_flag=True, help='Show text of Coldcard\'s interpretation of the transaction (does not create transaction, no interaction needed)')
|
||||
@click.option('--pushtx', '-p', default=None, help='Broadcast transaction via provided PushTx URL. Shortcut options: coldcard, mempool', metavar="URL")
|
||||
@click.option('--miniscript', '-m', default=None, help='Miniscript wallet name')
|
||||
@click.option('--signed', '-s', is_flag=True, help='Include a signature over visualization text')
|
||||
@click.option('--hex', '-x', 'hex_mode', is_flag=True, help="Write out (signed) PSBT in hexidecimal")
|
||||
@click.option('--base64', '-6', 'b64_mode', is_flag=True, help="Write out (signed) PSBT encoded in base64")
|
||||
@display_errors
|
||||
def sign_transaction(psbt_in, psbt_out=None, verbose=False, b64_mode=False, hex_mode=False, finalize=False, visualize=False, signed=False):
|
||||
def sign_transaction(psbt_in, psbt_out, pushtx, b64_mode, hex_mode, finalize, visualize, signed, miniscript):
|
||||
"""Approve a spending transaction by signing it on Coldcard"""
|
||||
with get_device() as dev:
|
||||
dev.check_mitm()
|
||||
|
||||
# Handle non-binary encodings, and incorrect files.
|
||||
taste = psbt_in.read(10)
|
||||
psbt_in.seek(0)
|
||||
if taste == b'70736274ff' or taste == b'70736274FF':
|
||||
data = psbt_in.read()
|
||||
|
||||
if data[:10].lower() == b'70736274ff':
|
||||
# Looks hex encoded; make into binary again
|
||||
hx = ''.join(re.findall(r'[0-9a-fA-F]*', psbt_in.read().decode('ascii')))
|
||||
psbt_in = io.BytesIO(a2b_hex(hx))
|
||||
elif taste[0:6] == b'cHNidP':
|
||||
hx = ''.join(re.findall(r'[0-9a-fA-F]*', data.decode('ascii')))
|
||||
data = a2b_hex(hx)
|
||||
elif data[:6] == b'cHNidP':
|
||||
# Base64 encoded input
|
||||
psbt_in = io.BytesIO(b64decode(psbt_in.read()))
|
||||
elif taste[0:5] != b'psbt\xff':
|
||||
data = b64decode(data)
|
||||
|
||||
if data[0:5] != b'psbt\xff':
|
||||
click.echo("File doesn't have PSBT magic number at start.")
|
||||
sys.exit(1)
|
||||
|
||||
# upload the transaction
|
||||
txn_len, sha = real_file_upload(psbt_in, dev)
|
||||
txn_len, sha = real_file_upload(io.BytesIO(data), dev)
|
||||
|
||||
if pushtx:
|
||||
finalize = True
|
||||
visualize = signed = False
|
||||
|
||||
flags = 0x0
|
||||
if visualize or signed:
|
||||
@ -518,17 +658,34 @@ def sign_transaction(psbt_in, psbt_out=None, verbose=False, b64_mode=False, hex_
|
||||
flags |= STXN_FINALIZE
|
||||
|
||||
# start the signing process
|
||||
ok = dev.send_recv(CCProtocolPacker.sign_transaction(txn_len, sha, flags=flags), timeout=None)
|
||||
ok = dev.send_recv(CCProtocolPacker.sign_transaction(txn_len, sha, flags=flags,
|
||||
miniscript_name=miniscript), timeout=None)
|
||||
assert ok is None
|
||||
|
||||
# errors will raise here, no need for error display
|
||||
result, _ = wait_and_download(dev, CCProtocolPacker.get_signed_txn(), 1)
|
||||
result, sha = wait_and_download(dev, CCProtocolPacker.get_signed_txn(), 1)
|
||||
|
||||
# If 'finalize' is set, we are outputing a bitcoin transaction,
|
||||
# ready for the p2p network. If the CC wasn't able to finalize it,
|
||||
# an exception would have occured. Most people will want hex here, but
|
||||
# resisting the urge to force it.
|
||||
|
||||
if pushtx:
|
||||
pushtx_url = {
|
||||
"coldcard": "https://coldcard.com/pushtx#",
|
||||
"mempool": "https://mempool.space/pushtx#"
|
||||
}.get(pushtx, pushtx)
|
||||
|
||||
chain = dev.send_recv(CCProtocolPacker.block_chain())
|
||||
|
||||
try:
|
||||
url = txn_to_pushtx_url(result, pushtx_url, sha=sha, chain=chain)
|
||||
click.launch(url)
|
||||
except Exception as e:
|
||||
click.echo(f"ERROR: {e}", err=True)
|
||||
|
||||
return # done here
|
||||
|
||||
if visualize:
|
||||
if psbt_out:
|
||||
psbt_out.write(result)
|
||||
@ -538,12 +695,12 @@ def sign_transaction(psbt_in, psbt_out=None, verbose=False, b64_mode=False, hex_
|
||||
# save it
|
||||
if hex_mode:
|
||||
result = b2a_hex(result)
|
||||
elif b64_mode or (not psbt_out and os.isatty(0)):
|
||||
elif b64_mode or (not psbt_out):
|
||||
result = b64encode(result)
|
||||
|
||||
if psbt_out:
|
||||
psbt_out.write(result)
|
||||
else:
|
||||
elif not pushtx:
|
||||
click.echo(result)
|
||||
|
||||
|
||||
@ -554,9 +711,8 @@ def sign_transaction(psbt_in, psbt_out=None, verbose=False, b64_mode=False, hex_
|
||||
@click.option('--outfile', '-o', metavar="filename.7z",
|
||||
help="Name for backup file", default=None,
|
||||
type=click.File('wb'))
|
||||
#@click.option('--verbose', '-v', is_flag=True, help='Show more details')
|
||||
@display_errors
|
||||
def start_backup(outdir, outfile, verbose=False):
|
||||
def start_backup(outdir, outfile):
|
||||
"""
|
||||
Creates 7z encrypted backup file after prompting user to remember a massive passphrase.
|
||||
Downloads the AES-encrypted data backup and by default, saves into current directory using
|
||||
@ -585,23 +741,49 @@ def start_backup(outdir, outfile, verbose=False):
|
||||
click.echo("Wrote %d bytes into: %s\nSHA256: %s" % (len(result), fn, str(b2a_hex(chk), 'ascii')))
|
||||
|
||||
|
||||
@main.command('restore')
|
||||
@click.argument('filename', type=click.File('rb'), metavar="backup.7z")
|
||||
@click.option('-c', '--plaintext', is_flag=True,
|
||||
help="Force plaintext restore. No need to use if file has proper '.txt' suffix")
|
||||
@click.option('-p', '--password', is_flag=True,
|
||||
help="This backup has custom password. Not words.")
|
||||
@click.option('-t', '--tmp', is_flag=True,
|
||||
help="Force restoring backup as temporary seed. Only works for seedless Coldcard.")
|
||||
@display_errors
|
||||
def restore_backup(filename, plaintext, password, tmp):
|
||||
"""
|
||||
Uploads 7z encrypted backup file & starts backup restore process. User needs to specify
|
||||
what kind of backup is being uploaded. Default is 7z encrypted file with word-based password.
|
||||
Use -p/--password flag if your backup has custom not word-based password.
|
||||
User is prompted to enter backup password on the device.
|
||||
"""
|
||||
|
||||
if not plaintext and filename.name.lower().endswith(".txt"):
|
||||
plaintext = True
|
||||
|
||||
if plaintext and password:
|
||||
# only 1 or None can be True
|
||||
click.echo("Failed: Plaintext backup cannot have custom password.")
|
||||
sys.exit(1)
|
||||
|
||||
with get_device() as dev:
|
||||
file_len, sha = real_file_upload(filename, dev)
|
||||
dev.send_recv(
|
||||
CCProtocolPacker.restore_backup(file_len, sha, password, plaintext, tmp),
|
||||
timeout=None)
|
||||
|
||||
|
||||
@main.command('addr')
|
||||
@click.argument('path', default=BIP44_FIRST, metavar='[m/1/2/3]', required=False)
|
||||
@click.argument('path', default=None, metavar='[m/1/2/3]', required=False)
|
||||
@click.option('--segwit', '-s', is_flag=True, help='Show in segwit native (p2wpkh, bech32)')
|
||||
@click.option('--wrap', '-w', is_flag=True, help='Show in segwit wrapped in P2SH (p2wpkh)')
|
||||
@click.option('--taproot', '-t', is_flag=True, help='Show in taproot (p2tr, bech32m)')
|
||||
@click.option('--wrap', '-w', is_flag=True, help='Show in segwit wrapped in P2SH (p2sh-p2wpkh)')
|
||||
@click.option('--quiet', '-q', is_flag=True, help='Show less details; just the address')
|
||||
@click.option('--path', '-p', default=BIP44_FIRST, help='Derivation for key to show (or first arg)')
|
||||
def show_address(path, quiet=False, segwit=False, wrap=False):
|
||||
def show_address(path, quiet, segwit, wrap, taproot):
|
||||
"""Show the human version of an address"""
|
||||
with get_device() as dev:
|
||||
if wrap:
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
elif segwit:
|
||||
addr_fmt = AF_P2WPKH
|
||||
else:
|
||||
addr_fmt = AF_CLASSIC
|
||||
|
||||
addr = dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None)
|
||||
addr_fmt, af_path = addr_fmt_help(dev, wrap, segwit, taproot)
|
||||
addr = dev.send_recv(CCProtocolPacker.show_address(path or af_path, addr_fmt), timeout=None)
|
||||
|
||||
if quiet:
|
||||
click.echo(addr)
|
||||
@ -635,10 +817,10 @@ def str_to_int_path(xfp, path):
|
||||
@main.command('p2sh')
|
||||
@click.argument('script', type=str, nargs=1, required=True)
|
||||
@click.argument('fingerprints', type=str, nargs=-1, required=True)
|
||||
@click.option('--segwit', '-s', is_flag=True, help='Show in segwit native (p2wpkh, bech32)')
|
||||
@click.option('--wrap', '-w', is_flag=True, help='Show as segwit wrapped in P2SH (p2wpkh)')
|
||||
@click.option('--segwit', '-s', is_flag=True, help='Show in segwit native (p2wsh, bech32)')
|
||||
@click.option('--wrap', '-w', is_flag=True, help='Show as segwit wrapped in P2SH (p2sh-p2wsh)')
|
||||
@click.option('--quiet', '-q', is_flag=True, help='Show less details; just the address')
|
||||
def show_address(script, fingerprints, quiet=False, segwit=False, wrap=False):
|
||||
def show_address(script, fingerprints, quiet, segwit, wrap):
|
||||
"""
|
||||
Show a multisig payment address on-screen.
|
||||
|
||||
@ -660,7 +842,7 @@ def show_address(script, fingerprints, quiet=False, segwit=False, wrap=False):
|
||||
script = a2b_hex(script)
|
||||
N = len(fingerprints)
|
||||
|
||||
assert 1 <= N <= 15, "bad N"
|
||||
assert 1 <= N <= MAX_SIGNERS, "bad N"
|
||||
|
||||
min_signers = script[0] - 80
|
||||
assert 1 <= min_signers <= N, "bad M"
|
||||
@ -689,7 +871,7 @@ def show_address(script, fingerprints, quiet=False, segwit=False, wrap=False):
|
||||
@click.option('--passphrase', prompt=True, hide_input=True,
|
||||
confirmation_prompt=False)
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Show new root xpub')
|
||||
def bip39_passphrase(passphrase, verbose=False):
|
||||
def bip39_passphrase(passphrase, verbose):
|
||||
"""Provide a BIP39 passphrase"""
|
||||
|
||||
with get_device() as dev:
|
||||
@ -719,15 +901,26 @@ def bip39_passphrase(passphrase, verbose=False):
|
||||
|
||||
|
||||
@main.command('multisig')
|
||||
@click.option('--min-signers', '-m', type=int, help='Minimum M signers of N required to approve (default: all)', default=0)
|
||||
@click.option('--signers', '-n', 'num_signers', type=int, help='N signers in wallet', default=3)
|
||||
@click.option('--name', '-l', type=str, help='Wallet name on Coldcard', default='Unnamed')
|
||||
@click.option('--min-signers', '-m', type=int, default=0,
|
||||
help='Minimum M signers of N required to approve (default: all)')
|
||||
@click.option('--signers', '-n', 'num_signers', type=int, default=3,
|
||||
help='N signers in wallet')
|
||||
@click.option('--name', '-l', type=str, default='Unnamed',
|
||||
help='Wallet name on Coldcard')
|
||||
@click.option('--output-file', '-f', type=click.File('wt', lazy=True),
|
||||
help='Save configuration to file')
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Show file uploaded')
|
||||
@click.option('--path', '-p', default="m/45'", help="Derivation for key (default: BIP45 = m/45')")
|
||||
@click.option('--add', '-a', 'just_add', is_flag=True, help='Just show line required to add this Coldcard')
|
||||
def enroll_xpub(name, min_signers, path, num_signers, output_file=None, verbose=False, just_add=False):
|
||||
help='Save configuration to file')
|
||||
@click.option('--verbose', '-v', is_flag=True,
|
||||
help='Show file uploaded')
|
||||
@click.option('--path', '-p', default="m/45'",
|
||||
help="Derivation for key (default: BIP45 = m/45')")
|
||||
@click.option('--add', '-a', 'just_add', is_flag=True,
|
||||
help='Just show line required to add this Coldcard')
|
||||
@click.option('--desc', '-d', 'descriptor', is_flag=True,
|
||||
help='Use BIP380 descriptor template')
|
||||
@click.option('--format', type=click.Choice(["p2sh", "p2sh-p2wsh", "p2wsh"]),
|
||||
help='Address format', default="p2wsh")
|
||||
def enroll_xpub(name, min_signers, path, num_signers, output_file, verbose,
|
||||
just_add, descriptor, format):
|
||||
"""
|
||||
Create a skeleton file which defines a multisig wallet.
|
||||
When completed, use with: "ckcc upload -m wallet.txt" or put on SD card.
|
||||
@ -737,7 +930,8 @@ def enroll_xpub(name, min_signers, path, num_signers, output_file=None, verbose
|
||||
|
||||
xfp = dev.master_fingerprint
|
||||
my_xpub = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
|
||||
new_line = "%s: %s" % (xfp2str(xfp), my_xpub)
|
||||
xfp_str = xfp2str(xfp)
|
||||
new_line = "%s: %s" % (xfp_str, my_xpub)
|
||||
|
||||
if just_add:
|
||||
click.echo(new_line)
|
||||
@ -763,14 +957,27 @@ def enroll_xpub(name, min_signers, path, num_signers, output_file=None, verbose
|
||||
click.echo("Name must be between 1 and 20 characters of ASCII.")
|
||||
sys.exit(1)
|
||||
|
||||
# render into a template
|
||||
config = f'name: {name}\npolicy: {min_signers} of {N}\n\n#path: {path}\n{new_line}\n'
|
||||
if num_signers != 1:
|
||||
config += '\n'.join(f'#{i+2}# FINGERPRINT: xpub123123123123123' for i in range(num_signers-1))
|
||||
config += '\n'
|
||||
if descriptor:
|
||||
if format == "p2sh":
|
||||
fmt = AF_P2SH
|
||||
elif format == "p2sh-p2wsh":
|
||||
fmt = AF_P2WSH_P2SH
|
||||
else:
|
||||
fmt = AF_P2WSH
|
||||
config = descriptor_template(xfp=xfp_str, xpub=my_xpub, path=path, fmt=fmt, m=min_signers)
|
||||
|
||||
if name != 'Unnamed':
|
||||
config = json.dumps({"name": name, "desc": config})
|
||||
|
||||
else:
|
||||
# render into a template
|
||||
config = f'name: {name}\npolicy: {min_signers} of {N}\nformat: {format.upper()}\n\n#path: {path}\n{new_line}\n'
|
||||
if num_signers != 1:
|
||||
config += '\n'.join(f'#{i+2}# FINGERPRINT: xpub123123123123123' for i in range(num_signers-1))
|
||||
config += '\n'
|
||||
|
||||
if verbose or not output_file:
|
||||
click.echo(config[:-1])
|
||||
click.echo(config[:-1] if config[-1] == "\n" else config)
|
||||
|
||||
if output_file:
|
||||
output_file.write(config)
|
||||
@ -781,7 +988,7 @@ def enroll_xpub(name, min_signers, path, num_signers, output_file=None, verbose
|
||||
@main.command('hsm-start')
|
||||
@click.argument('policy', type=click.Path(exists=True,dir_okay=False), metavar="policy.json", required=False)
|
||||
@click.option('--dry-run', '-n', is_flag=True, help="Just validate file, don't upload")
|
||||
def hsm_setup(policy=None, dry_run=False):
|
||||
def hsm_setup(policy, dry_run):
|
||||
"""
|
||||
Enable Hardware Security Module (HSM) mode.
|
||||
|
||||
@ -826,6 +1033,119 @@ def hsm_status():
|
||||
|
||||
click.echo(pformat(o))
|
||||
|
||||
@click.group()
|
||||
def miniscript():
|
||||
"""Miniscript related commands"""
|
||||
pass
|
||||
|
||||
|
||||
@miniscript.command('enroll')
|
||||
@click.argument('desc', type=str, metavar="DESCRIPTOR",
|
||||
required=True)
|
||||
@click.option('--blksize', default=MAX_BLK_LEN,
|
||||
type=click.IntRange(256, MAX_BLK_LEN),
|
||||
help='Block size to use (testing)')
|
||||
def miniscript_enroll(desc, blksize):
|
||||
"""
|
||||
Enroll miniscript wallet from string.
|
||||
Descriptor can be JSON wrapped.
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("wb+") as tmp:
|
||||
tmp.write(desc.encode())
|
||||
tmp.seek(0)
|
||||
with get_device() as dev:
|
||||
file_len, sha = real_file_upload(tmp, dev, blksize=blksize)
|
||||
dev.send_recv(CCProtocolPacker.miniscript_enroll(file_len, sha), timeout=None)
|
||||
|
||||
|
||||
@miniscript.command('ls')
|
||||
def miniscript_ls():
|
||||
"""
|
||||
List registered miniscript wallet names.
|
||||
"""
|
||||
with get_device() as dev:
|
||||
dev.check_mitm()
|
||||
|
||||
resp = dev.send_recv(CCProtocolPacker.miniscript_ls())
|
||||
o = json.loads(resp)
|
||||
|
||||
click.echo(pformat(o))
|
||||
|
||||
|
||||
@miniscript.command('del')
|
||||
@click.argument('name', type=str,
|
||||
metavar="MINISCRIPT_WALLET_NAME",
|
||||
required=True)
|
||||
def miniscript_del(name):
|
||||
"""
|
||||
Delete registered miniscript wallet by name with
|
||||
on device confirmation.
|
||||
"""
|
||||
with get_device() as dev:
|
||||
dev.check_mitm()
|
||||
|
||||
dev.send_recv(CCProtocolPacker.miniscript_delete(name))
|
||||
|
||||
|
||||
@miniscript.command('get')
|
||||
@click.argument('name', type=str,
|
||||
metavar="MINISCRIPT_WALLET_NAME",
|
||||
required=True)
|
||||
def miniscript_get(name):
|
||||
"""
|
||||
Get registered miniscript wallet by name.
|
||||
"""
|
||||
with get_device() as dev:
|
||||
dev.check_mitm()
|
||||
|
||||
resp = dev.send_recv(CCProtocolPacker.miniscript_get(name),
|
||||
timeout=None)
|
||||
o = json.loads(resp)
|
||||
|
||||
click.echo(pformat(o))
|
||||
|
||||
|
||||
@miniscript.command('policy')
|
||||
@click.argument('name', type=str,
|
||||
metavar="MINISCRIPT_WALLET_NAME",
|
||||
required=True)
|
||||
def miniscript_bip388_policy(name):
|
||||
"""
|
||||
Get registered miniscript wallet policy (BIP-388) by name.
|
||||
"""
|
||||
with get_device() as dev:
|
||||
dev.check_mitm()
|
||||
|
||||
resp = dev.send_recv(CCProtocolPacker.miniscript_policy(name),
|
||||
timeout=None)
|
||||
o = json.loads(resp)
|
||||
|
||||
click.echo(pformat(o))
|
||||
|
||||
|
||||
@miniscript.command('addr')
|
||||
@click.argument('name', type=str,
|
||||
metavar="MINISCRIPT_WALLET_NAME",
|
||||
required=True)
|
||||
@click.argument('index',
|
||||
type=click.IntRange(min=0, max=(2**31)-1),
|
||||
metavar="ADDR_IDX", required=True)
|
||||
@click.option('--change', is_flag=True, default=False,
|
||||
help='Use internal chain.')
|
||||
def miniscript_address(name, change, index):
|
||||
"""
|
||||
Get miniscript internal/external chain address by index
|
||||
with on device verification.
|
||||
"""
|
||||
with get_device() as dev:
|
||||
dev.check_mitm()
|
||||
|
||||
resp = dev.send_recv(
|
||||
CCProtocolPacker.miniscript_address(name, change, index),
|
||||
timeout=None
|
||||
)
|
||||
click.echo(resp)
|
||||
|
||||
|
||||
@main.command('user')
|
||||
@click.argument('username', type=str, metavar="USERNAME", required=True)
|
||||
@ -904,7 +1224,7 @@ def new_user(username, totp_create=False, totp_secret=None, text_secret=None, as
|
||||
@main.command('local-conf')
|
||||
@click.argument('psbt-file', type=click.File('rb'), required=True, metavar="Binary PSBT")
|
||||
@click.option('--next', '-n', 'next_code', type=str, help='next_local_code from Coldcard (default: ask it)')
|
||||
def user_auth(psbt_file, next_code=None):
|
||||
def user_auth(psbt_file, next_code):
|
||||
"""
|
||||
Generate the 6-digit code needed for a specific PSBT file to authorize
|
||||
it's signing on the Coldcard in HSM mode.
|
||||
@ -933,7 +1253,7 @@ def user_auth(psbt_file, next_code=None):
|
||||
@click.option('--password', '-p', is_flag=True, help="Prompt for password")
|
||||
@click.option('--debug', '-d', is_flag=True, help='Show values used')
|
||||
@click.option('--version3', '-3', is_flag=True, help='Support obsolete 3.x.x firmware')
|
||||
def user_auth(username, token=None, password=None, prompt=None, totp=None, psbt_file=None, debug=False, version3=False):
|
||||
def user_auth(username, token, password, psbt_file, debug, version3):
|
||||
"""
|
||||
Indicate specific user is present (for HSM).
|
||||
|
||||
@ -1096,4 +1416,7 @@ def electrum_convert2cc(file, outfile, dry_run, key, val):
|
||||
click.echo("Failed to dump wallet file: {}".format(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
main.add_command(miniscript)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -9,26 +9,26 @@
|
||||
#
|
||||
# - ec_mult, ec_setup, aes_setup, mitm_verify
|
||||
#
|
||||
import hid, sys, os
|
||||
from binascii import b2a_hex, a2b_hex
|
||||
import hid, os, socket, atexit
|
||||
from binascii import b2a_hex
|
||||
from hashlib import sha256
|
||||
from .constants import USB_NCRY_V1, USB_NCRY_V2
|
||||
from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN
|
||||
from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN
|
||||
from .utils import decode_xpub, get_pubkey_string
|
||||
|
||||
# unofficial, unpermissioned... USB numbers
|
||||
COINKITE_VID = 0xd13e
|
||||
CKCC_PID = 0xcc10
|
||||
|
||||
# Unix domain socket used by the simulator
|
||||
CKCC_SIMULATOR_PATH = '/tmp/ckcc-simulator.sock'
|
||||
DEFAULT_SIM_SOCKET = "/tmp/ckcc-simulator.sock"
|
||||
|
||||
|
||||
class ColdcardDevice:
|
||||
def __init__(self, sn=None, dev=None, encrypt=True, ncry_ver=USB_NCRY_V1):
|
||||
def __init__(self, sn=None, dev=None, encrypt=True, ncry_ver=USB_NCRY_V1, is_simulator=False):
|
||||
# Establish connection via USB (HID) or Unix Pipe
|
||||
self.is_simulator = False
|
||||
self.is_simulator = is_simulator
|
||||
|
||||
if not dev and sn and '/' in sn:
|
||||
if not dev and ((sn and ('/' in sn)) or self.is_simulator):
|
||||
dev = UnixSimulatorPipe(sn)
|
||||
found = 'simulator'
|
||||
self.is_simulator = True
|
||||
@ -334,23 +334,36 @@ class ColdcardDevice:
|
||||
|
||||
return pbkdf2_hmac('sha256' if v3 else 'sha512', text_password, salt, PBKDF2_ITER_COUNT)[:32]
|
||||
|
||||
def firmware_version(self):
|
||||
return self.send_recv(CCProtocolPacker.version()).split("\n")
|
||||
|
||||
def is_edge(self):
|
||||
# returns True if device is running EDGE firmware version
|
||||
if self.is_simulator:
|
||||
cmd = "import version; RV.write(str(int(getattr(version, 'is_edge', 0))))"
|
||||
rv = self.send_recv(b'EXEC' + cmd.encode('utf-8'), timeout=60000, encrypt=False)
|
||||
return rv == b"1"
|
||||
|
||||
return self.firmware_version()[1][-1] == "X"
|
||||
|
||||
|
||||
class UnixSimulatorPipe:
|
||||
# Use a UNIX pipe to the simulator instead of a real USB connection.
|
||||
# - emulates the API of hidapi device object.
|
||||
|
||||
def __init__(self, path):
|
||||
import socket, atexit
|
||||
def __init__(self, socket_path=None):
|
||||
self.socket_path = socket_path or DEFAULT_SIM_SOCKET
|
||||
self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
try:
|
||||
self.pipe.connect(path)
|
||||
self.pipe.connect(self.socket_path)
|
||||
except Exception:
|
||||
self.close()
|
||||
raise RuntimeError("Cannot connect to simulator. Is it running?")
|
||||
|
||||
last_err = None
|
||||
for instance in range(5):
|
||||
pn = '/tmp/ckcc-client-%d-%d.sock' % (os.getpid(), instance)
|
||||
# if simulator has PID in socket path, client will have matching, or empty
|
||||
pn = '/tmp/ckcc-client%s-%d-%d.sock' % (self.get_sim_pid(), os.getpid(), instance)
|
||||
try:
|
||||
self.pipe.bind(pn) # just needs any name
|
||||
break
|
||||
@ -365,6 +378,12 @@ class UnixSimulatorPipe:
|
||||
self.pipe_name = pn
|
||||
atexit.register(self.close)
|
||||
|
||||
def get_sim_pid(self):
|
||||
# return str PID if any in socket_path
|
||||
if self.socket_path == DEFAULT_SIM_SOCKET:
|
||||
return ""
|
||||
return "-" + self.socket_path.split(".")[0].split("-")[-1]
|
||||
|
||||
def read(self, max_count, timeout_ms=None):
|
||||
import socket
|
||||
if not timeout_ms:
|
||||
|
||||
@ -43,6 +43,16 @@ MAX_UPLOAD_LEN_MK4 = const(2*MAX_TXN_LEN_MK4)
|
||||
# Max length of text messages for signing
|
||||
MSG_SIGNING_MAX_LENGTH = const(240)
|
||||
|
||||
# Bitcoin limitation: max number of signatures in P2SH redeem script (non-segwit)
|
||||
# - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes
|
||||
# - serializations of M/N in redeem scripts assume this range
|
||||
MAX_SIGNERS = const(15)
|
||||
# taproot artificial multisig limit
|
||||
MAX_TR_SIGNERS = const(34)
|
||||
|
||||
TAPROOT_LEAF_MASK = 0xfe
|
||||
TAPROOT_LEAF_TAPSCRIPT = 0xc0
|
||||
|
||||
# Types of user auth we support
|
||||
USER_AUTH_TOTP = const(1) # RFC6238
|
||||
USER_AUTH_HOTP = const(2) # RFC4226
|
||||
@ -67,19 +77,23 @@ AFC_SEGWIT = const(0x02) # requires a witness to spend
|
||||
AFC_BECH32 = const(0x04) # just how we're encoding it?
|
||||
AFC_SCRIPT = const(0x08) # paying into a script
|
||||
AFC_WRAPPED = const(0x10) # for transition/compat types for segwit vs. old
|
||||
AFC_BECH32M = const(0x20) # no difference between script/key path in taproot
|
||||
|
||||
# Numeric codes for specific address types
|
||||
AF_BARE_PK = const(0x00) # p2pk bare public key address
|
||||
AF_CLASSIC = AFC_PUBKEY # 1addr
|
||||
AF_P2SH = AFC_SCRIPT # classic multisig / simple P2SH / 3hash
|
||||
AF_P2WPKH = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32 # bc1qsdklfj
|
||||
AF_P2WSH = AFC_SCRIPT | AFC_SEGWIT | AFC_BECH32 # segwit multisig
|
||||
AF_P2WPKH_P2SH = AFC_WRAPPED | AFC_PUBKEY | AFC_SEGWIT # looks classic P2SH, but p2wpkh inside
|
||||
AF_P2WSH_P2SH = AFC_WRAPPED | AFC_SCRIPT | AFC_SEGWIT # looks classic P2SH, segwit multisig
|
||||
AF_P2TR = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32M # bc1p
|
||||
|
||||
SUPPORTED_ADDR_FORMATS = frozenset([
|
||||
AF_CLASSIC,
|
||||
AF_P2SH,
|
||||
AF_P2WPKH,
|
||||
AF_P2TR,
|
||||
AF_P2WSH,
|
||||
AF_P2WPKH_P2SH,
|
||||
AF_P2WSH_P2SH,
|
||||
@ -87,21 +101,73 @@ SUPPORTED_ADDR_FORMATS = frozenset([
|
||||
|
||||
# BIP-174 aka PSBT defined values
|
||||
#
|
||||
PSBT_GLOBAL_UNSIGNED_TX = const(0)
|
||||
PSBT_GLOBAL_XPUB = const(1)
|
||||
# GLOBAL ===
|
||||
PSBT_GLOBAL_UNSIGNED_TX = const(0x00)
|
||||
PSBT_GLOBAL_XPUB = const(0x01)
|
||||
PSBT_GLOBAL_VERSION = const(0xfb)
|
||||
PSBT_GLOBAL_PROPRIETARY = const(0xfc)
|
||||
# BIP-370
|
||||
PSBT_GLOBAL_TX_VERSION = const(0x02)
|
||||
PSBT_GLOBAL_FALLBACK_LOCKTIME = const(0x03)
|
||||
PSBT_GLOBAL_INPUT_COUNT = const(0x04)
|
||||
PSBT_GLOBAL_OUTPUT_COUNT = const(0x05)
|
||||
PSBT_GLOBAL_TX_MODIFIABLE = const(0x06)
|
||||
# BIP-322
|
||||
PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = const(0x09)
|
||||
|
||||
PSBT_IN_NON_WITNESS_UTXO = const(0)
|
||||
PSBT_IN_WITNESS_UTXO = const(1)
|
||||
PSBT_IN_PARTIAL_SIG = const(2)
|
||||
PSBT_IN_SIGHASH_TYPE = const(3)
|
||||
PSBT_IN_REDEEM_SCRIPT = const(4)
|
||||
PSBT_IN_WITNESS_SCRIPT = const(5)
|
||||
PSBT_IN_BIP32_DERIVATION = const(6)
|
||||
PSBT_IN_FINAL_SCRIPTSIG = const(7)
|
||||
PSBT_IN_FINAL_SCRIPTWITNESS = const(8)
|
||||
# INPUTS ===
|
||||
PSBT_IN_NON_WITNESS_UTXO = const(0x00)
|
||||
PSBT_IN_WITNESS_UTXO = const(0x01)
|
||||
PSBT_IN_PARTIAL_SIG = const(0x02)
|
||||
PSBT_IN_SIGHASH_TYPE = const(0x03)
|
||||
PSBT_IN_REDEEM_SCRIPT = const(0x04)
|
||||
PSBT_IN_WITNESS_SCRIPT = const(0x05)
|
||||
PSBT_IN_BIP32_DERIVATION = const(0x06)
|
||||
PSBT_IN_FINAL_SCRIPTSIG = const(0x07)
|
||||
PSBT_IN_FINAL_SCRIPTWITNESS = const(0x08)
|
||||
PSBT_IN_POR_COMMITMENT = const(0x09)
|
||||
PSBT_IN_RIPEMD160 = const(0x0a)
|
||||
PSBT_IN_SHA256 = const(0x0b)
|
||||
PSBT_IN_HASH160 = const(0x0c)
|
||||
PSBT_IN_HASH256 = const(0x0d)
|
||||
# BIP-370
|
||||
PSBT_IN_PREVIOUS_TXID = const(0x0e)
|
||||
PSBT_IN_OUTPUT_INDEX = const(0x0f)
|
||||
PSBT_IN_SEQUENCE = const(0x10)
|
||||
PSBT_IN_REQUIRED_TIME_LOCKTIME = const(0x11)
|
||||
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = const(0x12)
|
||||
# BIP-371
|
||||
PSBT_IN_TAP_KEY_SIG = const(0x13)
|
||||
PSBT_IN_TAP_SCRIPT_SIG = const(0x14)
|
||||
PSBT_IN_TAP_LEAF_SCRIPT = const(0x15)
|
||||
PSBT_IN_TAP_BIP32_DERIVATION = const(0x16)
|
||||
PSBT_IN_TAP_INTERNAL_KEY = const(0x17)
|
||||
PSBT_IN_TAP_MERKLE_ROOT = const(0x18)
|
||||
|
||||
PSBT_OUT_REDEEM_SCRIPT = const(0)
|
||||
PSBT_OUT_WITNESS_SCRIPT = const(1)
|
||||
PSBT_OUT_BIP32_DERIVATION = const(2)
|
||||
PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = const(0x1a)
|
||||
PSBT_IN_MUSIG2_PUB_NONCE = const(0x1b)
|
||||
PSBT_IN_MUSIG2_PARTIAL_SIG = const(0x1c)
|
||||
|
||||
# OUTPUTS ===
|
||||
PSBT_OUT_REDEEM_SCRIPT = const(0x00)
|
||||
PSBT_OUT_WITNESS_SCRIPT = const(0x01)
|
||||
PSBT_OUT_BIP32_DERIVATION = const(0x02)
|
||||
# BIP-370
|
||||
PSBT_OUT_AMOUNT = const(0x03)
|
||||
PSBT_OUT_SCRIPT = const(0x04)
|
||||
# BIP-371
|
||||
PSBT_OUT_TAP_INTERNAL_KEY = const(0x05)
|
||||
PSBT_OUT_TAP_TREE = const(0x06)
|
||||
PSBT_OUT_TAP_BIP32_DERIVATION = const(0x07)
|
||||
PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = const(0x08)
|
||||
|
||||
RFC_SIGNATURE_TEMPLATE = '''\
|
||||
-----BEGIN BITCOIN SIGNED MESSAGE-----
|
||||
{msg}
|
||||
-----BEGIN BITCOIN SIGNATURE-----
|
||||
{addr}
|
||||
{sig}
|
||||
-----END BITCOIN SIGNATURE-----
|
||||
'''
|
||||
|
||||
# EOF
|
||||
|
||||
@ -64,9 +64,29 @@ class CCProtocolPacker:
|
||||
|
||||
@staticmethod
|
||||
def start_backup():
|
||||
# prompts user with password for encrytped backup
|
||||
# prompts user with password for encrypted backup
|
||||
return b'back'
|
||||
|
||||
@staticmethod
|
||||
def restore_backup(length, file_sha, custom_pwd=False, plaintext=False, tmp=False):
|
||||
# backup file has to be already uploaded
|
||||
# custom_pwd: (bool) .7z encrypted with custom password
|
||||
# plaintext: (bool) clear-text (dev)
|
||||
# tmp (bool) force load as tmp, effective only on seed-less CC
|
||||
assert len(file_sha) == 32
|
||||
assert not (custom_pwd and plaintext)
|
||||
|
||||
bf = 0
|
||||
if custom_pwd:
|
||||
bf |= 1
|
||||
if plaintext:
|
||||
bf |= 2
|
||||
if tmp:
|
||||
bf |= 4
|
||||
|
||||
return pack('<4sI32sB', b'rest', length, file_sha, bf)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def encrypt_start(device_pubkey, version=USB_NCRY_V1):
|
||||
supported_versions = [USB_NCRY_V1, USB_NCRY_V2]
|
||||
@ -92,11 +112,14 @@ class CCProtocolPacker:
|
||||
return b'sha2'
|
||||
|
||||
@staticmethod
|
||||
def sign_transaction(length, file_sha, finalize=False, flags=0x0):
|
||||
def sign_transaction(length, file_sha, finalize=False, flags=0x0, miniscript_name=None):
|
||||
# must have already uploaded binary, and give expected sha256
|
||||
assert len(file_sha) == 32
|
||||
flags |= (STXN_FINALIZE if finalize else 0x00)
|
||||
return pack('<4sII32s', b'stxn', length, int(flags), file_sha)
|
||||
rv = pack('<4sII32s', b'stxn', length, int(flags), file_sha)
|
||||
if miniscript_name:
|
||||
rv += pack("B", len(miniscript_name)) + miniscript_name.encode()
|
||||
return rv
|
||||
|
||||
@staticmethod
|
||||
def sign_message(raw_msg, subpath='m', addr_fmt=AF_CLASSIC):
|
||||
@ -125,6 +148,42 @@ class CCProtocolPacker:
|
||||
assert len(file_sha) == 32
|
||||
return pack('<4sI32s', b'enrl', length, file_sha)
|
||||
|
||||
@staticmethod
|
||||
def miniscript_ls():
|
||||
# list registered miniscript wallet names
|
||||
return b'msls'
|
||||
|
||||
@staticmethod
|
||||
def miniscript_delete(name):
|
||||
# delete registered miniscript wallet by name
|
||||
assert 2 <= len(name) <= 40, "name len"
|
||||
return b'msdl' + name.encode('ascii')
|
||||
|
||||
@staticmethod
|
||||
def miniscript_get(name):
|
||||
# get registered miniscript wallet object by name
|
||||
assert 2 <= len(name) <= 40, "name len"
|
||||
return b'msgt' + name.encode('ascii')
|
||||
|
||||
@staticmethod
|
||||
def miniscript_policy(name):
|
||||
# get BIP-388 policy of registered miniscript wallet object by name
|
||||
assert 2 <= len(name) <= 40, "name len"
|
||||
return b'mspl' + name.encode('ascii')
|
||||
|
||||
@staticmethod
|
||||
def miniscript_address(name, change=False, idx=0):
|
||||
# get miniscript address from internal or external chain by id
|
||||
assert 2 <= len(name) <= 40, "name len"
|
||||
assert 0 <= idx < (2**31), "child idx"
|
||||
return pack('<4sII', b'msas', int(change), idx) + name.encode('ascii')
|
||||
|
||||
@staticmethod
|
||||
def miniscript_enroll(length, file_sha):
|
||||
# miniscript details must already be uploaded as a text file, this starts approval process.
|
||||
assert len(file_sha) == 32
|
||||
return pack('<4sI32s', b'mins', length, file_sha)
|
||||
|
||||
@staticmethod
|
||||
def multisig_check(M, N, xfp_xor):
|
||||
# do we have a wallet already that matches M+N and xor(*xfps)?
|
||||
@ -229,52 +288,62 @@ class CCProtocolUnpacker:
|
||||
# - given full rx message to work from
|
||||
# - this is done after un-framing
|
||||
|
||||
@classmethod
|
||||
def decode(cls, msg):
|
||||
@staticmethod
|
||||
def decode(msg):
|
||||
assert len(msg) >= 4
|
||||
sign = str(msg[0:4], 'utf8', 'ignore')
|
||||
|
||||
d = getattr(cls, sign, cls)
|
||||
if d is cls:
|
||||
d = getattr(CCProtocolUnpacker, sign, None)
|
||||
if d is None:
|
||||
raise CCFramingError('Unknown response signature: ' + repr(sign))
|
||||
|
||||
return d(msg)
|
||||
|
||||
|
||||
# struct info for each response
|
||||
|
||||
|
||||
@staticmethod
|
||||
def okay(msg):
|
||||
# trivial response, w/ no content
|
||||
assert len(msg) == 4
|
||||
return None
|
||||
|
||||
# low-level errors
|
||||
@staticmethod
|
||||
def fram(msg):
|
||||
raise CCFramingError("Framing Error", str(msg[4:], 'utf8'))
|
||||
raise CCFramingError("Framing Error: " + str(msg[4:], 'utf8'))
|
||||
|
||||
@staticmethod
|
||||
def err_(msg):
|
||||
raise CCProtoError("Coldcard Error: " + str(msg[4:], 'utf8', 'ignore'), msg[4:])
|
||||
|
||||
@staticmethod
|
||||
def refu(msg):
|
||||
# user didn't want to approve something
|
||||
raise CCUserRefused()
|
||||
|
||||
@staticmethod
|
||||
def busy(msg):
|
||||
# user didn't want to approve something
|
||||
raise CCBusyError()
|
||||
|
||||
@staticmethod
|
||||
def biny(msg):
|
||||
# binary string: length implied by msg framing
|
||||
return msg[4:]
|
||||
|
||||
@staticmethod
|
||||
def int1(msg):
|
||||
return unpack_from('<I', msg, 4)[0]
|
||||
|
||||
@staticmethod
|
||||
def int2(msg):
|
||||
return unpack_from('<2I', msg, 4)
|
||||
|
||||
@staticmethod
|
||||
def int3(msg):
|
||||
return unpack_from('<3I', msg, 4)
|
||||
|
||||
@staticmethod
|
||||
def mypb(msg):
|
||||
# response to "ncry" command:
|
||||
# - the (uncompressed) pubkey of the Coldcard
|
||||
@ -285,16 +354,19 @@ class CCProtocolUnpacker:
|
||||
xpub = msg[-xpub_len:] if xpub_len else b''
|
||||
return dev_pubkey, fingerprint, xpub
|
||||
|
||||
@staticmethod
|
||||
def asci(msg):
|
||||
# hex/base58 string or other for-computers string, which isn't international
|
||||
return msg[4:].decode('ascii')
|
||||
|
||||
@staticmethod
|
||||
def smrx(msg):
|
||||
# message signing result. application specific!
|
||||
# returns actual address used (text), and raw binary signature (65 bytes)
|
||||
aln = unpack_from('<I', msg, 4)[0]
|
||||
return msg[8:aln+8].decode('ascii'), msg[8+aln:]
|
||||
|
||||
@staticmethod
|
||||
def strx(msg):
|
||||
# txn signing result, or other file operation. application specific!
|
||||
# returns length of resulting PSBT and it's sha256
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
# (c) Copyright 2018-2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
# Autogen'ed file, don't edit. See stm32/sigheader.h for original
|
||||
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
|
||||
# Our simple firmware header.
|
||||
# Although called a header, this data is placed into the middle of the binary.
|
||||
@ -10,9 +12,10 @@
|
||||
# - version_string is for humans only
|
||||
# - pubkey_num indicates which pubkey was used for signature
|
||||
# - firmware_length, must be:
|
||||
# - bigger than minimum length, less than max
|
||||
# - 512-byte aligned
|
||||
# - bigger than minimum length, less than max
|
||||
# - 512-byte aligned
|
||||
# - bootloader assumes the flash filesystem (FAT FS) follows the firmware.
|
||||
# - this C header file is somewhat parsed and used by python signature-adding code
|
||||
# - timestamp is YYMMDDHHMMSS0000 in BCD
|
||||
|
||||
|
||||
@ -26,8 +29,8 @@ FW_HEADER_MAGIC = 0xCC001234
|
||||
# arbitrary min size
|
||||
FW_MIN_LENGTH = (256*1024)
|
||||
|
||||
# (mk1-3) absolute max size: 1MB flash - 32k for bootloader
|
||||
# practical limit for our-protocol USB upgrades: 786432 (or else settings damaged)
|
||||
# (mk1-3) absolute max size: 1MB flash - 32k for bootloader = 1,015,808
|
||||
# - but practical limit for our-protocol USB upgrades: 786432 (or else settings damaged)
|
||||
FW_MAX_LENGTH = (0x100000 - 0x8000)
|
||||
|
||||
# .. for Mk4: 2Mbytes, less bootrom of 128k.
|
||||
@ -50,8 +53,8 @@ MK_1_OK = 0x01
|
||||
MK_2_OK = 0x02
|
||||
MK_3_OK = 0x04
|
||||
MK_4_OK = 0x08
|
||||
# RFU:
|
||||
MK_5_OK = 0x10
|
||||
MK_6_OK = 0x20
|
||||
MK_Q1_OK = 0x10
|
||||
MK_5_OK = 0x20
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
import binascii, hashlib, struct, hmac, base64
|
||||
from collections import namedtuple
|
||||
from typing import Optional
|
||||
|
||||
from ckcc.constants import AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
|
||||
from ckcc.constants import AF_P2WPKH, AF_P2TR, AF_CLASSIC, AF_P2WPKH_P2SH
|
||||
|
||||
|
||||
B2A = lambda x: binascii.b2a_hex(x).decode('ascii')
|
||||
@ -105,17 +108,74 @@ def calc_local_pincode(psbt_sha, next_local_code):
|
||||
# - next_local_code comes from the hsm_status response
|
||||
# - psbt_sha is sha256() over the binary PSBT you will be submitting
|
||||
#
|
||||
from binascii import a2b_base64
|
||||
from hashlib import sha256
|
||||
import struct, hmac
|
||||
|
||||
key = a2b_base64(next_local_code)
|
||||
key = binascii.a2b_base64(next_local_code)
|
||||
assert len(key) >= 15
|
||||
assert len(psbt_sha) == 32
|
||||
digest = hmac.new(key, psbt_sha, sha256).digest()
|
||||
digest = hmac.new(key, psbt_sha, hashlib.sha256).digest()
|
||||
|
||||
num = struct.unpack('>I', digest[-4:])[0] & 0x7fffffff
|
||||
|
||||
return '%06d' % (num % 1000000)
|
||||
|
||||
|
||||
def descriptor_template(xfp: str, xpub: str, path: str, fmt: int, m: int = None) -> Optional[str]:
|
||||
if m is None:
|
||||
m = "M"
|
||||
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
|
||||
if fmt == AF_P2SH:
|
||||
descriptor_template = "sh(sortedmulti(%s,%s,...))"
|
||||
elif fmt == AF_P2WSH_P2SH:
|
||||
descriptor_template = "sh(wsh(sortedmulti(%s,%s,...)))"
|
||||
elif fmt == AF_P2WSH:
|
||||
descriptor_template = "wsh(sortedmulti(%s,%s,...))"
|
||||
else:
|
||||
return None
|
||||
res = descriptor_template % (m, key_exp)
|
||||
return res
|
||||
|
||||
|
||||
def addr_fmt_help(dev, wrap=False, segwit=False, taproot=False):
|
||||
chain = 0
|
||||
if dev.master_xpub and dev.master_xpub[0] == "t":
|
||||
# testnet
|
||||
chain = 1
|
||||
if wrap:
|
||||
addr_fmt = AF_P2WPKH_P2SH
|
||||
af_path = f"m/49h/{chain}h/0h/0/0"
|
||||
elif segwit:
|
||||
addr_fmt = AF_P2WPKH
|
||||
af_path = f"m/84h/{chain}h/0h/0/0"
|
||||
elif taproot:
|
||||
addr_fmt = AF_P2TR
|
||||
af_path = f"m/86h/{chain}h/0h/0/0"
|
||||
else:
|
||||
addr_fmt = AF_CLASSIC
|
||||
af_path = f"m/44h/{chain}h/0h/0/0"
|
||||
|
||||
return addr_fmt, af_path
|
||||
|
||||
|
||||
def b2a_base64url(s):
|
||||
# see <https://datatracker.ietf.org/doc/html/rfc4648#section-5>
|
||||
# '=' still needs to be removed https://docs.python.org/3/library/base64.html#base64.urlsafe_b64encode
|
||||
return base64.urlsafe_b64encode(s).rstrip(b'=\n').decode()
|
||||
|
||||
|
||||
def txn_to_pushtx_url(txn, base_url, sha=None, chain="BTC", verify_sha=False):
|
||||
assert ("http://" in base_url) or ("https://" in base_url), "url schema"
|
||||
assert base_url[-1] in "#?&", "Final char must be # or ? or &."
|
||||
url = base_url
|
||||
url += 't=' + b2a_base64url(txn)
|
||||
|
||||
if sha is None:
|
||||
sha = hashlib.sha256(txn).digest()
|
||||
elif verify_sha:
|
||||
assert sha == hashlib.sha256(txn).digest(), "wrong hash"
|
||||
|
||||
url += '&c=' + b2a_base64url(sha[-8:])
|
||||
|
||||
if chain != 'BTC':
|
||||
url += '&n=' + chain # XTN or XRT
|
||||
return url
|
||||
|
||||
# EOF
|
||||
|
||||
BIN
dist/ckcc-protocol-1.2.0.tar.gz
vendored
BIN
dist/ckcc-protocol-1.2.0.tar.gz
vendored
Binary file not shown.
BIN
dist/ckcc-protocol-1.3.1.tar.gz
vendored
BIN
dist/ckcc-protocol-1.3.1.tar.gz
vendored
Binary file not shown.
BIN
dist/ckcc-protocol-1.3.2.tar.gz
vendored
Normal file
BIN
dist/ckcc-protocol-1.3.2.tar.gz
vendored
Normal file
Binary file not shown.
BIN
dist/ckcc-protocol-1.4.0.tar.gz
vendored
Normal file
BIN
dist/ckcc-protocol-1.4.0.tar.gz
vendored
Normal file
Binary file not shown.
BIN
dist/ckcc-protocol-1.5.0.tar.gz
vendored
Normal file
BIN
dist/ckcc-protocol-1.5.0.tar.gz
vendored
Normal file
Binary file not shown.
BIN
dist/ckcc_protocol-1.2.0-py3-none-any.whl
vendored
BIN
dist/ckcc_protocol-1.2.0-py3-none-any.whl
vendored
Binary file not shown.
BIN
dist/ckcc_protocol-1.3.1-py3-none-any.whl
vendored
BIN
dist/ckcc_protocol-1.3.1-py3-none-any.whl
vendored
Binary file not shown.
BIN
dist/ckcc_protocol-1.3.2-py3-none-any.whl
vendored
Normal file
BIN
dist/ckcc_protocol-1.3.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
dist/ckcc_protocol-1.4.0-py3-none-any.whl
vendored
Normal file
BIN
dist/ckcc_protocol-1.4.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
dist/ckcc_protocol-1.5.0-py3-none-any.whl
vendored
Normal file
BIN
dist/ckcc_protocol-1.5.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user