Compare commits

...

48 Commits

Author SHA1 Message Date
scgbckbone
3d1dfa858b PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE 2026-05-19 09:18:42 -04:00
russeree
5460f276bf [Req] Remove file extension check 2026-04-01 13:00:31 -04:00
russeree
81a3f5e552 [Cmd] msg-file 2026-04-01 13:00:31 -04:00
Peter D. Gray
6d9f7193b3
add mk5 2026-03-10 11:00:24 -04:00
scgbckbone
abf040789b ckcc sign PSBT from stdin 2026-03-04 10:26:22 -05:00
scgbckbone
0bd92d4d6d MuSig2 PSBT constants 2026-01-20 09:31:15 -05:00
pythcoiner
8ffb706ad2 update udev rules 2026-01-20 08:27:18 -05:00
scgbckbone
2afc7d34d2 stxn cmd with miniscript name lenght 2025-11-03 16:27:45 -05:00
scgbckbone
3ae7ae9cf2 add miniscript policy cmd 2025-10-31 09:41:47 -04:00
scgbckbone
ab317b00c7 --miniscript arguemnt for sign_transaction 2025-10-31 09:41:47 -04:00
Peter D. Gray
8642e5bc47
v1.5.0 2025-09-29 11:14:56 -04:00
Peter D. Gray
0246f35af3
version bump 2025-09-29 11:02:23 -04:00
Peter D. Gray
aee51fdc2c
updates 2025-09-29 10:59:21 -04:00
scgbckbone
51de25089e new constant AF_BARE_PK 2025-09-23 10:58:40 -04:00
scgbckbone
f1ce63812c restore backup via USB; new CLI commnad "restore"; add backup restore to "upload" cmd 2025-09-18 11:29:40 -04:00
scgbckbone
4f4d08c73f review 2025-09-08 09:59:08 -04:00
scgbckbone
c9ddf5d2a8 add convenience functions to client object to get firmware version 2025-09-08 09:59:08 -04:00
scgbckbone
f87d30f220 fix? 2025-06-06 09:28:13 -04:00
scgbckbone
c52657fe61 add verbose flag to xpub cmd; add named descriptor export in multisig cmd; remove unnecessary default arguments from function signatures 2025-06-06 09:27:44 -04:00
scgbckbone
c14b6b2807 convenience - no need to specify -x if --socket/-c already specified 2025-06-06 09:26:33 -04:00
scgbckbone
695e0f1eb6 multi_sim 2025-06-06 09:26:33 -04:00
scgbckbone
3d2749db33 bugfix: exception exporting multisig skeleton 2025-03-03 12:07:49 -05:00
scgbckbone
0e686dbda6 timeout cope for big miniscript wallets 2024-07-09 09:22:44 -04:00
scgbckbone
00e862d8df fix hash wrong 2024-06-13 08:36:43 -04:00
Peter D. Gray
cad2722d14
fine tuning 2024-06-06 12:21:04 -04:00
scgbckbone
a9fb98fd93 PushTX 2024-06-06 12:06:09 -04:00
scgbckbone
a6d901f9fc bugfix: AFC_BECH32M must not set AFC_WRAPPED and AFC_BECH32 2024-03-22 15:25:28 -04:00
scgbckbone
f924f6d35c bugfix: framing error msg was not shown 2024-02-05 10:34:33 -05:00
Peter D. Gray
2abb47844a
Q1 addition 2024-02-02 14:21:55 -05:00
scgbckbone
f7999f4288 no timeout for miniscript enroll 2024-01-22 08:34:30 -05:00
scgbckbone
cf270d922b proper metavars for miniscript cmds 2024-01-21 15:05:31 -05:00
scgbckbone
11c711e929 Miniscript USB interface 2023-12-21 08:47:21 -05:00
Peter D. Gray
1efa93c72c
files 2023-09-13 10:17:18 -04:00
Peter D. Gray
1ff205f668
updates 2023-09-13 10:11:14 -04:00
Peter D. Gray
52b5950105
v1.4.0 time 2023-09-13 09:53:05 -04:00
scgbckbone
196fd0c436 PSBT v2 constants 2023-09-11 11:26:28 -04:00
scgbckbone
aa75d04465 tr address 2023-09-11 11:16:12 -04:00
scgbckbone
78ce292c4d miniscript USB enroll 2023-07-25 08:40:15 -04:00
scgbckbone
7887bd21b5 add tapscript related constants 2023-05-17 09:09:27 -04:00
scgbckbone
6a3a3f9234 MAX_TR_SIGNERS 2023-05-10 08:18:55 -04:00
scgbckbone
e35e2d204e Updated ASCII armor for 'msg' command 2023-02-21 09:06:25 -05:00
scgbckbone
71afe5c9a8 invalid address format in help message for wrapped segwit 2023-01-12 11:13:29 -05:00
scgbckbone
38332ff2d0 bip-0371 2023-01-10 16:47:08 -05:00
scgbckbone
2216e4db0e add MAX_SIGNERS as constant to constants.py 2022-12-16 08:38:27 -05:00
scgbckbone
d696bac051 Add format to multisig command; add descriptor output to multisig commnad; descriptor_template function added to ckcc 2022-09-02 08:14:31 -04:00
scgbckbone
d9efc0baef Fix cli help messages 2022-08-30 08:30:50 -04:00
doc-hex
59c2e52876
Merge pull request #22 from kanzure/patch-1
Fix typo in CCProtocolPacker.
2022-06-29 16:32:39 -04:00
Bryan Bishop
757209900a
fix typo in CCProtocolPacker
from https://github.com/bitcoin-core/HWI/pull/616
2022-06-29 15:24:57 -05:00
20 changed files with 782 additions and 180 deletions

View File

@ -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"

View File

@ -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
View File

@ -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.
```

View File

@ -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" ]

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

BIN
dist/ckcc-protocol-1.5.0.tar.gz vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.