Skip to content

Commit d49ba0d

Browse files
committed
update ckcc to newest master f87d30f220cb6334eb3c4ace93c1b62e04942022
1 parent 4e342db commit d49ba0d

File tree

7 files changed

+235
-90
lines changed

7 files changed

+235
-90
lines changed

hwilib/devices/ckcc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

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

77
## Changes
88

hwilib/devices/ckcc/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11

2-
__version__ = '1.0.2'
3-
4-
__all__ = [ "client", "protocol", "constants" ]
5-
2+
__version__ = '1.4.0'
63

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

hwilib/devices/ckcc/client.py

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,26 @@
88
#
99
# - ec_mult, ec_setup, aes_setup, mitm_verify
1010
#
11-
import hid, sys, os, platform
12-
from binascii import b2a_hex, a2b_hex
11+
import hid, os, socket, atexit
12+
from binascii import b2a_hex
1313
from hashlib import sha256
14-
from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN
14+
from .constants import USB_NCRY_V1, USB_NCRY_V2
15+
from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN
1516
from .utils import decode_xpub, get_pubkey_string
1617

1718
# unofficial, unpermissioned... USB numbers
1819
COINKITE_VID = 0xd13e
1920
CKCC_PID = 0xcc10
2021

21-
# Unix domain socket used by the simulator
22-
CKCC_SIMULATOR_PATH = '/tmp/ckcc-simulator.sock'
22+
DEFAULT_SIM_SOCKET = "/tmp/ckcc-simulator.sock"
23+
2324

2425
class ColdcardDevice:
25-
def __init__(self, sn=None, dev=None, encrypt=True):
26+
def __init__(self, sn=None, dev=None, encrypt=True, ncry_ver=USB_NCRY_V1, is_simulator=False):
2627
# Establish connection via USB (HID) or Unix Pipe
27-
self.is_simulator = False
28+
self.is_simulator = is_simulator
2829

29-
if not dev and sn and '/' in sn:
30-
if platform.system() == 'Windows':
31-
raise RuntimeError("Cannot connect to simulator. Is it running?")
30+
if not dev and ((sn and ('/' in sn)) or self.is_simulator):
3231
dev = UnixSimulatorPipe(sn)
3332
found = 'simulator'
3433
self.is_simulator = True
@@ -49,7 +48,7 @@ def __init__(self, sn=None, dev=None, encrypt=True):
4948
break
5049

