Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/build-sim/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ runs:
cd test
./setup_environment.sh --"${{ inputs.name }}"
cd ..

# Remove .git directories to shrink the archive
for p in ${{ inputs.paths }}; do
# Skip speculos: it uses setuptools_scm which requires .git for version detection
if [[ "$p" != *speculos* ]]; then
find "$p" -name ".git" -type d -exec rm -rf {} + 2>/dev/null || true
fi
done
tar -czf "${{ inputs.archive }}.tar.gz" ${{ inputs.paths }}

- uses: actions/upload-artifact@v4
Expand Down
13 changes: 4 additions & 9 deletions .github/actions/install-sim/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,13 @@ runs:

- if: inputs.device == 'coldcard'
shell: bash
env:
# Keep in sync with test/setup_environment.sh
COLDCARD_VERSION: "2025-09-30T1238-v5.4.4"
run: |
apt-get update
apt-get install -y libpcsclite-dev libusb-1.0-0 swig
git config --global user.email "ci@ci.com"
git config --global user.name "ci"
# Note: cannot use --shallow-submodules because lwip submodule on git.savannah.gnu.org doesn't support it
pushd test/work; git clone --recursive --depth 1 --branch ${COLDCARD_VERSION} https://github.com/Coldcard/firmware.git; popd
tar -xvf coldcard-mpy.tar.gz
pushd test/work/firmware; git am ../../data/coldcard-multisig.patch; popd

# Extract the archive - this includes the full firmware directory
tar -xvf coldcard-firmware.tar.gz

