Merge branch 'master' of github.com:Coldcard/firmware

This commit is contained in:
Peter D. Gray 2023-06-19 13:43:53 -04:00
commit c488882862
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
14 changed files with 251 additions and 39 deletions

View File

@ -28,6 +28,7 @@ has been automated using Docker. Steps are as follows:
4. At the end of the process a clear confirmation message is shown, or the differences.
5. Build products can be found `firmware/stm32/built`.
6. If you do not trust the results of `make repro` refer to `docs/notes-on-repro.md` which breaks down the process.
## Long-Lived Branches

View File

@ -29,6 +29,7 @@ Step 2: Export descriptor from Coldcard to Core
- in Bitcoin Core, go to Windows -> Console
- select your newly created descriptor wallet in the wallet pulldown (top left)
- paste the `importdescriptor` command. It should respond with a success message
- in Bitcoin Core v24.1, the console response will include `"message": "Ranged descriptors should not have a label"` and Bitcoin Core won't allow address generation. Removing the entry `"label": "Coldcard x0x0x0x0"` from the .txt file fixes this issue.
NOTE: If you are importing an existing wallet this way, with UTXO on the blockchain,
you may need to rescan and/or delete "timestamp=now" from the command. If the

View File

@ -70,7 +70,7 @@
- change outputs (indicated with paths, scripts in output section) must correspond to
the active multisig wallet, and cannot be used to describe an unrelated (multisig) wallet.
- derivation path for each cosigner must be known and consistent with PSBT
- fixed: XFP values (fingerprints) for each of the co-signers must be unique (limitation removed)
- XFP values (fingerprints) MUST be unique for each of the co-signers
# SIGHASH types

190
docs/notes-on-repro.md Normal file
View File