5150
if not dev:
52-
raise KeyError("Could not find Coldcard!"
51+
raise KeyError("Could not find Coldcard!"
5352
if not sn else ('Cannot find CC with serial: '+sn))
5453
else:
5554
found = dev.get_serial_number_string()
@@ -58,6 +57,7 @@ def __init__(self, sn=None, dev=None, encrypt=True):
5857
self.serial = found
5958

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

6969
if encrypt:
70-
self.start_encryption()
70+
self.start_encryption(version=self.ncry_ver)
7171

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

104-
if not self.encrypt_request:
104+
if self.encrypt_request is None:
105105
# disable encryption if not already enabled for this connection
106106
encrypt = False
107107

108+
if self.encrypt_request and self.ncry_ver == USB_NCRY_V2:
109+
# ncry version 2 - everything needs to be encrypted
110+
encrypt = True
111+
108112
if encrypt:
109113
msg = self.encrypt_request(msg)
110114

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

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

234238
pubkey = self.ec_setup()
235239

236-
msg = CCProtocolPacker.encrypt_start(pubkey)
240+
msg = CCProtocolPacker.encrypt_start(pubkey, version=version)
237241

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

244+
self.ncry_ver = version
245+
240246
self.session_key = self.ec_mult(his_pubkey)
241247

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

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

319324
return data
320325

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

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

328-
return pbkdf2_hmac('sha256', text_password, salt, PBKDF2_ITER_COUNT)
334+
return pbkdf2_hmac('sha256' if v3 else 'sha512', text_password, salt, PBKDF2_ITER_COUNT)[:32]
329335

330336

331337
class UnixSimulatorPipe:
332338
# Use a UNIX pipe to the simulator instead of a real USB connection.
333339
# - emulates the API of hidapi device object.
334340

335-
def __init__(self, path):
336-
import socket, atexit
341+
def __init__(self, socket_path=None):
342+
self.socket_path = socket_path or DEFAULT_SIM_SOCKET
337343
self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
338344
try:
339-
self.pipe.connect(path)
345+
self.pipe.connect(self.socket_path)
340346
except Exception:
341347
self.close()
342348
raise RuntimeError("Cannot connect to simulator. Is it running?")
343349

344-
instance = 0
345-
while instance < 10:
346-
pn = '/tmp/ckcc-client-%d-%d.sock' % (os.getpid(), instance)
350+
last_err = None
351+
for instance in range(5):
352+
# if simulator has PID in socket path, client will have matching, or empty
353+
pn = '/tmp/ckcc-client%s-%d-%d.sock' % (self.get_sim_pid(), os.getpid(), instance)
347354
try:
348355
self.pipe.bind(pn) # just needs any name
349356
break
350-
except OSError:
351-
instance += 1
357+
except OSError as err:
358+
last_err = err
359+
if os.path.exists(pn):
360+
os.remove(pn)
352361
continue
362+
else:
363+
raise last_err # raise whatever was raised last in the loop
353364

354365
self.pipe_name = pn
355366
atexit.register(self.close)
356367

368+
def get_sim_pid(self):
369+
# return str PID if any in socket_path
370+
if self.socket_path == DEFAULT_SIM_SOCKET:
371+
return ""
372+
return "-" + self.socket_path.split(".")[0].split("-")[-1]
373+
357374
def read(self, max_count, timeout_ms=None):
358375
import socket
359376
if not timeout_ms:
@@ -383,7 +400,7 @@ def close(self):
383400
pass
384401

385402
def get_serial_number_string(self):
386-
return 'simulator'
403+
return 'F1'*6
387404

388405

389-
# EOF
406+
# EOF

hwilib/devices/ckcc/constants.py

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@
66
except ImportError:
77
const = int
88

9+
# USB encryption versions (default USB_NCRY_V1)
10+
#
11+
# This introduces a new ncry version to close a potential attack vector:
12+
#
13+
# A malicious program may re-initialize the connection encryption by sending the ncry command a second time during USB operation.
14+
# This may prove particularly harmful in HSM mode.
15+
#
16+
# Sending version USB_NCRY_V2 changes the behavior in two ways:
17+
# * All future commands must be encrypted
18+
# * Returns an error if the ncry command is sent again for the duration of the power cycle
19+
#
20+
# USB_NCRY_V2 is most suitable for HSM mode as in case of any communication issue or simply by closing `ColdcardDevice`
21+
# Coldcard will need to reboot to recover USB operation if USB_NCRY_V2.
22+
USB_NCRY_V1 = const(0x01)
23+
USB_NCRY_V2 = const(0x02)
24+
925
# For upload/download this is the max size of the data block.
1026
MAX_BLK_LEN = const(2048)
1127

@@ -17,17 +33,29 @@
1733
# - the max on the wire for mainnet is 100k
1834
# - but a PSBT might contain a full txn for each input
1935
MAX_TXN_LEN = const(384*1024)
36+
MAX_TXN_LEN_MK4 = const(2*1024*1024)
2037

2138
# Max size of any upload (firmware.dfu files in particular)
2239
MAX_UPLOAD_LEN = const(2*MAX_TXN_LEN)
40+
MAX_UPLOAD_LEN_MK4 = const(2*MAX_TXN_LEN_MK4)
2341

2442
# Max length of text messages for signing
2543
MSG_SIGNING_MAX_LENGTH = const(240)
2644

45+
# Bitcoin limitation: max number of signatures in P2SH redeem script (non-segwit)
46+
# - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes
47+
# - serializations of M/N in redeem scripts assume this range
48+
MAX_SIGNERS = const(15)
49+
# taproot artificial multisig limit
50+
MAX_TR_SIGNERS = const(34)
51+
52+
TAPROOT_LEAF_MASK = 0xfe
53+
TAPROOT_LEAF_TAPSCRIPT = 0xc0
54+
2755
# Types of user auth we support
2856
USER_AUTH_TOTP = const(1) # RFC6238
2957
USER_AUTH_HOTP = const(2) # RFC4226
30-
USER_AUTH_HMAC = const(3) # PBKDF2('hmac-sha256', secret, sha256(psbt), PBKDF2_ITER_COUNT)
58+
USER_AUTH_HMAC = const(3) # PBKDF2('hmac-sha512', scrt, sha256(psbt), PBKDF2_ITER_COUNT)[:32]
3159
USER_AUTH_SHOW_QR = const(0x80) # show secret on Coldcard screen (best for TOTP enroll)
3260

3361
MAX_USERNAME_LEN = 16
@@ -48,6 +76,7 @@
4876
AFC_BECH32 = const(0x04) # just how we're encoding it?
4977
AFC_SCRIPT = const(0x08) # paying into a script
5078
AFC_WRAPPED = const(0x10) # for transition/compat types for segwit vs. old
79+
AFC_BECH32M = const(0x20) # no difference between script/key path in taproot
5180

5281
# Numeric codes for specific address types
5382
AF_CLASSIC = AFC_PUBKEY # 1addr
@@ -56,33 +85,80 @@
5685
AF_P2WSH = AFC_SCRIPT | AFC_SEGWIT | AFC_BECH32 # segwit multisig
5786
AF_P2WPKH_P2SH = AFC_WRAPPED | AFC_PUBKEY | AFC_SEGWIT # looks classic P2SH, but p2wpkh inside
5887
AF_P2WSH_P2SH = AFC_WRAPPED | AFC_SCRIPT | AFC_SEGWIT # looks classic P2SH, segwit multisig
88+
AF_P2TR = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32M # bc1p
5989

6090
SUPPORTED_ADDR_FORMATS = frozenset([
6191
AF_CLASSIC,
6292
AF_P2SH,
6393
AF_P2WPKH,
94+
AF_P2TR,
6495
AF_P2WSH,
6596
AF_P2WPKH_P2SH,
6697
AF_P2WSH_P2SH,
6798
])
6899

69100
# BIP-174 aka PSBT defined values
70101
#
71-
PSBT_GLOBAL_UNSIGNED_TX = const(0)
72-
PSBT_GLOBAL_XPUB = const(1)
73-
74-
PSBT_IN_NON_WITNESS_UTXO = const(0)
75-
PSBT_IN_WITNESS_UTXO = const(1)
76-
PSBT_IN_PARTIAL_SIG = const(2)
77-
PSBT_IN_SIGHASH_TYPE = const(3)
78-
PSBT_IN_REDEEM_SCRIPT = const(4)
79-
PSBT_IN_WITNESS_SCRIPT = const(5)
80-
PSBT_IN_BIP32_DERIVATION = const(6)
81-
PSBT_IN_FINAL_SCRIPTSIG = const(7)
82-
PSBT_IN_FINAL_SCRIPTWITNESS = const(8)
83-
84-
PSBT_OUT_REDEEM_SCRIPT = const(0)
85-
PSBT_OUT_WITNESS_SCRIPT = const(1)
86-
PSBT_OUT_BIP32_DERIVATION = const(2)
87-
88-
# EOF
102+
# GLOBAL ===
103+
PSBT_GLOBAL_UNSIGNED_TX = const(0x00)
104+
PSBT_GLOBAL_XPUB = const(0x01)
105+
PSBT_GLOBAL_VERSION = const(0xfb)
106+
PSBT_GLOBAL_PROPRIETARY = const(0xfc)
107+
# BIP-370
108+
PSBT_GLOBAL_TX_VERSION = const(0x02)
109+
PSBT_GLOBAL_FALLBACK_LOCKTIME = const(0x03)
110+
PSBT_GLOBAL_INPUT_COUNT = const(0x04)
111+
PSBT_GLOBAL_OUTPUT_COUNT = const(0x05)
112+
PSBT_GLOBAL_TX_MODIFIABLE = const(0x06)
113+
114+
# INPUTS ===
115+
PSBT_IN_NON_WITNESS_UTXO = const(0x00)
116+
PSBT_IN_WITNESS_UTXO = const(0x01)
117+
PSBT_IN_PARTIAL_SIG = const(0x02)
118+
PSBT_IN_SIGHASH_TYPE = const(0x03)
119+
PSBT_IN_REDEEM_SCRIPT = const(0x04)
120+
PSBT_IN_WITNESS_SCRIPT = const(0x05)
121+
PSBT_IN_BIP32_DERIVATION = const(0x06)
122+
PSBT_IN_FINAL_SCRIPTSIG = const(0x07)
123+
PSBT_IN_FINAL_SCRIPTWITNESS = const(0x08)
124+
PSBT_IN_POR_COMMITMENT = const(0x09)
125+
PSBT_IN_RIPEMD160 = const(0x0a)
126+
PSBT_IN_SHA256 = const(0x0b)
127+
PSBT_IN_HASH160 = const(0x0c)
128+
PSBT_IN_HASH256 = const(0x0d)
129+
# BIP-370
130+
PSBT_IN_PREVIOUS_TXID = const(0x0e)
131+
PSBT_IN_OUTPUT_INDEX = const(0x0f)
132+
PSBT_IN_SEQUENCE = const(0x10)
133+
PSBT_IN_REQUIRED_TIME_LOCKTIME = const(0x11)
134+
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = const(0x12)
135+
# BIP-371
136+
PSBT_IN_TAP_KEY_SIG = const(0x13)
137+
PSBT_IN_TAP_SCRIPT_SIG = const(0x14)
138+
PSBT_IN_TAP_LEAF_SCRIPT = const(0x15)
139+
PSBT_IN_TAP_BIP32_DERIVATION = const(0x16)
140+
PSBT_IN_TAP_INTERNAL_KEY = const(0x17)
141+
PSBT_IN_TAP_MERKLE_ROOT = const(0x18)
142+
143+
# OUTPUTS ===
144+
PSBT_OUT_REDEEM_SCRIPT = const(0x00)
145+
PSBT_OUT_WITNESS_SCRIPT = const(0x01)
146+
PSBT_OUT_BIP32_DERIVATION = const(0x02)
147+
# BIP-370
148+
PSBT_OUT_AMOUNT = const(0x03)
149+
PSBT_OUT_SCRIPT = const(0x04)
150+
# BIP-371
151+
PSBT_OUT_TAP_INTERNAL_KEY = const(0x05)
152+
PSBT_OUT_TAP_TREE = const(0x06)
153+
PSBT_OUT_TAP_BIP32_DERIVATION = const(0x07)
154+
155+
RFC_SIGNATURE_TEMPLATE = '''\
156+
-----BEGIN BITCOIN SIGNED MESSAGE-----
157+
{msg}
158+
-----BEGIN BITCOIN SIGNATURE-----
159+
{addr}
160+
{sig}
161+
-----END BITCOIN SIGNATURE-----
162+
'''
163+
164+
# EOF

0 commit comments

Comments
 (0)