poetry run pip install -r test/work/firmware/requirements.txt
pip install -r test/work/firmware/requirements.txt
poetry run pip install pysdl2-dll
Expand Down
2 changes: 1 addition & 1 deletion .github/sim-build-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{ "name": "trezor-t", "archive": "trezor-firmware", "paths": "test/work/trezor-firmware" }
],
"coldcard": [
{ "name": "coldcard", "archive": "coldcard-mpy", "paths": "test/work/firmware/external/micropython/ports/unix/coldcard-mpy test/work/firmware/unix/coldcard-mpy test/work/firmware/unix/l-mpy test/work/firmware/unix/l-port" }
{ "name": "coldcard", "archive": "coldcard-firmware", "paths": "test/work/firmware" }
],
"bitbox": [
{ "name": "bitbox01", "archive": "mcu", "paths": "test/work/mcu" },
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/walkthrough/walkthrough.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ together with a Bitcoin Core Wallet. As hardware wallet example we have used a T
Create a watch-only Bitcoin Core wallet for Trezor
==================================================

Create your watch-only Bitcoin Core Wallet as described in `Using Bitcoin Core with Hardware Wallets <../bitcoin-core-usage.rst>`_.
Create your watch-only Bitcoin Core Wallet as described in `Using Bitcoin Core with Hardware Wallets <../bitcoin-core-usage.html>`_.
You find all the details well described in this link. But in summary, one opens a terminal and runs ``bitcoind``. E.g.

::
Expand Down
2 changes: 1 addition & 1 deletion hwilib/devices/ckcc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a stripped down and modified version of the official [ckcc-protocol](https://github.com/Coldcard/ckcc-protocol) library.

This stripped down version was made at commit [ca8d2b7808784a9f4927f3250bf52d2623a4e15b](https://github.com/Coldcard/ckcc-protocol/tree/ca8d2b7808784a9f4927f3250bf52d2623a4e15b).
This stripped down version was made at commit [f87d30f220cb6334eb3c4ace93c1b62e04942022](https://github.com/Coldcard/ckcc-protocol/commit/f87d30f220cb6334eb3c4ace93c1b62e04942022).

## Changes

Expand Down
6 changes: 2 additions & 4 deletions hwilib/devices/ckcc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

__version__ = '1.0.2'

__all__ = [ "client", "protocol", "constants" ]

__version__ = '1.4.0'

__all__ = [ "client", "protocol", "constants" ]
87 changes: 58 additions & 29 deletions hwilib/devices/ckcc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,26 @@
#
# - ec_mult, ec_setup, aes_setup, mitm_verify
#
import hid, sys, os, platform
from binascii import b2a_hex, a2b_hex
import hid, os, socket, atexit
from binascii import b2a_hex
from hashlib import sha256
from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN
from .constants import USB_NCRY_V1, USB_NCRY_V2
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):
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 platform.system() == 'Windows':
raise RuntimeError("Cannot connect to simulator. Is it running?")
if not dev and ((sn and ('/' in sn)) or self.is_simulator):
dev = UnixSimulatorPipe(sn)
found = 'simulator'
self.is_simulator = True
Expand All @@ -49,7 +48,7 @@ def __init__(self, sn=None, dev=None, encrypt=True):
break

if not dev:
raise KeyError("Could not find Coldcard!"
raise KeyError("Could not find Coldcard!"
if not sn else ('Cannot find CC with serial: '+sn))
else:
found = dev.get_serial_number_string()
Expand All @@ -58,6 +57,7 @@ def __init__(self, sn=None, dev=None, encrypt=True):
self.serial = found

# they will be defined after we've established a shared secret w/ device
self.ncry_ver = ncry_ver
self.session_key = None
self.encrypt_request = None
self.decrypt_response = None
Expand All @@ -67,7 +67,7 @@ def __init__(self, sn=None, dev=None, encrypt=True):
self.resync()

if encrypt:
self.start_encryption()
self.start_encryption(version=self.ncry_ver)

def close(self):
# close underlying HID device
Expand Down Expand Up @@ -101,17 +101,21 @@ def send_recv(self, msg, expect_errors=False, verbose=0, timeout=3000, encrypt=T
# first byte of each 64-byte packet encodes length or packet-offset
assert 4 <= len(msg) <= MAX_MSG_LEN, "msg length: %d" % len(msg)

if not self.encrypt_request:
if self.encrypt_request is None:
# disable encryption if not already enabled for this connection
encrypt = False

if self.encrypt_request and self.ncry_ver == USB_NCRY_V2:
# ncry version 2 - everything needs to be encrypted
encrypt = True

if encrypt:
msg = self.encrypt_request(msg)

left = len(msg)
offset = 0
while left > 0:
# Note: first byte always zero (HID report number),
# Note: first byte always zero (HID report number),
# [1] is framing header (length+flags)
# [2:65] payload (63 bytes, perhaps including padding)
here = min(63, left)
Expand Down Expand Up @@ -224,7 +228,7 @@ def aes_setup(self, session_key):
self.encrypt_request = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).encrypt
self.decrypt_response = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).decrypt

def start_encryption(self):
def start_encryption(self, version=USB_NCRY_V1):
# setup encryption on the link
# - pick our own key pair, IV for AES
# - send IV and pubkey to device
Expand All @@ -233,10 +237,12 @@ def start_encryption(self):

pubkey = self.ec_setup()

msg = CCProtocolPacker.encrypt_start(pubkey)
msg = CCProtocolPacker.encrypt_start(pubkey, version=version)

his_pubkey, fingerprint, xpub = self.send_recv(msg, encrypt=False)

self.ncry_ver = version

self.session_key = self.ec_mult(his_pubkey)

# capture some public details of remote side's master key
Expand All @@ -248,7 +254,6 @@ def start_encryption(self):
self.aes_setup(self.session_key)

def mitm_verify(self, sig, expected_xpub):
# If Pycoin is not available, do it using ecdsa
from ecdsa import BadSignatureError, SECP256k1, VerifyingKey
# of the returned (pubkey, chaincode) tuple, chaincode is not used
pubkey, _ = decode_xpub(expected_xpub)
Expand Down Expand Up @@ -318,42 +323,66 @@ def download_file(self, length, checksum, blksize=1024, file_number=1):

return data

def hash_password(self, text_password):
def hash_password(self, text_password, v3=False):
# Turn text password into a key for use in HSM auth protocol
# - changed from pbkdf2_hmac_sha256 to pbkdf2_hmac_sha512 in version 4 of CC firmware
from hashlib import pbkdf2_hmac, sha256
from .constants import PBKDF2_ITER_COUNT

salt = sha256(b'pepper' + self.serial.encode('ascii')).digest()

return pbkdf2_hmac('sha256', text_password, salt, PBKDF2_ITER_COUNT)
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?")

instance = 0
while instance < 10:
pn = '/tmp/ckcc-client-%d-%d.sock' % (os.getpid(), instance)
last_err = None
for instance in range(5):
# 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
except OSError:
instance += 1
except OSError as err:
last_err = err
if os.path.exists(pn):
os.remove(pn)
continue
else:
raise last_err # raise whatever was raised last in the loop

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:
Expand Down Expand Up @@ -383,7 +412,7 @@ def close(self):
pass

def get_serial_number_string(self):
return 'simulator'
return 'F1'*6


# EOF
# EOF
Loading
Loading