@ -0,0 +1,190 @@
# Notes on Reproducible Builds
The following document aims to breakdown how reproducibility is verified in the `make repro` build step.
## stm32/shared.mk
The entrypoint makefile for repro builds.
### repro
The `repro` command in `shared.mk` is the first step in the repro build process, which triggers a docker build and run process.
```makefile
repro:
docker build -t coldcard-build - < dockerfile.build
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(MK_NUM))
```
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
```stdout
+ mkdir /tmp/checkout
+ mount -t tmpfs tmpfs /tmp/checkout
...
```
We will pull the release from coldcard.com into the `/tmp/checkout` directory.
```
+ git clone /work/src/.git firmware
...
+ cd firmware/external
+ git submodule update --init
...
Successfully installed signit-1.0
...
+ cd ../stm32
+ cd ../releases
+ '[' -f '*-v5.0.7-mk4-coldcard.dfu' ]
+ dd 'bs=66' 'skip=1'
+ grep -F v5.0.7-mk4-coldcard.dfu signatures.txt
0+1 records in
0+1 records out
+ PUBLISHED_BIN=2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
+ '[' -z 2022-10-05T1724-v5.0.7-mk4-coldcard.dfu ]
+ wget -S https://coldcard.com/downloads/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
...
'2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' saved
...
+ PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
...
+ make -f MK4-Makefile setup
...
+ make -f MK4-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
...
signit sign -b l-port/build-COLDCARD_MK4 -m 4 5.0.7 -o firmware-signed.bin
...
signit sign -m 4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
You don't have that key (1), so using key zero instead!
...
cd ../external/micropython/ports/stm32 && make BOARD=COLDCARD_MK4 -j 4 EXCLUDE_NGU_TESTS=1 DEBUG_BUILD=0
...
../external/micropython/tools/dfu.py -b 0x08020000:dev.bin dev.dfu
arm-none-eabi-objdump -h -S l-port/build-COLDCARD_MK4/firmware.elf > firmware.lss
cp l-port/build-COLDCARD_MK4/firmware.elf .
+ '[' /tmp/checkout/firmware/stm32 '!=' /work/src/stm32 ]
+ rsync -av --ignore-missing-args firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf /work/built
sending incremental file list
dev.dfu
firmware-signed.bin
firmware-signed.dfu
firmware.elf
firmware.lss
production.bin
...
+ make -f MK4-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
...
Comparing against: /tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
test -n "/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu" -a -f /tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
rm -f -f check-fw.bin check-bootrom.bin
signit split /tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu check-fw.bin check-bootrom.bin
start 293 for 870400 bytes: Firmware => check-fw.bin
start 870701 for 114688 bytes: Bootrom => check-bootrom.bin
signit check check-fw.bin
magic_value: 0xcc001234
timestamp: 2022-10-05 17:24:55 UTC
version_string: 5.0.7
pubkey_num: 1
firmware_length: 870400
install_flags: 0x0 =>
hw_compat: 0x8 => Mk4
best_ts: b'\x00\x00\x00\x00\x00\x00\x00\x00'
future: 0000000000000000 ... 0000000000000000
signature: 293948e7ce4a3555 ... 766437aa65d3e88a
sha256^2: 7f3a7c5f794ce72f68280447cddc837fa62245fdf4b795822127624f8775dca2
ECDSA Signature: CORRECT
signit check firmware-signed.bin
magic_value: 0xcc001234
timestamp: 2022-10-24 13:33:16 UTC
version_string: 5.0.7
pubkey_num: 0
firmware_length: 870400
install_flags: 0x0 =>
hw_compat: 0x8 => Mk4
best_ts: b'\x00\x00\x00\x00\x00\x00\x00\x00'
future: 0000000000000000 ... 0000000000000000
signature: deb643d0a140d89e ... c544f09cd80fa65c
sha256^2: a46ddd6e599a49a573bf76054f438c9efe1ee031bfae74a00b0e7bbe76f516c3
ECDSA Signature: CORRECT
hexdump -C firmware-signed.bin | sed -e 's/^00003f[89abcdef]0 .*/(firmware signature here)/' > repro-got.txt
hexdump -C check-fw.bin | sed -e 's/^00003f[89abcdef]0 .*/(firmware signature here)/' > repro-want.txt
diff repro-got.txt repro-want.txt
SUCCESS.
You have built a bit-for-bit identical copy of Coldcard firmware for v5.0.7
```
## check-repro
The `check-repro` section of the makefile contains the steps required to verify that the build artifacts are infact a bit-for-bit match to the release candidates.
```makefile
check-repro: TRIM_SIG = sed -e 's/^00003f[89abcdef]0 .*/(firmware signature here)/'
check-repro: firmware-signed.bin
ifeq ($(PUBLISHED_BIN),)
@echo ""
@echo "Need published binary for: $(VERSION_STRING)"
@echo ""
@echo "Copy it into ../releases"
@echo ""
else
@echo Comparing against: $(PUBLISHED_BIN)
test -n "$(PUBLISHED_BIN)" -a -f $(PUBLISHED_BIN)
$(RM) -f check-fw.bin check-bootrom.bin
$(SIGNIT) split $(PUBLISHED_BIN) check-fw.bin check-bootrom.bin
$(SIGNIT) check check-fw.bin
$(SIGNIT) check firmware-signed.bin
hexdump -C firmware-signed.bin | $(TRIM_SIG) > repro-got.txt
hexdump -C check-fw.bin | $(TRIM_SIG) > repro-want.txt
diff repro-got.txt repro-want.txt
@echo ""
@echo "SUCCESS. "
@echo ""
@echo "You have built a bit-for-bit identical copy of Coldcard firmware for v$(VERSION_STRING)"
endif
```
To summarize `check-repro`:
- At the final `check-repro` step, we have a locally built `firmware-signed.bin` and we want to check that it matches the binary release provided by Coinkite.
- This step verifies the signature of the binary is valid, using either the Coinkite key factory key or the "debug" key zero which is public.
- An identical checksum match will not be possible as is, since there is signature data embedded into into the binary, which must be removed.
- The specific release of the version that is being built is fetched, and placed it under /tmp/checkout/firmware/releases/*.dfu
- `split` (cli/signit.py: Line 153-175) is run against the release `*.dfu` resulting in a `check-fw.bin` and `check-bootrom.bin`. "This splits the DFU file into the two parts it contains: the main firmware (COLDCARD application) and the boot loader code."
- `check` (cli/signit.py: Line 176-241) is run against each the release `check-fw.bin` and our built `firmware-signed.bin`.
- a hexdump is taken of each the release `check-fw.bin` and our built `firmware-signed.bin` piped through $TRIM_SIG which removes 64 bytes of signature data and subsitutes it with a common string.
- Finally the diff of the two hexdumps are compared to prove reproducibility.

View File

@ -9,7 +9,6 @@ font_files = {
}
# test with:
#
# ./build.py build --portable && ./testit.py --msg "hello→world←\n↳this\n•Bullet\n•Text" -f small
#
special_chars = dict(small=[
@ -65,5 +64,14 @@ special_chars = dict(small=[
xxxxx
'''),
('', dict(y=0), '''\
x x x x x
'''),
])

View File

@ -3,9 +3,12 @@
- Enhancement: change Key Origin Information export format in multisig `addresses.csv` to match
[BIP-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions)
`(m=0F056943)/m/48'/1'/0'/2'/0/0` --> `[0F056943/48'/1'/0'/2'/0/0]`
- Enhancement: Address explorer UX cosmetics.
- Change: `Unchained Capital` renamed to `Unchained`
- Bugfix: correct `scriptPubkey` parsing for segwit v1-v16
- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT. Improves
compatibility with Bitcoin Core.
- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT.
- Bugfix: remove label from Bitcoin Core `importdescriptors` export as it is no longer supported
with ranged descriptors in version `24.1`
## 5.1.2 - 2023-04-07

View File

@ -1251,7 +1251,7 @@ async def unchained_capital_export(*a):
# they were using our airgapped export, and the BIP-45 path from that
#
ch = await ux_show_story('''\
This saves multisig XPUB information required to setup on the Unchained Capital platform. \
This saves multisig XPUB information required to setup on the Unchained platform. \
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
account_num = 0
if ch == '1':
@ -1262,7 +1262,7 @@ This saves multisig XPUB information required to setup on the Unchained Capital
xfp = xfp2str(settings.get('xfp', 0))
fname = 'unchained-%s.json' % xfp
await make_json_wallet('Unchained Capital',
await make_json_wallet('Unchained',
lambda: generate_unchained_export(account_num),
fname)

View File

@ -18,8 +18,10 @@ from utils import addr_fmt_label
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
# - 16 chars screen width, so show 8 prefix, dash, and 7 of end of address
return addr[0:8] + '-' + addr[-7:]
# - 16 chars screen width
# - but 2 lost at left (menu arrow, corner arrow)
# - want to show not truncated on right side
return addr[0:5] + '' + addr[-6:]
class KeypathMenu(MenuSystem):
def __init__(self, path=None, nl=0):
@ -28,14 +30,14 @@ class KeypathMenu(MenuSystem):
if path is None:
# Top level menu; useful shortcuts, and special case just "m"
items = [
MenuItem("m/..", f=self.deeper),
MenuItem("m/49'/..", f=self.deeper),
MenuItem("m/84'/..", f=self.deeper),
MenuItem("m/44'/..", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done),
]
MenuItem("m/..", f=self.deeper),
MenuItem("m/44'/..", f=self.deeper),
MenuItem("m/49'/..", f=self.deeper),
MenuItem("m/84'/..", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done),
]
else:
# drill down one layer: (nl) is the current leaf
# - hardened choice first
@ -183,12 +185,12 @@ class AddressListMenu(MenuSystem):
items = []
for i, (address, path, addr_fmt) in enumerate(choices):
axi = address[-4:] # last 4 address characters
items.append(MenuItem(" "+addr_fmt_label(addr_fmt), f=self.pick_single,
items.append(MenuItem(addr_fmt_label(addr_fmt), f=self.pick_single,
arg=(path, addr_fmt, axi)))
items.append(MenuItem(address, f=self.pick_single,
items.append(MenuItem(''+address, f=self.pick_single,
arg=(path, addr_fmt, axi)))
# some other choices
# some other choices
if self.account_num == 0:
items.append(MenuItem("Applications", menu=ApplicationsMenu(self)))
items.append(MenuItem("Account Number", f=self.change_account))
@ -374,7 +376,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
if ms_wallet:
# For multisig, include redeem script and derivation for each signer
yield '"' + '","'.join(['Index', 'Payment Address',
'Redeem Script (%d of %d)' % (ms_wallet.M, ms_wallet.N)]
'Redeem Script (%d of %d)' % (ms_wallet.M, ms_wallet.N)]
+ (['Derivation'] * ms_wallet.N)) + '"\n'
for (idx, derivs, addr, script) in ms_wallet.yield_addresses(start, n, change_idx=change):

View File

@ -109,7 +109,6 @@ async def drv_entro_step2(_1, picked, _2):
from glob import dis
from files import CardSlot, CardMissingError, needs_microsd
the_ux.pop()
msg = "Index Number?"
if picked == 7:
# Passwords

View File

@ -258,7 +258,7 @@ def generate_bitcoin_core_wallet(account_num, example_addrs):
for internal in [False, True]
]
# for importdescriptors
imd_list = desc_obj.bitcoin_core_serialize(external_label="Coldcard %s" % txt_xfp)
imd_list = desc_obj.bitcoin_core_serialize()
return imm_list, imd_list
def generate_wasabi_wallet():

View File

@ -163,7 +163,7 @@ WalletExportMenu = [
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
MenuItem("Electrum Wallet", f=electrum_skeleton),
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
MenuItem("Unchained Capital", f=unchained_capital_export),
MenuItem("Unchained", f=unchained_capital_export),
MenuItem("Lily Wallet", f=lily_skeleton),
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),

View File

@ -123,8 +123,7 @@ class PinAttempt:
assert MAX_PIN_LEN == 32 # update FMT otherwise
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1, \
ustruct.calcsize(PIN_ATTEMPT_FMT)
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
# check for bricked system early

View File

@ -31,12 +31,13 @@ class FontBase:
class FontSmall(FontBase):
height = 14
code_range = range(32, 8627)
code_range = range(32, 8943)
_bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3, 7,
14, 5), (0, -3, 7, 14, 7), (0, -3, 7, 14, 9), (0, -3, 7, 14, 10),
(0, -3, 7, 14, 11), (0, -3, 7, 14, 12), (0, -3, 7, 14, 13), (0, -3,
7, 14, 14), (0, 0, 8, 8, 8), (0, 0, 11, 9, 18), (0, 0, 14, 10, 20)]
7, 14, 14), (0, 0, 8, 8, 8), (0, 0, 11, 8, 16), (0, 0, 11, 9, 18),
(0, 0, 14, 10, 20)]
_code_points = [
(range(32, 127), [1, 2, 14, 20, 31, 43, 55, 67, 73, 87, 101, 111, 122,
@ -51,6 +52,7 @@ class FontSmall(FontBase):
(range(8592, 8593), [1145]), # ←
(range(8594, 8595), [1166]), # →
(range(8627, 8628), [1187]), # ↳
(range(8943, 8944), [1208]), # ⋯
]
_bitmaps = b"""\
@ -116,12 +118,13 @@ class FontSmall(FontBase):
\x3a\x02\x02\x42\x3c\x07\x00\x00\x00\x00\x7e\x02\x04\x08\x10\x20\x7e\x09\
\x06\x08\x08\x08\x08\x08\x30\x08\x08\x08\x08\x08\x06\x08\x10\x10\x10\x10\
\x10\x10\x10\x10\x10\x10\x10\x10\x09\x60\x10\x10\x10\x10\x10\x0c\x10\x10\
\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0c\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0d\x00\x00\x00\x00\x00\x00\
\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0d\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\x00\x00\x0d\
\x10\x10\x10\x60\x03\x00\x00\x32\x4a\x44\x0d\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x03\x80\x03\x80\x03\x80\x00\x00\x0e\x00\x00\x00\x00\x00\x00\
\x00\x00\x08\x00\x18\x00\x3f\xf8\x18\x00\x08\x00\x00\x00\x0e\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x20\x00\x30\x3f\xf8\x00\x30\x00\x20\x00\x00\x0e\
\x00\x00\x10\x00\x10\x00\x10\x00\x10\x20\x10\x30\x1f\xf8\x00\x30\x00\x20\
\x00\x00\
\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2a\xa0\x00\
\x00\
"""

View File

@ -133,12 +133,17 @@ def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home,
assert expect in desc
assert expect+f'/{n}/*' in desc
if n == 0:
assert here['label'] == 'Coldcard ' + xfp
assert 'label' not in d
# test against bitcoind -- needs a "descriptor native" wallet
bitcoind_d_wallet.importdescriptors(obj)
res = bitcoind_d_wallet.importdescriptors(obj)
assert res[0]["success"]
assert res[1]["success"]
core_gen = []
for i in range(3):
core_gen.append(bitcoind_d_wallet.getnewaddress())
assert core_gen == addrs
x = bitcoind_d_wallet.getaddressinfo(addrs[-1])
pprint(x)
assert x['address'] == addrs[-1]
@ -342,19 +347,20 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap
def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_keypress, acct_num,
microsd_path, nfc_read_json, virtdisk_path, testnet, enter_number,
load_export, settings_set, use_mainnet):
# test UX and operation of the 'unchained capital export'
# test UX and operation of the 'unchained export'
if not testnet:
use_mainnet()
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('File Management')
pick_menu_item('Export Wallet')
pick_menu_item('Unchained Capital')
pick_menu_item('Unchained')
time.sleep(0.1)
title, story = cap_story()
assert 'Unchained Capital' in story
assert 'Unchained' in story
assert "Capital" not in story
assert 'Press (1) to' in story
if acct_num is not None:
need_keypress('1')
@ -364,7 +370,7 @@ def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_k
acct_num = '0'
need_keypress('y')
obj = load_export(way, label="Unchained Capital", is_json=True, sig_check=False)
obj = load_export(way, label="Unchained", is_json=True, sig_check=False)
root = BIP32Node.from_wallet_key(simulator_fixed_xprv)
if not testnet: