Compare commits

..

608 Commits

Author SHA1 Message Date
scgbckbone
542dcd32c7 revert SSSP bypass PIN login 2026-06-25 11:18:33 -04:00
scgbckbone
0ef6413cd8 apply note or pwd as b39 passphrase 2026-06-24 14:07:25 -04:00
Dmitry Monakhov
97d86c9571 Fix BBQr share of Unicode text: encode str to UTF-8 before sizing/splitting
For 'U'/'J' payloads data is a str; planning counted codepoints while
b32encode consumes UTF-8 bytes, so multi-byte text (e.g. paper-wallet QR
art) overflowed target_vers and tripped the assert in show_bbqr_codes.
2026-06-24 13:29:58 -04:00
scgbckbone
edae8c1ee6 Add groups for secure notes 2026-06-24 13:28:43 -04:00
scgbckbone
553405776f Keep scanner reinit state instance-local 2026-06-24 11:12:35 -04:00
scgbckbone
ad2088d231 Fix QR scanner setup and sleep handling 2026-06-24 11:12:35 -04:00
scgbckbone
67a5c6c270 fix bypass_tmp return to master secret with xprv type 2026-06-24 08:23:47 -04:00
scgbckbone
eb112eb3a1 fix tests 2026-06-24 08:22:54 -04:00
scgbckbone
0d04e5e1f8 bugfix: p2pk 2026-06-23 11:43:39 -04:00
scgbckbone
59eb529a20 Reject witness-only UTXO for legacy inputs; Suppress fee for unverified witness UTXOs;normalize legacy inputs to proper utxo 2026-06-23 11:25:53 -04:00
scgbckbone
d5aba396a6 improve USB validation 2026-06-23 10:55:08 -04:00
scgbckbone
6fd256dbdc bugfix: 1of1 multisig 2026-06-23 10:34:44 -04:00
scgbckbone
6716fcbacb keep NFC export tag live for repeated probes 2026-06-23 10:29:26 -04:00
scgbckbone
1dddd88525 WIF Store upgrade 2026-06-22 12:46:50 -04:00
scgbckbone
d656f371c7 BIP-322 changes after BIP got in to the complete state 2026-06-22 11:20:44 -04:00
scgbckbone
7e92e5162a Restore borrowed secret handling in SensitiveValues 2026-06-22 11:16:20 -04:00
scgbckbone
74d34cfcf7 stabilize tests 2026-06-22 10:39:46 -04:00
scgbckbone
64658621bb simulator:attribute catchup with real SCAN 2026-06-22 10:39:30 -04:00
scgbckbone
755353f029 docs: NFC antenna by HW 2026-06-22 09:28:53 -04:00
scgbckbone
2981d15933 build: automatic block height update 2026-06-22 09:28:30 -04:00
scgbckbone
9ff3f5c447 slight menu optimization for long menus 2026-06-19 12:51:07 -04:00
scgbckbone
f5a1ef32c9 test nits 2026-06-19 12:50:54 -04:00
scgbckbone
841e44335e testing: block_h bumped for SSSP too, when CCC overrides SSSP block 2026-06-19 12:50:26 -04:00
scgbckbone
38616234e7 testing: cope with bitcoin core v30 2026-06-19 12:50:10 -04:00
scgbckbone
8e3bbfdf84 docs: index all docs and fix drift vs firmware 2026-06-19 12:48:49 -04:00
Peter D. Gray
f9b65ce968
credit 2026-06-19 11:00:37 -04:00
Dmitry Monakhov
8d71040acf Don't restore cached backup password (bkpw) from backup file
Restore mirrored the write-side strip of bkpw: a crafted backup could inject
setting.bkpw and fixate the password used for future backups. Drop it on restore
2026-06-19 10:59:16 -04:00
dependabot[bot]
5feae87e03 Bump requests from 2.32.4 to 2.33.0 in /testing
Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.33.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.33.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 10:58:24 -04:00
scgbckbone
0949c0ac86 README.md update build repro steps (were misordered before) 2026-06-19 10:57:08 -04:00
scgbckbone
c36eac23d2 bundle small fixes 2026-06-19 10:56:45 -04:00
scgbckbone
a24a894cfd fix typo in nfc-pushtx.md 2026-05-16 10:59:23 -04:00
scgbckbone
ca06dfd250 unreleased regression introduced in 300323f18d 2026-04-25 10:36:15 -04:00
scgbckbone
3a1ef6fe50 bugfix: NFC verify address wrong error message 2026-04-20 15:21:43 -04:00
scgbckbone
883be60fc5 bugfix: attribute error on exception object + more 7z header tests 2026-04-20 15:19:31 -04:00
scgbckbone
393ebf5b43 bugfix: default menu position in custom path address format menu 2026-04-20 15:18:30 -04:00
scgbckbone
2b5178bd63 bugfix: "Send Password" menu item visibility reversed, do not store password as None, UX fixes 2026-04-20 15:15:19 -04:00
scgbckbone
44e7be3681 fix: correct container type for settings.wifs; proper button text UX with parentheses 2026-04-20 12:40:40 -04:00
scgbckbone
300323f18d final solution can_cancel=True 2026-04-20 12:40:07 -04:00
scgbckbone
c998432fc4 bugfix: exiting nickname entry with nickname already saved deleted previous nickname; fixed settings_get with prelogin arg 2026-04-20 12:37:54 -04:00
scgbckbone
9c6cfcbbd7 bugfix: enable disabled 7z magic check in check_file_headers 2026-04-20 11:28:28 -04:00
scgbckbone
be614dab92 bugfix: Delta Mode Trick PIN restore from backup 2026-04-20 11:17:52 -04:00
scgbckbone
02bd428786 do not repeat HSM_DISABLE_CMDS in HOBBLED_CMDS 2026-04-20 11:17:17 -04:00
scgbckbone
6869ba87b0 typos 2026-04-20 11:17:17 -04:00
scgbckbone
d0f834570b testing: fix bitcoind param list 2026-04-20 11:15:02 -04:00
scgbckbone
00afe533ca better fitting UX message for MK versions 2026-04-20 11:14:36 -04:00
scgbckbone
621523c1a8 remove redundant double newline in "Show Version" 2026-04-08 10:14:42 -04:00
scgbckbone
fdf630ab31 Wait UX for chain changes 2026-04-08 10:14:05 -04:00
Peter D. Gray
d2ad7a5923
Merge branch 'master' of github.com:Coldcard/firmware 2026-03-25 10:51:52 -04:00
Peter D. Gray
29ef16be63
Signed for Edge release. 2026-03-25 10:51:35 -04:00
scgbckbone
3344975607 regtest inherits chains parameters from testnet (saves flash space) 2026-03-16 13:23:02 -04:00
Peter D. Gray
15191eaaa5
edits 2026-03-12 10:17:09 -04:00
Peter D. Gray
717a3f591a
Mk5 hardware details 2026-03-12 10:12:44 -04:00
Peter D. Gray
74790ab80d
Merge branch 'master' of github.com:Coldcard/firmware 2026-03-10 10:55:49 -04:00
Peter D. Gray
50b1704c60
mk5 now public 2026-03-10 10:55:04 -04:00
scgbckbone
1e9338c550 testing: add BIP-322 POR signed with WIF Store test 2026-03-10 08:00:06 -04:00
scgbckbone
17fc097cbd bip322 doc mainnet 2026-03-06 15:05:16 -05:00
Peter D. Gray
6f6563c2cb
update logs 2026-03-05 17:27:00 -05:00
Peter D. Gray
4904d38bb6
New release: 2026-03-05T2052-v5.5.0 2026-03-05 15:52:33 -05:00
Peter D. Gray
1ea83d9b70
Signed for mk release. 2026-03-05 15:52:31 -05:00
Peter D. Gray
2410897c86
New release: 2026-03-05T2051-v1.4.0Q 2026-03-05 15:51:22 -05:00
Peter D. Gray
6541812e20
Signed for q1 release. 2026-03-05 15:51:20 -05:00
Peter D. Gray
2604f4d092
m 2026-03-05 15:50:03 -05:00
scgbckbone
c6da2612ef
handle WIF import duplicates 2026-03-05 15:47:09 -05:00
Peter D. Gray
538b1a6df8
rc2+factory 2026-03-05 14:37:20 -05:00
Peter D. Gray
2edf3c72e4
nits 2026-03-05 14:22:56 -05:00
scgbckbone
5e3f7a9321
Visualize WIF with ability to import to WIF store 2026-03-05 14:17:42 -05:00
scgbckbone
00beed7b94
bump ccc_min_block 2026-03-05 14:17:32 -05:00
scgbckbone
12e0af3f5c
WIF store testing 2026-03-05 12:49:31 -05:00
Peter D. Gray
01629e1396
WIF tests, support paper wallet format 2026-03-05 11:51:21 -05:00
Peter D. Gray
d893575ecf
bump 2026-03-05 11:51:01 -05:00
scgbckbone
0cc6818728
fix WIF store ownership showing QR address 2026-03-05 09:56:05 -05:00
Peter D. Gray
b7bc614323
text 2026-03-05 09:24:04 -05:00
Peter D. Gray
02b5a75675
new bootloader 2026-03-05 09:14:33 -05:00
Peter D. Gray
e35f60ed5e
version bumps 2026-03-05 09:12:25 -05:00
scgbckbone
0b425b8609
fix: file picker in import BIP-322 msg needs vdisk and slot_b args 2026-03-05 08:29:51 -05:00
Peter D. Gray
43ef951d83
Edits 2026-03-04 17:32:38 -05:00
Peter D. Gray
38553d1ac5
Mk hardware 2026-03-04 17:32:21 -05:00
scgbckbone
9b131b2eff WIF Store 2026-03-04 17:16:38 -05:00
scgbckbone
c19be4f41e bugfix: do not offer to show QR code of TXID if txn is not finalized 2026-03-04 15:24:42 -05:00
Peter D. Gray
e92a8ccde1
edits 2026-03-04 12:58:21 -05:00
Peter D. Gray
7d937aca84
code formating 2026-03-04 10:50:05 -05:00
Peter D. Gray
382eef61d2
add ./debug/* 2026-03-04 10:48:06 -05:00
scgbckbone
cfc46b565e show descriptor & key expression in story; signed key expression export 2026-03-04 10:46:45 -05:00
scgbckbone
45542d1d4f prevent dupe inputs for specific kind of path_mappers 2026-03-04 10:32:52 -05:00
scgbckbone
470fe2843c bugfix: dramatic pause progress bar off by one 2026-03-04 10:32:32 -05:00
scgbckbone
6c247d4852 typo in Nuke Device UX message 2026-03-04 10:32:09 -05:00
Peter D. Gray
6f366d1603
slight rework 2026-02-25 11:05:24 -05:00
Peter D. Gray
d53c7b2e1b
optimizations, petty 2026-02-25 11:04:13 -05:00
Peter D. Gray
598ccda8c0
edits 2026-02-25 10:18:09 -05:00
scgbckbone
1bbaeef439 bugfix: duplicate inputs 2026-02-25 10:16:49 -05:00
dependabot[bot]
eb50a0e198 Bump pillow from 10.3.0 to 12.1.1 in /misc/q1font
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.3.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.3.0...12.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-25 09:57:56 -05:00
Peter D. Gray
08d0a70de2
edits 2026-02-25 09:57:00 -05:00
Peter D. Gray
a3d5d6152e
MSG->Message when we have space 2026-02-25 09:54:29 -05:00
Peter D. Gray
9f832476eb
add BIP-322 feature 2026-02-25 09:44:14 -05:00
Peter D. Gray
78195d8fb9
edits 2026-02-25 09:41:50 -05:00
scgbckbone
aecc870c6b use HW accelerated tagged_sha256 2026-02-25 09:31:34 -05:00
scgbckbone
0c43c802e1 bump libngu to version with HW accelerated tagged_sha256 2026-02-25 09:31:34 -05:00
scgbckbone
4ce43c74c6 move 2 2026-02-25 09:31:34 -05:00
scgbckbone
1176c83e34 move 2026-02-25 09:31:34 -05:00
scgbckbone
ef2f35b1fb BIP-322 msg verification 2026-02-25 09:31:34 -05:00
scgbckbone
5d7d5d881d BIP-322 Proof of Reserves 2026-02-25 09:31:34 -05:00
scgbckbone
5047512bae blue wallet export option 2026-02-24 14:10:37 -05:00
scgbckbone
9862f53bec review 2026-02-24 12:34:33 -05:00
Peter D. Gray
6a34760943
fix tests 2026-02-24 11:03:00 -05:00
Peter D. Gray
d0576a205f
Updates 2026-02-24 10:00:54 -05:00
Peter D. Gray
dfd52e5c03
buried settings menu 2026-02-24 10:00:48 -05:00
Peter D. Gray
6fd47d4c16
Updated 2026-02-24 10:00:22 -05:00
Peter D. Gray
06879c59e0
test we show BIP-39 passphrase 2026-02-24 09:57:08 -05:00
Peter D. Gray
21392bd3df
show BIP-39 passphrase on-screen 2026-02-24 09:56:48 -05:00
Peter D. Gray
e4d2326959
edits 2026-02-24 08:58:07 -05:00
scgbckbone
72e336628a Nuke Device 2026-02-17 11:36:32 -05:00
scgbckbone
7ca3baae43 allow cancel from tx explorer goto index 2026-02-17 11:22:57 -05:00
scgbckbone
89d6e226d7 txexplorer: goto index 2026-02-17 11:22:57 -05:00
scgbckbone
f966d47012 input explorer 2026-02-13 11:56:41 -05:00
scgbckbone
dbf9482fea add note to "Verify Backup" that proper verify is attainable via Restore Backup 2026-02-11 14:33:36 -05:00
scgbckbone
ecd796b3a5 bugfix: empty notes in hobbled mode 2026-02-10 09:01:09 -05:00
scgbckbone
b1fe2cca26 bugfix: fwd slash in multisig name caused export to yikes. Replace fwd slash with dash in export filenames 2026-02-10 09:00:16 -05:00
scgbckbone
5d9ab62595 nit: tx nVersion serialization is signed integer 2026-02-10 08:59:48 -05:00
scgbckbone
7dbbf29b8d add missing QR tests for tmp secret import 2026-02-06 11:46:22 -05:00
scgbckbone
722c45df0a improve import secret tests 2026-02-06 10:42:01 -05:00
scgbckbone
88e4a5e8ab improve UX responsivness for Key Expression 2026-02-06 10:41:11 -05:00
scgbckbone
910c096145 allow resetting block_h in CCC menu 2026-02-02 11:35:38 -05:00
scgbckbone
a3d5485fd2 unified multisig import 2026-02-02 11:34:52 -05:00
scgbckbone
366670b4d5 USB send keystrokes for all BIP-85 secret types 2026-01-21 11:43:32 -05:00
scgbckbone
0fffb07e9e fix: remove unnecessary total_out counting in output_iter 2026-01-21 11:23:13 -05:00
Peter D. Gray
4e91d2ca3f
nit 2026-01-02 15:14:01 -05:00
tadeubas
eb467988bb chore: revert .gitignore 2026-01-02 15:10:22 -05:00
tadeubas
9fbf208c3f refactor: drop --shallow-submodules since lwIP submodule does not support shallow clone 2026-01-02 15:10:22 -05:00
tadeubas
da1d6e28bd refactor(simulator): --log output path respects --segregate 2026-01-02 15:10:22 -05:00
tadeubas
c1e17d3a26 feat: add --log flag to enable simulator logging 2026-01-02 15:10:22 -05:00
tadeubas
a2f0eb323a chore: removed Docker 2026-01-02 15:10:22 -05:00
tadeubas
53c6d33c2f chore: Simulator Docker instructions 2026-01-02 15:10:22 -05:00
russeree
a6a66bc367 [Policy] Support raw transaction versions == 3
Co-authored-by: scgbckbone <scgbckbone@proton.me>
2025-12-30 12:04:20 -05:00
scgbckbone
386cbcbb1d note that phone cannot be in airplane mode for NFC PushTx to work 2025-11-27 14:14:15 -05:00
Peter D. Gray
d5713851ea
Signed for Edge release. 2025-11-25 12:29:30 -05:00
Peter D. Gray
91f96ff870
Merge branch 'master' of github.com:Coldcard/firmware 2025-11-20 11:34:39 -05:00
Peter D. Gray
c25af2bfb1
Signed for Edge release. 2025-11-20 11:34:32 -05:00
scgbckbone
b3cd82ed61 allow viewing QR codes for XOR split mnemonics 2025-11-18 14:53:39 -05:00
scgbckbone
02ee6a58e2 fix login tests 2025-11-17 09:12:26 -05:00
scgbckbone
192f2d2dda Key expression export 2025-11-13 10:23:24 -05:00
Peter D. Gray
f235a83cf3
update changelogs 2025-11-03 10:44:28 -05:00
Peter D. Gray
d34f59697c
New release: 2025-11-03T1527-v5.4.5 2025-11-03 10:27:16 -05:00
Peter D. Gray
f966d3b079
Signed for mk4 release. 2025-11-03 10:27:12 -05:00
Peter D. Gray
6b7294cea0
New release: 2025-11-03T1525-v1.3.5Q 2025-11-03 10:25:55 -05:00
Peter D. Gray
efa8e1d56a
Signed for q1 release. 2025-11-03 10:25:49 -05:00
Peter D. Gray
946b4e9ae4
For 2025-11-03T1525-v1.3.5Q 2025-11-03 10:25:48 -05:00
Peter D. Gray
0d1104dfe0
block bump 2025-11-03 09:37:23 -05:00
Peter D. Gray
1e371d9297
bump 2025-10-30 14:17:17 -04:00
Peter D. Gray
f88b8da729
version bump 2025-10-30 13:48:14 -04:00
scgbckbone
52a090e31b bugfix: allow setting block_h from tmp seed 2025-10-30 13:16:17 -04:00
scgbckbone
cbd1b841b9 testing: add ability to set nLockTime in fake_{ms,}_txn 2025-10-30 13:16:17 -04:00
scgbckbone
82fabe75d4 test fixes 2025-10-30 09:43:56 -04:00
scgbckbone
bc8b55a059 re-fix: bugfix: exiting custom backup password text form causes yikes 2025-10-29 16:35:48 -04:00
scgbckbone
c54b3801ce UX confirm loading backup 2025-10-29 16:35:48 -04:00
scgbckbone
076bb34285 show backup filename during backup password entry (Q only) 2025-10-29 16:06:42 -04:00
scgbckbone
48709f3329 show fw version in hobbled mode 2025-10-29 13:26:34 -04:00
scgbckbone
ef0ba6a556 address format matching from PSBT witness/redeem script instead of PSBT_XPUBs derivation paths 2025-10-29 12:56:50 -04:00
scgbckbone
a08550cfd8 bugfix: exiting custom backup password text form causes yikes 2025-10-29 12:48:20 -04:00
scgbckbone
3e818cbbf6 remove unused import from SSSP menu constructor 2025-10-05 14:24:24 -04:00
Peter D. Gray
fe0041f99c
bump date 2025-09-30 08:42:29 -04:00
Peter D. Gray
15e571b0d9
New release: 2025-09-30T1238-v5.4.4 2025-09-30 08:39:04 -04:00
Peter D. Gray
36521dfef9
Signed for mk4 release. 2025-09-30 08:39:00 -04:00
Peter D. Gray
25249eb68c
New release: 2025-09-30T1237-v1.3.4Q 2025-09-30 08:37:38 -04:00
Peter D. Gray
be1328c720
Signed for q1 release. 2025-09-30 08:37:34 -04:00
Peter D. Gray
8d6ce99cd3
undo-gold-rc 2025-09-30 08:35:56 -04:00
Peter D. Gray
fcd848d821
deltamode timing fix 2025-09-29 17:19:44 -04:00
Peter D. Gray
203394a709
spelling 2025-09-29 12:11:24 -04:00
scgbckbone
284616d597
test_sssp.py more sleeps 2025-09-29 09:42:45 -04:00
Peter D. Gray
55c9ee4626
New release: 2025-09-26T1814-v5.4.4 2025-09-26 14:14:51 -04:00
Peter D. Gray
47430cb211
Signed for mk4 release. 2025-09-26 14:14:46 -04:00
Peter D. Gray
dd1bea1949
New release: 2025-09-26T1813-v1.3.4Q 2025-09-26 14:13:29 -04:00
Peter D. Gray
2aea18ba53
Signed for q1 release. 2025-09-26 14:13:24 -04:00
Peter D. Gray
3c4922e3ca
another day, another RC 2025-09-26 14:11:49 -04:00
Peter D. Gray
c6ec10206c
undo-rc 2025-09-26 14:10:17 -04:00
Peter D. Gray
88a9f4719b
cleanups 2025-09-26 11:14:19 -04:00
scgbckbone
6a3eec50f1
bugfix: only list files with proper extension delimited by dot; fix UX showing suffixes in file_picker when no suitable files found 2025-09-26 11:03:49 -04:00
Peter D. Gray
722facf0d9
add heartbeats, cleanups 2025-09-26 10:59:37 -04:00
Peter D. Gray
0a9d99429b
tweak 2025-09-26 09:27:36 -04:00
Peter D. Gray
d8c13ddc73
dont show "allow notes" on mk4 2025-09-25 14:59:33 -04:00
Peter D. Gray
d73c1bd8d3
New release: 2025-09-25T1503-v5.4.4 2025-09-25 11:03:27 -04:00
Peter D. Gray
5764073837
Signed for mk4 release. 2025-09-25 11:03:22 -04:00
Peter D. Gray
259be18962
New release: 2025-09-25T1501-v1.3.4Q 2025-09-25 11:02:01 -04:00
Peter D. Gray
248e0b568d
Signed for q1 release. 2025-09-25 11:01:56 -04:00
Peter D. Gray
ca00ee0798
Edits 2025-09-25 11:00:16 -04:00
scgbckbone
f3aaf3d5cb reload Trick Pins before deleting unlock pins 2025-09-25 10:07:00 -04:00
scgbckbone
06fa4f338c disallow Type Passwords if not okeys in sssp 2025-09-25 10:07:00 -04:00
scgbckbone
2d81b7251d proper label for Web 2FA warning 2025-09-25 10:07:00 -04:00
scgbckbone
91f10c2f35 do not allow empty BIP-39 passphrase over USB 2025-09-25 10:07:00 -04:00
scgbckbone
52e7c75539 fix SSSP unable to find unlock policy PIN 2025-09-25 10:07:00 -04:00
scgbckbone
a1e7e4c8de fix SSSP test drive to actually enforce policy 2025-09-25 10:07:00 -04:00
scgbckbone
fce9503d0e bump ccc_min_block (block height) 2025-09-25 10:07:00 -04:00
scgbckbone
5fc25566ee ownership improve UI 2025-09-25 10:07:00 -04:00
scgbckbone
b88590f8e8
fix multisig test ms_sign_simple 2025-09-24 08:28:05 -04:00
Peter D. Gray
765cc2a5a4
Merge branch 'master' of github.com:Coldcard/firmware 2025-09-24 08:27:26 -04:00
Peter D. Gray
1a591a70eb
reword 2025-09-23 15:44:14 -04:00
scgbckbone
26680a00f0 test fixes 2025-09-23 13:09:06 -04:00
scgbckbone
429b8e645e ckcc bump 2025-09-23 11:20:16 -04:00
Peter D. Gray
74fd862c9c
edits 2025-09-23 11:06:28 -04:00
Peter D. Gray
0625aa462c
comments 2025-09-23 10:53:31 -04:00
scgbckbone
faa5ebf11e test hobbled Teleport 2025-09-23 10:47:16 -04:00
scgbckbone
d0c5998e55 multisig input/output address format 2025-09-23 10:47:16 -04:00
Peter D. Gray
50d20713a0
hide empty menu 2025-09-23 09:33:05 -04:00
scgbckbone
2445b4d435
mk4 fix word entry after restore via USB 2025-09-22 12:14:27 -04:00
Peter D. Gray
a8366f55e0
nits 2025-09-22 12:13:35 -04:00
Peter D. Gray
41b8167837
seedxor allowed when hobbled 2025-09-22 11:57:21 -04:00
Peter D. Gray
3461ce336d
nit 2025-09-22 10:53:55 -04:00
Peter D. Gray
4457576ad4
Q becomes calculator rather than e-waste 2025-09-22 10:47:01 -04:00
scgbckbone
038199a3e2
review 2025-09-22 09:51:11 -04:00
scgbckbone
1929067aa1
Q: brick into forever calculator 2025-09-22 09:51:04 -04:00
scgbckbone
2b115059e8
add "Restore from XOR" to Temporary Seed menu 2025-09-22 09:44:13 -04:00
scgbckbone
88110bc5c5
SSSP settings shared across temporary seeds 2025-09-22 09:31:13 -04:00
scgbckbone
0a2c0cba12 update ckcc to latest master 2025-09-19 13:10:19 -04:00
scgbckbone
1e9e3ffb9d fix tests 2025-09-19 13:10:04 -04:00
Peter D. Gray
863e0d85ad
nits 2025-09-19 08:50:05 -04:00
Peter D. Gray
2109ab4ab1
ownership 2025-09-18 10:58:28 -04:00
Peter D. Gray
e5dce7105b
bug 2025-09-18 10:45:05 -04:00
scgbckbone
0237fd29ba txout explorer do not yikes on big QRs 2025-09-18 10:43:32 -04:00
scgbckbone
3efeb4f1ef docs & nits 2025-09-18 10:34:22 -04:00
scgbckbone
8b3603b15f ownership: search particular named wallet via BIP-21 wallet query param 2025-09-18 10:34:22 -04:00
scgbckbone
e8ba25fd04 bugfix: ownership check needed re-run for values near max 2025-09-18 10:34:22 -04:00
Peter D. Gray
9e762d29a5
edit 2025-09-18 10:27:37 -04:00
scgbckbone
de043f2250 restore backup via USB 2025-09-18 10:09:35 -04:00
Peter D. Gray
20ce5f2bae
edits 2025-09-18 08:57:22 -04:00
scgbckbone
3353f3d4a4 rename files on SD card via List Files 2025-09-18 08:37:14 -04:00
scgbckbone
4d2349fef4 lower Mk4 default wrap-around from 16 to 10 (same as Q) 2025-09-18 08:35:39 -04:00
Peter D. Gray
54dcf2dce8
little bug 2025-09-17 10:03:50 -04:00
scgbckbone
3291faa31e decouple wiping NFC chip from ux_animation routine 2025-09-17 09:31:42 -04:00
scgbckbone
bbac20b453 bugfix: premature wipe while exporting secret material via NFC - only first export loop (0th) was actually sending data 2025-09-17 09:31:42 -04:00
scgbckbone
98420f8ac3 bugfix: selftest MicroSD test 2025-09-17 08:45:54 -04:00
Peter D. Gray
5b26b306b5
version bump 2025-09-16 12:16:45 -04:00
Peter D. Gray
a3cac15a53 nits 2025-09-16 10:35:07 -04:00
scgbckbone
c7a19ee50f add SSSP login tests 2025-09-16 10:35:07 -04:00
Peter D. Gray
e42c0631d0 done 2025-09-16 10:35:07 -04:00
Peter D. Gray
609af3a257 cleanups 2025-09-16 10:35:07 -04:00
Peter D. Gray
7daa67cc63 test deltamode works 2025-09-16 10:35:07 -04:00
Peter D. Gray
3f24307dcd nits 2025-09-16 10:35:07 -04:00
scgbckbone
372954e43a SSSP update menu tree & related adjustments 2025-09-16 10:35:07 -04:00
Peter D. Gray
391aea0462 nits 2025-09-16 10:35:07 -04:00
scgbckbone
ee464f4a40 fixes after --eff changes 2025-09-16 10:35:07 -04:00
Peter D. Gray
54d58d4b43 block some USB command in hobble mode 2025-09-16 10:35:07 -04:00
Peter D. Gray
4bd8d12d9d tune 2025-09-16 10:35:07 -04:00
scgbckbone
8ceb6a4602 more tests 2025-09-16 10:35:07 -04:00
Peter D. Gray
76cc136a9e testing 2025-09-16 10:35:07 -04:00
Peter D. Gray
8d84979ddf forgotten pin 2025-09-16 10:35:07 -04:00
Peter D. Gray
bb391515a0 add seedvault 2025-09-16 10:35:07 -04:00
Peter D. Gray
16c3caee28 cleanup 2025-09-16 10:35:07 -04:00
Peter D. Gray
ab1b656277 improve --eff handling 2025-09-16 10:35:07 -04:00
Peter D. Gray
1a1daf32e3 nits 2025-09-16 10:35:07 -04:00
Peter D. Gray
6b1c38fe2f bug note 2025-09-16 10:35:07 -04:00
Peter D. Gray
43544f4f96 word entry 2025-09-16 10:35:07 -04:00
scgbckbone
d15de0321d Mk4 SSSP Word Check 2025-09-16 10:35:07 -04:00
scgbckbone
0420d4b6eb nits 2025-09-16 10:35:07 -04:00
Peter D. Gray
00b2f67d55 tidy 2025-09-16 10:35:07 -04:00
scgbckbone
6ab63b9dcf (some) policy test for sssp 2025-09-16 10:35:07 -04:00
Peter D. Gray
d053398a9a test cases 2025-09-16 10:35:07 -04:00
Peter D. Gray
d9a601e87c improvements 2025-09-16 10:35:07 -04:00
scgbckbone
6abb24443e fix0 2025-09-16 10:35:07 -04:00
Peter D. Gray
9d54e261ec passes test_ccc_magnitude 2025-09-16 10:35:07 -04:00
Peter D. Gray
335df666ab reword last_fail_reason 2025-09-16 10:35:07 -04:00
Peter D. Gray
7c5503d81a more 2025-09-16 10:35:07 -04:00
Peter D. Gray
a4c7f95dc1 more 2025-09-16 10:35:07 -04:00
Peter D. Gray
dbb2a21798 mroe docs 2025-09-16 10:35:07 -04:00
Peter D. Gray
f12457cbb5 spending policy implemented 2025-09-16 10:35:07 -04:00
Peter D. Gray
28ba1adce3 cleanup 2025-09-16 10:35:07 -04:00
Peter D. Gray
a62d5fb31e few notes 2025-09-16 10:35:07 -04:00
Peter D. Gray
b8435fdd79 slightly better SE2 emulation 2025-09-16 10:35:07 -04:00
Peter D. Gray
d3caf63265 planning 2025-09-16 10:35:07 -04:00
Peter D. Gray
c3a454abd6 hobbled mode support for spending policy 2025-09-16 10:35:07 -04:00
Peter D. Gray
73bb6b850d few notes 2025-09-16 10:35:07 -04:00
Peter D. Gray
bc49347a69 notes 2025-09-16 10:35:07 -04:00
scgbckbone
9a4d3986b7 bugfix: enter vfs after creating it 2025-09-16 09:54:49 -04:00
scgbckbone
f89061ffbe slip32 --> slip132 2025-09-04 09:50:05 -04:00
kdmukai
3eb99272b3 Clarify allowed usage of Seed XOR standard and name 2025-08-29 10:18:26 -04:00
nvk
56e5b98438
narrower 2025-08-18 15:20:57 -04:00
scgbckbone
123caec8d1 fix HSM UX message text 2025-08-13 08:13:34 -04:00
Peter D. Gray
637624dea9
Key Teleport easier to access 2025-08-11 09:33:53 -04:00
Peter D. Gray
9d5b86e39b
cleanups 2025-07-31 10:24:28 -04:00
scgbckbone
7bd952973e bugfix: use full LCD display width (34) when displaying seed words; new OUT_CTRL_NOWRAP flag for stories 2025-07-31 10:11:57 -04:00
chad
aa4524339b restore note on 520 byte stack element limit 2025-07-31 09:59:02 -04:00
chad
5dd034c051 update documentation for multisig quorum constraints 2025-07-31 09:59:02 -04:00
scgbckbone
f90973af9b Bull Bitcoin export 2025-07-31 09:58:06 -04:00
nvk
d71f24959c reorder/rename exports and add Cove 2025-06-12 12:26:56 -04:00
scgbckbone
00b05e20b8 remove linux address patch 2025-06-11 09:18:13 -04:00
scgbckbone
11da344abf multiprocess simulator 2025-06-11 08:32:22 -04:00
dependabot[bot]
89dfe1f6d4 Bump requests from 2.32.3 to 2.32.4 in /testing
Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-10 09:11:02 -04:00
scgbckbone
81f2830425 fix word_wrap; adjust tests for new double wide policy 2025-06-06 09:54:53 -04:00
scgbckbone
efb445fbe3 CHANGE -> CHANGE BACK 2025-06-03 11:34:30 -04:00
scgbckbone
0a899727b5 move OP_RETURN ux rendition from chains to render_output 2025-06-03 11:34:30 -04:00
scgbckbone
6d17350293 QRs in txn output explorer 2025-06-03 11:34:30 -04:00
scgbckbone
86d5eb890b remove dup comment 2025-06-03 10:54:34 -04:00
scgbckbone
b3b384b7d6 sort manifest modules alphabetically 2025-06-03 10:54:34 -04:00
scgbckbone
6dbedfaeb5 move shared modules from version manifests to default manifest.py 2025-06-03 10:54:34 -04:00
scgbckbone
e29e0b65e3 is None, not equals None 2025-06-03 09:20:12 -04:00
scgbckbone
2b750c993b word_wrap: if last character is double wide on Q move to next line 2025-06-03 09:19:31 -04:00
scgbckbone
17a715bfc5 bugfix: PSBT corner cases 2025-06-03 09:18:42 -04:00
Peter D. Gray
15678037d5
New release: 2025-05-14T1344-v5.4.3 2025-05-14 09:44:42 -04:00
Peter D. Gray
213fdd3c57
Signed for mk4 release. 2025-05-14 09:44:38 -04:00
Peter D. Gray
406ab5adaa
New release: 2025-05-14T1343-v1.3.3Q 2025-05-14 09:43:24 -04:00
Peter D. Gray
416f2efffd
Signed for q1 release. 2025-05-14 09:43:20 -04:00
Peter D. Gray
8aba8fe655
Revert "Signed for q1 release."
This reverts commit 4a0cd36c9c.
2025-05-14 09:41:29 -04:00
Peter D. Gray
68d5a8fc35
Merge branch 'master' of github.com:Coldcard/firmware 2025-05-14 09:37:59 -04:00
Peter D. Gray
170b701e94
New release: 2025-05-14T1337-v1.3.3Q 2025-05-14 09:37:25 -04:00
Peter D. Gray
4a0cd36c9c
Signed for q1 release. 2025-05-14 09:37:21 -04:00
Peter D. Gray
6a5311e8f5
new release 2025-05-14 09:35:59 -04:00
Peter D. Gray
20a8d14ede
updates 2025-05-14 09:34:56 -04:00
scgbckbone
bab58af710 UX story fix; improve warning tests 2025-05-14 09:16:29 -04:00
scgbckbone
d4c4cc1b69 remove exception dumping to console 2025-05-13 12:07:32 -04:00
Peter D. Gray
75a9f9f3eb
placement 2025-05-13 11:49:12 -04:00
Peter D. Gray
e856343c66
More 2025-05-13 11:11:47 -04:00
scgbckbone
51bbee9eb1 bugfix: Mk4: fix extended keys not fully visible in stories 2025-05-13 11:00:43 -04:00
Peter D. Gray
f40d16b76b
tweaks 2025-05-13 10:54:03 -04:00
Peter D. Gray
227196da42
version bump 2025-05-13 09:51:08 -04:00
scgbckbone
ea9d183a48 bugfix: PUSHDATA2 in scripts cause yikes
bugfix: missing warning summary in the top of the story for unknown scripts
2025-05-12 13:03:46 -04:00
scgbckbone
32cfd53569 exceptions also stop after first loop if nothing else enabled 2025-05-12 11:15:03 -04:00
scgbckbone
49087f6f41 bugfix: master settings corrupted by using "Add current tmp" in Seed Vault 2025-05-09 10:13:52 -04:00
scgbckbone
6c0ee684dc bugfix: Mk4: export loop for devices with NFC=ViDsk=0 2025-04-28 11:33:58 -04:00
scgbckbone
bee2f95e0a bugfix: disable teleport PSBT from CC without secret; update menu tree 2025-04-23 08:28:47 -04:00
Peter D. Gray
594e64affc
markdown 2025-04-16 15:56:48 -04:00
Peter D. Gray
6f3cbf20c8
New release: 2025-04-16T1908-v5.4.2 2025-04-16 15:08:10 -04:00
Peter D. Gray
7ca83c5929
Signed for mk4 release. 2025-04-16 15:08:06 -04:00
Peter D. Gray
3bf9c13bbb
New release: 2025-04-16T1906-v1.3.2Q 2025-04-16 15:06:51 -04:00
Peter D. Gray
063ee4cf4d
Signed for q1 release. 2025-04-16 15:06:48 -04:00
Peter D. Gray
d7b9ed4813
time levels 2025-04-16 15:05:28 -04:00
Peter D. Gray
10b82e080f
edits 2025-04-16 15:01:32 -04:00
Peter D. Gray
a1b319347a
update test 2025-04-16 14:10:37 -04:00
scgbckbone
709207f28e
bugfix: only export BBQr for multisig core, coldcard, and pretty descriptor (contains newlines, cannot read from simple QR) 2025-04-16 13:05:25 -04:00
Peter D. Gray
c680b0461f
Revert "mk4: decrease CHAR_PER_W from 19 to 18 (one char missing in view)"
This reverts commit 33a2567452d944a724ec0571da1859ab6c028910.
2025-04-16 09:17:11 -04:00
scgbckbone
a4fa421a17
mk4: decrease CHAR_PER_W from 19 to 18 (one char missing in view) 2025-04-16 08:59:52 -04:00
scgbckbone
2f35c0d496
small nits 2025-04-16 08:58:20 -04:00
scgbckbone
315e650344 refuse to do ownership scan with regtest address on mainnet 2025-04-15 11:09:17 -04:00
scgbckbone
d9be8cb2f1
locktime ux fix (less spacing) 2025-04-15 10:42:50 -04:00
scgbckbone
176a7f80cb
change: Lock Down Seed needs confirm key (4) 2025-04-15 10:42:00 -04:00
Peter D. Gray
2441e6044e
edits 2025-04-15 10:38:53 -04:00
Peter D. Gray
f076695120
cover -factory.dfu as well 2025-04-15 10:34:40 -04:00
Peter D. Gray
6a3c8ae676
New bootrom version releases 2025-04-15 10:06:16 -04:00
Peter D. Gray
28926acd06
bugfix 2025-04-15 09:36:19 -04:00
Peter D. Gray
adcf2c8e22
compilier compat 2025-04-15 09:09:17 -04:00
Peter D. Gray
1ae8c51a3c
feel good msg 2025-04-14 10:41:24 -04:00
Peter D. Gray
36d4df8a49
nits 2025-04-14 10:17:46 -04:00
scgbckbone
1aed412080 teleport testing protocol 2025-04-14 10:12:44 -04:00
Peter D. Gray
bc0f3e2d12
Merge branch 'master' of github.com:Coldcard/firmware 2025-04-14 09:55:26 -04:00
Peter D. Gray
cae59ecddd
edits 2025-04-14 09:55:22 -04:00
scgbckbone
9b95d29152 fix rename in Seed Vault 2025-04-14 09:47:25 -04:00
scgbckbone
d376c4efbf bugfix: temporary seed from CC backup failed to load stored multisig wallets 2025-04-14 09:27:09 -04:00
scgbckbone
e021fc7317 finalize foreign single sig outputs from PSBT partial signatures 2025-04-14 09:17:29 -04:00
scgbckbone
ef72dc00ae fix Mk4 re-export infinite loop 2025-04-14 09:14:02 -04:00
scgbckbone
e2fc69661a NFC selftest - revert to simple share and stop 2025-04-11 07:17:16 -04:00
Peter D. Gray
b65f2948d4
better feels on version number 2025-04-10 10:01:15 -04:00
scgbckbone
fbbf0a3413 bugfix: fix "Wipe -> Wallet" trick pin option 2025-04-10 09:49:37 -04:00
scgbckbone
c4b7260686 fix it 2025-04-10 09:49:04 -04:00
Peter D. Gray
6a63c7bde9
Merge branch 'master' of github.com:Coldcard/firmware 2025-04-10 09:48:51 -04:00
Peter D. Gray
efd5a4ff45
edits 2025-04-10 09:48:44 -04:00
scgbckbone
b866e0912d improve responsiveness of Teleport UX 2025-04-09 15:06:16 -04:00
scgbckbone
9ba8aeaaad kt_send_psbt fix 2025-04-09 14:14:01 -04:00
scgbckbone
3239dc6cd5 improve re-export UX; unify USB to done_signing; bugfixes 2025-04-09 14:04:26 -04:00
Peter D. Gray
8a18e413e9
better URL 2025-04-08 09:35:02 -04:00
scgbckbone
78fcfa56a5 backup fixes 2025-04-08 09:34:24 -04:00
Peter D. Gray
634bb69873
edits 2025-04-07 16:35:55 -04:00
Peter D. Gray
00f8d7a5ca
ux feedback 2025-04-07 15:38:27 -04:00
Peter D. Gray
0a200d1f1c
ktrx is semi-ephemeral 2025-04-07 15:38:17 -04:00
Peter D. Gray
30bc6a1f57
cleanups/bugs 2025-04-07 13:48:18 -04:00
Peter D. Gray
f23d7f09bf
add full backup to key-teleport 2025-04-05 13:33:09 -04:00
Peter D. Gray
5c5f8902a1
better pw display 2025-04-05 11:17:00 -04:00
Peter D. Gray
6bba62224c
add T shortcut for other modes where Advanced/Tools is shown 2025-04-05 11:00:29 -04:00
Peter D. Gray
8837cdcdda
Merge branch 'master' of github.com:Coldcard/firmware 2025-04-04 11:51:48 -04:00
Peter D. Gray
c4b56d95d0
key teleport MS test 2025-04-04 11:51:44 -04:00
scgbckbone
2156844d18 UX showing words now offers NFC, Q1 hint icons; full wipe NFC chip after secret data shared 2025-04-04 11:07:37 -04:00
scgbckbone
b47412bbc1 fixes 2025-04-03 10:05:00 -04:00
scgbckbone
1ca170946f sign multisig export artifacts 2025-04-02 13:53:12 -04:00
scgbckbone
0dedaf353d CCC usability improvements;ability to remove all addrs from whitelist (with confirmation) 2025-04-02 13:16:17 -04:00
scgbckbone
6779345665 fix default NFC screen prompt for Q 2025-04-02 12:51:26 -04:00
scgbckbone
698f84ff97 empty list checks 2025-04-01 16:22:12 -04:00
scgbckbone
6a5f4843aa fix error message order, first check for our/other keys then validate multisigs 2025-04-01 15:27:55 -04:00
scgbckbone
e851e4382a stop scanning upon receiving unsupported data 2025-03-31 14:48:23 -04:00
Peter D. Gray
fc21241a49
fun real vs simu test 2025-03-31 11:52:56 -04:00
scgbckbone
5de2ba364d finalize multisig error messages 2025-03-31 11:50:36 -04:00
scgbckbone
cc62502ab1 test fixes 2025-03-31 09:45:10 -04:00
Peter D. Gray
0815fbcc81 little fixes 2025-03-28 15:08:57 -04:00
Peter D. Gray
a676ef8eb7 rework post-signing save process 2025-03-28 15:08:57 -04:00
Peter D. Gray
78e560376c refactor-out msgsign.py from auth.py 2025-03-28 15:08:57 -04:00
Peter D. Gray
7e65cc3e47 robustness fix 2025-03-28 15:08:57 -04:00
Peter D. Gray
a954821826 sign our leg from teleport co-signer menu 2025-03-28 15:08:57 -04:00
Peter D. Gray
828b09dba2 teleport 2-of-15 2025-03-28 15:08:57 -04:00
Peter D. Gray
2b88a14124 bugfix 2025-03-28 15:08:57 -04:00
Peter D. Gray
8acb15e4ce historical note 2025-03-28 15:08:57 -04:00
scgbckbone
97d8398e8a remove finms: always finalize multisig txns if possible 2025-03-28 15:08:57 -04:00
Peter D. Gray
5f87d21811 refactoring NFC 2025-03-28 15:08:57 -04:00
Peter D. Gray
5c8a73ddb0 catch bad numeric password 2025-03-28 15:08:57 -04:00
Peter D. Gray
979a4e65e9 nits 2025-03-28 15:08:57 -04:00
Peter D. Gray
6e5c68abe5 bugfix, cleanups 2025-03-28 15:08:57 -04:00
Peter D. Gray
d3634b3448 test bug fixes 2025-03-28 15:08:57 -04:00
scgbckbone
67fa34666d fixes 2025-03-28 15:08:57 -04:00
Peter D. Gray
39adb2ac41 Multisig PSBT support 2025-03-28 15:08:57 -04:00
Peter D. Gray
6fd2ef619e remove checksum on rx pubkey 2025-03-28 15:08:57 -04:00
Peter D. Gray
4f25f6dbf5 nits 2025-03-28 15:08:57 -04:00
Peter D. Gray
b4eeeda53a key teleport tests 2025-03-28 15:08:57 -04:00
Peter D. Gray
f8bc38b558 fixes 2025-03-28 15:08:57 -04:00
Peter D. Gray
414793053e teleport tests 2025-03-28 15:08:57 -04:00
Peter D. Gray
b885976601 added quick note 2025-03-28 15:08:57 -04:00
Peter D. Gray
ec64a9aa38 Seedvault refactor, more on KT 2025-03-28 15:08:57 -04:00
Peter D. Gray
0aa0fc4500 Rebased 2025-03-28 15:08:57 -04:00
scgbckbone
3ebde0ea34 change: "Destroy Seed" purges all Trick PINs from SE2 2025-03-27 15:04:50 -04:00
scgbckbone
b6098a94e5 bugfix: check candidate Main PIN with trick pins from SE2, not just settings 2025-03-27 15:04:50 -04:00
scgbckbone
e726637319 add test for getting C key from Seed Vault 2025-03-27 13:52:56 -04:00
scgbckbone
446bea9926 bugfix: auto-vdisk, do not consider already signed PSBTs; fix stucked at Reading... screen 2025-03-27 12:37:11 -04:00
scgbckbone
9a28d36097 signing artifacts re-export 2025-03-27 11:00:07 -04:00
scgbckbone
a4d7f884c0 remove finms: always finalize multisig txns if possible 2025-03-27 10:02:46 -04:00
scgbckbone
ce1ce080ab fix qrs force_msg 2025-03-24 09:13:25 -04:00
Peter D. Gray
e06449f59f
I should prefer testing before pushing 2025-03-14 12:02:13 -04:00
Peter D. Gray
ffda830f66
personal perference 2025-03-14 11:53:40 -04:00
scgbckbone
d23187f187 multisig tx finalization 2025-03-14 11:50:56 -04:00
scgbckbone
15766b418d CCC test fix 2025-03-14 09:07:12 -04:00
Peter D. Gray
7450940730
tweak 2025-03-11 13:31:29 -04:00
scgbckbone
d1fd24c9ef improve CCC enable UX 2025-03-11 13:29:57 -04:00
scgbckbone
19ce22e607 deltamode: wipe mcu key slot before getting actual secret 2025-03-11 13:28:18 -04:00
scgbckbone
4476089d0f bugfix: Selftest SD card test for Q 2025-03-11 11:29:48 -04:00
scgbckbone
341265c486 bugfix: hanging progress bar after Selftest on Q 2025-03-11 11:29:48 -04:00
scgbckbone
f3a2f59549 bugfix: enable to restore backup with custom bkpw 2025-03-11 10:32:08 -04:00
scgbckbone
4a245ce553 more robust export NFC export; export wallet in loop with selected account number 2025-03-11 10:15:21 -04:00
Peter D. Gray
de443d5b89
by request of factory; saves a click 2025-03-10 14:42:34 -04:00
Peter D. Gray
ff756f086e
Signed for removing edge factory release. 2025-03-10 11:54:31 -04:00
scgbckbone
cd1728b81d new parameter to "force_msg" during QR display 2025-03-10 09:55:04 -04:00
scgbckbone
633120b760 fix mk4 word menu; use new address display format for whitelist; fix mk4 compat in tests 2025-03-10 09:41:27 -04:00
scgbckbone
3fd8fb9dc1 remove dead code around CLEAR_PIN 2025-03-10 09:30:41 -04:00
scgbckbone
7a27adfcfd bugfix: implement missing if wrong pin options 2025-03-10 09:26:40 -04:00
scgbckbone
33faa04652 bugfix: UI multisig derivation for root keys in Address Explorer 2025-03-10 09:08:19 -04:00
Peter D. Gray
56f0d56a08
missed year bump 2025-02-27 11:52:54 -05:00
Peter D. Gray
481d64d2dd
handle larger firmware 2025-02-27 11:52:15 -05:00
Peter D. Gray
e3ae6bcbdf
Cleanup use of MAX_TXN_LEN_MK4 vs. MAX_TXN_LEN 2025-02-27 10:37:33 -05:00
Peter D. Gray
e150150d11
performance upgrade 2025-02-27 09:50:28 -05:00
Peter D. Gray
f117423210
better progress bars 2025-02-27 09:46:26 -05:00
scgbckbone
962bb4b0f2 enumerate default option before offering to choose non-zero acct num in PICK_ACCOUNT 2025-02-27 09:04:00 -05:00
scgbckbone
ae9806f702 clear dispaly after usb failure - do not hang on Receiving... 2025-02-27 09:03:40 -05:00
Peter D. Gray
66e4cf130f
Updated 2025-02-27 09:03:12 -05:00
Peter D. Gray
72fef6d5b8
comments 2025-02-27 09:02:30 -05:00
Peter D. Gray
bb3073af76
edits 2025-02-26 10:28:51 -05:00
Peter D. Gray
b09bb521a8
version big bump 2025-02-26 10:16:25 -05:00
Peter D. Gray
22ff7b1fe9
edits 2025-02-26 10:12:39 -05:00
Peter D. Gray
7eb5a7ea03
copy tweets 2025-02-26 09:53:12 -05:00
doc-hex
6b9e2ef9b9
Merge pull request #473 from scgbckbone/ccc
ColdCard Cosign = CCC
2025-02-26 09:48:18 -05:00
scgbckbone
7c9436d237 after rebase fixes 2025-02-26 12:05:48 +01:00
scgbckbone
8bf4731cf5 remove ccc confirm with (4) key; remove ccc tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
e14fb64904 numbers for humans 2025-02-26 12:05:48 +01:00
scgbckbone
12f62f95bc unique ms names from ccc feature; multiple ms wallets test 2025-02-26 12:05:48 +01:00
scgbckbone
58eec8be8b load C key + seed vault access 2025-02-26 12:05:48 +01:00
scgbckbone
b5a6bf5d18 test signing with C key as tmp 2025-02-26 12:05:48 +01:00
scgbckbone
70acd6a602 test export C XPUBs 2025-02-26 12:05:48 +01:00
scgbckbone
1fe0c58b76 maxed tests 2025-02-26 12:05:48 +01:00
scgbckbone
18ea47f334 more velocity + warning ccc tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
96b8d48136 text 2025-02-26 12:05:48 +01:00
scgbckbone
4a463da9db master rebase; fix test_decoders.py; NLOCK_IS_TIME moved to constants.py 2025-02-26 12:05:48 +01:00
Peter D. Gray
239d1ebd00 Copy changes, 25-addr limit 2025-02-26 12:05:48 +01:00
Peter D. Gray
77956158ae key rotated to final value 2025-02-26 12:05:48 +01:00
Peter D. Gray
3d4336bab7 edits 2025-02-26 12:05:48 +01:00
Peter D. Gray
b2f6e290d1 lowercase 2025-02-26 12:05:48 +01:00
scgbckbone
f920020ba0 simulator nLockTime 2025-02-26 12:05:48 +01:00
Peter D. Gray
4a234e8452 tweaks 2025-02-26 12:05:48 +01:00
scgbckbone
d980c42dad ccc velocity review 2025-02-26 12:05:48 +01:00
scgbckbone
37c7119ae4 ccc velocity 2025-02-26 12:05:48 +01:00
scgbckbone
ab55c00065 non-async func 2025-02-26 12:05:48 +01:00
scgbckbone
6f93847f8a fixes 02 2025-02-26 12:05:48 +01:00
scgbckbone
2eb615b358 reworked 2025-02-26 12:05:48 +01:00
Peter D. Gray
c899f6e7ce key C from vault, stash cleanups 2025-02-26 12:05:48 +01:00
scgbckbone
d0f7a451ef small fixes 2025-02-26 12:05:48 +01:00
Peter D. Gray
14be94e049 file import 2025-02-26 12:05:48 +01:00
Peter D. Gray
5fb1839620 more 2025-02-26 12:05:48 +01:00
scgbckbone
a2eed31416 CCC config first time entry without C key prompt 2025-02-26 12:05:48 +01:00
Peter D. Gray
9203c4f2aa more 2025-02-26 12:05:48 +01:00
Peter D. Gray
3d08b749b3 sign w/ other key 2025-02-26 12:05:48 +01:00
Peter D. Gray
58b5d1071f cleanup 2025-02-26 12:05:48 +01:00
Peter D. Gray
f795f9027f generalize number entry (mk4) 2025-02-26 12:05:48 +01:00
Peter D. Gray
2cf006beaa moved to web2fa 2025-02-26 12:05:48 +01:00
Peter D. Gray
0c28987190 more web2fa code 2025-02-26 12:05:48 +01:00
Peter D. Gray
012433aba4 19 seems to work and looks better 2025-02-26 12:05:48 +01:00
Peter D. Gray
57cdd69c81 Lots of UX and some TODOs 2025-02-26 12:05:48 +01:00
Peter D. Gray
9e082570aa test code for web backend 2025-02-26 12:05:48 +01:00
Peter D. Gray
117daaf17f todoos 2025-02-26 12:05:48 +01:00
Peter D. Gray
359d05dc7b note 2025-02-26 12:05:48 +01:00
Peter D. Gray
77059ffcf1 about ccc 2025-02-26 12:05:48 +01:00
Peter D. Gray
17306e2a38 about ccc 2025-02-26 12:05:48 +01:00
Peter D. Gray
1f1045b401 docs 2025-02-26 12:05:48 +01:00
Peter D. Gray
29b860e84e cleanups 2025-02-26 12:05:48 +01:00
Peter D. Gray
0849e538b5 2fa link encryption, tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
44dae36141 faster q seed word entry 2025-02-26 12:05:48 +01:00
Peter D. Gray
8a8860d9a0 cleanups 2025-02-26 12:05:48 +01:00
Peter D. Gray
0a185669d0 more conservative 2025-02-26 12:05:48 +01:00
Peter D. Gray
6445fad042 lowercase 2025-02-26 12:05:48 +01:00
scgbckbone
0ccd701421 simulator nLockTime 2025-02-26 12:05:48 +01:00
Peter D. Gray
7ff8342e01 more 2025-02-26 12:05:48 +01:00
Peter D. Gray
0009028817 tweaks 2025-02-26 12:05:48 +01:00
scgbckbone
7e9ce496d8 ccc velocity review 2025-02-26 12:05:48 +01:00
scgbckbone
869e317db8 ccc velocity 2025-02-26 12:05:48 +01:00
Peter D. Gray
6df24f646d Bugs 2025-02-26 12:05:48 +01:00
Peter D. Gray
9d863edcdb misc 2025-02-26 12:05:48 +01:00
Peter D. Gray
4c308205a6 debug fails 2025-02-26 12:05:48 +01:00
scgbckbone
2641650090 non-async func 2025-02-26 12:05:48 +01:00
scgbckbone
84a476d586 addr -> addrs 2025-02-26 12:05:48 +01:00
scgbckbone
8b97535a40 whitelist init 2025-02-26 12:05:48 +01:00
scgbckbone
119fc35c43 fix 2025-02-26 12:05:48 +01:00
scgbckbone
2feade4f74 proper storage serialization + whitelist tests for Q 2025-02-26 12:05:48 +01:00
scgbckbone
9761c65614 fixes 02 2025-02-26 12:05:48 +01:00
Peter D. Gray
00d8c841f7 auth checks 2025-02-26 12:05:48 +01:00
scgbckbone
531ae613c7 reworked 2025-02-26 12:05:48 +01:00
scgbckbone
ac782fdd59 signing psbt POC 2025-02-26 12:05:48 +01:00
Peter D. Gray
d35ccc1ade key C from vault, stash cleanups 2025-02-26 12:05:48 +01:00
scgbckbone
2c52cca4bd test fix 2025-02-26 12:05:48 +01:00
scgbckbone
07e66811f3 yikes on Q 2025-02-26 12:05:48 +01:00
Peter D. Gray
077d502139 copy 2025-02-26 12:05:48 +01:00
scgbckbone
4030f6f59f small fixes 2025-02-26 12:05:48 +01:00
scgbckbone
15990fa0f6 use secp256k1 for 2FA tests 2025-02-26 12:05:48 +01:00
Peter D. Gray
ef01a57626 file import 2025-02-26 12:05:48 +01:00
Peter D. Gray
d21515e2cf whitelist import by qr 2025-02-26 12:05:48 +01:00
Peter D. Gray
fc5e99c226 more 2025-02-26 12:05:48 +01:00
scgbckbone
bc59709966 only push CCCConfig if first time 2025-02-26 12:05:48 +01:00
scgbckbone
d983549fae CCC config first time entry without C key prompt 2025-02-26 12:05:48 +01:00
scgbckbone
b6fc24e705 CCC setup test fixtures 2025-02-26 12:05:47 +01:00
Peter D. Gray
fcae022ba8 block exit 2025-02-26 12:05:47 +01:00
Peter D. Gray
65d202160e oops 2025-02-26 12:05:47 +01:00
Peter D. Gray
4fb37d7850 multisig 2025-02-26 12:05:47 +01:00
Peter D. Gray
5b69ec07fc more 2025-02-26 12:05:47 +01:00
Peter D. Gray
60d254314b sign w/ other key 2025-02-26 12:05:47 +01:00
Peter D. Gray
0f3fe830b9 Cleanup 2025-02-26 12:05:47 +01:00
Peter D. Gray
4087f8f25b cleanup 2025-02-26 12:05:47 +01:00
Peter D. Gray
6b770024ce rename url_encode to quote 2025-02-26 12:05:47 +01:00
Peter D. Gray
bbd8f9b282 generalize number entry (mk4) 2025-02-26 12:05:47 +01:00
Peter D. Gray
d81d6ea80c moved to web2fa 2025-02-26 12:05:47 +01:00
Peter D. Gray
8edef87ae5 refactor for reuse 2025-02-26 12:05:47 +01:00
Peter D. Gray
9cc6ce368d more web2fa code 2025-02-26 12:05:47 +01:00
Peter D. Gray
e037037924 19 seems to work and looks better 2025-02-26 12:05:47 +01:00
Peter D. Gray
5b38dd87a6 Lots of UX and some TODOs 2025-02-26 12:05:47 +01:00
Peter D. Gray
b55db05aab test code for web backend 2025-02-26 12:05:47 +01:00
doc-hex
6d4b3d6990
Merge pull request #471 from scgbckbone/unreadable_qrs
always choose the biggest possible display size for QR on LCD
2025-02-24 08:39:05 -05:00
doc-hex
c7d216fff3
Merge pull request #470 from scgbckbone/bitcoin_safe_export
add Bitcoin Safe to Export menu
2025-02-24 08:37:52 -05:00
scgbckbone
a3f815a74d always choose the biggest possible display size for QR on LCD 2025-02-24 13:53:50 +01:00
scgbckbone
e61493668b add Bitcoin Safe to Export menu 2025-02-24 13:39:41 +01:00
Peter D. Gray
eb325e5fec
Signed for Edge release. 2025-02-19 15:41:02 -05:00
Peter D. Gray
242641d396
version bump 2025-02-13 09:35:13 -05:00
Peter D. Gray
a507677dc1
New release: 2025-02-13T1415-v5.4.1 2025-02-13 09:15:16 -05:00
Peter D. Gray
262c4ea717
Signed for mk4 release. 2025-02-13 09:15:13 -05:00
Peter D. Gray
a16516b8e1
New release: 2025-02-13T1413-v1.3.1Q 2025-02-13 09:14:01 -05:00
Peter D. Gray
695c3b4b29
Signed for q1 release. 2025-02-13 09:13:57 -05:00
Peter D. Gray
08bb9783d2
update for new release 2025-02-13 09:09:43 -05:00
Peter D. Gray
4515e688ed
fixing tests 2025-02-13 09:04:21 -05:00
Peter D. Gray
5694ada611
Ownership success UX story title compat Mk4/Q 2025-02-13 09:02:04 -05:00
Peter D. Gray
d2636c7621
edits 2025-02-12 13:23:28 -05:00
scgbckbone
67febe2758 BBQr switch in QR File Share 2025-02-12 13:10:10 -05:00
scgbckbone
7e6be45e2d fix: multisg address display 2025-02-12 12:19:53 -05:00
scgbckbone
660b4617bd fix: busy bar after failed calc_qr 2025-02-12 12:08:56 -05:00
scgbckbone
afe85a3772 fix: id hint while displaying qr segwit address overlap QR 2025-02-12 12:08:30 -05:00
scgbckbone
6c91bd7328 allow multisig descriptor with root keys 2025-02-12 11:12:28 -05:00
Peter D. Gray
9c7f4c5451
Merge branch 'master' of github.com:Coldcard/firmware 2025-02-11 10:03:07 -05:00
Peter D. Gray
4ea455aa20
add sort-notes feature 2025-02-11 10:02:55 -05:00
scgbckbone
9e38974f7a revert: mpy submodule commit 2025-02-11 09:07:13 -05:00
scgbckbone
75751b8d2b testing: fix multisg tests after UI text improvements 2025-02-11 08:42:18 -05:00
Peter D. Gray
dd66cd8811
verify addr in QR better, fix some related issues 2025-02-10 11:48:59 -05:00
Peter D. Gray
a640dd5d78
bugfix 2025-02-10 10:44:00 -05:00
Peter D. Gray
14e85304df
asperation 2025-02-10 10:33:15 -05:00
Peter D. Gray
1eec58ece7
merge and reword 2025-02-10 10:28:15 -05:00
Peter D. Gray
41cde6be6c
text tweaks 2025-02-10 09:55:50 -05:00
Peter D. Gray
15536e4c9e
fix double space 2025-02-10 09:43:39 -05:00
scgbckbone
2feb991d96 msg sign: address format from standard derivation paths if address format not specified 2025-02-07 11:50:20 -05:00
scgbckbone
a2bdfc9a58 msg sign: Sparrow QR compat 2025-02-07 11:50:20 -05:00
Peter D. Gray
d3c50521e8
linebreak 2025-02-07 11:41:13 -05:00
Peter D. Gray
2558ed7ac0
text tweaks 2025-02-07 11:30:39 -05:00
Peter D. Gray
4f8f1fe593
edits 2025-02-07 11:26:51 -05:00
scgbckbone
3878897369 testing: fix test_msas_enable_disable 2025-02-06 08:06:09 -05:00
scgbckbone
1a18258b5a new address format for UX display 2025-02-05 11:28:09 -05:00
scgbckbone
38c92ef0c1 show ms address qr is msas=1 2025-02-04 12:48:38 -05:00
scgbckbone
cc7097b4f7 stabilize temporary seed tests 2025-02-02 11:46:06 -05:00
scgbckbone
9b597592bc option to show/export full multisig addresses; do not return to home menu after setting unsort_ms 2025-01-31 12:20:48 -05:00
scgbckbone
70d303af78 add ability to use master bkpw for tmp seeds; add bkpw override 2025-01-28 12:08:01 -05:00
Peter D. Gray
cd6d74d9f3
note 2025-01-23 09:14:00 -05:00
Dmitry Monakhov
a71f350c78 deltamode & xor_seed
Die rather than give up our secrets

- Do not allow split master via SeedXOR
- Do not allow to use master and seedvault in SeedXOR restore.
2025-01-23 09:11:56 -05:00
scgbckbone
ce1026cb4b fix test_iss6743 after removal of SIGHASH_ALL from psbt input 2025-01-23 08:08:05 -05:00
scgbckbone
e039fb8603 update mpy submodule 2025-01-22 08:41:35 -05:00
scgbckbone
a0949ecb87 upgrade msg signing 2025-01-21 13:05:21 -05:00
scgbckbone
a8202972b3 default non-root derivation paths for sd/nfc msg signing 2025-01-21 13:05:21 -05:00
Peter D. Gray
85ff2dcf45
robustness 2025-01-17 12:04:37 -05:00
Peter D. Gray
262df4a257
bump 2025-01-17 12:04:29 -05:00
Peter D. Gray
e9d17e5efb
Merge branch 'master' of github.com:Coldcard/firmware 2025-01-17 09:05:12 -05:00
Peter D. Gray
513a3b8258
NFC hardware detect bug 2025-01-17 09:04:35 -05:00
scgbckbone
ac761c23d5 add message about successful master seed recovery when trying to use master as tmp 2025-01-17 08:30:14 -05:00
scgbckbone
de0a679eef add ability to switch between slip132 and bip32 representations of extended public keys in Export XPUB 2025-01-15 13:05:11 -05:00
scgbckbone
92a776cfc3 testnet4 2025-01-15 13:04:17 -05:00
Peter D. Gray
f2a3667593
fix for blank/3dots screen crash 2025-01-15 09:47:56 -05:00
scgbckbone
6b73eb2fa6 save bytes by removing some duplicate from glob import settings 2025-01-15 08:38:42 -05:00
scgbckbone
66b01c1fd5 special menu keys 1..9 change from cursor scroll-only to skip-to-item 2025-01-14 09:56:07 -05:00
scgbckbone
14ce2ca6e0 remove unnecessary validate_func arg from ux_input_numbers (Mk4) 2025-01-14 09:35:20 -05:00
scgbckbone
86fe33137f psbt: sighash not included in input data if SIGHASH_ALL 2025-01-14 08:18:54 -05:00
scgbckbone
c65280cd42 cope with buggy core 28.0 behavior wrt labels in importdescriptors 2025-01-07 08:52:07 -05:00
scgbckbone
2fb66da58d wider mk4 QR check 2025-01-07 08:49:08 -05:00
Tyler Nieman
87b5142145 add missing word to seed xor docs 2025-01-01 10:29:17 -05:00
scgbckbone
6aedb0a73a testing: speed up backup tests by removing artifacts after test 2024-12-19 08:43:45 -05:00
Peter D. Gray
072eb24ed9
Signed for q1 release. 2024-12-18 11:05:37 -05:00
spicyzboss
b443f38d60 docs: add space between words 2024-12-12 08:39:37 -05:00
scgbckbone
8957ad3c10 prevent ownership yikes 2024-11-25 09:43:52 -05:00
scgbckbone
d270cf66c6 provide generalized nfc reader function (saving bytes) 2024-11-25 09:43:32 -05:00
scgbckbone
c425fc6bcc improve Wipe LFS UX message 2024-11-25 09:42:17 -05:00
scgbckbone
c9882d7a8a save bytes drv_entro.py 2024-10-25 09:28:39 -04:00
scgbckbone
85b478346b Mk4: export descriptor as simple QR 2024-10-18 08:41:01 -04:00
scgbckbone
95b13083dc do not allow to delete current active tmp seed from seed vault and purge its settings 2024-10-17 12:44:45 -04:00
scgbckbone
5568082f35 deltamode & Seed Vault 2024-10-16 13:47:23 -04:00
scgbckbone
8f86ed1c0e deltamode & secure notes and passwords 2024-10-16 13:30:37 -04:00
scgbckbone
1b54536eff bugfix: bless firmware causes hanging progress bar 2024-10-16 13:29:58 -04:00
Henrique Albuquerque
d1d104cb7e Fix grammar error 2024-09-30 09:10:20 -04:00
scgbckbone
9e1ce7a956 do NOT allow to enable/disable Seed Vault while in temporary seed mode 2024-09-27 11:56:04 -04:00
scgbckbone
f30686f252 update menu-tree.txt 2024-09-24 08:54:56 -04:00
Peter D. Gray
3dbc9caa73
Bugfix: blank screen on boot 2024-09-23 14:39:00 -04:00
169 changed files with 12667 additions and 21836 deletions

View File

@ -28,9 +28,11 @@ has been automated using Docker. Steps are as follows:
```shell
git clone https://github.com/Coldcard/firmware.git
git checkout 2023-12-21T1526-v5.2.2
# get a copy of that binary into ./releases/2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
cd firmware/stm32
cd firmware
# DOWNLOAD https://coldcard.com/downloads
# get a copy of binary into ./releases/2026-03-05T2052-v5.5.0-mk-coldcard.dfu
git checkout 2026-03-05T2052-v5.5.0
cd stm32
make -f MK4-Makefile repro
```

View File

@ -3,14 +3,32 @@
These docs are meant for you hackers out there... but also for anyone who
wants to understand why it's safe to put your moneys into Coldcard.
- [`security-model.md`](security-model.md) The COLDCARD Mk4/Mk5/Q security model.
- [`pin-entry.md`](pin-entry.md) Huge and detailed discussion of PIN codes and the security element that holds the secrets.
- [`secure-elements.md`](secure-elements.md) How the dual secure elements work together.
- [`dev-access.md`](dev-access.md) How developers can modify Coldcard to extend it.
- [`memory-map.md`](memory-map.md) Memory map highlights
- [`notes-on-repro.md`](notes-on-repro.md) Detailed breakdown of the reproducible build process.
- [`upgrade-recovery.md`](upgrade-recovery.md) Firmware upgrade and recovery process.
- [`backup-files.md`](backup-files.md) Some details of our encrypted backup files.
- [`temporary-seeds.md`](temporary-seeds.md) Temporary (ephemeral) seeds and the Seed Vault.
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
- [`key-teleport.md`](key-teleport.md) Key Teleport: encrypted transfer of seeds and secrets between Q devices.
- [`spending-policy.md`](spending-policy.md) Spending policy: autonomous signing with configurable limits.
- [`microsd-2fa.md`](microsd-2fa.md) Using a MicroSD card as a second factor for login.
- [`web2fa.md`](web2fa.md) Web 2FA authentication.
- [`bip85-passwords.md`](bip85-passwords.md) Deriving deterministic passwords via BIP-85.
- [`msg-signing.md`](msg-signing.md) COLDCARD message signing.
- [`proof-of-reserves-bip-322.md`](proof-of-reserves-bip-322.md) BIP-322 generic signed message format and proof of reserves.
- [`generic-wallet-export.md`](generic-wallet-export.md) Generic JSON wallet export file format.
- [`bip-21-extensions.md`](bip-21-extensions.md) Coldcard's BIP-21 URI extensions, including multisig ownership address check.
- [`nfc-coldcard.md`](nfc-coldcard.md) NFC support on Coldcard Mk4 and Q.
- [`nfc-pushtx.md`](nfc-pushtx.md) NFC Push Transaction: broadcast a signed transaction via your phone.
- [`usb-batteries.md`](usb-batteries.md) Using USB battery packs with Coldcard.
- [`electrum-usage.md`](electrum-usage.md) Importing seed words into Electrum for funds usage (and other tips).
- [`bitcoin-core-usage.md`](bitcoin-core-usage.md) How to use with Bitcoin Core.
- [`bitcoin-core2of2desc.md`](bitcoin-core2of2desc.md) Airgapped 2-of-2 multisig with Bitcoin Core using descriptors.
- [`limitations.md`](limitations.md) Documented limitations, policy choices, and TODO items.
- [`paperwallet.pdf`](paperwallet.pdf) Example paper wallet template file.
- [`seed-xor.md`](seed-xor.md) More about _Seed XOR_ feature, including fully worked Seed XOR example, and useful XOR lookup chart.
- [`menu-tree.txt`](menu-tree.txt) Dump of the menu system. Incomplete, may be out of date.

View File

@ -5,13 +5,13 @@ according to [BIP-85 PWD BASE64](https://github.com/bitcoin/bips/blob/master/bip
Generated passwords can be sent as keystrokes via USB to the host computer,
effectively using Coldcard as specialized password manager.
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard Mk4
In addition to deriving up to 10,000 distinct secure passwords, the Coldcard
can also type them into a computer by emulating a USB keyboard, and simulating the
keystrokes needed to type the password.
#### Requirements
* Coldcard Mk4 with version 5.0.5 or newer
* Coldcard Mk4 or Mk5 (firmware 5.0.5 or newer), or any Q
* USB-C with data link (won't work with power only cable from Coinkite)
## Type Passwords over USB
@ -32,11 +32,13 @@ to exit. Exiting from "Type Passwords" will cause Coldcard to turn off keyboard
1. Go to Advanced/Tools -> Derive Seed B85 -> Passwords
2. Choose "Password/Index number" (BIP-85 index) and press OK to generate password.
3. Screen shows generated password, path, and entropy from which password was derived
4. A few different options are available at this point:
1. press 1 to save password backup file on MicroSD card (cleartext!)
2. press 2 to send keystrokes (this will first of all enable keyboard emulation, then send keystrokes + enter, and finally disables keyboard emulation)
3. press 3 to view password as QR code
4. press 4 to send over NFC (only appears when NFC is enabled)
4. A few different options are available at this point (on Mk; on Q the NFC and
QR buttons are used instead of (3)/(4)):
1. press (1) to save password backup file on MicroSD card (cleartext!)
2. press (2) to save to Virtual Disk (only when available)
3. press (3) to send over NFC (only appears when NFC is enabled)
4. press (4) to view password as QR code
5. press (6) to send keystrokes over USB (this enables keyboard emulation, sends keystrokes + enter, then disables keyboard emulation)
## Keyboard language settings

View File

@ -5,9 +5,12 @@ wallet systems, but we also have a file format for general purpose
exports, which we hope future wallet makers will leverage.
It contains master XPUB, XFP for that, and derived values for the top hardened
position of BIP44, BIP84 and BIP49.
position of the single-signature schemes BIP44, BIP49 and BIP84, plus the
multisig schemes BIP48 (`bip48_1` = `.../1h` P2SH-P2WSH and `bip48_2` = `.../2h` P2WSH).
When the account number is zero, a BIP45 (`m/45h`) multisig section is also included
(it is omitted for non-zero accounts, as in the example below).
The feature can be found here: _Advanced > MicroSD > Export Wallet > Generic JSON_
The feature can be found here: _Advanced/Tools > Export Wallet > Generic JSON_
Please contact us (or better yet, make a pull request), if you need something
more in this file.
@ -18,32 +21,51 @@ Here is an example, produced by the Simulator for account number 123.
```javascript
{
"chain": "XTN",
"chain": "BTC",
"xfp": "0F056943",
"xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh",
"account": 123,
"xpub": "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7",
"bip44": {
"deriv": "m/44'/1'/123'",
"first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD",
"name": "p2pkh",
"xfp": "B7908B26",
"xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo"
"xfp": "5F898064",
"deriv": "m/44h/0h/123h",
"xpub": "xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ",
"desc": "pkh([0f056943/44h/0h/123h]xpub6DStQXfAgHuLbMpCf86ruVkF4yT9pSLyWsFiqQTWY9osuinq8Dyee4W5jCjMfyku5LNkRB9oFinrY5ufn9XXEn8Vvzc2jnifKMaQCNV7RBZ/<0;1>/*)#4tl8jryn",
"first": "1GTNtzG5xX2UhdD5e3Nu7i1WPxFdjxQMJt"
},
"bip49": {
"_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52",
"deriv": "m/49'/1'/123'",
"first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB",
"name": "p2wpkh-p2sh",
"xfp": "CEE1D809",
"xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T"
"name": "p2sh-p2wpkh",
"xfp": "A748B1FC",
"deriv": "m/49h/0h/123h",
"xpub": "xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164",
"desc": "sh(wpkh([0f056943/49h/0h/123h]xpub6DDm8WzH5a9qjKkttzqSB3uGofNohU9D3n3UG8WMxkUZzJEMPTYiQRf1dvTFCQR82MjGW4LUMVuTtnW4hF17RpzCqVwhf6Z2fnJPWtjG164/<0;1>/*))#5j7t2n2u",
"_pub": "ypub6Y42SBfCEFhKacx1jMd4P8zmydXFe68hxtZh3XQFLkrT3Q3ae7iH2VK9f8QqCK53Rzr5FXw2pAG1n57dQwR8E4fohqe8F1NWwWN2uVRfBry",
"first": "3CeBRbJKCpg7BpJME2vM8ZxhCjBnhG4toy"
},
"bip84": {
"_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL",
"deriv": "m/84'/1'/123'",
"first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l",
"name": "p2wpkh",
"xfp": "78CF94E5",
"xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"
"xfp": "2C5207AA",
"deriv": "m/84h/0h/123h",
"xpub": "xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16",
"desc": "wpkh([0f056943/84h/0h/123h]xpub6CaWStGvcXqSW9BzU2vpCoP7aWjz9VfR5DS2nuYWVvKV2nug2dESg3HdFsaWHeoZaxuAhNcPB3TH2gq8MugS3JX1yGuhB4QbC2BneaYqB16/<0;1>/*)#yk84tprf",
"_pub": "zpub6rF34DckutvQCjaE8kW4cya7vT2t2jeQuSUUMhLHFw5F8zY8XwZZvAbuJHVgHU7QQF8nCKoW6NANoG4FoJWTdmtDhxJYLt3ZjUK5RqUSMdF",
"first": "bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an"
},
"bip48_1": {
"name": "p2sh-p2wsh",
"xfp": "845A3542",
"deriv": "m/48h/0h/123h/1h",
"xpub": "xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La",
"desc": "sh(wsh(sortedmulti(M,[0f056943/48h/0h/123h/1h]xpub6EkcQSTygvxVnBP2X2fM6HY5D7wv46tWbBc54ADaypuCr47vQh1GPdPAZFdx81ou5Rp4vBnzeJT5MDWDZstzijxkHfrofXRycpt1ASfg1La/0/*,...)))",
"_pub": "Ypub6kUxqLsLQa4M43jXJ3ux8SyP6t8dD5ZbpZmxkpP1jc7VXLW4RkZ76ouEPAZ1gMgiiXzrYFPfzBC8MfjYaoTxfTm1zUfdeqiTnHDX8raCfeg"
},
"bip48_2": {
"name": "p2wsh",
"xfp": "2A01C6B0",
"deriv": "m/48h/0h/123h/2h",
"xpub": "xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF",
"desc": "wsh(sortedmulti(M,[0f056943/48h/0h/123h/2h]xpub6EkcQSTygvxVneXmk3ywiS2PFhBdiPxeMxYf6RFxHCHH36NxdcN7DjUpudCppAAxs58CG6DQLjtqZNmyC3MpgVob6wpdeATjpZZ1woX92EF/0/*,...))",
"_pub": "Zpub75KE91YFZFbpup5PMS2AxgZCKRWnozdEWTEmaUKGQysSmUaKuL5WYyf2kk5UNQhhupRnddQe9GzST7crvfLoRTHTg6KtDPZiFjxBJobzcUz"
}
}
```
@ -51,7 +73,14 @@ Here is an example, produced by the Simulator for account number 123.
## Notes
1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed
to be the first (non-change) receive address for the wallet.
to be the first (non-change) receive address for the wallet. It is only present on the
single-signature sections (`bip44`, `bip49`, `bip84`); multisig sections omit it.
1a. Each section includes a `desc` field: a ready-to-import Bitcoin output descriptor
(with `#checksum`). Single-sig descriptors use the `<0;1>/*` multipath form. Multisig
sections (`bip48_1`, `bip48_2`, and `bip45` when present) emit a `sortedmulti(...)`
template with `M` and a trailing `...` as placeholders, to be completed with your
threshold and the other co-signers' keys.
2. The user may specify any value (up to 9999) for the account number, and it's meant to
segregate funds into sub-wallets. Don't assume it's zero.
@ -59,8 +88,8 @@ segregate funds into sub-wallets. Don't assume it's zero.
3. When making your PSBT files to spend these amounts, remember that the XFP of the master
(`0F056943` in this example) is the root of the subkey paths found in the file, and
you must include the full derivation path from master. So based on this example,
to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section
of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`.
to spend a UTXO on `bc1qhj6avwmp5lhpgqwm6dgxrf3v5lf67rjm99a8an`, the input section
of your PSBT would need to specify `(m=0F056943)/84'/0'/123'/0/0`.
4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies
a specific address format.

View File

@ -77,7 +77,6 @@
- derivation path for each cosigner must be known and consistent with PSBT
- XFP values (fingerprints) MUST be unique for each of the co-signers
- multisig wallet `name` can only contain printable ASCII characters `range(32, 127)`
- for taproot multisig (musig) limitations check musig.md
### BIP-67
@ -135,8 +134,7 @@ We will summarize transaction outputs as "change" back into same wallet, however
- `p2wsh-p2sh`: _redeemScript_ (which is: `0x00 + 0x20 + sha256(witnessScript)`), and
_witnessScript_ (which contains the multisig script)
- `p2wsh`: only _witnessScript_ (which contains the actual multisig script)
- `p2tr`(keypath singlesig): no _redeemScript_, no _witnessScript_ and output key MUST commit to an unspendable script path as follows `Q = P + int(hashTapTweak(bytes(P)))G`
- `p2tr`(scriptpath multisig): _taproot_merkle_root_ and _leaf_script_ more info in docs/taproot.md
# Derivation Paths

View File

@ -38,7 +38,7 @@ directly from python programs.
| Start | Size | Notes
|---------------|-----------|--------------------------
| 0x0800 0000 | 128k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
| 0x0800 0000 | 112k | Bootloader code, including reset vector. See `stm32/mk4-bootloader`
| 0x0801 c000 | 8k | Sensitive "pairing secrets" for SE1 and SE2
| 0x0801 e000 | 8k | MCU keys, consumable; 256 32-bit write-once slots.
| 0x0802 0000 | 16k | Interrupt handlers, file header (Micropython and Coldcard code)

View File

@ -247,13 +247,14 @@
Bitcoin Core
Nunchuk
Bull Bitcoin
Zeus
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
@ -279,13 +280,14 @@
Bitcoin Core
Nunchuk
Bull Bitcoin
Zeus
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
@ -360,6 +362,7 @@
Enable
User Management [MAYBE]
Paper Wallets
WIF Store
NFC Tools [IF NFC ENABLED]
Sign PSBT
Show Address
@ -630,13 +633,14 @@
Bitcoin Core
Nunchuk
Bull Bitcoin
Zeus
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
@ -661,13 +665,14 @@
Bitcoin Core
Nunchuk
Bull Bitcoin
Zeus
Blue Wallet
Electrum Wallet
Wasabi Wallet
Fully Noded
Unchained
Theya
Bitcoin Safe
Zeus
Samourai Postmix
Samourai Premix
Descriptor
@ -694,6 +699,7 @@
Coldcard Backup
Restore Seed XOR
Paper Wallets
WIF Store
NFC Tools [IF NFC ENABLED]
Sign PSBT
Show Address

View File

@ -1,27 +0,0 @@
# Miniscript
**COLDCARD<sup>&reg;</sup>** `EDGE` versions support Miniscript and MiniTapscript.
## Import/Export
* `Settings` -> `Miniscript` -> `Import from file`
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) allowed for import
* `Settings` -> `Miniscript` -> `<name>` -> `Descriptors`
* only [descriptors](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) are exported
* export extended keys to participate in miniscript:
* `Advanced/Tools` -> `Export Wallet` -> `Generic JSON`
* `Settings` -> `Multisig Wallets` -> `Export XPUB`
## Address Explorer
Same as with basic multisig. After miniscript wallet is imported,
item with `<name>` is added to `Address Explorer` menu.
## Limitations
* no duplicate keys in miniscript (at least change indexes in subderivation has to be different)
* subderivation may be omitted during the import - default `<0;1>/*` is implied
* both keys with key origin info `[xfp/p/a/t/h]xpub/<0;1>/*` & blinded keys `xpub/<2;3>/*` allowed
* use of blinded keys for co-signers requires PSBT provider to supply path from current key fingerprint
* maximum number of keys allowed in segwit v0 miniscript is 20
* check MiniTapscript limitations in `docs/taproot.md`

View File

@ -2,15 +2,16 @@
COLDCARD can sign messages send to it via USB with the help of `ckcc` utility,
sign messages provided via specially crafted file on SD card or Vdisk,
and Mk4 can also sign messages sent to COLDCARD via NFC.
and NFC-equipped models (Mk4, Mk5, and Q) can also sign messages sent to COLDCARD via NFC.
The resulting signature can be returned over SD card/Vdisk, NFC, or — on Q — as a QR code.
Signature format follows [BIP-0137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki) specification.
COLDCARD Mk3 and COLDCARD Mk4 up to version `5.1.0` used compressed P2PKH header byte for all script types.
From Mk4 `5.1.0` correct header byte is used for corresponding script type.
From version `5.1.0` correct header byte is used for corresponding script type.
### Verification
From COLDCARD Mk4 version `5.1.0` users can verify signed messages directly on the device.
From version `5.1.0` users can verify signed messages directly on the device.
If signature file is on SD card or Virtual disk `Advanced/Tools -> File Management -> Verify Sig File`. In case
signature file is detached signature of signed export (or any other file), COLDCARD can check if digest of file
specified in the message matches contents of file. This requires file signed to be available on SD card or Vdisk.
@ -21,7 +22,7 @@ Bitcoin core can only verify P2PKH.
## Signed Exports
From Mk4 version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
From version `5.1.0` most of SD card and Virtual disk exports are accompanied by detached signature file.
If exported file name is `addresses.csv` signature file name will be `addresses.sig`.
### Message construction and signature file format
@ -39,8 +40,6 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
-----END BITCOIN SIGNATURE-----
```
### What is signed
### What Is Signed
1. **Single sig address explorer exports:** Signed by the key corresponding to the first (0th) address on the exported list.
@ -60,14 +59,4 @@ IFOvGVJrm31S0j+F4dVfQ5kbRKWKcmhmXIn/Lw8iIgaCG5QNZswjrN4X673R7jTZo1kvLmiD4hlIrbuL
6. **Multisig exports:** public keys are encoded as P2PKH address for all multisg signature exports
* Multisig wallet descriptor: signed by the key corresponding to the first external address of own enrolled extended key `my_key/0/0`
* Generic XPUBs export: signed by the key corresponding to the first external address of own standard P2WSH derivation `m/48h/<coin_type>h/<account>h/2h/0/0`
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`
### What is NOT signed
Multisig exports and generic multisig xpub exports are not signed. It is not clear at this point
whether to sign these exports with some generic single signature key (i.e. `m/44'/<coin_type>'/0'/0/0`)
or with our portion (leg) of script. In both cases script type (address format) would not match as multisignature
message signing is not standardized.
1. **Multisig exports**
2. **Generic multisig exports**
* Multisig address explorer export: Signed by own key at the same derivation as first (0th) row on exported list. `my_key/<change>/<start_index>`

View File

@ -1,40 +0,0 @@
# MuSig2
**COLDCARD<sup>&reg;</sup>** `EDGE` versions support [MuSig2](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) from version `6.5.0X` & `6.5.0QX`.
COLDCARD implements all following BIPs, further restricting their scope (read more in Limitations section):
* PSBT fields [BIP-373](https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki)
* `musig()` descriptor key expression [BIP-390](https://github.com/bitcoin/bips/blob/master/bip-0390.mediawiki)
* Derivation Scheme for MuSig2 Aggregate Keys [BIP-328](https://github.com/bitcoin/bips/blob/master/bip-0328.mediawiki)
### Why MuSig2?
* higher level of **privacy** than OP_CHECKSIGADD. MuSig2 Taproot outputs are indistinguishable for a blockchain observer from regular, single-signer Taproot outputs even though they are actually controlled by multiple signers
* **on-chain footprint** of a MuSig2 Taproot output is essentially a single BIP340 public key. This is more compact and has lower verification cost than each signer providing an individual public key and signature
### Limitations:
* COLDCARD must stay powered up between 1st and 2nd round as necessary musig session data are stored in volatile memory only
* `musig()` can only be used inside `tr()` expression as key expression
* cannot be nested within another `musig()` expression
* only one own key in `musig()` expression
* `musig(KEY, KEY, ..., KEY)/<NUM;NUM;...>/*`
* all `KEY`s MUST be unique - no repeated keys
* `KEY` expression MUST be extended key (not plain pubkey)
* `KEY` expression cannot contain child derivation, only `musig()` expression can contain derivation steps
* `KEY`s are sorted prior to aggregation
* hardened derivation not allowed for `musig()` expression
* derivation must end with `*` - only ranged `musig()` expression allowed, if `musig()` derivation is omitted, `/<0;1>/*` is implied
* PSBT must contain all the data required by BIP-373
* COLDCARD strictly differentiate between 1st & 2nd MuSig2 round. If COLDCARD provides nonce, it will not attempt to sign even if it could (a.k.a enough nonces from cosigners are available).
To provide both nonce(s) & signature(s) signing needs to be preformed twice.
* keys from WIF Store cannot be used for MuSig2 signing
* `musig()` key expression is not allowed inside `multi_a` & `sortedmulti_a` fragments, use `thresh` instead
* inputs that are in different musig rounds in same PSBT are not allowed
* transaction cannot be modified after 1st musig round was initiated as that would change musig session
### Example
Following policy is example how to do threshold multisig with MuSig2 (and Taptree) even thought MuSig2 is not a native threshold scheme.
`tr(musig(@0,@1,@2),{{pk(musig(@0,@1)),pk(musig(@1,@2))},pk(musig(@0,@2))})`

View File

@ -1,10 +1,20 @@
# NFC and Coldcard Mk4
# NFC and Coldcard
(Applies to Coldcard Mk4 only)
(Applies to NFC-equipped models: Mk4, Mk5, and Q)
## Usage
Mk4 NFC antenna is centered under number `8` on the keypad. Before using NFC,
The NFC antenna location depends on the hardware:
- **Mk4**: a PCB trace loop, centered under number `8` on the keypad.
- **Mk5**: a discrete coil (`L6`) in the **top-right corner** of the device
- **Q1**: a flexible "sticker" antenna behind the display. The green LED below the
bottom-right of the display (`D12`) lights up while an NFC transfer is active —
it is the activity indicator, not the antenna.
![nfc tap sweet-spots per model](nfc-tap-locations.png)
Before using NFC,
it is important to locate the position of NFC antenna on your device and point it
correctly towards the Coldcard NFC antenna. Picture below shows an example with iPhone
that has NFC antenna located at the top right edge. The NFC smartphone antenna
@ -36,7 +46,7 @@ in general. Good interoperability is critical with radio standards.
## Lower Layers
The Coldcard Mk4 has an chip that acts as a Type 5 NFC tag. The
The Coldcard has a chip that acts as a Type 5 NFC tag. The
radio standard is called "NFC-V" or ISO-15693, and operates on a
13.56 Mhz carrier wave.
@ -58,9 +68,13 @@ unless we are actively sharing something. We disable the "energy
harvesting" features of the chip, so it will not do anything when
the Coldcard is powered-down, regardless of the NFC setting.
If the above is not enough for you, the antenna can be destroyed
by cutting the trace labeled "NFC" inside the hole for the MicroSD
card. Use the point of a sharp knife to cut and peel up the trace.
If the above is not enough for you, the antenna can be destroyed:
- **Mk4**: cut the trace labeled "NFC" inside the hole for the MicroSD card,
using the point of a sharp knife to cut and peel up the trace.
- **Mk5**: has no such trace — its antenna is the discrete coil `L6` in the
top-right corner, which would have to be physically removed instead.
- **Q1**: cut the trace labeled "NFC DATA" under the batteries.
The NFC traffic is not encrypted and is subject to eavesdropping.
While the NFC feature is active, your Coldcard can be uniquely

View File

@ -34,7 +34,7 @@ The COLDCARD needs a URL prefix. To that it appends some values:
- when RegTest is enabled, the value will be `XRT`
We provide a few default URL values to our customers, including one backend we
will operate on `colcard.com`. The URL can also be directly entered by the
will operate on `coldcard.com`. The URL can also be directly entered by the
customer. On the Q, it can be scanned from a QR code.
For COLDCARD backend, the url used is:

BIN
docs/nfc-tap-locations.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -11,11 +11,17 @@ The entrypoint makefile for repro builds.
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: submods-match code-committed
repro:
docker build -t coldcard-build - < dockerfile.build
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(MK_NUM))
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh $(VERSION_STRING) $(HW_MODEL) $(PARENT_MKFILE))
```
`$(HW_MODEL)` is the model string (e.g. `mk4`, `q1`) and `$(PARENT_MKFILE)` is the
top-level makefile being used (`MK-Makefile` or `Q1-Makefile`). The `submods-match`
and `code-committed` prerequisites ensure the submodules and working tree are clean
before a repro build.
Below are interesting sections from the docker logs that give an idea as to what is going on in build process:
```stdout
@ -61,19 +67,19 @@ Successfully installed signit-1.0
...
+ make -f MK4-Makefile setup
+ make -f MK-Makefile setup
...
+ make -f MK4-Makefile firmware-signed.bin firmware-signed.dfu production.bin dev.dfu firmware.lss firmware.elf
+ make -f MK-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 -b l-port/build-COLDCARD_MK4 -m mk4 5.0.7 -o firmware-signed.bin
...
signit sign -m 4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
signit sign -m mk4 5.0.7 -r firmware-signed.bin -k 1 -o production.bin
You don't have that key (1), so using key zero instead!
...
@ -96,7 +102,7 @@ production.bin
...
+ make -f MK4-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
+ make -f MK-Makefile 'PUBLISHED_BIN=/tmp/checkout/firmware/releases/2022-10-05T1724-v5.0.7-mk4-coldcard.dfu' check-repro
...
@ -183,7 +189,7 @@ To summarize `check-repro`:
- `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`.
- `check` (cli/signit.py: Line 176-243) 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.

View File

@ -9,56 +9,104 @@ BIP-322 specification: <https://github.com/bitcoin/bips/blob/master/bip-0322.med
COLDCARD accepts a specially crafted PSBT file to sign as BIP-322 Proof of Reserves. The PSBT
must meet all these requirements:
* COLDCARD acts as a BIP-322 PSBT signer. It validates the BIP-322 `to_sign`
transaction, shows the message from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE`, and
adds signatures to the PSBT. Finalizing and encoding the final BIP-322
signature string is the responsibility of the finalizer.
* PSBT MUST include `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE = 0x09`; the value is
the exact message shown to the user and signed by BIP-322.
* PSBT requires `PSBT_IN_BIP32_DERIVATION` for each input
* P2SH wrapped segwit addresses MUST have proper redeem script in PSBT: `PSBT_IN_REDEEM_SCRIPT`
* P2WSH segwit addresses MUST have proper witness script in PSBT: `PSBT_IN_WITNESS_SCRIPT`
* First (0th) input in `to_sign` transaction MUST have full (pre-segwit) UTXO (`PSBT_IN_NON_WITNESS_UTXO`) a.k.a `to_spend`.
* First (0th) input in `to_sign` `PSBT_IN_NON_WITNESS_UTXO` transaction (`to_spend`) is as defined
in [BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full):
* PSBT (`to_sign`) MUST have at least one input.
* First (0th) input of `to_sign` MUST spend the BIP-322 `to_spend` output.
* Input 0 MUST include one of `PSBT_IN_NON_WITNESS_UTXO` or `PSBT_IN_WITNESS_UTXO`.
* When input 0 provides `PSBT_IN_WITNESS_UTXO`, COLDCARD reconstructs the
expected `to_spend` txid from `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and the
witness UTXO scriptPubKey.
* When input 0 provides `PSBT_IN_NON_WITNESS_UTXO`, it MUST be the BIP-322
`to_spend` transaction as defined in
[BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full):
* 1 input, 1 output
* output nValue is 0
* input prevout hash is 0
* input prevout n is 0xffffffff
* input scriptSig is `OP_0 PUSH32 message_hash`
* PSBT (`to_sign`) MUST have at least one input & first input MUST be `to_spend` full txn
* PSBT (`to_sign`) MUST only have one output with null-data `OP_RETURN`
* `to_sign` transaction version MUST be 0 or 2.
* Optionally inputs can be added to `to_sign` for Proof of Reserve signing.
* PSBT MUST be version 0.
* PSBT MUST be version 0 or 2.
* Foreign inputs not allowed in POR PSBT.
The signatures created by the BIP-322 process will never be suitable
for a on-chain Bitcoin transaction that could move funds, because
of these restrictions imposed by BIP-322.
### Output
COLDCARD always returns a signed PSBT for BIP-322 message signing and Proof of
Reserves. It never returns an extracted/finalized transaction for these PSBTs.
This is true even when finalization is requested over USB, such as with
`ckcc unsigned.psbt --finalize`.
The signed PSBT is the handoff artifact for the external finalizer/verifier. It
keeps the PSBT metadata needed to verify or finalize the BIP-322 signature,
including public keys, scripts, partial signatures, and UTXO data. This matters
because the address being proven normally commits only to a hash of the public
key or script, not the public key or script itself.
### Proof of Reserves Signing Experience
After Coldcard recognizes BIP-322 PoR PSBT it asks the user to
import a human-readable message that was used to build `to_spend`
scriptSig. This message must hash exactly the `message_hash` from
the PSBT, otherwise signing is not offered.
After Coldcard recognizes a BIP-322 PSBT it reads the message from
`PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` and shows it to the user for approval.
COLDCARD verifies that the message hash matches the input 0 `to_spend`
commitment before offering to sign.
When the PSBT contains only input 0, COLDCARD labels the request as
`BIP-322 Message`, because it is message signing and does not prove ownership
of any additional reserve UTXOs. In that case it does not show transaction
input/output counts. When the PSBT contains additional inputs, COLDCARD labels
the request as `Proof of Reserves` and shows the reserve amount.
If the message contains non-ASCII characters, COLDCARD warns that some
characters may not be readable on screen.
Legacy PoR PSBTs without `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE` are rejected by
this flow.
Read more [here.](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
Example screen text:
Example screen text for a one-input BIP-322 message signing PSBT:
```text
BIP-322 Message
Message:
This is the signed message
Challenge Address:
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
Press ENTER to approve and sign message. Press (2) to explore transaction.
CANCEL to abort.
```
Example screen text for a Proof of Reserves PSBT:
```text
Proof of Reserves
Message:
POR
Amount 0.20000000 BTC
Message Hash:
11b5fe357842f5c368d2e3884d6a5ba577e3bc7cde132004f39b8c2a43a9cdec
Challenge Address:
bc1qzvjnhf7k70uxv6xvneaqxql7k09dd6nsr5wheq
Message Challenge:
00140b2537a7d6f3cc668c9e9fa0303ffb3cad6e9b81
21 inputs
1 output
21 inputs
1 output
0.00000000 BTC
- OP_RETURN -
null-data
Press ENTER to approve and sign transaction. Press (2) to explore txn
outputs. CANCEL to abort.
Press ENTER to approve and sign proof of reserves. Press (2) to explore transaction.
CANCEL to abort.
```

View File

@ -92,8 +92,8 @@ increases flexibility and resistance to known plain text attacks.
| `pin stretch` | slot 2 | HMAC | SE1 | Key stretching for PIN entry and anti-phish words
| `firmware` | slot 14 | SHA256d | SE1 | Firmware checksum, controls green/red LEDs
| `nonce/chksum` | slot 10 | data | SE1 | AES nonce and GMAC tag, protected by PIN
| `SE2 easy key` | page 15 | AES via HMAC | SE2 | Another SE2 part of AES seed key
| `SE2 hard key` | page 14 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
| `SE2 easy key` | page 14 | AES via HMAC | SE2 | Another SE2 part of AES seed key
| `SE2 hard key` | page 15 | AES via ECC | SE2 | SE2's part of AES seed key; ECC used to unlock
| `tpin key` | `tpin_key` | HMAC(key) | MCU | Key for HMAC used to encrypt trick PINs
| `trick PIN slots` | pages 0-12 | HMAC | SE2 | Protect duress wallet seeds and pins (6 spots)
| `SE2 trash` | secret B | HMAC | SE2 | Used to destroy values (only SE2 knows the value)

View File

@ -1,4 +1,4 @@
# COLDCARD Mk4/Q Security Model
# COLDCARD Mk4/Mk5/Q Security Model
## Abstract
@ -96,9 +96,10 @@ user entered the True PIN. An attacker will only have access to the
duress wallet. They won't have access to steal the main stash.
The private key can be automatically derived using BIP-85 methods,
based on account numbers 1001, 1002, or 1003. Because this is BIP-85
based and uses a 24-word seed, it behaves exactly like a normal
wallet. Defining a passphrase for the wallet is also possible.
based on account numbers 1001, 1002, or 1003 for a 24-word duress wallet
(or 2001, 2002, 2003 for a 12-word one). Because this is BIP-85
based, it behaves exactly like a normal wallet. Defining a passphrase
for the wallet is also possible.
The Mk4 also supports older COLDCARD duress wallets and their UTXOs
on the blockchain. There is an option to create compatible wallets
@ -243,7 +244,7 @@ COLDCARD's case to do so, but the option is there if needed.
## SD Card Recovery Mode
Mk4/Q bootloader is smart enough to be able to read an SD card. You
Mk4/Mk5/Q bootloader is smart enough to be able to read an SD card. You
will only be able to trigger the SD card loading code, if the
COLDCARD was powered down during the upgrade process. At that point,
the intended firmware image has been lost because it it held in

View File

@ -12,7 +12,7 @@ are not discrete and you could be compelled to produce the passphrase.
Enter [_Seed XOR_](https://seedxor.com), a plausibly deniable means
of storing secrets in two or more parts that look and behave just
like the original secret. One 12 or 24-word seed phrase becomes two or more parts
like the original secret. One 12-, 18-, or 24-word seed phrase becomes two or more parts
that are also BIP-39 compatible seeds phrases. These should be backed up in your
preferred method, metal or otherwise. These parts can be individually loaded
with honeypot funds as each one has same word length, with the last being
@ -78,10 +78,12 @@ words right the next day.
When the parts are made deterministically, we take a double-SHA256 over
a fixed string (`Batshitoshi`), your master secret, and the text
`1 of 4 parts` which changes for each part.
`0 of 4 parts` which changes for each part (the index is 0-based).
In random mode, we simply pick 32 random bytes (and then double-SHA256
them) from the Coldcard's True Random Number Generator (TRNG)..
In random mode, we simply pick random bytes (and then double-SHA256
them) from the Coldcard's True Random Number Generator (TRNG). The number
of bytes matches your secret length: 16, 24, or 32 bytes for a 12-, 18-,
or 24-word seed respectively.
This is done to make all but the one part. The final part is the
value needed to get back to your secret, so it's the XOR of the

View File

@ -1,77 +0,0 @@
# Taproot
**COLDCARD<sup>&reg;</sup>** Mk4 experimental `EDGE` versions
support Schnorr signatures ([BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)),
Taproot ([BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki))
and Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support.
## Output script (a.k.a address) generation
If the spending conditions do not require a script path, the output key MUST commit to an unspendable script path.
`Q = P + int(hashTapTweak(bytes(P)))G` a.k.a internal key MUST be tweaked by `TapTweak` tagged hash of itself. If
the spending conditions require script path, internal key MUST be tweaked by `TapTweak` tagged hash of tree merkle root.
Addresses in `Address Explorer` for `p2tr` are generated with above-mentioned methods. Outputs `scriptPubkeys` in PSBT
MUST be generated with above-mentoned methods to be considered change.
## Allowed descriptors
1. Single signature wallet without script path: `tr(key)`
2. Tapscript multisig with internal key and up to 8 leaf scripts:
* `tr(internal_key, sortedmulti_a(2,@0,@1))`
* `tr(internal_key, pk(@0))`
* `tr(internal_key, {sortedmulti_a(2,@0,@1),pk(@2)})`
* `tr(internal_key, {or_d(pk(@0),and_v(v:pkh(@1),older(1000))),pk(@2)})`
## Provably unspendable internal key
There are 2 methods to provide provably unspendable internal key, if users wish to only use tapscript script path.
1. **(recommended)** Origin-less extended key serialization with H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs) as BIP-32 key and random chaincode.
`tr(xpub/<0:1>/*, sortedmulti_a(2,@0,@1))` which is the same thing as `tr(xpub, sortedmulti_a(2,@0,@1))` because `/<0;1>/*` is implied if not derivation path not provided.
### Below option was deprecated in version 6.3.5X & 6.3.5QX
2. Use `unspend(` [notation](https://gist.github.com/sipa/06c5c844df155d4e5044c2c8cac9c05e#unspendable-keys). Has to be ranged.
`tr(unspend(77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76)/<0:1>/*, sortedmulti_a(2,@0,@1))`
### Below option were deprecated in version 6.3.5X & 6.3.5QX
3. use **static** provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs).
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`
4. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable.
`tr(r=@, sortedmulti_a(MofN))`
5. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created.
`tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(2,@0,@1))`
Option 3. leaks the information that key path spending is not possible and therefore is not recommended privacy-wise.
Options 4. and 5. are problematic to some extent as internal key is static. Use recommended options 1. and 2. if the fact that internal key is unspendable should remain private.
## Limitations
### Tapscript Limitations
In current version only `TREE` of max depth 4 is allowed (max 8 leaf script allowed).
Taproot single leaf multisig has artificial limit of max 32 signers (M=N=32).
Number of keys in whole taptree is limited to 32.
If Coldcard can sign by both key path and script path - key path has precedence.
### PSBT Requirements
PSBT provider MUST provide following Taproot specific input fields in PSBT:
1. `PSBT_IN_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
2. `PSBT_IN_TAP_INTERNAL_KEY` MUST match internal key provided in `PSBT_IN_TAP_BIP32_DERIVATION`
3. `PSBT_IN_TAP_MERKLE_ROOT` MUST be empty if there is no script path. Otherwise it MUST match what Coldcard can calculate from registered descriptor.
4. `PSBT_IN_TAP_LEAF_SCRIPT` MUST be specified if there is a script path.
PSBT provider MUST provide following Taproot specific output fields in PSBT:
1. `PSBT_OUT_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes.
2. `PSBT_OUT_TAP_INTERNAL_KEY` must match internal key provided in `PSBT_OUT_TAP_BIP32_DERIVATION`
3. `PSBT_OUT_TAP_TREE` with depth, leaf version and script defined.

View File

@ -1,7 +1,7 @@
# Temporary Seeds
[_(new in v5.0.7, requires Mk4)_](upgrade.md)
[_(new in v5.0.7, requires Mk4, Mk5, or Q)_](upgrade.md)
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
@ -42,7 +42,7 @@ Read more about `Seed Vault` feature below.
- `24 words`
- `XPRV (BIP-32)`
- pick derivation `Index` in next prompt, or just press OK for index 0
- Press (2) in next prompt to activate derived secret as a temporary seed
- Press (0) in next prompt to activate derived secret as a temporary seed
* temporary seed can be activated from Duress Wallet
- go to `Settings -> Login Settings -> Trick Pins`
@ -66,7 +66,7 @@ Ability to generate and use **Temporary seed** is available on Coldcard when:
# Restore Master
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
From version `5.2.0` users no longer need to reboot COLDCARD to return
to their "master seed" (one stored in SE2). Once COLDCARD has temporary
@ -84,7 +84,7 @@ Seed Vault entries can only be deleted in Seed Vault menu.
# Seed Vault
[_(new in v5.2.0, requires Mk4)_](upgrade.md)
[_(new in v5.2.0, requires Mk4, Mk5, or Q)_](upgrade.md)
Seed Vault adds the ability to store multiple temporary secrets into encrypted settings for simple
recall and later use (AES-256-CTR encrypted with your master seed's key).

View File

@ -1,7 +1,7 @@
# Firmware Upgrade and Recovery Process
_This document applies only to the Mk4. Earlier COLDCARDs did not use this approach._
_This document applies to the Mk4, Mk5, and Q. Earlier COLDCARDs did not use this approach._
On the COLDCARD, we have done away with the slow external SPI flash
(serial flash) chip entirely (used in Mk1-Mk3). In it's place we

View File

@ -30,8 +30,8 @@ the correct code.
- Usual 2fa base32 secret is picked by CC and stored in CC (so that server is stateless)
- CC creates URL encrypted to the pubkey of server, containing args:
- shared secret for TOTP (same value as held in user's phone)
- the response nonce (16 bytes, or 8 digits for Mk4) to be revealed to the user
on successful auth
- the response nonce (32 bytes, shown as 64 hex chars, on Q; or 8 digits on Mk4)
to be revealed to the user on successful auth
- flag if Q model, so can provide a QR to be scanned in that case (rather than digits)
- some text label for what's being approved, which is presented to user so they can pick
correct 2fa shared secret.
@ -82,12 +82,15 @@ the correct code.
## URL Format
https://coldcard.com/2fa?ss={shared_secret}&q={is_q}&g={nonce}&nm={label_text}
https://coldcard.com/2fa?g={nonce}&ss={shared_secret}&nm={label_text}&q={is_q}
(the query string is then encrypted to the server's pubkey, so the args above
are what is inside the encrypted payload.)
- `nonce`: text string that is either 8 digits on Mk4, or 64 hex chars on Q
- `shared_secret`: 16 chars of Base32-encoded pre-shared secret
- `is_q`: flag indicating use of QR to provide nonce back to user
- `nonce`: text string that is either 8 digits for Mk4, or hex digits for QR
- `nm`: human readable label for the transaction/purpose
- `is_q`: flag indicating use of QR to provide nonce back to user
Server will accept plaintext arguments as above, but normally everything
after the question mark is encrypted.

@ -1 +1 @@
Subproject commit 6d9f7193b336ab1097c7f941ce8c7e2ae80bfe29
Subproject commit 3d1dfa858beb58b8dac37d8c66d7aed2909812f2

2
external/libngu vendored

@ -1 +1 @@
Subproject commit b0ce9acffa455d9630c64d3614d0fb9b913c919e
Subproject commit 537519a829259622ea6b0334fbafd6cae852852f

View File

@ -1,37 +0,0 @@
# Change Log
## Warning: Edge Version
```diff
- This preview version of firmware has not yet been qualified
- and tested to the same standard as normal Coinkite products.
- It is recommended only for developers and early adopters
- for experimental use.
```
This lists the changes in the most recent EDGE firmware, for each hardware platform.
# Shared Improvements - Both Mk4 and Q
- New Feature: Ability to sign MuSig2 UTXOs. Read more [here](https://github.com/Coldcard/firmware/blob/new_edge/docs/musig.md)
- New Feature: BIP-322 Proof of Reserves for Miniscript & MuSig2 UTXOs
- Bugfix: PSBT global XPUBs validation when signing with specific wallet
- Bugfix: Do not allow sighash DEFAULT outside taproot context
# Mk4 Specific Changes
## 6.5.0X - 2026-03-24
- synced with master up to `5.5.0`
# Q Specific Changes
## 6.5.0QX - 2026-03-24
- synced with master up to `1.4.0Q`
# Release History
- [`History-Edge.md`](History-Edge.md)

View File

@ -1,161 +0,0 @@
## Warning: Edge Version
```diff
- This preview version of firmware has not yet been qualified
- and tested to the same standard as normal Coinkite products.
- It is recommended only for developers and early adopters
- for experimental use. DO NOT use for large Bitcoin amounts.
```
## 6.4.1X & 6.4.1QX
-Bugfix: Multisig migration only worked for K of K multisig wallets (those where M is the same as N)
# Shared Improvements - Both Mk4 and Q
### WARNING: 6.4.0X is not backwards-compatible with previous EDGE firmware versions.
#### 6.4.0X stores multisig wallet internally as Miniscript wallets. Newly created multisig wallets won't be visible if you downgrade after creating them on 6.4.0X. Existing multisig wallets will be converted into Miniscript, yet preserved in old format if downgrade is desired.
- New Feature: Key Teleport
- New Feature: Spending Policy for Miniscript Wallets
- New Feature: Internal descriptor cache speeding up sequential operation with miniscript wallets.
To take full advantage of the feature work with miniscript wallets sequentially. First, do all operations
needed with `wallet1` before changing to `wallet2`.
- New Feature: Add ability to import/export [BIP-388](https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki) Wallet Policies.
BIP-388 policies are now also used as our wallet serialization format, which optimized setting storage.
- New Feature: Sign with specific miniscript wallet. `Settings -> Multisig/Miniscript -> <name> -> Sign PSBT`
- New Feature: Miniscript wallet name can be specified for `sign` USB command
- New Feature: Rename Miniscript wallet via UX. `Settings -> Multisig/Miniscript -> <wallet> -> Rename`.
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
Navigate to `Advanced/Tools -> Export Wallet -> Key Expression`
- Enhancement: Slightly faster HW accelerated tagged hash
- Enhancement: PSBT class optimizations. Ability to sign bigger txn.
- Enhancement: Signing TXN UI shows Miniscript wallet name.
- Change: Deprecation of legacy mulitsig import format. Ability to import/export in this format was removed.
Old functionality - renaming by reimporting descriptor with different name was removed.
Use descriptors or BIP-388 wallet policies
- Change: Deprecated `p2sh` USB command. Use `miniscript` USB commands to handle multisig wallets.
- Change: Descriptor template was remove from Generic JSON export, and `key_exp` was added
with BIP-380 extended key expression `[xfp/origin_path]xpub`.
- Bugfix: Disjoint derivation in miniscript wallets
- Bugfix: Disallow P2SH legacy miniscript
- Bugfix: Do not allow to import miniscripts with relative lock without consensus meaning.
Only allow to import block-based in range `older(1 - 65535)` & time-based in range `older(4194305 - 4259839)`
# Mk4 Specific Changes
## 6.4.0X - 2025-11-20
- synced with master up to `5.4.5`
- Enhancement: Show QR of XOR-split seeds
# Q Specific Changes
## 6.4.0QX - 2025-11-20
- synced with master up to `1.3.5Q`
# 6.3.5X & 6.3.5QX Shared Improvements - Both Mk4 and Q
Change: Allow origin-less extended keys in multisig & miniscript descriptors
Change: Static internal keys disallowed - all keys need to be ranged extended keys
# Mk4 Specific Changes
- all updates from `5.4.1`
# Q Specific Changes
- all updates from version `1.3.1Q`
# 6.3.4X & 6.3.4QX Shared Improvements - Both Mk4 and Q
- Bugfix: Complex miniscript wallets with keys in policy that are not in strictly ascending order were incorrectly filled
upon load from settings. All users on versions `6.2.2X`+ needs to update.
- Bugfix: Single key miniscript descriptor support
- Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed.
- Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed.
- Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode
- Bugfix: Bless Firmware causes hanging progress bar
- Bugfix: Prevent yikes in ownership search
- Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault
# Mk4 Specific Changes
- all updates from `5.4.0`
- Enhancement: Export single sig descriptor with simple QR
# Q Specific Changes
- all updates from version `1.3.0Q`
- Bugfix: Properly re-draw status bar after Restore Master on COLDCARD without master seed.
## 6.3.3X & 6.3.3QX Shared Improvements - Both Mk4 and Q (2024-07-04)
- New Feature: Ranged provably unspendable keys and `unspend(` support for Taproot descriptors
- New Feature: Address ownership for miniscript and tapscript wallets
- Enhancement: Address explorer simplified UI for tapscript addresses
- Bugfix: Constant `AFC_BECH32M` incorrectly set `AFC_WRAPPED` and `AFC_BECH32`.
- Bugfix: Trying to set custom URL for NFC push transaction caused yikes
### Mk4 Specific Changes
- Bugfix: Fix yikes displaying BIP-85 WIF when both NFC and VDisk are OFF
- Bugfix: Fix inability to export change addresses when both NFC and Vdisk id OFF
- Bugfix: In BIP-39 words menu, show space character rather than Nokia-style placeholder
which could be confused for an underscore.
### Q Specific Changes
- Enhancement: Miniscript and (BB)Qr codes
- Bugfix: Properly clear LCD screen after simple QR code is shown
## 6.2.2X - 2024-01-18
- New Feature: Miniscript [USB interface](https://github.com/Coldcard/ckcc-protocol/blob/master/README.md#miniscript)
- New Feature: Named miniscript imports. Wrap descriptor in json
`{"name:"n0", "desc":"<descriptor>"}` with `name` key to use this name instead of the
filename. Mostly usefull for USB and NFC imports that have no file, in which case name
was created from descriptor checksum.
- Enhancement: Allow keys with same origin, differentiated only by change index derivation
in miniscript descriptor.
- Enhancement: HSM `wallet` rule enabled for miniscript
- Enhancement: Add `msas` in to the `share_addrs` HSM [rule](https://coldcard.com/docs/hsm/rules/)
to be able to check miniscript addresses in HSM mode.
- Enhancement: HW Accelerated AES CTR for BSMS and passphrase saver
- Bugfix: Do not allow to import duplicate miniscript
wallets (thanks to [AnchorWatch](https://www.anchorwatch.com/))
- Bugfix: Saving passphrase on SD Card caused a freeze that required reboot
## 6.2.1X - 2023-10-26
- New Feature: Enroll Miniscript wallet via USB (requires ckcc `v1.4.0`)
- New Feature: Temporary Seed from COLDCARD encrypted backup
- Enhancement: Add current temporary seed to Seed Vault from within Seed Vault menu.
If current active temporary seed is not saved yet, `Add current tmp` menu item is
present in Seed Vault menu.
- Reorg: `12 Words` menu option preferred on the top of the menu in all the seed menus
- Enhancement: Mainnet/Testnet separation. Only show wallets for current active chain.
- contains all the changes from the newest stable `5.2.0-mk4` firmware
## 6.1.0X - 2023-06-20
- New Feature: Miniscript and MiniTapscript support (`docs/miniscript.md`)
- Enhancement: Tapscript up to 8 leafs
- Address explorer display refined slightly (cosmetic)
## 6.0.0X - 2023-05-12
- New Feature: Taproot keyspend & Tapscript multisig `sortedmulti_a` (tree depth = 0)
- New Feature: Support BIP-0129 Bitcoin Secure Multisig Setup (BSMS).
Both Coordinator and Signer roles are supported.
- Enhancement: change Key Origin Information export format in multisig `addresses.csv` according to [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]`
- Bugfix: correct `scriptPubkey` parsing for segwit v1-v16
- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT

View File

@ -192,44 +192,6 @@
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
## 1.3.0Q - 2024-09-12
- New Feature: Opt-in support for unsorted multisig, which ignores BIP-67 policy. Use
descriptor with `multi(...)`. Disabled by default, Enable in
`Settings > Multisig Wallets > Legacy Multisig`. Recommended for existing multisig
wallets, not new ones.
- New Feature: Named multisig descriptor imports. Wrap descriptor in json:
`{"name:"ms0", "desc":"<descriptor>"}` to provide a name for the menu in `name`.
instead of the filename. Most useful for USB and NFC imports which have no filename,
(name is created from descriptor checksum in those cases).
- New Feature: XOR from Seed Vault (select other parts of the XOR from seeds in the vault).
- Enhancement: upgrade to latest
[libsecp256k1: 0.5.0](https://github.com/bitcoin-core/secp256k1/releases/tag/v0.5.0)
- Enhancement: Signature grinding optimizations. Now about 30% faster signing!
- Enhancement: Improve side-channel protection: libsecp256k1 context randomization now happens
before each signing session.
- Enhancement: Allow JSON files in `NFC File Share`.
- Change: Do not require descriptor checksum when importing multisig wallets.
- Bugfix: Do not allow import of multisig wallet when same keys are shuffled.
- Bugfix: Do not read whole PSBT into memory when writing finalized transaction (performance).
- Bugfix: Prevent user from restoring Seed XOR when number of parts is smaller than 2.
- Bugfix: Fix display alignment of Seed Vault menu.
- Bugfix: Properly handle null data in `OP_RETURN`.
- Bugfix: Do not allow lateral scroll in Address Explorer when showing single address
from custom path.
- Change: Remove Lamp Test from Debug Options (covered by selftest).
- New Feature: Seed XOR can be imported by scanning SeedQR parts.
- New Feature: Input backup password from QR scan.
- New Feature: (BB)QR file share of arbitrary files.
- New Feature: `Create Airgapped` now works with BBQRs.
- Change: Default brightness (on battery) adjusted from 80% to 95%.
- Bugfix: Properly clear LCD screen after BBQR is shown.
- Bugfix: Writing to empty slot B caused broken card reader.
- Bugfix: During Seed XOR import, display correct letter B if own seed already added to the mix.
- Bugfix: Stop re-wording UX stories using a regular expression.
- Bugfix: Fixed "easy exit" from quiz after split Seed XOR.
## 1.2.3Q - 2024-07-05
- New Feature: PushTX: once enabled with a service provider's URL, you can tap the COLDCARD

View File

@ -4,7 +4,48 @@ This lists the new changes that have not yet been published in a normal release.
# Shared Improvements - Both Mk and Q
- tbd
- Change: BIP-322 Proof of Reserves & message signing PSBT requires PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE
(read more [BIP-322 Proof of Reserves documentation](../docs/proof-of-reserves-bip-322.md) )
- Enhancement: WIF Store export watch-only descriptor
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
- Enhancement: Improve USB length validation
- Bugfix: Fixes legacy input amount spoofing by rejecting witness-utxo-only PSBT inputs when Coldcard is expected to sign a non-segwit input.
When both UTXO fields are present the full non_witness_utxo is now preferred for amount/script lookup. Thanks, @Damir
- Bugfix: Emit warning and do not calculate fee for legacy UTXOs with only witness utxo
- Bugfix: Disable Virtual Disk and NFC before activating HSM
- Bugfix: P2PK signing was broken. Now supports both compressed and uncompressed P2PK spend
- Bugfix: Custom address default menu position wrong
- Bugfix: Delta Mode Trick PIN was never restored from backup
- Bugfix: Proper error message for incorrect 7z headers
- Bugfix: Exiting nickname entry with nickname already saved deleted previous nickname
- Bugfix: "Send Password" menu item inside Notes & Passwords visibility reversed
- Bugfix: Yikes when using "Send Password" on entry with password None field
- Bugfix: Do not show "Saving..." UX after failed Notes & Passwords import
- Bugfix: Incorrect error message caused by error in Verify/Decrypt Backup
- Bugfix: NFC Verify Address raised incorrect error message
- Bugfix: Notes & Passwords bulk import JSON with BBQr encoded as text
- Bugfix: CCC key C challenge handled bad BIP-39 checksum by crashing the UX; now treated as a wrong attempt (counts toward 3-strike lockout)
- Bugfix: CCC magnitude reset from CANCEL on empty input
- Bugfix: OP_RETURN in CCC with whitelist enabled caused yikes
- Bugfix: TX Explorer crashed on foreign input with non-standard sighash
- Bugfix: Malformed JSON message-sign request crashed signing UX
- Bugfix: Reject UI-control bytes in JSON / QR text message-signing
- Bugfix: Non-standard OP_RETURN outputs shown as "null-data", hiding part of the script
- Bugfix: Over-limit CCC address-whitelist import was rejected but still modified the policy
- Bugfix: Deleting a file right after renaming it (List Files) blanked the old name, leaving the renamed file
- Bugfix: Reordered `multi(...)` multisig with same keys was misreported as name-only change. Now blocked as duplicate.
- Bugfix: Max WIF store capacity limit was ignored if saving via QR WIF visualization
- Bugfix: Force Seed XOR restore from Temporary Seed menu to remain temporary even when master seed is blank
- Bugfix: Q1 seed word entry cursor alignment for 12-word seeds and preserve visible words after failed QR scans
- Bugfix: Binary signed-transaction (.txn) failed in NFC/QR file share
- Bugfix: yikes in transaction explorer for goto index for tx with only one output
- Bugfix: Sending `signmessage` payload encoded as BBQr caused yikes
- Bugfix: CCC/SSSP NFC whitelist import caused Yikes
- Bugfix: Stricter address ownership validation rejects unrecognized payment addresses before wallet search
- Bugfix: Handle malformed NDEF records robustly. Thanks, @Damir
- Bugfix: Ignore `bkpw` if added to backup. Thanks [@dmonakhov](https://github.com/dmonakhov)
- Bugfix: Keep NFC export tag live for repeated probes
- Bugfix: Fix 1of1 multisig signing failure
# Mk Specific Changes
@ -17,6 +58,17 @@ This lists the new changes that have not yet been published in a normal release.
## 1.4.xQ - 2065-04-xx
- tbd
- New Feature: Secure Notes & Passwords UX groups
- New Feature: Apply Secure Note text, or Secure Note password as BIP-39 passphrase
- Bugfix: Teleporting a multisig PSBT file (without signing it first) sent stale data instead of the selected file
- Bugfix: Fix export UX message after teleport PSBT import & sign
- Bugfix: BIP-21 QR `amount` rendered with wrong decimal scaling on the Payment Address screen (e.g. `amount=1.1` was shown as `1.00000001 BTC`)
- Bugfix: QR scan import (Scan Any QR Code, master/temp seed via QR) now surfaces a clean error story on any parser or seed-loading failure (e.g. wordlist-valid but bad-checksum SeedQR) instead of yikesing the menu task
- Bugfix: Yikes when showing "QR too big" for a transaction output alone on an output-explorer page
- Bugfix: Yikes receiving a malformed full-backup via Key Teleport
- Bugfix: Keyboard debounce could leave a key stuck as "pressed" after release when another key was held
- Bugfix: Scanner robustness
- Avoid holding the QR scanner reset line low; reset is now only pulsed and then left deasserted.
- Recover scanner setup failures by retrying configuration and reinitializing on the next scan when needed.
- Prevent delayed scanner sleep commands from racing with a newly started scan.
- Improve scanner shutdown/recovery after scan cancel or command timeout.

View File

@ -3,52 +3,136 @@ Hash: SHA256
95eff9e044cdb6b3d00961ae72d450684d5441c6a3661ab550a3c3aa0882e754 README.md
412597a0e30684400cb61ee04650c13ef9fc3dc16fc2570bd5e33a1dc0085d7a Next-ChangeLog.md
7d9dd67289f717aeb80f13a8e283e2dcc0da3036359afd2a5774dc04a2947680 History-Q.md
72458ab9eb2872d263bf4d3f4ca0fbf0ff9c6186f08d27f13fd600cb511ed2a7 History-Q.md
d4891b509915800650a881556cca37604caab7a268afc0b1ed31021cea125891 History-Mk4.md
c8ad43b4e3f9d77777026da6d1210c6fc5cfe435bcfcd241c0f67c9392ad7b82 History-Mk3.md
6dbf9ddb58b4fcc25b084ffd57a0fd7ad56bd0934ac464735420c6c835b31a19 History-Edge.md
cb8316310a4c165b26cc68ed584775125d52abc0bf744712662e91ded4286e01 EdgeChangeLog.md
9ebab063b57ff07e5d8df20c266ac94736a6ad0e4c71ad1f1db46ec16b0c94be ChangeLog.md
b2fa9f4b9a9778b71cf4b09ad79732192fdb457214f868a3af5234094deea33f 2026-03-25T1408-v6.5.0X-mk-coldcard.dfu
400789bfd4fe4912161078b28a547ec029f1d108d109715c57919697746c6c31 2026-03-25T1408-v6.5.0X-mk-coldcard-factory.dfu
f7bed9f1d2d49a35e7c53c8208e73ceaccbee2ab3e7fcd7c020fbd4923140313 2026-03-25T1407-v6.5.0QX-q1-coldcard.dfu
a45770254c1fcfa09324cc9ff0d85c4e19559f493ec78c25d1bdf7cabc5cc65e 2026-03-25T1407-v6.5.0QX-q1-coldcard-factory.dfu
2b7b4d95cd5d606b0a32e692db7a27c1d860140c6e919e20ea6672ad6afc3088 2026-03-05T2052-v5.5.0-mk-coldcard.dfu
15f26aa0b8fe33e29e338b74acc52d5532922af56bcf486e38085de55c86b82a 2026-03-05T2052-v5.5.0-mk-coldcard-factory.dfu
b7ce3ba55ae4bb1e1ebe0090507bcada5a4439e57a19552c78da7ef103bd144c 2026-03-05T2051-v1.4.0Q-q1-coldcard.dfu
82c2ed5ee5cf75cc8af0f54839b9a1bc8e4a329174657d61861a6576fb6e31d9 2026-03-05T2051-v1.4.0Q-q1-coldcard-factory.dfu
372fa1f82e54f632574c56a695a1ed332464bf029bd733b2db2131a591d8f126 2025-11-25T1618-v6.4.1X-mk4-coldcard.dfu
580acb64157cf3e2167d3afd46e1e406d75c3532356c36b67321cd2f1a218fc8 2025-11-25T1618-v6.4.1X-mk4-coldcard-factory.dfu
1059560fb598e5e8fd6aed0164aa4cad166552bf8e47a0365e986429c9a15346 2025-11-25T1617-v6.4.1QX-q1-coldcard.dfu
dc5fcc6a633c2cca1d1d709accc556a3ae4730f1a579062745b830cd5fd07656 2025-11-25T1617-v6.4.1QX-q1-coldcard-factory.dfu
f04617b52fc0db6e95cac0dddd9ddd90754219f38b63a26d08c848e208069edb 2025-11-20T1602-v6.4.0X-mk4-coldcard.dfu
993ef645ca83988c576febfaa248c0a5044e948d3d1e4443f31d5f9fd5734fe1 2025-11-20T1602-v6.4.0X-mk4-coldcard-factory.dfu
371f13f3e1a5ef28d14933daf03820f0e51d26ffa96008dd5595da0dfac646cf 2025-11-20T1601-v6.4.0QX-q1-coldcard.dfu
f7e73850b3c3dc33b1cd0fa7a94909931c1a4bbd881a7224a71da77807976640 2025-11-20T1601-v6.4.0QX-q1-coldcard-factory.dfu
7076ae29c509d3120db0fae434c132e6abd3fb79c1a2a2f1383ab3b2acaba27c 2025-11-03T1527-v5.4.5-mk4-coldcard.dfu
00a337888ff86bf875bcfdab7a734981bce29a49f94f3df9f932924765848ab0 2025-11-03T1527-v5.4.5-mk4-coldcard-factory.dfu
ff6371545943518eb4eb00ba73b6aa3a5ac4e63459621ecec8a300c28c281b3c 2025-11-03T1525-v1.3.5Q-q1-coldcard.dfu
0ce02c8e549cb67b682d621b4a628f3fba2c56350a9ab090b9f08532f49e7afa 2025-11-03T1525-v1.3.5Q-q1-coldcard-factory.dfu
8a8c94e5f64d0bfe4914a236fb8a779f956989fe8de998133b85b23920f46283 2025-09-30T1238-v5.4.4-mk4-coldcard.dfu
0d0aba89027d5127f74b2a2b777a7c592cba12903a3c4c3ce9b0e060c09dddb7 2025-09-30T1238-v5.4.4-mk4-coldcard-factory.dfu
bc9918968b67fefe634342c77513c9c354e7821e9ff002c7e5c8c356d7507892 2025-09-30T1237-v1.3.4Q-q1-coldcard.dfu
00cb1fc2ef360aacf48ba8c9dd2167b3f5c5f1241ba1b2b17d61ea1b7bff0a45 2025-09-30T1237-v1.3.4Q-q1-coldcard-factory.dfu
be166b3bb3ec2259991db998c20c3d44e88eeaa73c2b8114f31cb14cab5e66e6 2025-05-14T1344-v5.4.3-mk4-coldcard.dfu
876932d4ea7634d268145d5bf45577c7198c9d60e8a271b5079faba4d4c91acd 2025-05-14T1344-v5.4.3-mk4-coldcard-factory.dfu
aaed0b90be5de310c8ac9f2d0cb3a7eea58923a53d349eb4b9ac8a902e5cba4e 2025-05-14T1343-v1.3.3Q-q1-coldcard.dfu
9daa2b48abdfa2303a43ee1d0ba3d0d905e7f6286018f44a0dda3c755c46039e 2025-05-14T1343-v1.3.3Q-q1-coldcard-factory.dfu
c1202ba30db68a12b882176997f08844da4ec31087ac2f507ea1d4281d2faa9a 2025-04-16T1907-v5.4.2-mk4-coldcard.dfu
a82fff91f8da35c122b09d54b01b3d3f3b8825fbc7cddee8e686fd8d57a69285 2025-04-16T1907-v5.4.2-mk4-coldcard-factory.dfu
4393c67f8dcbb8890950678f5856d2cca81042b9a447ce149fe624ddfe336a2a 2025-04-16T1906-v1.3.2Q-q1-coldcard.dfu
6f2d77f99d61cad9328cc617754aadb3b828150386def41c946d90db8cfb2277 2025-04-16T1906-v1.3.2Q-q1-coldcard-factory.dfu
495f37ce7ddaba2e9fc3f03dec582f1646f258a3d0cec5e71c04d127357b2fa3 2025-02-19T1941-v6.3.5X-mk4-coldcard.dfu
580701fb2de24362d8de6cf998d5fd42ca9ab003aff75f3c0140d915a06a6803 2025-02-19T1941-v6.3.5X-mk4-coldcard-factory.dfu
605ebb5acde19447e5c1d7c8cfd0302c89de5c5870d85f06b185ecab3437f94e 2025-02-19T1939-v6.3.5QX-q1-coldcard.dfu
245db07574a535a3f068ed9a759bf0088f0d0e1e39704a0e0727f90119833602 2025-02-19T1939-v6.3.5QX-q1-coldcard-factory.dfu
eb750a4f095eacc6133b2c8b38fe0738a22b2496a6cdf423ca865acde8c9bc4e 2025-02-13T1415-v5.4.1-mk4-coldcard.dfu
4236453fea241fe044a462a560d8b42df43e560683110306a2714a2ef561eac5 2025-02-13T1415-v5.4.1-mk4-coldcard-factory.dfu
2e1aad0a7a3ceb84db34322b54855a0c5496699e46e53606bfa443fcc992adec 2025-02-13T1413-v1.3.1Q-q1-coldcard.dfu
e43932d04bf782f7b9ba218b54f29b9cd361b83ac3aadff9722714bca1ab7ee9 2025-02-13T1413-v1.3.1Q-q1-coldcard-factory.dfu
681874256bcfca71a3908f1dd6c623804517fdba99a51ed04c73b96119650c13 2024-12-18T1413-v6.3.4X-mk4-coldcard.dfu
73f31fbcb064a6b763d50852aafcdff01d7ec72906b5cb0af6cf28328fd80a89 2024-12-18T1413-v6.3.4X-mk4-coldcard-factory.dfu
93ab7615bcedeeff123498c109e5859dae28e58885e29ed86b6f3fd6ba709cce 2024-12-18T1407-v6.3.4QX-q1-coldcard.dfu
7e284bcead1f9c2f468230a588ddf62064014682772a552d05f453d91d55b6ae 2024-12-18T1407-v6.3.4QX-q1-coldcard-factory.dfu
237cfcb3fdf9217550eae1d9ea6fc828c1c8d09470bd60c9f72f9b00a3bb2d11 2024-09-12T1734-v5.4.0-mk4-coldcard.dfu
6d1178f07d543e1777dbbdca41d872b00ca9c40e0c0c1ffb8ef96e19c51daa52 2024-09-12T1734-v5.4.0-mk4-coldcard-factory.dfu
d840fa4e83ebc7b0f961f30f68d795bed61271e2314dda4ab0eb0b8bfe7192f4 2024-09-12T1733-v1.3.0Q-q1-coldcard.dfu
4db89ecffa1376bfc68a37110c2041a29afe52b005d527ecde701131168fc19c 2024-09-12T1733-v1.3.0Q-q1-coldcard-factory.dfu
4d83715772b31643abde3b9a0bb328003f4a31d14e2fe9c1e038077a518acaea 2024-07-05T1348-v5.3.3-mk4-coldcard.dfu
020d6d5c3baa724713b2f906112bb95f7eff43c3f5a4f8f11b77d8c2e96ccc88 2024-07-05T1348-v5.3.3-mk4-coldcard-factory.dfu
54da941c8df84fcb84adcc62fdd3ee97d1fc12e2a9a648551ca614fcbacade3f 2024-07-05T1342-v1.2.3Q-q1-coldcard.dfu
7f704aa37887ed84d6a25f124e9b4a31187430d7cf6b198eb83b86af8ae4e5ea 2024-07-05T1342-v1.2.3Q-q1-coldcard-factory.dfu
ddf5ce1ef1ee2e6ba2922b333213d0cb939a2658b294c0f24c0e489de3fe7c75 2024-07-04T1501-v6.3.3X-mk4-coldcard.dfu
9a2c5ef80a6f8212caa3b455e203da3549a79b08b473113662cf80fff587566a 2024-07-04T1459-v6.3.3QX-q1-coldcard.dfu
a990cc94066486a37071c011cd85a29caed433cb4ca3f1c4dce7f715ef81dc3c 2024-06-26T1741-v5.3.2-mk4-coldcard.dfu
218d17069d05c0ec2829e5629c5216121028d15b145c31b552e2f52daa7bf172 2024-06-26T1741-v5.3.2-mk4-coldcard-factory.dfu
b87505b407b0477e2d15f71cfb20645ac55ac5b7c74493d25a2c9c97e807b2b3 2024-06-26T1739-v1.2.2Q-q1-coldcard.dfu
efff41069f3f82d4e69d08a02a565ae0d2cd55c07dbbbe4c1328e6e3b6d8faa1 2024-06-26T1739-v1.2.2Q-q1-coldcard-factory.dfu
90b1edfbe194b093258f9cda8f4add4aa3317e9ea205ff35914da7d91410fdae 2024-05-09T1529-v1.2.1Q-q1-coldcard.dfu
c7889532323f7b0c08e84589c7cc756e2c46e209b4eea031bdfef4a633a813c1 2024-05-09T1529-v1.2.1Q-q1-coldcard-factory.dfu
ef6526d37bc1a929c94dc8388f3863f6cc1582addf26495f761123f0bfb7aa30 2024-05-09T1527-v5.3.1-mk4-coldcard.dfu
98c675e98a18b2437c52e30a9867c271bbca9969771caa34299556ef3fcb1a43 2024-05-09T1527-v5.3.1-mk4-coldcard-factory.dfu
c7c79a21c206e8b0e816c86ef1b43cd6932cb767ed97291d5fbc2f0e749f95b7 2024-05-06T1812-v1.2.0Q-q1-coldcard.dfu
5c6b69948f0193b3a7bd252195136d6d9f84ab14fbc8c5349150e7d238708c6f 2024-05-06T1812-v1.2.0Q-q1-coldcard-factory.dfu
bab6818787eec45ef28b6c297e2504ffd4fa041ab19da8a3fd27543dffe876b8 2024-05-06T1811-v5.3.0-mk4-coldcard.dfu
3da458c0dabe9a17eaeb92ee959006a64a3e6838eeb31f887a18840f020ef8b9 2024-05-06T1811-v5.3.0-mk4-coldcard-factory.dfu
101f336310b9b460d717d91d2572ea9e9ef7ac3edbdaf132c7c3aa46bb89050a 2024-04-02T1416-v1.1.0Q-q1-coldcard.dfu
5d034bc6b1abec49a067a90766bdb769faf9a1b52b2c9b7e541d32484cf783fc 2024-04-02T1416-v1.1.0Q-q1-coldcard-factory.dfu
6ea843a56e87d7d811d90be6bfa4703794bbc8318d9709e88ada05740e03b12d 2024-03-14T1419-v1.0.1Q-q1-coldcard.dfu
f53c79c64f02dd1e860a8d32f9319edd279485d97f07815b2a1eb180a1305459 2024-03-14T1419-v1.0.1Q-q1-coldcard-factory.dfu
122e6d757eb5a8ce073d98a85851f376adec97856336c5a8f05b953b5c87a533 2024-03-10T1537-v1.0.0Q-q1-coldcard.dfu
ae04aaac47f07e10143c75b5c772b54739830214c8234356d003137897f3f4f4 2024-03-10T1537-v1.0.0Q-q1-coldcard-factory.dfu
6aaa9d5bf1726fe4d4a4834010d9b9b6525e8592bb97945cd08cc728fc884068 2024-03-02T1750-v0.0.8Q-q1-coldcard.dfu
a0cd556693fae5b8b03f2a498c0abb1e6d747f91a92bd8f2559a676f8707d840 2024-03-02T1750-v0.0.8Q-q1-coldcard-factory.dfu
18fe081d84a950e1fddb2151ad50917697dfc218cd68e2e359229b0bdadbff37 2024-02-26T1442-v0.0.7Q-q1-coldcard.dfu
e4f4fe89cf3743d794568fd5b32b14551966139e9199602ea10468f925fab1cf 2024-02-26T1442-v0.0.7Q-q1-coldcard-factory.dfu
2dc7a27f43958f2de9851f221183c94258ac915ae43d997b39b644e7b9daff8f 2024-02-22T1423-v0.0.6Q-q1-coldcard.dfu
1e4f4d4c04835d78fcc4857d3264034a56dccf594e307d7408d7c4cdcdb0a926 2024-02-22T1423-v0.0.6Q-q1-coldcard-factory.dfu
d51573c72d8958ea35357d4e0a36ce6aaa2d05924577efb219e2cc189be63f08 2024-02-16T1635-v0.0.5Q-q1-coldcard.dfu
55f4ef9c3ae116f50db938acfc3a4b09717965f82cf6de8cc7385f68cd66d285 2024-02-16T1635-v0.0.5Q-q1-coldcard-factory.dfu
8fd1ced0d5e0338d845f6d5ec5ab069a5143cceade02d4f17e86b7d182b489eb 2024-02-15T1843-v0.0.4Q-q1-coldcard.dfu
43fac084727b0e69bae7fc040a62854673fd585dc2435d93bf146c80762e41cf 2024-02-15T1843-v0.0.4Q-q1-coldcard-factory.dfu
3064bf7f1a039e7cd5c1a13c6aff8cc4338e52ef2177abbdca4b196955f9e434 2024-02-08T2005-v0.0.3Q-q1-coldcard.dfu
788e7a1b182f920016617411b875fa7095ae007c6a53fc476afb1c93f0eed1c9 2024-02-08T2005-v0.0.3Q-q1-coldcard-factory.dfu
a9d0b416c3cb4f122f2826283fce82bbc5fe4464817b601a3a5787b1f8aaba20 2024-01-18T1507-v6.2.2X-mk4-coldcard.dfu
cc93209e800bc05386b5613969e62c27b9acd4388e3a922686525da90a505778 2024-01-18T1507-v6.2.2X-mk4-coldcard-factory.dfu
4651fb81dc04ac07ae53535f4246ef7f32611c50853de9edaefa68f3c64e1fac 2023-12-21T1526-v5.2.2-mk4-coldcard.dfu
a49cd00808732c67b359c9f86814ddeafc63a1040823b6c1d2035a870575c9ed 2023-12-21T1526-v5.2.2-mk4-coldcard-factory.dfu
06d1048bea43c5d7c72c5e5f395a676620ce884aed0cd152627a86d922e2f3ab 2023-12-19T1444-v5.2.1-mk4-coldcard.dfu
3eb9c4b1add88a6fe412d783b8f4b895241a67e423bbacc6a13816a5216a30fe 2023-12-19T1444-v5.2.1-mk4-coldcard-factory.dfu
f4457dc44d08cbed9517e6260aa7163ecc254457276d3cdb0c2611af0f49ba9b 2023-10-26T1343-v6.2.1X-mk4-coldcard.dfu
1dcfb450f81883afe8f655239f06e238de7bae51e740cd4aa5ae6a0541772ad8 2023-10-26T1343-v6.2.1X-mk4-coldcard-factory.dfu
7fbed097d2757b21fde920f4b10f5f50d7e1aeca01ff52186dfde4883af5cace 2023-10-10T1735-v5.2.0-mk4-coldcard.dfu
4e3023676be88d6c6480c7f37de302f3a865077f9a2214de9c5a55b24afcba2c 2023-10-10T1735-v5.2.0-mk4-coldcard-factory.dfu
fd707f2f69d006c9db84ceacd2a0dde79c3cb71730750e2676af610942898717 2023-09-08T2009-v5.1.4-mk4-coldcard.dfu
d2a4a8b71b0b102971bf8a6c98968dee776a77e0a5707db862e34be5276fbc78 2023-09-08T2009-v5.1.4-mk4-coldcard-factory.dfu
c03d4e2d1115e9440d1762c95fc82ae5a31122e84ee88d6537a8e75f26f66954 2023-09-07T1501-v5.1.3-mk4-coldcard.dfu
3602f307df06b6658d7731172c2eb3f192a0bc8ee02c606e3cb97c1aa8d49af2 2023-09-07T1501-v5.1.3-mk4-coldcard-factory.dfu
f6fb19d95bd1e38535f137bed60cafbfcd52379a686e3d12f372f881d78e640e 2023-06-26T1241-v4.1.9-coldcard.dfu
489e161f686a0c631fc605054f8e7271208b16191b669174b8a58f5af28b0f4a 2023-06-20T1506-v6.1.0X-mk4-coldcard.dfu
66c83c3f95fd3d0796b1e452d2e8ed8ac6a4abead53faf5ae793eceb6f7bbdb5 2023-06-20T1506-v6.1.0X-mk4-coldcard-factory.dfu
233398cc8f6b9e894072448eb8b8a82a4f546219ce461dd821f0ed0a38b61900 2023-06-19T1627-v4.1.8-coldcard.dfu
2e8ed970f518a476d0b34752ecbad75bab246669aa65de8f43801364c6f5753e 2023-05-12T1316-v6.0.0X-mk4-coldcard.dfu
8dd5ff029bb2b08c857604f0c9b5773931f6683ee331ecbc35d9ab4c460b745f 2023-05-12T1316-v6.0.0X-mk4-coldcard-factory.dfu
7aefd5bcce533f15337e83618ebbd42925d336792c82a5ca19a430b209b30b8a 2023-04-07T1330-v5.1.2-mk4-coldcard.dfu
a6c007992139a847f0f238769023727e8cbc05c54c916b388a4dd8bc7490f0aa 2023-04-07T1330-v5.1.2-mk4-coldcard-factory.dfu
99804b440f41ea47675456b4e20e7bb4e9cb434556c5813ab83c26fcda0f4e80 2023-02-27T2105-v5.1.1-mk4-coldcard.dfu
8b37d0f2bf9ca8990f424e5a79fe62405e1ec3aca515760e509afec8f2dbacbc 2023-02-27T2105-v5.1.1-mk4-coldcard-factory.dfu
bcf4284f7733e9de8d4dba238368552b056a27308e466721be7ca624192e257f 2023-02-27T1509-v5.1.0-mk4-coldcard.dfu
cc946bcb63211e15d85db577e25ab2432d4a74d5dad77d710539e505dce7914a 2022-11-14T1854-v4.1.7-coldcard.dfu
010827a60ebfc25b8a6e2bb94cc69b938419957ac6d4a9b6c0b1357c4c6c8632 2022-10-05T1724-v5.0.7-mk4-coldcard.dfu
bc4d0b2b985aea3a78eb9351cdadf60d1ab00801ed1e7192765b94181cb8933b 2022-10-05T1517-v4.1.6-coldcard.dfu
884f373717c9c605920a1dc29e0f890bf7b3cc6b141666814e396094aeedb3f8 2022-07-29T1816-v5.0.6-mk4-coldcard.dfu
3c680195ef49cd0eb86d8e2426443511e8834bcea2d0a86ab52a35cc9365a801 2022-07-20T1508-v5.0.5-mk4-coldcard.dfu
7bd2b98186370f2d895e1e43949694f6ba61a1c021f72a63f0f86a30f338a0fc 2022-05-27T1500-v5.0.4-mk4-coldcard.dfu
5aa2ccc65e2e5279db78b3068b9f3c60c34dd7cc330c2cc1243160db31a2d0f0 2022-05-04T1258-v4.1.5-coldcard.dfu
6dbf0aca0f98fb7bdc761eeead4786617b804dad4afb42ee02febf23d31b5e9b 2022-05-04T1254-v5.0.3-mk3-coldcard.dfu
d5d9bf50892a4aab6e2ffb106a3d206853a60f879daa94a6f90d68a69bf4fa33 2022-05-04T1252-v5.0.3-mk4-coldcard.dfu
9bb028d3e60239f0fcdb3b1f91075785e2c21795789b38c4c619c1f64c2950ef 2022-04-25T1618-v4.1.4-coldcard.dfu
a363b1f0d1b27b8f21dbaac32844a59dacab8c2fee126815cda84c4df31fd7cd 2022-04-19T1805-v5.0.2-mk4-coldcard.dfu
afb6048397af4093e63567563544098e1cfb45b7ca673536253eb6494d60125c 2022-03-24T1645-v5.0.1-mk3-coldcard.dfu
605807bd448711d54e14057892a100bac299a103f5b5fb6466d73f9a36d0694b 2022-03-24T1643-v5.0.1-mk4-coldcard.dfu
badd10c078996516c6464c9bfa5f696747dd7206c97d1e6a75d6f5ee0436619a 2022-03-14T1907-v5.0.0-mk4-coldcard.dfu
dedfcf8385e35dbdbb26b92f8c0667105404062ad83c8830d809cf9193434d9c 2021-09-02T1752-v4.1.3-coldcard.dfu
d01d81305b209dadcf960b9e9d20affb8d4f11e9f9f916c5a06be29298c80dc2 2021-07-28T1347-v4.1.2-coldcard.dfu
08e1ec1fd073afbbc9014db6da07fd96c6b20a6710fe491eb805afeba865fe3f 2021-04-30T1748-v4.1.1-coldcard.dfu
2c39330bef467af8dcd7e2f393a970e1ca177b1812f830269916657ff79598eb 2021-04-29T1725-v4.1.0-coldcard.dfu
5e0c5f4ba9fa0e5fd7f9846e25c6cd28821a86ff5e1207c56cc3a4f4c3741f15 2021-04-07T1424-v4.0.2-coldcard.dfu
f05bc8dfed047c0c0abe5ed60621d2d14899b10717221c4af0942d96a1754f33 2021-03-29T1927-v4.0.1-coldcard.dfu
3097fa3c173247637aa27376036e384940adeb67ce727c9795471f46deaa5210 2021-01-14T1617-v3.2.2-coldcard.dfu
9e4aeee48d4399a761fec5d4c65cb2495ef5bc0b46995c085d63a65cf67362cb 2021-01-07T1439-v3.2.1-coldcard.dfu
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD7GQACgkQo6MbrVoq
WxCMxwf/Q4oCzsi5r/CKHXHMgxO+vZdVS21pbwKBO8myjWkvsje2DKran9djUIj0
GO+J9fMYuIRqzKocwh3R7NLRTBYl2CWvNA8X5t6HBgg8GekRonVRtYz3v5layj/I
DO13xenkUBsCi0R/pkTVK+flKx720Ph/JEf5yRclhpWPZtuj5FztEFxJ+iwK+ipV
OG1JwlMRFoNRKwC+ayp8Fz607dPrI5dSd7TTz02PcCNXkMauQYwhvzxHWF6ExLly
ddSmHdBFRxDS8PRokOvXQOQkzsF55aiv+UMt76l37FPmmbHTPCWdrHYguVm/g9Tz
roezfeiGOcyfvodyr9mjq7PUB75A9g==
=08Ws
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmnD9ncACgkQo6MbrVoq
WxD9RAf+JkP/XVUPMDyfz+79AxBWFNU9r6RuYzXdzX3Z/XCKomZCZDtV7Ak6XlZi
GTNfsUNHaPC8WP6smFzYg07NoY2U1fVdY7+qeOi7UXF0hBBDJw7Gsa49P2zmt+DB
lfzivQG2n+mT4cM64Z0WF3BYBWmCuDJdctqUAnLJe2p8bh6S8n5hFeKqndRhffNK
773amkUrDW3RkHkIuevH4MQlR4ozWBmHzcehFDlTYT8BVLR8gg6hBzBEylxyDJNO
Ld4W5SzsT6We0RGX2uOpMERDjkizqT9t5J63drzpuPrUQA8XVQPaOc07vpFHRbbZ
BhA61XO8yazNLVvata611pSTikNnDQ==
=8Ti0
-----END PGP SIGNATURE-----

View File

@ -16,7 +16,7 @@ from export import export_contents, make_summary_file, make_descriptor_wallet_ex
from export import make_bitcoin_core_wallet, generate_wasabi_wallet, generate_generic_export
from export import generate_unchained_export, generate_electrum_wallet, make_key_expression_export
from files import CardSlot, CardMissingError, needs_microsd
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from glob import settings
from pincodes import pa
from menu import start_chooser, MenuSystem, MenuItem
@ -459,21 +459,27 @@ async def pick_nickname(*a):
# Value is not stored with normal settings, it's part of "prelogin" settings
# which are encrypted with zero-key.
s = SettingsObject.prelogin()
nick = s.get('nick', '')
k = "nick"
nick = s.get(k, '')
if not nick:
ch = await ux_show_story('''\
You can give this Coldcard a nickname and it will be shown before login.''')
ch = await ux_show_story("You can give this Coldcard a nickname"
" and it will be shown before login.")
if ch != 'y': return
nn = await ux_input_text(nick, confirm_exit=False, prompt="Enter Nickname")
if nn is None or (nick == nn): return # user exit & same value - noop
from glob import dis
dis.fullscreen("Saving...")
dis.busy_bar(True)
nn = nn.strip() if nn else None
s.set('nick', nn)
if not nn:
s.remove_key(k)
else:
s.set(k, nn.strip())
s.save()
dis.busy_bar(False)
del s
@ -523,6 +529,7 @@ async def new_from_dice(menu, label, item):
async def any_active_duress_ux():
from trick_pins import tp
tp.reload()
# if TPs are hidden this msg will not be shown
if any(tp.get_duress_pins()):
await ux_show_story('You have one or more duress wallets defined '
@ -755,10 +762,6 @@ async def version_migration():
# version 5.0.6 is installed
settings.remove_key('vdsk')
# 6.4.0 multisig migration
from wallet import do_640_multisig_migration
await do_640_multisig_migration()
async def version_migration_prelogin():
# same, but for setting before login
# these have moved into SE2 for Mk4 and so can be removed
@ -825,7 +828,7 @@ async def start_login_sequence():
# PIN again) and if it's a duress wallet, that's cool...
# Do we need to do countdown delay? (real or otherwise)
# - wiping has already occured if that was selected by trick details
# - wiping has already occurred if that was selected by trick details
# - delay is variable, stored in tc_arg
delay = tp.was_countdown_pin()
@ -910,13 +913,6 @@ async def start_login_sequence():
# is early in boot process
print("XFP save failed: %s" % exc)
# Version warning before HSM is offered
if version.is_edge and not ckcc.is_simulator():
await ux_show_story("This firmware version is qualified for use with wallets (such as"
" AnchorWatch, Liana, and Nunchuk) that keep redundant key schemas for recovery"
" independent of COLDCARD. We support the very latest Bitcoin innovations"
" in the Edge Version.", title="Edge Version")
dis.draw_status(xfp=settings.get('xfp'))
# If HSM policy file is available, offer to start that,
@ -942,12 +938,14 @@ async def start_login_sequence():
settings.master_set("seedvault", False)
except: pass
if version.has_nfc and settings.get('nfc', 0):
from glob import hsm_active
if version.has_nfc and settings.get('nfc', 0) and not hsm_active:
# Maybe allow NFC now
import nfc
nfc.NFCHandler.startup()
if settings.get('vidsk', 0):
if settings.get('vidsk', 0) and not hsm_active:
# Maybe start virtual disk
import vdisk
vdisk.VirtDisk()
@ -1070,7 +1068,7 @@ async def export_xpub(label, _2, item):
path = "m"
addr_fmt = AF_CLASSIC
else:
remap = {44:0, 49:1, 84:2,86:3}[mode]
remap = {44:0, 49:1, 84:2}[mode]
_, path, addr_fmt = chains.CommonDerivations[remap]
path = path.format(account=acct, coin_type=chain.b44_cointype,
change=0, idx=0)[:-4]
@ -1081,7 +1079,7 @@ async def export_xpub(label, _2, item):
if path != "m":
esc += "1"
msg += "Press (1) to select account other than %s." % (acct or "zero")
if addr_fmt not in (AF_CLASSIC, AF_P2TR):
if addr_fmt != AF_CLASSIC:
esc += "2"
slp_af = addr_fmt
if slip132:
@ -1105,8 +1103,10 @@ async def export_xpub(label, _2, item):
if ch == "2":
slip132 = not slip132
continue
if ch == '1':
acct = await ux_enter_bip32_index('Account Number:') or 0
acct = await ux_enter_bip32_index('Account Number:')
if acct is None: continue
pth_split = path.split("/")
pth_split[-1] = ("%dh" % acct)
path = "/".join(pth_split)
@ -1148,15 +1148,16 @@ async def electrum_skeleton(a, b, item):
ch = await ux_show_story(electrum_export_story(title), escape='1')
account_num = 0
acct = 0
if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0
elif ch != 'y':
acct = await ux_enter_bip32_index('Account Number:')
if (ch not in '1y') or acct is None:
return
rv = [
MenuItem(chains.addr_fmt_label(af), f=electrum_skeleton_step2,
arg=(af, account_num, title, fname_pat))
arg=(af, acct, title, fname_pat))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
@ -1177,13 +1178,14 @@ async def ss_descriptor_skeleton(_0, _1, item):
int_ext, allowed_af, ll, f_pattern, direct_way = item.arg
addition = " for " + ll
account_num = 0
acct = 0
if not direct_way:
ch = await ux_show_story(ss_descriptor_export_story(addition), escape='1')
if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
elif ch != 'y':
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
if (ch not in '1y') or acct is None:
return
if int_ext is None:
@ -1195,12 +1197,12 @@ async def ss_descriptor_skeleton(_0, _1, item):
int_ext = False if ch == "1" else True
if len(allowed_af) == 1:
await make_descriptor_wallet_export(allowed_af[0], account_num, int_ext=int_ext,
await make_descriptor_wallet_export(allowed_af[0], acct, int_ext=int_ext,
fname_pattern=f_pattern, direct_way=direct_way)
else:
rv = [
MenuItem(chains.addr_fmt_label(af), f=descriptor_skeleton_step2,
arg=(af, account_num, int_ext, f_pattern, direct_way))
arg=(af, acct, int_ext, f_pattern, direct_way))
for af in allowed_af
]
the_ux.push(MenuSystem(rv))
@ -1214,23 +1216,22 @@ async def key_expression_skeleton_step2(_1, _2, item):
async def key_expression_skeleton(_0, _1, item):
# Export key expression -> [xfp/d/e/r]xpub
acct_num = 0
acct = 0
ch = await ux_show_story("This saves a extended key expression."
+ PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
if ch == '1':
acct_num = await ux_enter_bip32_index('Account Number:', unlimited=True) or 0
elif ch != 'y':
acct = await ux_enter_bip32_index('Account Number:', unlimited=True)
if (ch not in '1y') or acct is None:
return
# element on 2nd index is address format for signed exports
# if multisig key use p2pkh
todo = [
("Segwit P2WPKH", "m/84h/%dh/%dh", AF_P2WPKH),
("Taproot P2TR", "m/86h/%dh/%dh", AF_P2TR),
("Classic P2PKH", "m/44h/%dh/%dh", AF_CLASSIC),
("P2SH-Segwit", "m/49h/%dh/%dh", AF_P2WPKH_P2SH),
("Multi P2WSH", "m/48h/%dh/%dh/2h", AF_CLASSIC),
("Multi P2TR", "m/48h/%dh/%dh/3h", AF_CLASSIC),
("Multi P2SH-P2WSH", "m/48h/%dh/%dh/1h", AF_CLASSIC),
]
@ -1241,11 +1242,9 @@ async def key_expression_skeleton(_0, _1, item):
ct = chains.current_chain().b44_cointype
rv = [
MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct_num), af))
for label, orig_der, af in todo
]
rv += [MenuItem("Custom Path", menu=doit)]
rv = [ MenuItem(label, f=key_expression_skeleton_step2, arg=(orig_der % (ct, acct), af))
for label, orig_der, af in todo ]
rv += [ MenuItem("Custom Path", menu=doit) ]
the_ux.push(MenuSystem(rv))
@ -1293,14 +1292,15 @@ You can then run the commands in Bitcoin Core's console window, \
without ever connecting this Coldcard to a computer.\
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape='1')
account_num = 0
acct = 0
if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0
elif ch != 'y':
acct = await ux_enter_bip32_index('Account Number:')
if (ch not in '1y') or acct is None:
return
# no choices to be made, just do it.
await make_bitcoin_core_wallet(account_num)
await make_bitcoin_core_wallet(acct)
async def electrum_skeleton_step2(_1, _2, item):
@ -1314,13 +1314,14 @@ async def _generic_export(prompt, label, f_pattern):
# like the Multisig export, make a single JSON file with
# basically all useful XPUB's in it.
ch = await ux_show_story(prompt + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
account_num = 0
acct = 0
if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0
elif ch != 'y':
acct = await ux_enter_bip32_index('Account Number:')
if (ch not in '1y') or acct is None:
return
await export_contents(label, lambda: generate_generic_export(account_num),
await export_contents(label, lambda: generate_generic_export(acct),
f_pattern, is_json=True)
async def generic_skeleton(*A):
@ -1365,16 +1366,17 @@ async def unchained_capital_export(*a):
ch = await ux_show_story('''\
This saves multisig XPUB information required to setup on the Unchained platform. \
''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET, escape="1")
account_num = 0
acct = 0
if ch == '1':
account_num = await ux_enter_bip32_index('Account Number:') or 0
elif ch != 'y':
acct = await ux_enter_bip32_index('Account Number:')
if (ch not in '1y') or acct is None:
return
xfp = xfp2str(settings.get('xfp', 0))
fname = 'unchained-%s.json' % xfp
await export_contents('Unchained', lambda: generate_unchained_export(account_num),
await export_contents('Unchained', lambda: generate_unchained_export(acct),
fname, is_json=True)
@ -1404,6 +1406,10 @@ async def import_extended_key_as_secret(extended_key, ephemeral, origin=None):
await seed.set_ephemeral_seed_extended_key(extended_key, origin=origin)
else:
await seed.set_seed_extended_key(extended_key)
except ValueError:
msg = ("Sorry, wasn't able to find a valid extended private key to import. "
"It should be at the start of a line, and probably starts with 'xprv'.")
await ux_show_story(title="FAILED", msg=msg)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
@ -1619,7 +1625,7 @@ async def qr_share_file(_1, _2, item):
# it's a txn, and we wrote as hex
data = data.decode()
else:
assert data[2:8] == bytes(6)
assert data[1:4] == bytes(3)
data = b2a_hex(data).decode()
elif data[0:5] == b'psbt\xff':
tc = "P"
@ -1774,6 +1780,7 @@ async def list_files(*A):
assert s not in new_basename, "illegal char"
uos.rename(path + "/" + basename, path + "/" + new_basename)
basename = new_basename
fn = path + "/" + basename # keep full path in sync (delete/sign use it)
except Exception as e:
await ux_show_story("Failed to rename the file. " + str(e),
title="Failure")
@ -1972,22 +1979,20 @@ async def batch_sign(_1, _2, item):
import sys
await ux_show_story("FAILURE: batch sign failed\n\n" + problem_file_line(e))
async def _ready2sign(intro="", probe=True, miniscript_wallet=None):
# - if probe=True -> check if any signable in SD card (A slot on Q), if so do it
# - if probe=False -> offer all enabled import options via UX
async def ready2sign(*a):
# Top menu choice of top menu! Signing!
# - check if any signable in SD card, if so do it
# - if no card, check virtual disk for PSBT
# - if still nothing, then talk about USB connection
from pincodes import pa
from glob import NFC
opt = {}
choices = []
sb_only = False
if probe:
# just check if we have candidates, no UI
sb_only = True
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt)
# just check if we have candidates, no UI
choices = await file_picker(suffix='.psbt', min_size=50, ux=False,
max_size=MAX_TXN_LEN, taster=is_psbt)
if pa.tmp_value:
title = '[%s]' % xfp2str(settings.get('xfp'))
@ -1995,13 +2000,21 @@ async def _ready2sign(intro="", probe=True, miniscript_wallet=None):
title = None
if not choices:
msg = '''Coldcard is ready to sign spending transactions!
Put the proposed transaction onto MicroSD card \
in PSBT format (Partially Signed Bitcoin Transaction) \
or upload a transaction to be signed \
from your desktop wallet software or command line tools.'''
footnotes = ("You will always be prompted to confirm the details "
"before any signature is performed.")
# if we have only one SD card inserted, at this point, we know no PSBTs on them
# as above file_picker already checked
# if we have both inserted, A was already checked - so only care about B
footnotes = ("You will always be prompted to confirm the details "
"before any signature is performed.")
picked = await import_export_prompt("PSBT", is_import=True, intro=intro,
footnotes=footnotes, slot_b_only=sb_only,
picked = await import_export_prompt("PSBT", is_import=True, intro=msg,
footnotes=footnotes, slot_b_only=True,
title=title)
if isinstance(picked, dict):
opt = picked # reset options to what was chosen by user
@ -2014,9 +2027,9 @@ async def _ready2sign(intro="", probe=True, miniscript_wallet=None):
return
else:
if NFC and picked == KEY_NFC:
await NFC.start_psbt_rx(miniscript_wallet)
await NFC.start_psbt_rx()
if picked == KEY_QR:
await _scan_any_qr(miniscript_wallet=miniscript_wallet)
await _scan_any_qr()
return
@ -2032,23 +2045,10 @@ async def _ready2sign(intro="", probe=True, miniscript_wallet=None):
# start the process
from auth import sign_psbt_file
opt["miniscript_wallet"] = miniscript_wallet
await sign_psbt_file(input_psbt, **opt)
async def ready2sign(*a):
# Top menu choice of top menu! Signing!
# - check if any signable in SD card, if so do it
# - if no card, check virtual disk for PSBT
await _ready2sign('''Coldcard is ready to sign spending transactions!
Put the proposed transaction onto MicroSD card \
in PSBT format (Partially Signed Bitcoin Transaction) \
or upload a transaction to be signed \
from your desktop wallet software or command line tools.''')
async def sign_message_on_sd(*a):
# Menu item: choose a file to be signed (as a short text message)
#
@ -2313,7 +2313,7 @@ async def wipe_address_cache(*a):
async def wipe_ovc(*a):
ok = await ux_confirm('''Clear history of segwit UTXO input values we have seen already. \
This data protects you against specific attacks. Use this only if certain a false-positive \
has occured in the detection logic.''')
has occurred in the detection logic.''')
if not ok: return
import history
@ -2444,11 +2444,14 @@ async def scan_any_qr(menu, label, item):
expect_secret, tmp = item.arg
await _scan_any_qr(expect_secret, tmp)
async def _scan_any_qr(expect_secret=False, tmp=False, miniscript_wallet=None):
async def _scan_any_qr(expect_secret=False, tmp=False):
from ux_q1 import QRScannerInteraction
x = QRScannerInteraction()
await x.scan_anything(expect_secret=expect_secret, tmp=tmp,
miniscript_wallet=miniscript_wallet)
try:
await x.scan_anything(expect_secret=expect_secret, tmp=tmp)
except Exception as e:
await ux_show_story(msg="Failed to import from QR.\n\n%s\n%s" % (e, problem_file_line(e)),
title="ERROR")
PUSHTX_SUPPLIERS = [

View File

@ -7,17 +7,27 @@
import chains, stash, version
from ux import ux_show_story, the_ux, ux_enter_bip32_index
from ux import export_prompt_builder, import_export_prompt_decode
from menu import MenuSystem, MenuItem, ToggleMenuItem
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_P2TR, AF_CLASSIC
from wallet import MiniScriptWallet
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_CLASSIC
from multisig import MultisigWallet
from uasyncio import sleep_ms
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from glob import settings
from msgsign import write_sig_file
from charcodes import KEY_QR, KEY_NFC, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_HOME, KEY_LEFT, KEY_RIGHT
from charcodes import KEY_CANCEL
from utils import show_single_address, problem_file_line, truncate_address
def censor_address(addr):
# We don't like to show the user full multisig addresses because we cannot be certain
# they could actually be signed. And yet, don't blank too many
# spots or else an attacker could grind out a suitable replacement.
# 3 chars in the middle hidden by default
# censoring can be disabled by msas setting
if settings.get("msas", 0):
return addr
return addr[0:12] + '___' + addr[12+3:]
class KeypathMenu(MenuSystem):
def __init__(self, path=None, nl=0, ranged=True, done_fn=None):
@ -32,7 +42,6 @@ class KeypathMenu(MenuSystem):
MenuItem("m/44h/⋯", f=self.deeper),
MenuItem("m/49h/⋯", f=self.deeper),
MenuItem("m/84h/⋯", f=self.deeper),
MenuItem("m/86h/⋯", f=self.deeper),
MenuItem("m", f=self.done),
]
if self.ranged:
@ -65,7 +74,7 @@ class KeypathMenu(MenuSystem):
pl = p[0:p.rfind('/')].rfind('/')
else:
self.prefix = p # displayed on mk4 only
pl = len(p)-2
pl = len(p)-2
for mi in items:
mi.arg = mi.label
mi.label = ''+mi.label[pl:]
@ -106,7 +115,7 @@ class KeypathMenu(MenuSystem):
val = item.arg or item.label
assert val.endswith('/⋯')
cpath = val[:-2]
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True)
nl = await ux_enter_bip32_index('%s/' % cpath, unlimited=True, can_cancel=False)
return KeypathMenu(cpath, nl, ranged=self.ranged, done_fn=self.done_fn)
class PickAddrFmtMenu(MenuSystem):
@ -117,9 +126,10 @@ class PickAddrFmtMenu(MenuSystem):
for af in chains.SINGLESIG_AF
]
super().__init__(items)
if path.startswith("m/84h"):
# below is sensitive to order in chains.SINGLESIG_AF
if path.startswith("m/44h"):
self.goto_idx(1)
if path.startswith("m/49h"):
elif path.startswith("m/49h"):
self.goto_idx(2)
async def done(self, _1, _2, item):
@ -209,15 +219,10 @@ class AddressListMenu(MenuSystem):
items.append(MenuItem("Account Number", f=self.change_account))
items.append(MenuItem("Custom Path", menu=self.make_custom))
# if they have miniscript wallets, add those next
if MiniScriptWallet.exists():
items.append(ToggleMenuItem('MS Scripts/Derivs', 'aemscsv',
['Default Off', 'Enable'], story=(
"Enable this option to add script(s) and derivations to the CSV export"
" of Multisig/Miniscript wallets. Default is to only export addresses.")))
for msc in MiniScriptWallet.iter_wallets():
items.append(MenuItem(msc.name, f=self.pick_miniscript, arg=msc))
# if they have MS wallets, add those next
for ms in MultisigWallet.iter_wallets():
if not ms.addr_fmt: continue
items.append(MenuItem(ms.name, f=self.pick_multisig, arg=ms))
else:
items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))
@ -237,11 +242,15 @@ class AddressListMenu(MenuSystem):
self.goto_idx(axi)
async def change_account(self, *a):
self.account_num = await ux_enter_bip32_index('Account Number:') or 0
acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
self.account_num = acct
await self.render()
async def change_start_idx(self, *a):
self.start = await ux_enter_bip32_index("Start index:", unlimited=True)
idx = await ux_enter_bip32_index("Start index:", unlimited=True)
if idx is None: return
self.start = idx
await self.render()
async def pick_single(self, _1, _2, item):
@ -249,10 +258,10 @@ class AddressListMenu(MenuSystem):
settings.put('axi', axi) # update last clicked address
await self.show_n_addresses(path, addr_fmt, None)
async def pick_miniscript(self, _1, _2, item):
msc_wallet = item.arg
settings.put('axi', item.label) # update last clicked address
await self.show_n_addresses(None, msc_wallet.addr_fmt, msc_wallet)
async def pick_multisig(self, _1, _2, item):
ms_wallet = item.arg
settings.put('axi', item.label) # update last clicked address
await self.show_n_addresses(None, None, ms_wallet)
async def make_custom(self, *a):
# picking a custom derivation path: makes a tree of menus, with chance
@ -278,13 +287,14 @@ Press (3) if you really understand and accept these risks.
async def show_n_addresses(self, path, addr_fmt, ms_wallet, start=0, n=10, allow_change=True):
# Displays n addresses by replacing {idx} in path format.
# - also for other {account} numbers
# - or miniscript case
# - or multisig case
from glob import dis, NFC
from wallet import MAX_BIP32_IDX
start = self.start
allow_qr = (not ms_wallet) or settings.get("msas", 0)
def make_msg(change=0, start=start, n=n):
def make_msg(change=0):
# Build message and CTA about export, plus the actual addresses.
if n:
msg = "Addresses %d%d:\n\n" % (start, min(start + n - 1, MAX_BIP32_IDX))
@ -297,7 +307,23 @@ Press (3) if you really understand and accept these risks.
dis.fullscreen('Wait...')
if ms_wallet:
msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change)
# IMPORTANT safety feature: do not show complete address unless user opt-in
# but show enough they can verify addrs shown elsewhere.
# - makes a redeem script
# - converts into addr
# - assumes 0/0 is first address.
for idx, addr, paths, script in ms_wallet.yield_addresses(start, n, change):
addr = censor_address(addr)
addrs.append(addr)
if idx == 0 and ms_wallet.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '⋯/%d/%d =>\n' % (change, idx)
msg += show_single_address(addr) + '\n\n'
dis.progress_sofar(idx-start+1, n)
else:
# single-signer wallets
from wallet import MasterSingleSigWallet
@ -315,10 +341,11 @@ Press (3) if you really understand and accept these risks.
k0 = 'to show change addresses' if allow_change and change == 0 else None
export_msg, escape = export_prompt_builder(
'address summary file',
no_qr=not allow_qr,
key0=k0, force_prompt=True
)
if version.has_qwerty:
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN+KEY_QR
escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN
else:
escape += "79"
@ -330,14 +357,13 @@ Press (3) if you really understand and accept these risks.
if n:
msg += "Press RIGHT to see next group, LEFT to go back. X to quit."
else:
if addr_fmt != AF_P2TR:
escape += "0"
msg += " Press (0) to sign message with this key."
escape += "0"
msg += " Press (0) to sign message with this key."
return msg, addrs, escape
msg, addrs, escape = make_msg()
change = 0
msg, addrs, escape = make_msg(change, start)
while 1:
ch = await ux_show_story(msg, escape=escape)
@ -359,9 +385,10 @@ Press (3) if you really understand and accept these risks.
elif choice == KEY_QR:
from ux import show_qr_codes
addr_fmt = addr_fmt or ms_wallet.addr_fmt
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
if allow_qr:
addr_fmt = addr_fmt or ms_wallet.addr_fmt
is_alnum = bool(addr_fmt & (AFC_BECH32 | AFC_BECH32M))
await show_qr_codes(addrs, is_alnum, start, is_addrs=True)
continue
@ -405,7 +432,7 @@ Press (3) if you really understand and accept these risks.
else:
continue # 3 in non-NFC mode
msg, addrs, escape = make_msg(change, start)
msg, addrs, escape = make_msg(change)
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, change=0):
# Produce CSV file contents as a generator
@ -413,11 +440,26 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
from ownership import OWNERSHIP
if ms_wallet:
# For multisig, include redeem script and derivation for each signer
yield '"' + '","'.join(['Index', 'Payment Address', 'Redeem Script']
+ ['Derivation (%d of %d)' % (i+1, ms_wallet.N) for i in range(ms_wallet.N)]
) + '"\n'
# saver will be None if we don't think it worth saving these addresses
saver = OWNERSHIP.saver(ms_wallet, change, start, n)
for line in ms_wallet.generate_address_csv(start, n, change, saver=saver):
yield line
for (idx, addr, derivs, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
if saver:
saver(addr, idx)
# policy choice: never provide a complete multisig address to user.
addr = censor_address(addr)
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
ln += '"\n'
yield ln
if saver:
saver(None, 0) # close cache file
@ -432,7 +474,7 @@ def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, cha
saver = OWNERSHIP.saver(main, change, start, n)
yield '"Index","Payment Address","Derivation"\n'
for (idx, addr, deriv) in main.yield_addresses(start, n, change):
for (idx, addr, deriv) in main.yield_addresses(start, n, change_idx=change):
if saver:
saver(addr, idx)
@ -470,23 +512,18 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
h.update(ep)
dis.progress_sofar(idx, count or 1)
sig_nice = None
if ms_wallet:
# sign with my key at the same path as first address of export
addr_fmt = AF_CLASSIC
derive = ms_wallet.get_my_deriv()
derive = ms_wallet.get_my_deriv(settings.get('xfp'))
derive += "/%d/%d" % (change, start)
else:
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2TR else addr_fmt
derive = path.format(account=account_num, change=change, idx=start) # first addr
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)
msg = '''Address summary file written:\n\n%s''' % nice
if sig_nice:
msg += "\n\nAddress signature file written:\n\n%s" % sig_nice
await ux_show_story(msg)
await ux_show_story("Address summary file written:\n\n%s\n\nAddress"
" signature file written:\n\n%s" % (nice, sig_nice))
except CardMissingError:
await needs_microsd()

View File

@ -8,8 +8,7 @@ from ubinascii import b2a_base64, a2b_base64
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from ustruct import pack
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS, AF_P2TR
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, SUPPORTED_ADDR_FORMATS
from public_constants import STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED, AF_P2SH, AF_P2WPKH_P2SH
from sffile import SFFile
from menu import MenuSystem, MenuItem
@ -132,7 +131,7 @@ Press %s to continue, otherwise %s to cancel.''' % (OK, X)
class ApproveMessageSign(UserAuthorizedAction):
def __init__(self, text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, only_printable=True, privkey=None):
msg_sign_request=None, allow_tab_nl=False, privkey=None):
super().__init__()
is_json = False
@ -142,17 +141,13 @@ class ApproveMessageSign(UserAuthorizedAction):
text, subpath, addr_fmt, is_json = parse_msg_sign_request(msg_sign_request)
self.text = validate_text_for_signing(
text, only_printable=not is_json and only_printable
text, allow_tab_nl=is_json and allow_tab_nl
)
self.subpath = cleanup_deriv_path(subpath)
self.addr_fmt = chains.parse_addr_fmt_str(addr_fmt)
self.approved_cb = approved_cb
self.privkey = privkey
# temporary - no p2tr support
if self.addr_fmt == AF_P2TR:
raise ValueError("Unsupported address format: 'p2tr'")
from glob import dis
dis.fullscreen('Wait...')
@ -208,7 +203,7 @@ def sign_msg(text, subpath, addr_fmt):
async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
msg_sign_request=None, kill_menu=False,
only_printable=True, privkey=None):
allow_tab_nl=False, privkey=None):
# Ask user if they want to sign some short text message.
UserAuthorizedAction.cleanup()
@ -218,7 +213,7 @@ async def approve_msg_sign(text, subpath, addr_fmt, approved_cb=None,
text, subpath, addr_fmt,
approved_cb=approved_cb,
msg_sign_request=msg_sign_request,
only_printable=only_printable,
allow_tab_nl=allow_tab_nl,
privkey=privkey
)
@ -276,8 +271,7 @@ async def try_push_tx(data, txid, txn_sha=None):
class ApproveTransaction(UserAuthorizedAction):
def __init__(self, psbt_len, flags=None, psbt_sha=None, input_method=None,
output_encoder=None, filename=None, miniscript_wallet=None,
offset=TXN_INPUT_OFFSET):
output_encoder=None, filename=None, offset=TXN_INPUT_OFFSET):
super().__init__()
self.offset = offset
self.psbt_len = psbt_len
@ -297,57 +291,6 @@ class ApproveTransaction(UserAuthorizedAction):
self.filename = filename
self.result = None # will be (len, sha256) of the resulting PSBT
self.chain = chains.current_chain()
self.miniscript_wallet = miniscript_wallet
async def por322_msg_verify(self):
# https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c
from glob import NFC
from ux import import_export_prompt
from actions import file_picker
ch = await import_export_prompt("message", is_import=True, force_prompt=True,
intro="Import msg that hashes to 'to_spend' msg hash.",
key0="to input message manually",
title="BIP-322 Messsage" if version.has_qwerty else 'BIP-322 MSG',
no_qr=not version.has_qwerty)
# single sha256 of b'BIP0322-signed-message'
bip322_tag_hash = b'te\x84\xa1\x87/\xa1\x00AUN\xff\xa08\xd6\x12IB\xddy\xb4\xe5\x8aL\xda\x18N\x13\xdb\xe6,I'
if ch == KEY_CANCEL:
return
elif ch == "0":
msg = await ux_input_text("", confirm_exit=False)
elif ch == KEY_NFC:
msg = await NFC.read_bip322_msg()
elif ch == KEY_QR:
from ux_q1 import QRScannerInteraction
msg = await QRScannerInteraction().scan_text('Scan message from a QR code')
else:
choices = await file_picker(suffix='.txt', ux=False, **ch)
target = "%s.txt" % b2a_hex(self.psbt.por322_msg_hash).decode()
for fname, dir, _ in choices:
if target == fname:
fn = dir + "/" + fname
break
else:
fn = await file_picker(choices=choices, **ch)
if not fn: return
with CardSlot(readonly=True, **ch) as card:
with open(fn, 'rt') as fd:
msg = fd.read()
assert msg, "need msg"
msg_hash = ngu.hash.sha256t(bip322_tag_hash, msg, True)
assert msg_hash == self.psbt.por322_msg_hash, "hash verification failed"
ch = await ux_show_story(
msg+"\n\nPress %s to approve message, otherwise %s to exit." % (OK, X),
title="Message:"
)
return True if ch == "y" else False
def render_output(self, o):
# Pretty-print a transactions output.
@ -412,7 +355,6 @@ class ApproveTransaction(UserAuthorizedAction):
# NOTE: psbtObject captures the file descriptor and uses it later
self.psbt = psbtObject.read_psbt(fd)
except BaseException as exc:
# sys.print_exception(exc)
if isinstance(exc, MemoryError):
msg = "Transaction is too complex"
exc = None
@ -422,25 +364,29 @@ class ApproveTransaction(UserAuthorizedAction):
return await self.failure(msg, exc)
dis.fullscreen("Validating...")
self.psbt.active_miniscript = self.miniscript_wallet
# Do some analysis/ validation
try:
await self.psbt.validate() # might do UX: accept multisig import
dis.progress_sofar(10, 100)
if not self.psbt.wif_store:
self.psbt.consider_keys()
dis.progress_sofar(20, 100)
ccc_c_xfp = CCCFeature.get_xfp() # can be None
args = self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
self.psbt.consider_outputs(*args, cosign_xfp=ccc_c_xfp)
del args # not needed anymore
# we can properly assess sighash only after we know
# which outputs are change
self.psbt.consider_dangerous_sighash()
self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
if self.psbt.wif_store:
self.psbt.consider_keys()
dis.progress_sofar(50, 100)
if self.psbt.session:
self.psbt.session.update(pack('<I', self.psbt.lock_time))
self.psbt.consider_outputs()
dis.progress_sofar(75, 100)
self.psbt.consider_dangerous_sighash()
dis.progress_sofar(90, 100)
except FraudulentChangeOutput as exc:
# sys.print_exception(exc)
#print('FraudulentChangeOutput: ' + exc.args[0])
return await self.failure(exc.args[0], title='Change Fraud')
except FatalPSBTIssue as exc:
@ -482,6 +428,7 @@ class ApproveTransaction(UserAuthorizedAction):
#
try:
msg = uio.StringIO()
is_por = self.psbt.por322 and (self.psbt.num_inputs > 1)
# mention warning at top
wl= len(self.psbt.warnings)
@ -491,25 +438,16 @@ class ApproveTransaction(UserAuthorizedAction):
msg.write('(%d warnings below)\n\n' % wl)
if self.psbt.por322:
msg.write("%s\n\n" % ("Proof of Reserves" if is_por else "BIP-322 Message"))
msg.write("Message:\n%s\n\n" % self.psbt.por322_msg)
if is_por:
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
try:
if not await self.por322_msg_verify():
self.refused = True
await ux_dramatic_pause("Refused.", 1)
self.done()
return
except Exception as exc:
return await self.failure("Msg verification failed.", exc)
msg.write("Proof of Reserves\n\n")
msg.write("Amount %s %s\n\n" % self.chain.render_value(self.psbt.total_value_in))
msg.write("Message Hash:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_hash).decode())
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
addr = self.chain.render_address(self.psbt.por322_msg_challenge)
msg.write("Challenge Address:\n%s\n\n" % show_single_address(addr))
except ValueError:
msg.write("Message Challenge:\n%s\n\n" % b2a_hex(self.psbt.por322_msg_challenge).decode())
else:
if self.psbt.active_miniscript:
# show name of the multisig/miniscript wallet that we signed with
msg.write("Wallet: " + self.psbt.active_miniscript.name + "\n\n")
if self.psbt.consolidation_tx:
# consolidating txn that doesn't change balance of account.
msg.write("Consolidating %s %s\nwithin wallet.\n\n" %
@ -522,15 +460,18 @@ class ApproveTransaction(UserAuthorizedAction):
if fee is not None:
msg.write("Network fee %s %s\n\n" % self.chain.render_value(fee))
msg.write(" %d %s\n %d %s\n\n" % (
self.psbt.num_inputs,
"input" if self.psbt.num_inputs == 1 else "inputs",
self.psbt.num_outputs,
"output" if self.psbt.num_outputs == 1 else "outputs",
))
if not self.psbt.por322 or is_por:
msg.write(" %d %s\n %d %s\n\n" % (
self.psbt.num_inputs,
"input" if self.psbt.num_inputs == 1 else "inputs",
self.psbt.num_outputs,
"output" if self.psbt.num_outputs == 1 else "outputs",
))
if not self.psbt.por322:
# outputs + change story created here
self.output_summary_text(msg)
# outputs + change story created here
self.output_summary_text(msg)
gc.collect()
if self.psbt.ux_notes:
@ -558,8 +499,13 @@ class ApproveTransaction(UserAuthorizedAction):
if not hsm_active:
esc = "2"
msg.write("Press %s to approve and sign transaction."
" Press (2) to explore transaction." % OK)
noun = "transaction"
if self.psbt.por322:
noun = "proof of reserves" if is_por else "message"
msg.write("Press %s to approve and sign %s."
" Press (2) to explore transaction." % (OK, noun))
if (self.input_method == "sd") and CardSlot.both_inserted():
esc += "b"
msg.write(" (B) to write to lower SD slot.")
@ -631,7 +577,7 @@ class ApproveTransaction(UserAuthorizedAction):
CCCFeature.sign_psbt(self.psbt)
if SSSPFeature.is_enabled():
# capture new min-height for velocity limit
# update SSSP block_h even if SSSP blocks and overridden by CCC
SSSPFeature.update_last_signed(self.psbt)
except FraudulentChangeOutput as exc:
@ -640,7 +586,6 @@ class ApproveTransaction(UserAuthorizedAction):
msg = "Transaction is too complex"
return await self.failure(msg)
except BaseException as exc:
# sys.print_exception(exc)
return await self.failure("Signing failed late", exc)
try:
@ -706,7 +651,8 @@ class ApproveTransaction(UserAuthorizedAction):
has_change = True
total_change += tx_out.nValue
if len(largest_change) < MAX_VISIBLE_CHANGE:
largest_change.append((tx_out.nValue, self.chain.render_address(tx_out.scriptPubKey)))
_, addr = self.render_output(tx_out)
largest_change.append((tx_out.nValue, addr))
if len(largest_change) == MAX_VISIBLE_CHANGE:
largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True)
continue
@ -731,12 +677,9 @@ class ApproveTransaction(UserAuthorizedAction):
continue # too small
largest.pop(-1)
if outp.is_change:
ret = (here, self.chain.render_address(tx_out.scriptPubKey))
else:
rendered, _ = self.render_output(tx_out)
ret = (here, rendered)
largest.insert(keep, ret)
rendered, dest = self.render_output(tx_out)
largest.insert(keep, (here, dest if outp.is_change else rendered))
# foreign outputs (soon to be other people's coins)
visible_out_sum = 0
@ -775,14 +718,12 @@ class ApproveTransaction(UserAuthorizedAction):
msg.write('%s %s\n\n' % self.chain.render_value(total_change - visible_change_sum))
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, miniscript_wallet=None,
offset=TXN_INPUT_OFFSET):
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None, input_method="usb", offset=TXN_INPUT_OFFSET):
# transaction (binary) loaded into PSRAM already, checksum checked
# optional miniscript_wallet arg, choose particular enrolled wallet by name to sign
UserAuthorizedAction.check_busy(ApproveTransaction)
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, flags, psbt_sha=psbt_sha, input_method="usb",
miniscript_wallet=miniscript_wallet, offset=offset
psbt_len, flags, psbt_sha=psbt_sha, input_method=input_method,
offset=offset
)
# kill any menu stack, and put our thing at the top
@ -823,30 +764,30 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
first_time = True
msg = None
title = None
base_title = "PSBT " + ("Signed" if psbt.sig_added else "Updated")
is_complete = psbt.is_complete()
if finalize is not None:
# USB case - user can choose whether to attempt finalization
is_complete = finalize
if psbt.por322:
# network txn strips PSBT BIP-32 with paths with pubkey required for verification
# overrides --finalize from USB
# disable pushTX for BIP-322
is_complete = False
with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram:
if is_complete:
txid = psbt.finalize(psram)
noun = "Finalized TX ready for broadcast"
else:
psbt.serialize(psram)
noun = "Partly Signed PSBT"
noun = "Signed BIP-322 PSBT" if psbt.por322 else "Partly Signed PSBT"
txid = None
data_len = psram.tell()
data_sha2 = psram.checksum.digest()
# BBQR is at TMP_OUTPUT_OFFSET + 1MB - allowing it in this case would overwrite txn
# allow_qr = data_len < (1024*1024)
# actual more reasonable limit - as BBQR has some overhead and only 1Mbit of space
allow_qr = data_len < (671*1024)
if input_method == "usb":
# return result over USB before going to all options
tx_req.result = data_len, data_sha2
@ -857,7 +798,11 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
first_time = False
msg = noun + " shared via USB."
title = base_title
title = "PSBT Signed"
elif input_method == "kt":
first_time = False
title = "PSBT Signed"
if txid and await try_push_tx(data_len, txid, data_sha2):
# go directly to reexport menu after pushTX
@ -866,19 +811,17 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# for specific cases, key teleport is an option
offer_kt = False
if not is_complete and version.has_qwerty and psbt.active_miniscript:
if not is_complete and psbt.active_multisig and version.has_qwerty:
offer_kt = 'use Key Teleport to send PSBT to other co-signers'
while True:
ch = None
if first_time:
# first time, assume they want to send out same way it came in -- don't prompt
if (input_method == "qr") and allow_qr:
if input_method == "qr":
ch = KEY_QR
elif input_method == "nfc":
ch = KEY_NFC
elif input_method == "kt":
ch = 't'
else:
# SD/VDisk
ch = {"force_vdisk": input_method == "vdisk", "slot_b": slot_b}
@ -898,9 +841,8 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# In that case this would just return dict and keep producing signed
# files on SD infinitely (would never actually prompt).
ch = await import_export_prompt(noun, intro="\n\n".join(intro), offer_kt=offer_kt,
key6=key6, title=title,
force_prompt=not first_time,
no_qr=not version.has_qwerty or not allow_qr)
key6=key6, title=title, force_prompt=not first_time,
no_qr=not version.has_qwerty)
if ch == KEY_CANCEL:
UserAuthorizedAction.cleanup()
break
@ -911,7 +853,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
elif ch == KEY_QR:
here = PSRAM.read_at(TXN_OUTPUT_OFFSET, data_len)
msg = txid or 'Partly Signed PSBT'
msg = txid or noun
try:
if len(here) > 920:
# too big for simple QR - use BBQr instead
@ -940,11 +882,14 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# updated PSBT is at TXN_OUTPUT_OFFSET (at TXN_INPUT_OFFSET is PSBT that is NOT updated)
from teleport import kt_send_psbt
ok = await kt_send_psbt(psbt, data_len, psbt_offset=TXN_OUTPUT_OFFSET)
if ok:
title = "Sent by Teleport"
else:
if ok is None:
title = "Failed to Teleport"
else:
title = "Sent by Teleport"
_, num_sigs_needed = ok
if num_sigs_needed > 0:
s, aux = ("", "is") if num_sigs_needed == 1 else ("s", "are")
msg = "%d more signature%s %s still required." % (num_sigs_needed, s, aux)
continue
else:
@ -955,7 +900,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
input_method = None
first_time = False
title = base_title
title = "PSBT Signed"
async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None):
# Saving a PSBT from PSRAM to something disk-like.
@ -1085,8 +1030,7 @@ async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_
return msg
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False,
miniscript_wallet=None):
async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=False, ux_abort=False):
# sign a PSBT file found on a MicroSD card
# - or from VirtualDisk (mk4)
# - to re-use reading/decoding logic, pass just_read
@ -1144,7 +1088,6 @@ async def sign_psbt_file(filename, force_vdisk=False, slot_b=None, just_read=Fal
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, input_method="vdisk" if force_vdisk else "sd",
filename=filename, output_encoder=output_encoder,
miniscript_wallet=miniscript_wallet,
)
if ux_abort:
# needed for auto vdisk mode
@ -1375,35 +1318,57 @@ class ShowPKHAddress(ShowAddressBase):
sp=self.subpath)
class ShowMiniscriptAddress(ShowAddressBase):
class ShowP2SHAddress(ShowAddressBase):
def setup(self, msc, change, idx):
self.msc = msc
self.change = change
self.idx = idx
def setup(self, ms, addr_fmt, xfp_paths, witdeem_script):
d = self.msc.to_descriptor().derive(None, change=change).derive(idx)
self.address = self.msc.chain.render_address(d.script_pubkey())
self.addr_fmt = self.msc.addr_fmt
self.witdeem_script = witdeem_script
self.addr_fmt = addr_fmt
self.ms = ms
# calculate all the pubkeys involved.
self.subpath_help = ms.validate_script(witdeem_script, xfp_paths=xfp_paths)
self.address = ms.chain.p2sh_address(addr_fmt, witdeem_script)
def get_msg(self):
return '''\
{addr}
Wallet:
{name}
{M} of {N}
Index:
{idx}
Paths:
Change:
{change}'''.format(addr=show_single_address(self.address), name=self.msc.name,
idx=self.idx, change=bool(self.change))
{sp}'''.format(addr=show_single_address(self.address), name=self.ms.name,
M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help))
def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script):
# Show P2SH address to user, also returns it.
# - first need to find appropriate multisig wallet associated
# - they must provide full redeem script, and we will re-verify it and check pubkeys inside it
from multisig import MultisigWallet
try:
assert addr_format in SUPPORTED_ADDR_FORMATS
assert addr_format & AFC_SCRIPT
except:
raise AssertionError('Unknown/unsupported addr format')
# Search for matching multisig wallet that we must already know about
xs = list(xfp_paths)
xs.sort()
ms = MultisigWallet.find_match(M, N, xs)
assert ms, 'Multisig wallet with those fingerprints not found'
assert ms.M == M
assert ms.N == N
def start_show_miniscript_address(msc, change, index):
UserAuthorizedAction.check_busy(ShowAddressBase)
UserAuthorizedAction.active_request = ShowMiniscriptAddress(msc, change, index)
UserAuthorizedAction.active_request = ShowP2SHAddress(ms, addr_format, xfp_paths, witdeem_script)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
@ -1411,7 +1376,6 @@ def start_show_miniscript_address(msc, change, index):
# provide the value back to attached desktop
return UserAuthorizedAction.active_request.address
def show_address(addr_format, subpath, restore_menu=False):
try:
assert addr_format in SUPPORTED_ADDR_FORMATS
@ -1439,112 +1403,64 @@ def usb_show_address(addr_format, subpath):
return active_request.address
class MiniscriptDeleteRequest(UserAuthorizedAction):
def __init__(self, msc):
class NewEnrollRequest(UserAuthorizedAction):
def __init__(self, ms):
super().__init__()
self.wallet = msc
self.wallet = ms
# self.result ... will be re-serialized xpub
async def interact(self):
from wallet import miniscript_delete
await miniscript_delete(self.wallet)
self.done()
def maybe_delete_miniscript(msc):
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = MiniscriptDeleteRequest(msc)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
class NewMiniscriptEnrollRequest(UserAuthorizedAction):
def __init__(self, msc, bsms_index=None):
super().__init__()
self.wallet = msc
self.bsms_index = bsms_index
async def interact(self):
from wallet import WalletOutOfSpace
from multisig import MultisigOutOfSpace
ms = self.wallet
try:
approved = await ms.confirm_import()
if not approved:
ch = await ms.confirm_import()
if ch != 'y':
# they don't want to!
self.refused = True
await ux_dramatic_pause("Refused.", 2)
elif self.bsms_index is not None:
# remove signer round 2 from settings after multisig import is approved by user
from bsms import BSMSSettings
BSMSSettings.signer_delete(self.bsms_index)
except WalletOutOfSpace:
except MultisigOutOfSpace:
return await self.failure('No space left')
except BaseException as exc:
self.failed = "Exception"
# sys.print_exception(exc)
finally:
UserAuthorizedAction.cleanup() # because no results to store
if self.bsms_index is not None:
# bsms special case, get him back to multisig menu
from ux import the_ux, restore_menu
from wallet import MiniscriptMenu
while 1:
top = the_ux.top_of_stack()
if not top: break
if not isinstance(top, MiniscriptMenu):
the_ux.pop()
continue
break
restore_menu()
else:
self.pop_menu()
UserAuthorizedAction.cleanup() # because no results to store
self.pop_menu()
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False,
bsms_index=None, desc_obj=None):
# Offer to import (enroll) a new multisig/miniscript wallet. Allow reject by user.
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False):
# Offer to import (enroll) a new multisig wallet. Allow reject by user.
from glob import dis
from wallet import MiniScriptWallet
from multisig import MultisigWallet
UserAuthorizedAction.cleanup()
dis.fullscreen('Wait...')
dis.fullscreen('Wait...') # needed
dis.busy_bar(True)
bip388 = False
try:
if desc_obj:
# caller is sending us already validated descriptor object
assert name
msc = MiniScriptWallet.from_descriptor_obj(name, desc_obj)
else:
if sf_len:
with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd:
config = fd.read(sf_len).decode()
if sf_len:
with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd:
config = fd.read(sf_len).decode()
try:
j_conf = ujson.loads(config)
if "desc_template" in j_conf and "keys_info" in j_conf:
assert "name" in j_conf
config = j_conf
bip388 = True
else:
assert "desc" in j_conf, "'desc' key required"
config = j_conf["desc"]
assert config, "'desc' empty"
try:
j_conf = ujson.loads(config)
assert "desc" in j_conf, "'desc' key required"
config = j_conf["desc"]
assert config, "'desc' empty"
if "name" in j_conf:
# name from json has preference over filenames and desc checksum
name = j_conf["name"]
assert 2 <= len(name) <= 40, "'name' length"
except ValueError: pass
if "name" in j_conf:
# name from json has preference over filenames and desc checksum
name = j_conf["name"]
assert 2 <= len(name) <= 40, "'name' length"
except ValueError: pass
# this call will raise on parsing errors, so let them rise up
# and be shown on screen/over usb
msc = MiniScriptWallet.from_file(config, name=name, bip388=bip388)
# this call will raise on parsing errors, so let them rise up
# and be shown on screen/over usb
ms = MultisigWallet.from_file(config, name=name)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(msc, bsms_index=bsms_index)
UserAuthorizedAction.active_request = NewEnrollRequest(ms)
if ux_reset:
# for USB case, and import from PSBT
@ -1555,9 +1471,9 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False,
from ux import the_ux
the_ux.push(UserAuthorizedAction.active_request)
finally:
# always finish busy bar
dis.busy_bar(False)
class FirmwareUpgradeRequest(UserAuthorizedAction):
def __init__(self, hdr, length, hdr_check=False, psram_offset=None):
super().__init__()
@ -1643,6 +1559,9 @@ class TXExplorer:
self.qr_msgs = []
self.title = None
def can_goto_idx(self):
return self.max_items > 1
@classmethod
async def start(cls, user_auth_action):
rv = [
@ -1655,6 +1574,7 @@ class TXExplorer:
def make_ux_msg(self, offset, count):
from glob import dis
dis.fullscreen('Wait...')
esc = "4"+KEY_QR
rv = ""
qrs = []
change = []
@ -1663,18 +1583,29 @@ class TXExplorer:
rv += item
dis.progress_sofar(idx-offset+1, count)
rv += 'Press RIGHT to see next group'
hints = []
if end < self.max_items:
hints.append('RIGHT to see next group')
esc += KEY_RIGHT + "9"
if offset:
rv += ', LEFT to go back'
hints.append('LEFT to go back')
esc += KEY_LEFT + "7"
rv += ", (2) to go to index"
if self.can_goto_idx():
hints.append("(2) to go to index")
esc += "2"
if not version.has_qwerty:
# Q has hint key
rv += ", (4) to show QR code"
rv += ('. %s to quit.' % X)
hints.append("(4) to show QR code")
return rv, qrs, change, end
if hints:
rv += 'Press ' + ', '.join(hints)
rv += ('. %s to quit.' % X)
else:
rv += 'Press %s to quit.' % X
return rv, qrs, change, end, esc
async def explore(self, *a):
@ -1683,11 +1614,10 @@ class TXExplorer:
# - shows all inputs: utxo amount and address, txid & tx index.
start = 0
msg, addrs, change, end = self.make_ux_msg(start, self.n)
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
while True:
ch = await ux_show_story(msg, title=self.title, escape='2479'+KEY_RIGHT+KEY_LEFT+KEY_QR,
hint_icons=KEY_QR)
ch = await ux_show_story(msg, title=self.title, hint_icons=KEY_QR, escape=esc)
if ch == 'x':
del msg
return
@ -1709,17 +1639,16 @@ class TXExplorer:
else:
# go forwards
start += self.n
elif ch == "2":
elif (ch == "2") and (self.max_items > 1):
max_v = self.max_items - 1
res = await ux_enter_number("Start Idx (0-%d):" % max_v, max_value=max_v,
can_cancel=True)
res = await ux_enter_number("Start Idx (0-%d):" % max_v, max_value=max_v)
if res is None: continue
start = res
else:
# nothing changed - do not recalc msg
continue
msg, addrs, change, end = self.make_ux_msg(start, self.n)
msg, addrs, change, end, esc = self.make_ux_msg(start, self.n)
class TXOutExplorer(TXExplorer):
@ -1770,9 +1699,11 @@ class TXInpExplorer(TXExplorer):
item += "=== UTXO ===\n\n%s %s\n\n%s\n\n" % (val, unit, spk)
if addr:
item += show_single_address(addr) + "\n\n"
item += "Address Format: %s\n\n" % chains.AF_TO_STR_AF[inp.af]
qr_items.append(addr)
if inp.addr_fmt is not None:
item += "Address Format: %s\n\n" % chains.addr_fmt_str(inp.addr_fmt)
if self.user_auth_action.psbt.txn_version >= 2:
has_rtl = inp.has_relative_timelock(txin)
if has_rtl:
@ -1784,56 +1715,45 @@ class TXInpExplorer(TXExplorer):
item += "Input has relative %s\n\n" % msg
psbt_item = ""
if inp.sp_idxs:
if inp.required_key:
ws = self.user_auth_action.psbt.wif_store
psbt_item += "Our key%s:\n\n" % ("s" if len(inp.sp_idxs) > 1 else "")
for i in inp.sp_idxs:
# get node required
if inp.taproot_subpaths:
pubk = inp.taproot_subpaths[i][0]
sp = inp.taproot_subpaths[i][1][2]
our = [inp.required_key] if isinstance(inp.required_key, bytes) else inp.required_key
psbt_item += "Our key%s:\n\n" % ("s" if len(our) > 1 else "")
wif_note = "(WIF Store)"
for k in our:
pubkey = b2a_hex(k).decode()
pth = inp.subpaths.get(k)
note = ""
if pth:
label = keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0]))
if ws and k in ws:
note = "\n" + wif_note
psbt_item += "%s:\n%s%s\n\n" % (label, pubkey, note)
else:
pubk = inp.subpaths[i][0]
sp = inp.subpaths[i][1]
psbt_item += "%s\n%s\n\n" % (pubkey, wif_note)
pth = inp.parse_xfp_path(sp)
k = inp.get(pubk)
ws_note = "\n(WIF Store)" if (ws and k in ws) else ""
psbt_item += "%s:\n%s%s\n\n" % (keypath_to_str(pth, prefix="%s/" % xfp2str(pth[0])),
b2a_hex(k).decode(), ws_note)
M = None
if inp.is_miniscript:
if inp.is_multisig:
ks_coord = inp.witness_script or inp.redeem_script
if ks_coord:
ks = inp.get(ks_coord)
ks = self.user_auth_action.psbt.get(ks_coord)
from psbt import disassemble_multisig_mn
from multisig import disassemble_multisig_mn
try:
M, N = disassemble_multisig_mn(ks)
psbt_item += "Multisig: %dof%d\n\n" % (M, N)
except: pass
if inp.is_musig:
psbt_item += "MuSig2\n\n"
if inp.part_sigs or inp.taproot_script_sigs:
# do not show XFPs in case input is fully signed
if inp.part_sigs:
# do not show XFPs in case input is fully signed --> elif
# only part_sig should be available, as we haven't signed yet so added_sigs empty
done = []
if inp.part_sigs:
signed = {inp.get(k) for k, _ in inp.part_sigs}
for pk, pth in inp.subpaths:
if inp.get(pk) in signed:
done.append(xfp2str(inp.parse_xfp_path(pth)[0]))
else: # inp.taproot_script_sigs
signed = {xo for xo, _ in inp.get_taproot_script_sigs()}
for pk, val in inp.taproot_subpaths:
if inp.get(pk) in signed:
done.append(xfp2str(inp.parse_xfp_path(val[2])[0]))
for pk, pth in inp.subpaths.items():
if pk in inp.part_sigs:
done.append(xfp2str(pth[0]))
if inp.fully_signed or (M and (len(done) >= M)):
if inp.fully_signed:
psbt_item += "Input fully signed.\n\n"
else:
psbt_item += "Already signed:\n"
@ -1841,15 +1761,14 @@ class TXInpExplorer(TXExplorer):
psbt_item += " %s\n" % xfp
psbt_item += "\n"
if inp.sighash is not None:
# only show sighash value to the user if it is non-standard for particular script type
if (inp.af == AF_P2TR and inp.sighash != 0) or (inp.af != AF_P2TR and inp.sighash != 1):
psbt_item += "sighash: %s\n\n" % {
0: "DEFAULT", 1: "ALL", 2: "NONE", 3: "SINGLE",
1 | 0x80: "ALL|ANYONECANPAY",
2 | 0x80: "NONE|ANYONECANPAY",
3 | 0x80: "SINGLE|ANYONECANPAY",
}[inp.sighash]
if inp.sighash and (inp.sighash != SIGHASH_ALL):
# only show sighash value to the user if it is non-standard
psbt_item += "sighash: %s\n\n" % {
1: "ALL", 2: "NONE", 3: "SINGLE",
1 | 0x80: "ALL|ANYONECANPAY",
2 | 0x80: "NONE|ANYONECANPAY",
3 | 0x80: "SINGLE|ANYONECANPAY",
}.get(inp.sighash, "0x%02x (non-standard)" % inp.sighash)
if psbt_item:
psbt_item = "=== PSBT ===\n\n" + psbt_item

View File

@ -49,7 +49,7 @@ def render_backup_contents(bypass_tmp=False):
if sv.mode == 'words':
ADD('mnemonic', bip39.b2a_words(sv.raw))
if sv.mode == 'master':
elif sv.mode == 'master':
ADD('bip32_master_key', b2a_hex(sv.raw))
ADD('chain', chain.ctype)
@ -76,7 +76,12 @@ def render_backup_contents(bypass_tmp=False):
current_tmp = pa.tmp_value[:]
pa.tmp_value = None
# we also need correct settings from main seed
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
if sv.mode == 'words':
nv = stash.SecretStash.encode(seed_phrase=sv.raw)
else:
assert sv.mode == "xprv"
nv = stash.SecretStash.encode(xprv=sv.node)
settings.set_key(nv)
settings.load()
stash.blank_object(nv)
@ -201,6 +206,13 @@ def restore_from_dict_ll(vals, raw):
k = key[8:]
if k == 'bkpw':
# never import a cached backup password from a backup file.
# write-side (render_backup_contents) strips bkpw, so a present
# value means a tampered/crafted file trying to fixate the
# password used for all FUTURE backups - drop it.
continue
if k == 'sd2fa':
# do NOT restore sd2fa as SD card can be lost or damaged
# new version of firmware 5.1.3+ will not back sd2fa
@ -291,7 +303,7 @@ async def restore_tmp_from_dict_ll(vals, raw):
if not k[:8] == "setting.":
continue
key = k[8:]
if key == "miniscript":
if key in ["multisig"]:
# whitelist
settings.set(key, v)
@ -577,7 +589,11 @@ async def restore_complete(fname_or_fd, temporary=False, words=True, usb=False):
# give them a menu to pick from, and start picking
if usb:
# we're not originating from a menu
words = await seed.WordNestMenu.get_n_words(12)
words = await seed.WordNestMenu.get_n_words(num_pw_words)
if len(words) != num_pw_words:
seed.WordNestMenu.pop_all()
return
await done(words)
else:
m = seed.WordNestMenu(num_words=num_pw_words, has_checksum=False, done_cb=done)

View File

@ -138,12 +138,15 @@ async def batt_idle_logout():
# - even before login
import glob
from uasyncio import sleep_ms
from glob import settings, dis
from glob import settings, dis, SCAN
import utime
while True:
await sleep_ms(20000) # 20 seconds
if SCAN.busy_scanning:
continue
if get_batt_level() is None:
# on USB power
continue

View File

@ -54,7 +54,7 @@ def calc_num_qr(char_capacity, char_len, split_mod):
if char_len > actual:
need += 1
# Challenge: the final QR might have just a a few chars in it, if we redistribute
# Challenge: the final QR might have just a few chars in it, if we redistribute
# the data into the other parts, then each QR can have more forward error correction
# and be more robust. Must respect split_mod alignment tho.
level = ceil(char_len / need)
@ -439,5 +439,18 @@ class BBQrPsramStorage(BBQrStorage):
from glob import PSRAM
return PSRAM.read_at(0, self.final_size)
def finalize(self):
self._finalize()
if self.hdr.encoding == 'Z':
self.zlib_decompress()
# PSBT-typed BBQrs end up at PSRAM[0..size]
# skip a redundant PSRAM->heap->PSRAM round-trip
if self.hdr.file_type == 'P':
return self.hdr.file_type, self.final_size, 'PSRAM'
return self.hdr.file_type, self.final_size, self.get_buffer()
# EOF

9
shared/block_height.py Normal file
View File

@ -0,0 +1,9 @@
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# AUTO-generated.
#
# Updated: 2026-06-19 14:13:23 UTC
BLOCK_HEIGHT = 932301
# EOF

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
# - "hobbled" refers to less-than full control over Coldcard, even though you have main PIN
#
import gc, chains, version, ngu, web2fa, bip39, re
from ubinascii import hexlify as b2a_hex
from chains import NLOCK_IS_TIME
from utils import swab32, xfp2str, truncate_address, deserialize_secret, show_single_address
from glob import settings, dis
@ -61,19 +62,20 @@ class SpendingPolicy(dict):
self.update(v.items()) # mpy bugfix, when called with SpendingPolicy
def _save_policy(self):
def _save_policy(self, master_only=True):
# serialize the spending policy, save it
v = dict(settings.master_get(self.nvkey, {}))
v['pol'] = self.copy()
settings.master_set(self.nvkey, v, master_only=True)
settings.master_set(self.nvkey, v, master_only=master_only)
def update_policy_key(self, _quiet=False, **kws):
def update_policy_key(self, _quiet=False, _master_only=True, **kws):
# Update a few elements of the spending policy
# - all changes are saved immediately (which is a little slow/visible)
if not _quiet:
dis.fullscreen("Saving...")
self.update(kws)
self._save_policy()
self._save_policy(_master_only)
def meets_policy(self, psbt):
# Does policy allow signing this? Else raise why. Return T if web2fa required.
@ -121,7 +123,10 @@ class SpendingPolicy(dict):
for idx, txo in psbt.output_iter():
out = psbt.outputs[idx]
if not out.is_change: # ignore change
addr = c.render_address(txo.scriptPubKey)
try:
addr = c.render_address(txo.scriptPubKey)
except ValueError:
addr = str(b2a_hex(txo.scriptPubKey), 'ascii')
if addr not in wl:
raise SpendPolicyViolation("whitelist: " + addr)
@ -156,7 +161,8 @@ class SpendingPolicy(dict):
# always update last block height, even if velocity isn't enabled yet
# - attacker might have changed to testnet, but there is no
# reason to ever lower block height. strictly ascending
self.update_policy_key(_quiet=True, block_h=psbt.lock_time)
# allow update block_h from temporary seed
self.update_policy_key(_quiet=True, _master_only=False, block_h=psbt.lock_time)
class SSSPFeature:
# Using setting value "sssp"
@ -170,8 +176,6 @@ class SSSPFeature:
@classmethod
def update_last_signed(cls, psbt):
# new PSBT has been completely signed successfully.
if not cls.is_enabled():
return
pol = cls.get_policy()
pol.update_last_signed(psbt)
@ -232,7 +236,12 @@ class CCCFeature:
@classmethod
def words_check(cls, words):
# Test if words provided are right
enc = seed_words_to_encoded_secret(words)
try:
# a2b_words with checksum check
enc = seed_words_to_encoded_secret(words)
except:
return False
exp = cls.get_encoded_secret()
return enc == exp
@ -308,7 +317,7 @@ class CCCFeature:
if not cls.is_enabled():
return False, False
ms = psbt.active_miniscript
ms = psbt.active_multisig
if not ms:
# not multisig, so ignore/permit
return False, False
@ -317,7 +326,7 @@ class CCCFeature:
# don't try to sign; maybe show warning?
xfp = cls.get_xfp()
if xfp not in [i[0] for i in ms.to_descriptor().xfp_paths()]:
if xfp not in ms.xfp_paths:
# does not involve us
return False, False
@ -366,7 +375,7 @@ class CCCConfigMenu(MenuSystem):
self.replace_items(tmp)
def construct(self):
from wallet import MiniScriptWallet, make_miniscript_wallet_menu
from multisig import MultisigWallet, make_ms_wallet_menu
my_xfp = CCCFeature.get_xfp()
items = [
@ -380,13 +389,10 @@ class CCCConfigMenu(MenuSystem):
# look for wallets that are defined related to CCC feature, shortcut to them
count = 0
for i, ms in enumerate(MiniScriptWallet.iter_wallets()):
if not ms.m_n: # basic multisig check
continue
if my_xfp in [i[0] for i in ms.xfp_paths()]:
M, N = ms.m_n
items.append(MenuItem('%d/%d: %s' % (M, N, ms.name),
menu=make_miniscript_wallet_menu, arg=(i,ms)))
for ms in MultisigWallet.get_all():
if my_xfp in ms.xfp_paths:
items.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
menu=make_ms_wallet_menu, arg=ms.storage_idx))
count += 1
items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=count))
@ -461,8 +467,8 @@ class CCCConfigMenu(MenuSystem):
xfp = CCCFeature.get_xfp()
enc = CCCFeature.get_encoded_secret()
from wallet import export_miniscript_xpubs
await export_miniscript_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
from multisig import export_multisig_xpubs
await export_multisig_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
async def build_2ofN(self, m, l, i):
count = i.arg
@ -588,11 +594,12 @@ class SPAddrWhitelist(MenuSystem):
if choice == KEY_CANCEL:
return
elif choice == KEY_NFC:
addr = await NFC.read_address()
if not addr:
res = await NFC.read_address()
if not res:
# error already displayed in nfc.py
return
_, addr, _ = res
await self.add_addresses([addr])
return
@ -654,11 +661,12 @@ class SPAddrWhitelist(MenuSystem):
async def add_addresses(self, more_addrs):
# add new entries, if unique; preserve ordering
addrs = self.policy.get('addrs', [])
# - work on a copy and check the limit *before* committing: the list
# from get('addrs') is the live, settings-backed one
addrs = list(self.policy.get('addrs', []))
new = []
for a in more_addrs:
if a not in addrs:
addrs.append(a)
if a not in addrs and a not in new:
new.append(a)
if not new:
@ -666,10 +674,10 @@ class SPAddrWhitelist(MenuSystem):
'\n\n'.join(show_single_address(a) for a in more_addrs))
return
if len(addrs) > MAX_WHITELIST:
if len(addrs) + len(new) > MAX_WHITELIST:
return await self.maxed_out()
self.policy.update_policy_key(addrs=addrs)
self.policy.update_policy_key(addrs=addrs + new)
self.update_contents()
if len(new) > 1:
@ -749,15 +757,11 @@ class SpendingPolicyMenu(MenuSystem):
# Looks decent on both Q and Mk4...
was = self.policy.get('mag', 0)
val = await ux_enter_number('Transaction Max:', max_value=int(1e8),
can_cancel=True, value=(was or ''))
value=(was or ''))
if val is None: return
args = dict(mag=val)
if (val is None) or (val == was):
msg = "Did not change"
val = was
else:
msg = "You have set the"
unchanged = False
msg = "Did not change" if val == was else "You have set the"
if not val:
msg = "No check for maximum transaction size will be done. "
@ -1092,7 +1096,7 @@ is locked into a special mode that restricts seed access, backups, settings and
First step is to define a new PIN code that is used when you want to bypass or \
disable this feature.
''',
title="Spending Policy")
title="Spending Policy" if version.has_qwerty else "Spend Policy")
if ch != 'y':
# just a tourist

View File

@ -5,17 +5,16 @@
import ngu
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR, AF_BARE_PK
from public_constants import AF_BARE_PK, AF_CLASSIC, AF_P2WPKH, AF_P2TR
from public_constants import AF_P2SH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AFC_PUBKEY, AFC_BECH32, AFC_SCRIPT
from public_constants import TAPROOT_LEAF_TAPSCRIPT, TAPROOT_LEAF_MASK
from serializations import hash160, ser_compact_size, disassemble, ser_string
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
from block_height import BLOCK_HEIGHT
from serializations import hash160, ser_compact_size, disassemble
from ucollections import namedtuple
from opcodes import OP_RETURN, OP_1, OP_16
from precomp_tag_hash import TAP_TWEAK_H, TAP_LEAF_H
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2TR, AF_P2WPKH_P2SH)
# DO NOT CHANGE ORDER! PickAddrFmtMenu.__init__ expects correct order
SINGLESIG_AF = (AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH)
# See SLIP 132 <https://github.com/satoshilabs/slips/blob/master/slip-0132.md>
# for background on these version bytes. Not to be confused with SLIP-32 which involves Bech32.
@ -33,30 +32,11 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
NLOCK_IS_TIME = const(500000000)
def taptweak(internal_key, tweak=None):
# BIP 341 states: "If the spending conditions do not require a script path,
# the output key should commit to an unspendable script path instead of having no script path.
# This can be achieved by computing the output key point as:
# Q = P + int(hashTapTweak(bytes(P)))G."
actual_tweak = internal_key if tweak is None else internal_key + tweak
tweak = ngu.hash.sha256t(TAP_TWEAK_H, actual_tweak, True)
xo_pubkey = ngu.secp256k1.xonly_pubkey(internal_key)
xo_pubkey_tweaked = xo_pubkey.tweak_add(tweak)
return xo_pubkey_tweaked.to_bytes()
def tapscript_serialize(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
# leaf version is only 7 msb
lv = leaf_version % TAPROOT_LEAF_MASK
return bytes([lv]) + ser_string(script)
def tapleaf_hash(script, leaf_version=TAPROOT_LEAF_TAPSCRIPT):
return ngu.hash.sha256t(TAP_LEAF_H, tapscript_serialize(script, leaf_version), True)
class ChainsBase:
curve = 'secp256k1'
menu_name = None # use 'name' if this isn't defined
core_name = None # name of chain's "core" p2p software
ccc_min_block = 0
# b44_cointype comes from
@ -91,6 +71,16 @@ class ChainsBase:
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
return node.serialize(cls.slip132[addr_fmt].pub, False)
@classmethod
def deserialize_node(cls, text, addr_fmt):
# xpub/xprv to object
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
node = ngu.hdnode.HDNode()
version = node.deserialize(text)
assert (version == cls.slip132[addr_fmt].pub) \
or (version == cls.slip132[addr_fmt].priv)
return node
@classmethod
def script_pubkey(cls, addr_fmt, pubkey=None, script=None):
digest = None
@ -114,10 +104,7 @@ class ChainsBase:
else:
assert pubkey
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_P2TR:
assert len(pubkey) == 32 # internal
spk = b'\x51\x20' + taptweak(pubkey)
elif addr_fmt == AF_CLASSIC:
if addr_fmt == AF_CLASSIC:
spk = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
@ -129,6 +116,29 @@ class ChainsBase:
return spk, digest
@classmethod
def p2sh_address(cls, addr_fmt, witdeem_script):
# Multisig and general P2SH support
# - witdeem => witness script for segwit, or redeem script otherwise
# - redeem script can be generated from witness script if needed.
# - this function needs a witdeem script to be provided, not simple to make
# - more verification needed to prove it's change/included address (NOT HERE)
# - reference: <https://bitcoincore.org/en/segwit_wallet_dev/>
# - returns: str(address)
assert addr_fmt & AFC_SCRIPT, 'for p2sh only'
_, digest = cls.script_pubkey(addr_fmt, script=witdeem_script)
if addr_fmt == AF_P2WSH:
# bech32 encoded segwit p2sh
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
else:
# segwit p2wsh encoded as classic P2SH
# and P2SH classic
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
return addr
@classmethod
def pubkey_to_address(cls, pubkey, addr_fmt):
# - renders a pubkey to an address
@ -140,9 +150,6 @@ class ChainsBase:
@classmethod
def address(cls, node, addr_fmt):
# return a human-readable, properly formatted address
if addr_fmt == AF_P2TR:
xo_pk = node.pubkey()[1:]
return ngu.codecs.segwit_encode(cls.bech32_hrp, 1, taptweak(xo_pk))
if addr_fmt == AF_CLASSIC:
# olde fashioned P2PKH
@ -150,7 +157,7 @@ class ChainsBase:
return node.addr_help(cls.b58_addr[0])
if addr_fmt & AFC_SCRIPT:
# use chain.render_address
# use p2sh_address() instead.
raise ValueError(hex(addr_fmt))
# so must be P2PKH, fetch it.
@ -247,7 +254,7 @@ class ChainsBase:
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
# segwit v0 (P2WPKH, P2WSH)
if script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
if ll in (22, 34) and script[0] == 0 and script[1] in (0x14, 0x20) and (ll-2) == script[1]:
return ngu.codecs.segwit_encode(cls.bech32_hrp, script[0], script[2:])
# segwit v1 (P2TR) and later segwit version
@ -258,56 +265,40 @@ class ChainsBase:
@classmethod
def op_return(cls, script):
# returns decoded string op return data if script is op return otherwise None
gen = disassemble(script)
script_type = next(gen)
if OP_RETURN not in script_type:
return
try:
gen = disassemble(script)
item, opcode = next(gen)
except (StopIteration, ValueError):
return None
if opcode != OP_RETURN:
return None
try:
data = next(gen)[0]
if data:
return data
except StopIteration:
pass
try:
data, opcode = next(gen)
except StopIteration:
return b"" # bare OP_RETURN
return b""
try:
next(gen)
return None # extra ops/pushes -> raw script display
except StopIteration: pass
@classmethod
def possible_address_fmt(cls, addr):
# Given a text (serialized) address, return what
# address format applies to the address, but
# for AF_P2SH case, could be: AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH. .. we don't know
hrp = cls.bech32_hrp + "1"
if addr.startswith(hrp):
if addr.startswith(hrp+'p'):
# segwit v1 (any ver=1 script or address, but for now just taproot...)
return AF_P2TR
elif addr.startswith(hrp+'q'):
# segwit v0
return AF_P2WPKH if len(addr) < 55 else AF_P2WSH
return 0
try:
raw = ngu.codecs.b58_decode(addr)
except ValueError:
# not base58, not an error
return 0
if raw[0] == cls.b58_addr[0]:
return AF_CLASSIC
if raw[0] == cls.b58_script[0]:
return AF_P2SH
return 0
except ValueError:
return None
if isinstance(data, bytes):
return data
if data is None and opcode == 0:
return b"" # OP_RETURN OP_0
return None
class BitcoinMain(ChainsBase):
# see <https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L140>
ctype = 'BTC'
name = 'Bitcoin Mainnet'
ccc_min_block = 939464 # Mar 5/2026
ccc_min_block = BLOCK_HEIGHT
slip132 = {
AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
@ -315,7 +306,6 @@ class BitcoinMain(ChainsBase):
AF_P2WPKH: Slip132Version(0x04b24746, 0x04b2430c, 'z'),
AF_P2WSH_P2SH: Slip132Version(0x0295b43f, 0x0295b005, 'Y'),
AF_P2WSH: Slip132Version(0x02aa7ed3, 0x02aa7a99, 'Z'),
AF_P2TR: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'),
}
bech32_hrp = 'bc'
@ -337,7 +327,6 @@ class BitcoinTestnet(ChainsBase):
AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'),
AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'),
AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'),
AF_P2TR: Slip132Version(0x043587cf, 0x04358394, 't'),
}
bech32_hrp = 'tb'
@ -378,13 +367,6 @@ def current_chain():
return get_chain(chain)
def current_key_chain():
c = current_chain()
if c == BitcoinRegtest:
# regtest has same extended keys as testnet
c = BitcoinTestnet
return c
# Overbuilt: will only be testnet and mainchain.
AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest]
@ -412,8 +394,6 @@ CommonDerivations = [
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
( 'BIP-84 (Native Segwit P2WPKH)', "m/84h/{coin_type}h/{account}h/{change}/{idx}",
AF_P2WPKH ), # generates bc1 bech32 addresses
('BIP-86 (Taproot Segwit P2TR)', "m/86h/{coin_type}h/{account}h/{change}/{idx}",
AF_P2TR), # generates bc1p bech32m addresses
]
STD_DERIVATIONS = {
@ -421,38 +401,21 @@ STD_DERIVATIONS = {
"p2sh-p2wpkh": CommonDerivations[1][1],
"p2wpkh-p2sh": CommonDerivations[1][1],
"p2wpkh": CommonDerivations[2][1],
"p2tr": CommonDerivations[3][1],
}
MS_STD_DERIVATIONS = {
("p2sh", "m/45h", AF_P2SH),
("p2sh_p2wsh", "m/48h/{coin}h/{acct_num}h/1h", AF_P2WSH_P2SH),
("p2wsh", "m/48h/{coin}h/{acct_num}h/2h", AF_P2WSH),
('p2tr', "m/48h/{coin}h/{acct_num}h/3h", AF_P2TR),
}
AF_TO_STR_AF = {
AF_BARE_PK: "p2pk",
AF_CLASSIC: "p2pkh",
AF_P2TR: "p2tr",
AF_P2WPKH: "p2wpkh",
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
AF_P2SH: "p2sh",
AF_P2WSH: "p2wsh",
AF_P2WSH_P2SH: "p2sh-p2wsh",
}
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
# integers are coming from USB
try:
if isinstance(addr_fmt, int):
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
return addr_fmt
else:
try:
addr_fmt = AF_TO_STR_AF[addr_fmt] # just for error msg
except: pass
raise ValueError
addr_fmt = addr_fmt.lower()
@ -462,12 +425,11 @@ def parse_addr_fmt_str(addr_fmt):
return AF_CLASSIC
elif addr_fmt == "p2wpkh":
return AF_P2WPKH
elif addr_fmt == "p2tr":
return AF_P2TR
else:
raise ValueError
except ValueError:
raise ValueError("Unsupported address format: '%s'" % addr_fmt)
raise ValueError("Invalid address format: '%s'\n\n"
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
def af_to_bip44_purpose(addr_fmt):
@ -475,19 +437,26 @@ def af_to_bip44_purpose(addr_fmt):
# - single signature only
return {AF_CLASSIC: 44,
AF_P2WPKH_P2SH: 49,
AF_P2WPKH: 84,
AF_P2TR: 86}[addr_fmt]
AF_P2WPKH: 84}[addr_fmt]
def addr_fmt_label(addr_fmt):
return {
AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH",
AF_P2TR: "Taproot P2TR",
AF_P2WSH: "Segwit P2WSH",
AF_P2WSH_P2SH: "P2SH-P2WSH",
AF_P2SH: "Legacy P2SH",
}[addr_fmt]
# Text used in menus
return {AF_CLASSIC: "Classic P2PKH",
AF_P2WPKH_P2SH: "P2SH-Segwit",
AF_P2WPKH: "Segwit P2WPKH"}[addr_fmt]
def addr_fmt_str(addr_fmt):
# Short string codes used for address format (industry standard)
return {AF_CLASSIC: "p2pkh",
AF_BARE_PK: "p2pk",
AF_P2SH: "p2sh",
AF_P2TR: "p2tr",
AF_P2WPKH: "p2wpkh",
AF_P2WSH: "p2wsh",
AF_P2WPKH_P2SH: "p2sh-p2wpkh",
AF_P2WSH_P2SH: "p2sh-p2wsh"}[addr_fmt]
def verify_recover_pubkey(sig, digest):
# verifies a message digest against a signature and recovers

View File

@ -51,9 +51,7 @@ def decode_utf_16_le(s):
'''
def read_var64(f):
'''
Decode their silly 64-bit encoding.
'''
# Decode their silly 64-bit encoding.
first = ord(f.read(1))
if first < 128:
return first
@ -100,7 +98,7 @@ def check_file_headers(f):
# assume f is seekable
fh = FileHeader.read(f)
if not fh.has_good_magic:
if not fh.has_good_magic():
raise ValueError("Bad magic bytes")
# read only first header
@ -113,22 +111,21 @@ def check_file_headers(f):
if sh.size > 10000:
raise ValueError("Second header too big")
# capture this spot
# TODO 'data_start' unused
data_start = f.tell() # expect 0x20
# FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes
# SectionHeader.read() always reads exactly calcsize('<QQL') = 20 bytes
# after those two calls, f.tell() is always start_pos + 32
# assert f.tell() == 0x20 # expect 0x20
try:
f.seek(sh.offset, 1)
th = f.read(sh.size)
if len(th) != sh.size:
raise IndexError("Truncated file?")
assert len(th) == sh.size, "Truncated file?"
# Look for properties about compression. this could be
# faked-out but good enough for now
if b'\x24\x06\xf1\x07\x01' not in th:
raise RuntimeError("Not marked as AES+SHA encrypted?")
assert b'\x24\x06\xf1\x07\x01' in th, "Not marked as AES+SHA encrypted?"
except Exception as e:
raise ValueError("Confused file? %s" % e.message)
raise ValueError("Confused file? %s" % e)
if masked_crc(th) != sh.crc:
raise ValueError("Trailing header has wrong CRC")
@ -174,7 +171,6 @@ class FileHeader(object):
def actual_crc(self):
return masked_crc(self.bits)
class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
@ -213,6 +209,7 @@ class SectionHeader(namedtuple('SectionHeader', ['offset', 'size', 'crc' ])):
def actual_crc(self):
return masked_crc(self.bits)
class Builder(object):
def __init__(self, password=None, salt_len=16, iv_len=16, rounds_pow=13, progress_fcn=None):
self.rounds_pow = rounds_pow # standard is 19, 16 and 17 work fine

View File

@ -11,6 +11,15 @@ from bbqr import TYPE_LABELS
from utils import decode_bip21_text
def decode_qr_text(got):
if isinstance(got, str):
return got
try:
return got.decode()
except UnicodeError:
raise QRDecodeExplained('UTF-8 decode failed')
def decode_seed_qr(data):
# SeedQR: 4 digit groups of index into word list
parts = [data[pos:pos + 4] for pos in range(0, len(data), 4)]
@ -39,6 +48,8 @@ def decode_secret(got):
# - xprv / tprv
# - words (either full or prefixes, case insensitive)
# - SeedQR (github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md)
# - word lists are NOT BIP-39-checksum-validated here. Callers that
# require a valid seed must run bip39.a2b_words(...)
if len(got) > 300:
raise ValueError("Too big.")
@ -51,7 +62,7 @@ def decode_secret(got):
# xprv or tprv: private key import for sure
# - verify checksum is right
try:
raw = ngu.codecs.b58_decode(got)
ngu.codecs.b58_decode(got)
except:
raise ValueError('corrupt xprv?')
@ -63,7 +74,7 @@ def decode_secret(got):
kp, testnet, compressed = decode_wif(got)
return 'wif', (got, kp, compressed, testnet)
except: pass
taste = got.strip().lower()
if taste.isdigit():
@ -108,11 +119,8 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
return got.decode()
if ty == 'P':
# may already be in PSRAM, avoid a copy here
from glob import PSRAM
if PSRAM.is_at(got, 0):
got = 'PSRAM' # see qr_psbt_sign()
# `got` is the literal 'PSRAM' from BBQrPsramStorage when data already there
# otherwise it's real bytes
return 'psbt', (None, final_size, got)
elif ty == 'T':
@ -120,9 +128,10 @@ def decode_qr_result(got, expect_secret=False, expect_text=False, expect_bbqr=Fa
elif ty == 'U':
# continue thru code below for TEXT
pass
got = decode_qr_text(got)
elif ty == 'J':
got = decode_qr_text(got)
what = "json"
if "msg" in got:
what = "smsg"
@ -187,12 +196,7 @@ def decode_short_text(got):
# - if bad checksum on bitcoin addr, we treat as text... since might be
# return: what-it-is, (tuple)
if not isinstance(got, str):
# decode utf-8
try:
got = got.decode()
except UnicodeError:
raise QRDecodeExplained('UTF-8 decode failed')
got = decode_qr_text(got)
# might be a PSBT?
if len(got) > 100:
@ -215,9 +219,26 @@ def decode_short_text(got):
# was something else.
pass
from descriptor import Descriptor
if Descriptor.is_descriptor(got):
return 'minisc', (got,)
# multisig descriptor
# multi( catches both multi( and sortedmulti(
if ("multi(" in got):
return 'multi', (got,)
if ("\n" in got) and ('pub' in got):
# legacy multisig import/export format
# [0-9a-fA-F]{8}\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]{107}
# above is more precise BUT counted repetitions not supported in mpy
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
rgx = ure.compile(cc_ms_pat)
# go line by line and match above, once 2 matches observed - considered multisig
# important to not use ure.search for big strings (can run out of stack);
# a real line here is a "<8-hex xfp>: <xpub>" key (~121 chars)
c = 0 # match count
for l in got.split("\n"):
if len(l) <= 150 and rgx.search(l):
c += 1
if c > 1:
return 'multi', (got,)
# Things with newlines in them are not URL's
# - working URLs are not >4k

View File

@ -1,620 +0,0 @@
# (c) Copyright 2020 by Stepan Snigirev, see <https://github.com/diybitcoinhardware/embit/blob/master/LICENSE>
#
# Changes (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import ngu, chains, ustruct, stash
from io import BytesIO
from public_constants import MAX_PATH_DEPTH
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from utils import keypath_to_str, str_to_keypath, swab32, xfp2str
from serializations import ser_compact_size
WILDCARD = "*"
PROVABLY_UNSPENDABLE = b'\x02P\x92\x9bt\xc1\xa0IT\xb7\x8bK`5\xe9z^\x07\x8aZ\x0f(\xec\x96\xd5G\xbf\xee\x9a\xce\x80:\xc0'
# sha256(b"MuSig2MuSig2MuSig2")
MUSIG_CHAIN_CODE = b'\x86\x80\x87\xca\x02\xa6\xf9t\xc4Y\x89$\xc3kWv-2\xcbEqqg\xe3\x00b,qg\xe3\x89e'
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def polymod(c, val):
c0 = c >> 35
c = ((c & 0x7ffffffff) << 5) ^ val
if (c0 & 1):
c ^= 0xf5dee51989
if (c0 & 2):
c ^= 0xa9fdca3312
if (c0 & 4):
c ^= 0x1bab10e32d
if (c0 & 8):
c ^= 0x3706b1677a
if (c0 & 16):
c ^= 0x644d626ffd
return c
def descriptor_checksum(desc):
c = 1
cls = 0
clscount = 0
for ch in desc:
pos = INPUT_CHARSET.find(ch)
if pos == -1:
raise ValueError(ch)
c = polymod(c, pos & 31)
cls = cls * 3 + (pos >> 5)
clscount += 1
if clscount == 3:
c = polymod(c, cls)
cls = 0
clscount = 0
if clscount > 0:
c = polymod(c, cls)
for j in range(0, 8):
c = polymod(c, 0)
c ^= 1
rv = ''
for j in range(0, 8):
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
return rv
def append_checksum(desc):
return desc + "#" + descriptor_checksum(desc)
def parse_desc_str(string):
"""Remove comments, empty lines and strip line. Produce single line string"""
res = ""
for l in string.split("\n"):
strip_l = l.strip()
if not strip_l:
continue
if strip_l.startswith("#"):
continue
res += strip_l
return res
def read_until(s, chars=b",)(#"):
res = b""
while True:
chunk = s.read(1)
if len(chunk) == 0:
return res, None
if chunk in chars:
return res, chunk
res += chunk
def musig_synthetic_node(agg_pk_bytes):
assert len(agg_pk_bytes) == 33 # need non-xonly pubkey
node = ngu.hdnode.HDNode()
node.from_chaincode_pubkey(MUSIG_CHAIN_CODE, agg_pk_bytes)
return node
class KeyOriginInfo:
def __init__(self, fingerprint: bytes, derivation: list, cc_fp=None):
self.fingerprint = fingerprint
self.derivation = derivation
self._cc_fp = cc_fp
def __eq__(self, other):
return self.psbt_derivation() == other.psbt_derivation()
def __hash__(self):
return hash(tuple(self.psbt_derivation()))
@property
def cc_fp(self):
if self._cc_fp is None:
self._cc_fp = ustruct.unpack('<I', self.fingerprint)[0]
return self._cc_fp
def str_derivation(self):
return keypath_to_str(self.derivation, prefix='m/', skip=0)
def psbt_derivation(self):
res = [self.cc_fp]
for i in self.derivation:
res.append(i)
return res
@classmethod
def from_string(cls, s: str):
arr = s.split("/")
xfp = a2b_hex(arr[0])
assert len(xfp) == 4
arr[0] = "m"
path = "/".join(arr)
derivation = str_to_keypath(xfp, path)[1:] # ignoring xfp here, already stored
assert len(derivation) <= MAX_PATH_DEPTH, "origin too deep"
return cls(xfp, derivation)
def __str__(self):
rv = "%s" % b2a_hex(self.fingerprint).decode()
if self.derivation:
rv += "/%s" % keypath_to_str(self.derivation, prefix='', skip=0)
return rv
class KeyDerivationInfo:
def __init__(self, indexes=None):
self.indexes = indexes
if self.indexes is None:
self.indexes = ((0, 1), WILDCARD)
self.multi_path_index = 0
else:
self.multi_path_index = None
def __hash__(self):
return hash(self.indexes)
@staticmethod
def not_hardened(x):
assert (b"'" not in x) and (b"h" not in x), "Cannot use hardened sub derivation path"
def get_ext_int(self):
return self.indexes[self.multi_path_index]
@classmethod
def parse(cls, s):
err = "Malformed key derivation"
multi_i = None
idxs = []
while True:
got, char = read_until(s, b"<,)/")
if char == b"<":
assert multi_i is None, "too many multipaths"
ext_num, char = read_until(s, b";")
assert char, err
cls.not_hardened(ext_num)
int_num, char = read_until(s, b">")
assert char, err
assert b";" not in int_num, "Solved cardinality > 2"
cls.not_hardened(int_num)
assert int_num != ext_num # cannot be the same
multi_i = len(idxs)
idxs.append((int(ext_num.decode()), int(int_num.decode())))
else:
# char in "/),"
if got == b"*":
# every derivation has to end with wildcard (only ranged keys allowed)
idxs.append(WILDCARD)
break
elif got:
cls.not_hardened(got)
idxs.append(int(got.decode()))
# comma and parenthesis not allowed in subderivation, marker of the end
if char in b",)": break
assert idxs[-1] == WILDCARD, "All keys must be ranged"
if idxs == [0, WILDCARD]:
# normalize and instead save as <0;1> as change derivation was not provided
obj = cls()
else:
assert multi_i is not None, "need multipath"
assert len(idxs[multi_i]) == 2, "wrong multipath"
obj = cls(tuple(idxs))
obj.multi_path_index = multi_i
return obj
def to_string(self, external=True, internal=True):
res = []
for i in self.indexes:
if isinstance(i, tuple):
if internal is True and external is False:
i = str(i[1])
elif internal is False and external is True:
i = str(i[0])
else:
i = "<%d;%d>" % (i[0], i[1])
else:
i = str(i)
res.append(i)
return "/".join(res)
def der_index(self, idx, change=False):
if isinstance(idx, list):
for i in idx:
mp_i = self.multi_path_index or 0
if i in self.indexes[mp_i]:
idx = i
break
else:
assert False
elif idx is None:
# derive according to key subderivation if any
if self is None:
idx = 1 if change else 0
else:
if self.multi_path_index is not None:
ext, inter = self.indexes[self.multi_path_index]
idx = inter if change else ext
return idx
class ExtendedKey:
def __init__(self, node, origin, derivation=None, taproot=False, chain_type=None):
self.origin = origin
self.node = node
self.derivation = derivation or KeyDerivationInfo()
self.taproot = taproot
self.chain_type = chain_type
def __eq__(self, other):
return hash(self) == hash(other)
def __hash__(self):
return hash(self.node.pubkey()) + hash(self.derivation)
def __len__(self):
return 34 - int(self.taproot) # <33:sec> or <32:xonly>
@property
def fingerprint(self):
return self.origin.fingerprint
def serialize(self):
return self.key_bytes()
def compile(self):
d = self.serialize()
return ser_compact_size(len(d)) + d
@classmethod
def parse_key(cls, key_str):
assert key_str[1:4].lower() == b"pub", "only extended pubkeys allowed"
# extended key
# or xpub or tpub as we use descriptors (SLIP-132 NOT allowed)
hint = key_str[0:1].lower()
if hint == b"x":
chain_type = "BTC"
elif hint == b"t":
chain_type = "XTN"
else:
# slip (ignore any implied address format)
chain_type = "BTC" if hint in b"yz" else "XTN"
node = ngu.hdnode.HDNode()
node.deserialize(key_str)
try:
assert node.privkey() is None, "no privkeys"
except ValueError:
# ValueError is thrown from libngu if key is public
pass
return node, chain_type
def validate(self, my_xfp, disable_checks=False):
assert self.chain_type == chains.current_key_chain().ctype, "wrong chain"
# xfp is always available, even if key was serialized without origin info
# upon parse root origin info is generated from key itself
xfp = self.origin.cc_fp
is_mine = (xfp == my_xfp)
# raises ValueError on invalid pubkey (should be in libngu)
# invalid public key not allowed even with disable checks
ngu.secp256k1.pubkey(self.node.pubkey())
if not disable_checks:
depth = self.node.depth()
# we now allow blinded keys that have depth X but derivation len is 0,
# where only fingerprint constitutes key origin
# only check if derivation length is greater than 0
if self.origin.derivation:
assert len(self.origin.derivation) == depth, \
"deriv len != xpub depth (xfp=%s)" % xfp2str(xfp)
if depth == 0:
# blinded keys allowed
# assert not self.node.parent_fp()
# assert self.node.child_number()[0] == 0
assert swab32(self.node.my_fp()) == xfp, "master xfp mismatch"
elif depth == 1:
target = swab32(self.node.parent_fp())
assert xfp == target, 'xfp depth=1 wrong'
if is_mine:
# it's supposed to be my key, so I should be able to generate pubkey
# - might indicate collision on xfp value between co-signers,
# and that's not supported
deriv = self.origin.str_derivation()
with stash.SensitiveValues() as sv:
chk_node = sv.derive_path(deriv)
assert self.node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
return is_mine
def derive(self, idx=None, change=False):
if self.derivation:
idx = self.derivation.der_index(idx, change)
else:
assert idx
new_node = self.node.copy()
new_node.derive(idx, False)
if self.origin:
origin = KeyOriginInfo(self.origin.fingerprint, self.origin.derivation + [idx],
self.origin.cc_fp)
else:
origin = KeyOriginInfo(self.origin.fingerprint, [idx], self.origin.cc_fp)
new_der = None
if self.derivation:
new_der = KeyDerivationInfo(self.derivation.indexes[1:])
return type(self)(new_node, origin, new_der, taproot=self.taproot)
@classmethod
def read_from(cls, s, taproot=False, musig=False):
first = s.read(1)
origin = None
if first == b"[":
prefix, char = read_until(s, b"]")
if char != b"]":
raise ValueError("Invalid key - missing ] in key origin info")
origin = KeyOriginInfo.from_string(prefix.decode())
else:
s.seek(-1, 1)
k, char = read_until(s, b",)/")
if musig and char not in b",)":
assert b"musig(" not in k, "nested musig not allowed"
assert char != b"/", "key derivation not allowed inside musig"
der = None
if char == b"/":
der = KeyDerivationInfo.parse(s)
if char is not None:
s.seek(-1, 1)
# parse key
node, chain_type = cls.parse_key(k)
if origin is None:
cc_fp = swab32(node.my_fp())
origin = KeyOriginInfo(ustruct.pack('<I', cc_fp), [], cc_fp)
return cls(node, origin, der, chain_type=chain_type, taproot=taproot)
@classmethod
def from_cc_data(cls, xfp, deriv, xpub):
xfp_str = xfp if isinstance(xfp, str) else xfp2str(xfp)
koi = KeyOriginInfo.from_string("%s/%s" % (xfp_str, deriv.replace("m/", "")))
node, chain_type = cls.parse_key(xpub.encode())
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
@classmethod
def from_cc_json(cls, vals, af_str):
key_exp = af_str + "_key_exp"
if key_exp in vals:
# new firmware, prefer key expression
return cls.from_string(vals[key_exp])
# TODO
node, _, _, _ = chains.slip132_deserialize(vals[af_str])
ek = chains.current_chain().serialize_public(node)
return cls.from_cc_data(vals["xfp"], vals["%s_deriv" % af_str], ek)
@classmethod
def from_psbt_xpub(cls, ek_bytes, xfp_path):
xfp, *path = xfp_path
koi = KeyOriginInfo(a2b_hex(xfp2str(xfp)), path)
# TODO this should be done by C code, no need to base58 encode/decode
# byte-serialized key should be decodable
ek = ngu.codecs.b58_encode(ek_bytes)
node, chain_type = cls.parse_key(ek.encode())
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
@property
def is_provably_unspendable(self):
if PROVABLY_UNSPENDABLE == self.node.pubkey():
return True
return False
@property
def prefix(self):
if self.origin and self.origin.derivation:
return "[%s]" % self.origin
# jut a bare [xfp]key - omit origin info (jut xfp)
# or no origin at all
return ""
def key_bytes(self):
kb = self.node.pubkey()
if self.taproot:
# xonly
kb = kb[1:]
return kb
def extended_public_key(self):
return chains.current_chain().serialize_public(self.node)
def to_string(self, external=True, internal=True):
key = self.prefix
key += self.extended_public_key()
if self.derivation and (external or internal):
key += "/" + self.derivation.to_string(external, internal)
return key
@classmethod
def from_string(cls, s):
s = BytesIO(s.encode())
return cls.read_from(s)
class MusigKey:
def __init__(self, keys, der=None, node=None):
self.keys = keys
self.derivation = der or KeyDerivationInfo()
self._node = node
def __len__(self):
return 33 # length + <32:xonly>
def __eq__(self, other):
return hash(self) == hash(other)
def __hash__(self):
return hash(self.node.pubkey()) + hash(self.derivation)
def serialize(self):
return self.key_bytes()
def compile(self):
d = self.serialize()
return ser_compact_size(len(d)) + d
@property
def node(self):
if self._node is None:
self._node = musig_synthetic_node(self.aggregate_pubkey().to_bytes())
return self._node
def validate(self, my_xfp, disable_checks=False):
has_mine = 0
for k in self.keys:
assert not k.is_provably_unspendable, "unspendable key inside musig"
if k.validate(my_xfp, disable_checks):
has_mine += 1
assert len(self.keys) == len(set(self.keys)), "musig keys not unique"
assert has_mine <= 1, "multiple own keys in musig"
return has_mine
def key_bytes(self):
return ngu.secp256k1.pubkey(self.node.pubkey()).to_xonly().to_bytes()
def aggregate_pubkey(self):
keyagg_cache = ngu.secp256k1.MusigKeyAggCache()
secp_pubkeys = [ngu.secp256k1.pubkey(k.node.pubkey()) for k in self.keys]
ngu.secp256k1.musig_pubkey_agg(secp_pubkeys, keyagg_cache)
return keyagg_cache.agg_pubkey()
def to_string(self, external=True, internal=True):
base = "musig(%s)" % (",".join([k.to_string(False, False) for k in self.keys]))
base += "/" + self.derivation.to_string(external, internal)
return base
def derive(self, idx=None, change=False):
idx = self.derivation.der_index(idx, change)
new_node = self.node.copy()
new_node.derive(idx, False)
return type(self)(self.keys, KeyDerivationInfo(self.derivation.indexes[1:]),
node=new_node)
@property
def is_provably_unspendable(self):
return False
@classmethod
def read_from(cls, s, taproot=True):
assert taproot, "musig in non-taproot context"
assert s.read(6) == b"musig(", "not musig()"
der = None
keys = []
while True:
k = ExtendedKey.read_from(s, taproot=taproot, musig=True)
k.der = None
k.taproot = taproot
# already verified that no der present in keys
k.derivation = None
keys.append(k)
c = s.read(1)
if c == b")":
sep = s.read(1)
if sep == b"/":
der = KeyDerivationInfo.parse(s)
s.seek(-1, 1)
break
assert c == b","
return cls(keys, der)
@classmethod
def from_string(cls, s):
s = BytesIO(s.encode())
return cls.read_from(s)
class KeyExpression:
@classmethod
def read_from(cls, s, taproot=False):
is_musig = (s.read(6) == b"musig(")
s.seek(-6, 1)
if is_musig:
return MusigKey.read_from(s, taproot=taproot)
else:
return ExtendedKey.read_from(s, taproot=taproot)
def bip388_wallet_policy_to_descriptor(desc_tmplt, keys_info):
for i in range(len(keys_info) - 1, -1, -1):
k_str = keys_info[i]
ph = "@%d" % i
desc_tmplt = desc_tmplt.replace(ph, k_str)
return desc_tmplt.replace("/**", "/<0;1>/*")
def bip388_validate_policy(desc_tmplt, keys_info):
s = BytesIO(desc_tmplt)
r = []
while True:
g1, char = read_until(s, b"@")
if not char:
# no more - done
break
# key derivation info required for policy
g2, char = read_until(s, b"/")
assert char, "key derivation missing"
if g1.endswith(b"musig("):
# key derivations not allowed inside musig
assert b"/" not in g2
assert g2[-1:] == b")"
for i, num in enumerate(g2[:-1].split(b",")):
if i:
# 0th element has @ already removed
assert num[0:1] == b"@"
num = num[1:]
num = int(num.decode())
if num not in r:
r.append(num)
else:
num = int(g2.decode())
if num not in r:
r.append(num)
assert s.read(1) in b"<*", "need multipath"
assert len(r) == len(keys_info), "Invalid policy"
assert r == list(range(len(r))), "Out of order"

View File

@ -1,333 +1,251 @@
# (c) Copyright 2020 by Stepan Snigirev, see <https://github.com/diybitcoinhardware/embit/blob/master/LICENSE>
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Changes (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
# descriptor.py - Bitcoin Core's descriptors and their specialized checksums.
#
import ngu, chains
from io import BytesIO
from collections import OrderedDict
from utils import xfp2str, swab32
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from public_constants import AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, MAX_TR_SIGNERS
from desc_utils import (parse_desc_str, append_checksum, descriptor_checksum,
KeyExpression, ExtendedKey, MusigKey)
from miniscript import Miniscript
from precomp_tag_hash import TAP_BRANCH_H
# Based on: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
#
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
MULTI_FMT_TO_SCRIPT = {
AF_P2SH: "sh(%s)",
AF_P2WSH_P2SH: "sh(wsh(%s))",
AF_P2WSH: "wsh(%s)",
None: "wsh(%s)",
# hack for tests
"p2sh": "sh(%s)",
"p2sh-p2wsh": "sh(wsh(%s))",
"p2wsh-p2sh": "sh(wsh(%s))",
"p2wsh": "wsh(%s)",
}
SINGLE_FMT_TO_SCRIPT = {
AF_P2WPKH: "wpkh(%s)",
AF_CLASSIC: "pkh(%s)",
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
None: "wpkh(%s)",
"p2pkh": "pkh(%s)",
"p2wpkh": "wpkh(%s)",
"p2sh-p2wpkh": "sh(wpkh(%s))",
"p2wpkh-p2sh": "sh(wpkh(%s))",
}
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
try:
from utils import xfp2str, str2xfp
except ModuleNotFoundError:
import struct
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
# assuming not micro python
def xfp2str(xfp):
# Standardized way to show an xpub's fingerprint... it's a 4-byte string
# and not really an integer. Used to show as '0x%08x' but that's wrong endian.
return b2a_hex(struct.pack('<I', xfp)).decode().upper()
def str2xfp(txt):
# Inverse of xfp2str
return struct.unpack('<I', a2b_hex(txt))[0]
class Tapscript:
def __init__(self, tree):
self.tree = tree # miniscript or (tapscript, tapscript)
self._merkle_root = None
self._processed_tree = None
class WrongCheckSumError(Exception):
pass
def iter_leaves(self):
if isinstance(self.tree, Miniscript):
yield self.tree
else:
for ts in self.tree:
yield from ts.iter_leaves()
@property
def merkle_root(self):
if not self._merkle_root:
self._processed_tree, self._merkle_root = self.process_tree()
return self._merkle_root
def polymod(c, val):
c0 = c >> 35
c = ((c & 0x7ffffffff) << 5) ^ val
if (c0 & 1):
c ^= 0xf5dee51989
if (c0 & 2):
c ^= 0xa9fdca3312
if (c0 & 4):
c ^= 0x1bab10e32d
if (c0 & 8):
c ^= 0x3706b1677a
if (c0 & 16):
c ^= 0x644d626ffd
def derive(self, idx, key_map, change=False):
if isinstance(self.tree, Miniscript):
tree = self.tree.derive(idx, key_map, change=change)
else:
l, r = self.tree
tree = [l.derive(idx, key_map, change=change),
r.derive(idx, key_map, change=change)]
return c
return type(self)(tree)
def descriptor_checksum(desc):
c = 1
cls = 0
clscount = 0
for ch in desc:
pos = INPUT_CHARSET.find(ch)
if pos == -1:
raise ValueError(ch)
def process_tree(self):
if isinstance(self.tree, Miniscript):
script = self.tree.compile()
h = chains.tapleaf_hash(script)
return [(chains.TAPROOT_LEAF_TAPSCRIPT, script, bytes())], h
c = polymod(c, pos & 31)
cls = cls * 3 + (pos >> 5)
clscount += 1
if clscount == 3:
c = polymod(c, cls)
cls = 0
clscount = 0
l, r = self.tree
left, left_h = l.process_tree()
right, right_h = r.process_tree()
left = [(version, script, control + right_h) for version, script, control in left]
right = [(version, script, control + left_h) for version, script, control in right]
if right_h < left_h:
right_h, left_h = left_h, right_h
if clscount > 0:
c = polymod(c, cls)
for j in range(0, 8):
c = polymod(c, 0)
c ^= 1
h = ngu.hash.sha256t(TAP_BRANCH_H, left_h + right_h, True)
return left + right, h
rv = ''
for j in range(0, 8):
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
# UNUSED - using above proces tree cached result to dump scripts to CSV
# def script_tree(self):
# if isinstance(self.tree, Miniscript):
# return b2a_hex(chains.tapscript_serialize(self.tree.compile())).decode()
#
# l, r = self.tree
# return "{" + l.script_tree() + "," +r.script_tree() + "}"
return rv
@classmethod
def read_from(cls, s):
c = s.read(1)
assert len(c)
if c == b"{": # more than one miniscript
left = cls.read_from(s)
c = s.read(1)
if c == b"}":
return left
if c != b",":
raise ValueError("Invalid tapscript: expected ','")
def append_checksum(desc):
return desc + "#" + descriptor_checksum(desc)
right = cls.read_from(s)
if s.read(1) != b"}":
raise ValueError("Invalid tapscript: expected '}'")
return cls((left, right))
def parse_desc_str(string):
"""Remove comments, empty lines and strip line. Produce single line string"""
res = ""
for l in string.split("\n"):
strip_l = l.strip()
if not strip_l:
continue
if strip_l.startswith("#"):
continue
res += strip_l
return res
s.seek(-1, 1)
ms = Miniscript.read_from(s, taproot=True)
return cls(ms)
def to_string(self, external=True, internal=True):
if isinstance(self.tree, Miniscript):
return self.tree.to_string(external, internal)
l, r = self.tree
return ("{" + l.to_string(external,internal) + ","
+ r.to_string(external, internal) + "}")
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
if addr_fmt == AF_P2WSH_P2SH:
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
elif addr_fmt == AF_P2WSH:
descriptor_template = "wsh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2SH:
descriptor_template = "sh(sortedmulti(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
class Descriptor:
def __init__(self, key=None, miniscript=None, tapscript=None, addr_fmt=None, keys=None):
if addr_fmt in [AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH]:
assert miniscript
assert not key
else:
# single-sig + taproot/tapscript
assert miniscript is None
assert key
__slots__ = (
"keys",
"addr_fmt",
)
self.key = key
self.miniscript = miniscript
self.tapscript = tapscript
def __init__(self, keys, addr_fmt):
self.keys = keys
self.addr_fmt = addr_fmt
# cached keys
self._keys = keys
def validate(self, disable_checks=False):
# should only be run once while importing wallet
from glob import settings
@staticmethod
def checksum_check(desc_w_checksum , csum_required=False):
try:
desc, checksum = desc_w_checksum.split("#")
except ValueError:
if csum_required:
raise ValueError("Missing descriptor checksum")
return desc_w_checksum, None
calc_checksum = descriptor_checksum(desc)
if calc_checksum != checksum:
raise WrongCheckSumError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
return desc, checksum
c = 0
has_mine = 0
err_top_B = "Top level miniscript should be 'B'"
max_signers = 20
@staticmethod
def parse_key_orig_info(key):
# key origin info is required for our MultisigWallet
close_index = key.find("]")
if key[0] != "[" or close_index == -1:
raise ValueError("Key origin info is required for %s" % (key))
key_orig_info = key[1:close_index] # remove brackets
key = key[close_index + 1:]
return key_orig_info, key
if self.tapscript:
assert self.key # internal key (would fail during parse)
max_signers = MAX_TR_SIGNERS
for l in self.tapscript.iter_leaves():
assert l.type == "B", err_top_B
l.verify()
l.is_sane(taproot=True)
# cannot have same keys in single miniscript
# provably unspendable taproot internal key is not covered here
assert len(l.keys) == len(set(l.keys)), "Insane"
@staticmethod
def parse_key_derivation_info(key):
invalid_subderiv_msg = "Invalid subderivation path - only 0/* or <0;1>/* allowed"
slash_split = key.split("/")
assert len(slash_split) > 1, invalid_subderiv_msg
if all(["h" not in elem and "'" not in elem for elem in slash_split[1:]]):
assert slash_split[-1] == "*", invalid_subderiv_msg
assert slash_split[-2] in ["0", "<0;1>", "<1;0>"], invalid_subderiv_msg
assert len(slash_split[1:]) == 2, invalid_subderiv_msg
return slash_split[0]
else:
raise ValueError("Cannot use hardened sub derivation path")
elif self.miniscript:
assert self.key is None
assert self.miniscript.type == "B", err_top_B
self.miniscript.verify()
self.miniscript.is_sane(taproot=False)
# cannot have same keys in single miniscript
assert len(self.miniscript.keys) == len(set(self.miniscript.keys)), "Insane"
def checksum(self):
return descriptor_checksum(self._serialize())
my_xfp = settings.get('xfp', 0)
ext_nums = set()
int_nums = set()
for k in self.keys:
has_mine += k.validate(my_xfp, disable_checks)
ext, int = k.derivation.get_ext_int()
ext_nums.add(ext)
int_nums.add(int)
c += 1
if not self.tapscript and not self.is_basic_multisig:
# this is non-taproot Miniscript
# Miniscript expressions can only be used in wsh or tr.
assert self.addr_fmt != AF_P2SH, "Miniscript in legacy P2SH not allowed"
assert ext_nums.isdisjoint(int_nums), "Non-disjoint multipath"
assert c <= max_signers, "max signers"
assert has_mine > 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
def bip388_wallet_policy(self):
# Return compact descriptor (BIP-388 style) template and key info
# - only same origin keys
keys_info = OrderedDict()
for k in self.keys:
ks = k.keys if isinstance(k, MusigKey) else [k]
for kk in ks:
pk = kk.node.pubkey()
if pk not in keys_info:
keys_info[pk] = kk.to_string(external=False, internal=False)
desc_tmplt = self.to_string(checksum=False).replace("/<0;1>/*", "/**")
keys_info = list(keys_info.values())
for i, k_str in enumerate(keys_info):
desc_tmplt = desc_tmplt.replace(k_str, '@%d' % i)
return desc_tmplt, keys_info
@property
def script_len(self):
if self.is_taproot:
return 34 # OP_1 <32:xonly>
if self.miniscript:
return len(self.miniscript)
if self.addr_fmt == AF_P2WPKH:
return 22 # 00 <20:pkh>
return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG
def xfp_paths(self, skip_unspend_ik=False):
res = []
for k in self.keys:
if self.is_taproot and k.is_provably_unspendable and skip_unspend_ik:
continue
if isinstance(k, MusigKey):
agg_k = [swab32(k.node.my_fp())]
# even if dupes - add
res.append(agg_k)
for kk in k.keys:
psbt_der = kk.origin.psbt_derivation()
if psbt_der not in res:
res.append(psbt_der)
def serialize_keys(self, internal=False, int_ext=False):
result = []
for xfp, deriv, xpub in self.keys:
if deriv[0] == "m":
# get rid of 'm'
deriv = deriv[1:]
elif deriv[0] != "/":
# input "84'/0'/0'" would lack slash separtor with xfp
deriv = "/" + deriv
if not isinstance(xfp, str):
xfp = xfp2str(xfp)
koi = xfp + deriv
# normalize xpub to use h for hardened instead of '
key_str = "[%s]%s" % (koi.lower(), xpub)
if int_ext:
key_str = key_str + "/" + "<0;1>" + "/" + "*"
else:
res.append(k.origin.psbt_derivation())
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
result.append(key_str.replace("'", "h"))
return result
return res
def _serialize(self, internal=False, int_ext=False):
"""Serialize without checksum"""
assert len(self.keys) == 1 # "Multiple keys for single signature script"
desc_base = SINGLE_FMT_TO_SCRIPT[self.addr_fmt]
inner = self.serialize_keys(internal=internal, int_ext=int_ext)[0]
return desc_base % (inner)
@property
def is_segwit_v0(self):
return self.addr_fmt in [AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH]
def serialize(self, internal=False, int_ext=False):
"""Serialize with checksum"""
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
@property
def is_segwit(self):
return self.is_taproot or self.is_segwit_v0
@classmethod
def parse(cls, desc_w_checksum):
# remove garbage
desc_w_checksum = parse_desc_str(desc_w_checksum)
# check correct checksum
desc, checksum = cls.checksum_check(desc_w_checksum)
# legacy
if desc.startswith("pkh("):
addr_fmt = AF_CLASSIC
tmp_desc = desc.replace("pkh(", "")
tmp_desc = tmp_desc.rstrip(")")
@property
def is_taproot(self):
return self.addr_fmt == AF_P2TR
# native segwit
elif desc.startswith("wpkh("):
addr_fmt = AF_P2WPKH
tmp_desc = desc.replace("wpkh(", "")
tmp_desc = tmp_desc.rstrip(")")
@property
def is_legacy_sh(self):
return self.addr_fmt in [AF_P2SH, AF_P2WSH_P2SH, AF_P2WPKH_P2SH]
@property
def is_basic_multisig(self):
return self.miniscript and self.miniscript.NAME in ["multi", "sortedmulti"]
@property
def is_sortedmulti(self):
return self.is_basic_multisig and self.miniscript.NAME == "sortedmulti"
@property
def keys(self):
if self._keys:
return self._keys
if self.is_taproot:
# internal is always first
# use ordered dict as order preserving set
keys = OrderedDict()
# add internal key (whether musig or not)
keys[self.key] = None
if self.tapscript:
# taptree keys
for lv in self.tapscript.iter_leaves():
for k in lv.keys:
keys[k] = None
self._keys = list(keys)
elif self.miniscript:
self._keys = self.miniscript.keys
# wrapped segwit
elif desc.startswith("sh(wpkh("):
addr_fmt = AF_P2WPKH_P2SH
tmp_desc = desc.replace("sh(wpkh(", "")
tmp_desc = tmp_desc.rstrip("))")
else:
# single-sig
self._keys = [self.key]
raise ValueError("Unsupported descriptor. Supported: pkh(), wpkh(), sh(wpkh()).")
return self._keys
koi, key = cls.parse_key_orig_info(tmp_desc)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
def derive(self, idx=None, change=False):
if self.is_taproot:
# derive keys first
# duplicate keys can be may be found in different leaves
# use map to derive each key just once
derived_keys = OrderedDict()
for i, k in enumerate(self.keys):
if not isinstance(k, MusigKey):
dk = k.derive(idx, change=change)
dk.taproot = self.is_taproot
derived_keys[k] = dk
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
derived_tapsript = None
if self.tapscript:
derived_tapsript = self.tapscript.derive(idx, derived_keys, change=change)
return type(self)(self.key.derive(idx, change=change),
tapscript=derived_tapsript, addr_fmt=self.addr_fmt,
keys=list(derived_keys.values()))
if self.miniscript:
return type(self)(
None,
self.miniscript.derive(idx, change=change),
addr_fmt=self.addr_fmt,
)
# single-sig
return type(self)(self.key.derive(idx, change=change))
def script_pubkey(self, compiled_scr=None):
if self.is_taproot:
tweak = None
if self.tapscript:
tweak = self.tapscript.merkle_root
output_pubkey = chains.taptweak(self.key.serialize(), tweak)
return b"\x51\x20" + output_pubkey
if self.is_legacy_sh:
if self.miniscript:
# caller may have already built a script
scr = compiled_scr or self.miniscript.compile()
redeem_scr = scr
if self.addr_fmt == AF_P2WSH_P2SH:
redeem_scr = b"\x00\x20" + ngu.hash.sha256s(scr)
else:
redeem_scr = b"\x00\x14" + ngu.hash.hash160(self.key.node.pubkey())
return b"\xa9\x14" + ngu.hash.hash160(redeem_scr) + b"\x87"
if self.addr_fmt == AF_P2WSH:
# witness script p2wsh only
return b"\x00\x20" + ngu.hash.sha256s(compiled_scr or self.miniscript.compile())
if self.addr_fmt == AF_P2WPKH:
return b"\x00\x14" + ngu.hash.hash160(self.key.serialize())
# p2pkh
assert self.addr_fmt == AF_CLASSIC
return b"\x76\xa9\x14" + ngu.hash.hash160(self.key.serialize()) + b"\x88\xac"
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
@classmethod
def is_descriptor(cls, desc_str):
@ -348,131 +266,142 @@ class Descriptor:
return True
return False
@staticmethod
def checksum_check(desc_w_checksum, csum_required=False):
try:
desc, checksum = desc_w_checksum.split("#")
except ValueError:
if csum_required:
raise ValueError("Missing descriptor checksum")
return desc_w_checksum, None
calc_checksum = descriptor_checksum(desc)
if calc_checksum != checksum:
raise ValueError("Wrong checksum %s, expected %s" % (checksum, calc_checksum))
return desc, checksum
@classmethod
def from_string(cls, desc, checksum=False):
desc = parse_desc_str(desc)
desc, cs = cls.checksum_check(desc)
s = BytesIO(desc.encode())
res = cls.read_from(s)
left = s.read()
if len(left) > 0:
raise ValueError("Unexpected characters after descriptor: %r" % left)
if checksum:
if cs is None:
_, cs = res.to_string().split("#")
return res, cs
return res
@classmethod
def read_from(cls, s):
start = s.read(8)
af = AF_CLASSIC
internal_key = None
tapscript = None
if start.startswith(b"tr("):
af = AF_P2TR
s.seek(-5, 1)
internal_key = KeyExpression.read_from(s, taproot=True)
sep = s.read(1)
if sep == b")":
s.seek(-1, 1)
else:
assert sep == b","
tapscript = Tapscript.read_from(s)
elif start.startswith(b"sh(wsh("):
af = AF_P2WSH_P2SH
s.seek(-1, 1)
elif start.startswith(b"wsh("):
af = AF_P2WSH
s.seek(-4, 1)
elif start.startswith(b"sh(wpkh("):
af = AF_P2WPKH_P2SH
elif start.startswith(b"wpkh("):
af = AF_P2WPKH
s.seek(-3, 1)
elif start.startswith(b"pkh("):
s.seek(-4, 1)
elif start.startswith(b"sh("):
af = AF_P2SH
s.seek(-5, 1)
else:
raise ValueError("Invalid descriptor")
miniscript = None
if af == AF_P2TR:
key = internal_key
nbrackets = 1
elif af in [AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH]:
miniscript = Miniscript.read_from(s)
key = internal_key
nbrackets = 1 + int(af == AF_P2WSH_P2SH)
else:
key = ExtendedKey.read_from(s, taproot=False)
nbrackets = 1 + int(af == AF_P2WPKH_P2SH)
end = s.read(nbrackets)
if end != b")" * nbrackets:
raise ValueError("Invalid descriptor")
desc = cls(key, miniscript, tapscript, af)
return desc
def to_string(self, external=True, internal=True, checksum=True):
if self.is_taproot:
desc = "tr(%s" % self.key.to_string(external, internal)
if self.tapscript:
desc += ","
tree = self.tapscript.to_string(external, internal)
desc += tree
res = desc + ")"
else:
if self.miniscript is not None:
res = self.miniscript.to_string(external, internal)
if self.addr_fmt in [AF_P2WSH, AF_P2WSH_P2SH]:
res = "wsh(%s)" % res
else:
if self.addr_fmt in [AF_P2WPKH, AF_P2WPKH_P2SH]:
res = "wpkh(%s)" % self.key.to_string(external, internal)
else:
res = "pkh(%s)" % self.key.to_string(external, internal)
if self.is_legacy_sh:
res = "sh(%s)" % res
if checksum:
res = append_checksum(res)
return res
def bitcoin_core_serialize(self):
def bitcoin_core_serialize(self, external_label=None):
# this will become legacy one day
# instead use <0;1> descriptor format
res = []
for external in (True, False):
for internal in [False, True]:
desc_obj = {
"desc": self.to_string(external, not external),
"desc": self.serialize(internal=internal),
"active": True,
"timestamp": "now",
"internal": not external,
"internal": internal,
"range": [0, 100],
}
if internal is False and external_label:
desc_obj["label"] = external_label
res.append(desc_obj)
return res
class MultisigDescriptor(Descriptor):
# only supprt with key derivation info
# only xpubs
# can be extended when needed
__slots__ = (
"M",
"N",
"keys",
"addr_fmt",
"is_sorted" # whether to use sortedmulti() or multi()
)
def __init__(self, M, N, keys, addr_fmt, is_sorted=True):
self.M = M
self.N = N
self.is_sorted = is_sorted
super().__init__(keys, addr_fmt)
@classmethod
def parse(cls, desc_w_checksum):
# remove garbage
desc_w_checksum = parse_desc_str(desc_w_checksum)
# check correct checksum
desc, checksum = cls.checksum_check(desc_w_checksum)
is_sorted = "sortedmulti(" in desc
rplc = "sortedmulti(" if is_sorted else "multi("
# wrapped segwit
if desc.startswith("sh(wsh("+rplc):
addr_fmt = AF_P2WSH_P2SH
tmp_desc = desc.replace("sh(wsh("+rplc, "")
tmp_desc = tmp_desc.rstrip(")))")
# native segwit
elif desc.startswith("wsh("+rplc):
addr_fmt = AF_P2WSH
tmp_desc = desc.replace("wsh("+rplc, "")
tmp_desc = tmp_desc.rstrip("))")
# legacy
elif desc.startswith("sh("+rplc):
addr_fmt = AF_P2SH
tmp_desc = desc.replace("sh("+rplc, "")
tmp_desc = tmp_desc.rstrip("))")
else:
raise ValueError("Unsupported descriptor. Supported: sh(), sh(wsh()), wsh().")
splitted = tmp_desc.split(",")
M, keys = int(splitted[0]), splitted[1:]
N = int(len(keys))
if M > N:
raise ValueError("M must be <= N: got M=%d and N=%d" % (M, N))
res_keys = []
for key in keys:
koi, key = cls.parse_key_orig_info(key)
if key[0:4] not in ["tpub", "xpub"]:
raise ValueError("Only extended public keys are supported")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
res_keys.append((xfp, origin_deriv, xpub))
return cls(M=M, N=N, keys=res_keys, addr_fmt=addr_fmt, is_sorted=is_sorted)
def _serialize(self, internal=False, int_ext=False):
"""Serialize without checksum"""
desc_base = MULTI_FMT_TO_SCRIPT[self.addr_fmt]
_type = "sortedmulti" if self.is_sorted else "multi"
_type += "(%s)"
desc_base = desc_base % _type
assert len(self.keys) == self.N
inner = str(self.M) + "," + ",".join(
self.serialize_keys(internal=internal, int_ext=int_ext))
return desc_base % (inner)
def pretty_serialize(self):
"""Serialize in pretty and human-readable format"""
_type = "sortedmulti" if self.is_sorted else "multi"
res = "# Coldcard descriptor export\n"
if self.is_sorted:
res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n"
else:
res += ("# !!! DANGER: order of keys in descriptor MUST be preserved. "
"Correct order of keys is required to compose valid redeem/witness script.\n")
if self.addr_fmt == AF_P2SH:
res += "# bare multisig - p2sh\n"
res += "sh("+_type+"(\n%s\n))"
# native segwit
elif self.addr_fmt == AF_P2WSH:
res += "# native segwit - p2wsh\n"
res += "wsh("+_type+"(\n%s\n))"
# wrapped segwit
elif self.addr_fmt == AF_P2WSH_P2SH:
res += "# wrapped segwit - p2sh-p2wsh\n"
res += "sh(wsh(" + _type + "(\n%s\n)))"
else:
raise ValueError("Malformed descriptor")
assert len(self.keys) == self.N
inner = "\t" + "# %d of %d (%s)\n" % (
self.M, self.N,
"requires all participants to sign" if self.M == self.N else "threshold")
inner += "\t" + str(self.M) + ",\n"
ser_keys = self.serialize_keys()
for i, key_str in enumerate(ser_keys, start=1):
if i == self.N:
inner += "\t" + key_str
else:
inner += "\t" + key_str + ",\n"
checksum = self.serialize().split("#")[1]
return (res % inner) + "#" + checksum
# EOF

View File

@ -149,12 +149,6 @@ class Display:
self.text(-2, 21, 'D', font=FontTiny, invert=1)
self.text(-2, 28, 'E', font=FontTiny, invert=1)
self.text(-2, 35, 'V', font=FontTiny, invert=1)
elif version.is_edge:
self.dis.fill_rect(128 - 6, 19, 5, 26, 1)
self.text(-2, 20, 'E', font=FontTiny, invert=1)
self.text(-2, 27, 'D', font=FontTiny, invert=1)
self.text(-2, 33, 'G', font=FontTiny, invert=1)
self.text(-2, 39, 'E', font=FontTiny, invert=1)
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".

View File

@ -124,8 +124,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
msg = "Password Index?" if picked == 7 else "Index Number?"
index = await ux_enter_bip32_index(msg, unlimited=settings.get("b85max", False))
if index is None:
return
if index is None: return
dis.fullscreen("Working...")
new_secret, width, s_mode, path = bip85_derive(picked, index)
@ -292,7 +291,7 @@ async def password_entry(*args, **kwargs):
while True:
the_ux.pop()
index = await ux_enter_bip32_index("Password Index?", can_cancel=True)
index = await ux_enter_bip32_index("Password Index?")
if index is None:
break

View File

@ -5,11 +5,11 @@
import stash, chains, version, ujson, ngu
from uio import StringIO
from ucollections import OrderedDict
from utils import xfp2str, swab32, problem_file_line
from utils import xfp2str, swab32
from ux import ux_show_story, import_export_prompt
from glob import settings
from msgsign import write_sig_file
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR
from ownership import OWNERSHIP
from exceptions import QRTooBigError
@ -55,10 +55,7 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
# len() is O(1)
no_qr = not version.has_qwerty and (len(contents) >= MAX_V11_CHAR_LIMIT)
if addr_fmt == AF_P2TR:
sig = None
else:
sig = not (derive is None and addr_fmt is None)
sig = not (derive is None and addr_fmt is None)
ch = direct_way # set it to direct way only once, outside the loop
while True:
@ -99,7 +96,7 @@ async def export_contents(title, contents, fname_pattern, derive=None, addr_fmt=
except CardMissingError:
await needs_microsd()
except Exception as e:
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
await ux_show_story('Failed to write!\n\n' + str(e))
# both exceptions & success gets here
if no_qr and (NFC is None) and (VD is None) and not force_prompt:
@ -174,7 +171,7 @@ be needed for different systems.
node = sv.derive_path(hard_sub, register=False)
yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node)))
if addr_fmt not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
if addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
yield ("%s => %s ##SLIP-132##\n" % (
hard_sub, chain.serialize_public(node, addr_fmt)))
@ -191,12 +188,18 @@ be needed for different systems.
yield ('\n\n')
from wallet import MiniScriptWallet
if MiniScriptWallet.exists():
yield '\n# Your Multisig/Miniscript Wallets\n\n'
from multisig import MultisigWallet
if MultisigWallet.exists():
yield '\n# Your Multisig Wallets\n\n'
for msc in MiniScriptWallet.iter_wallets():
yield msc.to_string() + "\n---\n"
for ms in MultisigWallet.get_all():
fp = StringIO()
ms.render_export(fp)
print("\n---\n", file=fp)
yield fp.getvalue()
del fp
async def make_summary_file(fname_pattern='public.txt'):
@ -220,11 +223,10 @@ async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.tx
# make the data
examples = []
imp_multi, imp_desc, imp_desc_tr = generate_bitcoin_core_wallet(account_num, examples)
imp_multi, imp_desc = generate_bitcoin_core_wallet(account_num, examples)
imp_multi = ujson.dumps(imp_multi)
imp_desc = ujson.dumps(imp_desc)
imp_desc_tr = ujson.dumps(imp_desc_tr)
body = '''\
# Bitcoin Core Wallet Import File
@ -240,10 +242,7 @@ Wallet operates on blockchain: {nb}
The following command can be entered after opening Window -> Console
in Bitcoin Core, or using bitcoin-cli:
p2wpkh:
importdescriptors '{imp_desc}'
p2tr:
importdescriptors '{imp_desc_tr}'
importdescriptors '{imp_desc}'
> **NOTE** If your UTXO was created before generating `importdescriptors` command, you should adjust the value of `timestamp` before executing command in bitcoin core.
By default it is set to `now` meaning do not rescan the blockchain. If approximate time of UTXO creation is known - adjust `timestamp` from `now` to UNIX epoch time.
@ -258,15 +257,13 @@ importmulti '{imp_multi}'
## Resulting Addresses (first 3)
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, imp_desc_tr=imp_desc_tr,
xfp=xfp, nb=chains.current_chain().name)
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, xfp=xfp, nb=chains.current_chain().name)
body += '\n'.join('%s => %s' % t for t in examples)
body += '\n'
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
ch = chains.current_chain()
derive = "84h/{coin_type}h/{account}h".format(account=account_num, coin_type=ch.b44_cointype)
@ -275,63 +272,44 @@ importmulti '{imp_multi}'
def generate_bitcoin_core_wallet(account_num, example_addrs):
# Generate the data for an RPC command to import keys into Bitcoin Core
# - yields dicts for json purposes
from descriptor import Descriptor, ExtendedKey
from descriptor import Descriptor
chain = chains.current_chain()
derive_v0 = "84h/{coin_type}h/{account}h".format(
account=account_num, coin_type=chain.b44_cointype
)
derive_v1 = "86h/{coin_type}h/{account}h".format(
account=account_num, coin_type=chain.b44_cointype
)
derive = "84h/{coin_type}h/{account}h".format(account=account_num,
coin_type=chain.b44_cointype)
with stash.SensitiveValues() as sv:
prefix = sv.derive_path(derive_v0)
xpub_v0 = chain.serialize_public(prefix)
prefix = sv.derive_path(derive)
xpub = chain.serialize_public(prefix)
for i in range(3):
sp = '0/%d' % i
node = sv.derive_path(sp, master=prefix)
a = chain.address(node, AF_P2WPKH)
example_addrs.append(('m/%s/%s' % (derive_v0, sp), a))
with stash.SensitiveValues() as sv:
prefix = sv.derive_path(derive_v1)
xpub_v1 = chain.serialize_public(prefix)
for i in range(3):
sp = '0/%d' % i
node = sv.derive_path(sp, master=prefix)
a = chain.address(node, AF_P2TR)
example_addrs.append(('m/%s/%s' % (derive_v1, sp), a))
example_addrs.append( ('m/%s/%s' % (derive, sp), a) )
xfp = settings.get('xfp')
key0 = ExtendedKey.from_cc_data(xfp, derive_v0, xpub_v0)
desc_v0 = Descriptor(key=key0, addr_fmt=AF_P2WPKH)
key1 = ExtendedKey.from_cc_data(xfp, derive_v1, xpub_v1)
desc_v1 = Descriptor(key=key1, addr_fmt=AF_P2TR)
_, vers, _ = version.get_mpy_version()
OWNERSHIP.note_wallet_used(AF_P2WPKH, account_num)
OWNERSHIP.note_wallet_used(AF_P2TR, account_num)
desc_obj = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=AF_P2WPKH)
# for importmulti
imm_list = [
{
'desc': desc_v0.to_string(external, internal),
'desc': desc_obj.serialize(internal=internal),
'range': [0, 1000],
'timestamp': 'now',
'internal': internal,
'keypool': True,
'watchonly': True
}
for external, internal in [(True, False), (False, True)]
for internal in [False, True]
]
# for importdescriptors
imd_list = desc_v0.bitcoin_core_serialize()
imd_list_v1 = desc_v1.bitcoin_core_serialize()
return imm_list, imd_list, imd_list_v1
imd_list = desc_obj.bitcoin_core_serialize()
return imm_list, imd_list
def generate_wasabi_wallet():
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
@ -391,7 +369,7 @@ def generate_unchained_export(account_num=0):
def generate_generic_export(account_num=0):
# Generate data that other programers will use to import Coldcard (single-signer)
from descriptor import Descriptor, ExtendedKey
from descriptor import Descriptor, multisig_descriptor_template
chain = chains.current_chain()
master_xfp = settings.get("xfp")
@ -405,14 +383,12 @@ def generate_generic_export(account_num=0):
with stash.SensitiveValues() as sv:
# each of these paths would have /{change}/{idx} in usage (not hardened)
for name, deriv, fmt, atype, is_ms in [
('bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False),
('bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False), # was "p2wpkh-p2sh"
('bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False),
('bip86', "m/86h/{ct}h/{acc}h", AF_P2TR, 'p2tr', False),
('bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True),
('bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True),
('bip48_3', "m/48h/{ct}h/{acc}h/3h", AF_P2TR, 'p2tr', True),
('bip45', "m/45h", AF_P2SH, 'p2sh', True),
( 'bip44', "m/44h/{ct}h/{acc}h", AF_CLASSIC, 'p2pkh', False ),
( 'bip49', "m/49h/{ct}h/{acc}h", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh"
( 'bip84', "m/84h/{ct}h/{acc}h", AF_P2WPKH, 'p2wpkh', False ),
( 'bip48_1', "m/48h/{ct}h/{acc}h/1h", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ),
( 'bip48_2', "m/48h/{ct}h/{acc}h/2h", AF_P2WSH, 'p2wsh', True ),
( 'bip45', "m/45h", AF_P2SH, 'p2sh', True ),
]:
if fmt == AF_P2SH and account_num:
continue
@ -421,25 +397,24 @@ def generate_generic_export(account_num=0):
node = sv.derive_path(dd)
xfp = xfp2str(swab32(node.my_fp()))
xp = chain.serialize_public(node, AF_CLASSIC)
zp = chain.serialize_public(node, fmt) if fmt not in (AF_CLASSIC, AF_P2TR) else None
key = ExtendedKey.from_cc_data(master_xfp, dd, xp)
key_exp = key.to_string(external=False, internal=False)
zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None
if is_ms:
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
else:
desc = Descriptor(keys=[(master_xfp, dd, xp)], addr_fmt=fmt).serialize(int_ext=True)
OWNERSHIP.note_wallet_used(fmt, account_num)
rv[name] = OrderedDict(name=atype,
xfp=xfp,
deriv=dd,
xpub=xp,
key_exp=key_exp)
desc=desc)
if zp and zp != xp:
rv[name]['_pub'] = zp
if not is_ms:
desc_obj = Descriptor(key=key, addr_fmt=fmt)
rv[name]['desc'] = desc_obj.to_string()
OWNERSHIP.note_wallet_used(fmt, account_num)
# bonus/check: first non-change address: 0/0
node.derive(0, False).derive(0, False)
rv[name]['first'] = chain.address(node, fmt)
@ -491,7 +466,7 @@ def generate_electrum_wallet(addr_type, account_num):
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
fname_pattern="descriptor.txt", direct_way=None):
from descriptor import Descriptor, ExtendedKey
from descriptor import Descriptor
from glob import dis
dis.fullscreen('Generating...')
@ -504,28 +479,25 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
OWNERSHIP.note_wallet_used(addr_type, account_num)
derive = "m/{mode}h/{coin_type}h/{account}h".format(
mode=mode, account=account_num, coin_type=chain.b44_cointype
)
derive = "m/{mode}h/{coin_type}h/{account}h".format(mode=mode,
account=account_num, coin_type=chain.b44_cointype)
dis.progress_bar_show(0.2)
with stash.SensitiveValues() as sv:
dis.progress_bar_show(0.3)
xpub = chain.serialize_public(sv.derive_path(derive))
dis.progress_bar_show(0.7)
key = ExtendedKey.from_cc_data(xfp, derive, xpub)
desc = Descriptor(key=key, addr_fmt=addr_type)
desc = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=addr_type)
dis.progress_bar_show(0.8)
if int_ext:
# with <0;1> notation
body = desc.to_string()
body = desc.serialize(int_ext=True)
else:
# external descriptor
# internal descriptor
body = "%s\n%s" % (
desc.to_string(internal=False),
desc.to_string(external=False),
desc.serialize(internal=False),
desc.serialize(internal=True),
)
dis.progress_bar_show(1)

View File

@ -9,7 +9,7 @@ from glob import settings
from actions import *
from choosers import *
from mk4 import dev_enable_repl
from wallet import make_miniscript_menu, import_miniscript_nfc
from multisig import make_multisig_menu, import_multisig_nfc
from seed import make_ephemeral_seed_menu, make_seed_vault_menu, start_b39_pw
from address_explorer import address_explore
from drv_entro import drv_entro_start, password_entry
@ -20,11 +20,10 @@ from paper import make_paper_wallet
from trick_pins import TrickPinMenu
from tapsigner import import_tapsigner_backup_file
from ccc import toggle_ccc_feature, sssp_spending_policy, sssp_feature_menu
from wif import WIFStore
from wif import WIFStoreMenu
# useful shortcut keys
from charcodes import KEY_QR, KEY_NFC
from public_constants import AF_P2WPKH_P2SH, AF_P2WPKH
# Optional feature: HSM, depends on hardware
@ -105,7 +104,7 @@ def hsm_available():
def qr_and_ms():
# has QR scanner, and at least one MS wallet
if not version.has_qr: return False
return bool(settings.get('miniscript', False))
return bool(settings.get('multisig', False))
def has_pushtx_url():
# they want to use PushTX feature
@ -185,8 +184,8 @@ SettingsMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Login Settings', menu=LoginPrefsMenu),
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
NonDefaultMenuItem('Multisig/Miniscript', 'miniscript',
menu=make_miniscript_menu, predicate=has_secrets, shortcut="m"),
NonDefaultMenuItem('Multisig Wallets', 'multisig',
menu=make_multisig_menu, predicate=has_secrets, shortcut='m'),
NonDefaultMenuItem('NFC Push Tx', 'ptxurl', menu=pushtx_setup_menu),
MenuItem('Display Units', chooser=value_resolution_chooser),
MenuItem('Max Network Fee', chooser=max_fee_chooser),
@ -216,7 +215,6 @@ XpubExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
MenuItem("Taproot/P2TR"+("(BIP-86)" if version.has_qwerty else "(86)"), f=export_xpub, arg=86),
MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49),
MenuItem("Master XPUB", f=export_xpub, arg=0),
MenuItem("Current XFP", f=export_xpub, arg=-1),
@ -257,7 +255,7 @@ FileMgmtMenu = [
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
MenuItem('Teleport Multisig/Miniscript PSBT', predicate=qr_and_has_secrets, f=kt_send_file_psbt),
MenuItem('Teleport Multisig PSBT', predicate=qr_and_has_secrets, f=kt_send_file_psbt),
MenuItem('List Files', f=list_files),
MenuItem('Verify Sig File', f=verify_sig_file),
MenuItem('NFC File Share', predicate=nfc_enabled, f=nfc_share_file, shortcut=KEY_NFC),
@ -395,7 +393,7 @@ NFCToolsMenu = [
MenuItem('Verify Sig File', f=nfc_sign_verify),
MenuItem('Verify Address', f=nfc_address_verify),
MenuItem('File Share', f=nfc_share_file),
MenuItem('Import Miniscript', f=import_miniscript_nfc),
MenuItem('Import Multisig', f=import_multisig_nfc),
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=has_pushtx_url),
]
@ -427,7 +425,7 @@ AdvancedNormalMenu = [
MenuItem("Key Teleport (start)", f=kt_start_rx, predicate=version.has_qr),
MenuItem("Spending Policy", menu=SpendingPolicySubMenu,shortcut='s',predicate=has_real_secret),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('WIF Store', menu=WIFStore.make_menu),
MenuItem('WIF Store', menu=WIFStoreMenu.make),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu, shortcut=KEY_NFC),
MenuItem("Danger Zone", menu=DangerZoneMenu, shortcut='z'),
]
@ -490,7 +488,7 @@ NormalSystem = [
MenuItem("Address Explorer", menu=address_explore, shortcut='x'),
MenuItem('Secure Notes & Passwords', menu=make_notes_menu, shortcut='n',
predicate=lambda: version.has_qwerty and settings.get("secnap", False)),
MenuItem('Type Passwords', f=password_entry, shortcut='t',
MenuItem('Type Passwords', f=password_entry, shortcut='e',
predicate=lambda: settings.get("emu", False) and has_secrets()),
MenuItem('Seed Vault', menu=make_seed_vault_menu, shortcut='v',
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
@ -549,12 +547,12 @@ HobbledAdvancedMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("File Management", menu=HobbledFileMgmtMenu),
MenuItem('Export Wallet', menu=WalletExportMenu, shortcut='x'), # also inside FileMgmt
MenuItem('Teleport Multisig/Miniscript PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
MenuItem('Teleport Multisig PSBT', predicate=qr_and_ms, f=kt_send_file_psbt),
MenuItem("View Identity", f=view_ident),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu, predicate=sssp_related_keys),
MenuItem('Paper Wallets', f=make_paper_wallet),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=HobbledNFCToolsMenu, shortcut=KEY_NFC),
MenuItem('WIF Store', menu=WIFStore.make_menu, predicate=sssp_related_keys),
MenuItem('WIF Store', menu=WIFStoreMenu.make, predicate=sssp_related_keys),
MenuItem('Show %s Version' % ("Firmware" if version.has_qwerty else "FW"), f=show_version),
MenuItem("Destroy Seed", f=clear_seed, predicate=has_real_secret),
]

View File

@ -29,9 +29,4 @@ NFC = None
# QR scanner (Q1 only)
SCAN = None
# Multisig/Miniscript descriptor cache
# mapping from unique wallet name to Descriptor object
# cache size = 1
DESC_CACHE = {}
# EOF

View File

@ -5,14 +5,15 @@
# Unattended signing of transactions and messages, subject to a set of rules.
#
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path, keypath_to_str
from utils import cleanup_payment_address
from pincodes import AE_LONG_SECRET_LEN
from stash import blank_object
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
from public_constants import MAX_USERNAME_LEN
from wallet import MiniScriptWallet
from multisig import MultisigWallet
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from uhashlib import sha256
from ucollections import OrderedDict
from files import CardSlot, CardMissingError
@ -87,13 +88,13 @@ def pop_list(j, fld_name, cleanup_fcn=None):
else:
return []
def pop_deriv_list(j, fld_name, extra_vals=None):
def pop_deriv_list(j, fld_name, extra_val=None):
# expect a list of derivation paths, but also 'any' meaning accept all
# - maybe also 'p2sh' as special value
# - also, path can have n
def cu(s):
if extra_vals and s.lower() in extra_vals:
return s.lower()
if s.lower() == 'any': return s.lower()
if extra_val and s.lower() == extra_val: return s.lower()
try:
return cleanup_deriv_path(s, allow_star=True)
except:
@ -178,7 +179,7 @@ class ApprovalRule:
# - users: list of authorized users
# - min_users: how many of those are needed to approve
# - local_conf: local user must also confirm w/ code
# - wallet: which miniscript wallet to restrict to, or '1' for single signer only
# - wallet: which multisig wallet to restrict to, or '1' for single signer only
# - min_pct_self_transfer: minimum percentage of own input value that must go back to self
# - patterns: list of transaction patterns to check for. Valid values:
# * EQ_NUM_INS_OUTS: the number of inputs and outputs must be equal
@ -219,10 +220,10 @@ class ApprovalRule:
# redundant w/ code in pop_int() above
assert 1 <= self.min_users <= len(self.users), "range"
# if specified, 'wallet' must be an existing miniscript wallet's name
# if specified, 'wallet' must be an existing multisig wallet's name
if self.wallet and self.wallet != '1':
msc_names = [msc.name for msc in MiniScriptWallet.iter_wallets()]
assert self.wallet in msc_names, "unknown wallet: " + self.wallet
names = [ms.name for ms in MultisigWallet.get_all()]
assert self.wallet in names, "unknown MS wallet: "+self.wallet
# patterns must be valid
for p in self.patterns:
@ -266,9 +267,9 @@ class ApprovalRule:
rv = 'Any amount'
if self.wallet == '1':
rv += ' (singlesig only)'
rv += ' (non multisig)'
elif self.wallet:
rv += ' from miniscript wallet "%s"' % self.wallet
rv += ' from multisig wallet "%s"' % self.wallet
if self.users:
rv += ' may be authorized by '
@ -309,11 +310,12 @@ class ApprovalRule:
# Does this rule apply to this PSBT file?
if self.wallet:
# rule limited to one wallet
if psbt.active_miniscript:
assert self.wallet == psbt.active_miniscript.name, 'wrong miniscript wallet'
if psbt.active_multisig:
# if multisig signing, might need to match specific wallet name
assert self.wallet == psbt.active_multisig.name, 'wrong wallet'
else:
# not miniscript, but does this rule apply to all wallets or single-singers
assert self.wallet == '1', 'singlesig only'
# non multisig, but does this rule apply to all wallets or single-singers
assert self.wallet == '1', 'not multisig'
if self.max_amount is not None:
assert total_out <= self.max_amount, 'amount exceeded'
@ -349,7 +351,7 @@ class ApprovalRule:
# we are verifying the whole consensus-encoded txout
txo_bytes = CTxOut(txo.nValue, txo.scriptPubKey).serialize()
digest = chain.hash_message(txo_bytes)
addr_fmt, pubkey = chains.verify_recover_pubkey(psbt.get(o.attestation), digest)
addr_fmt, pubkey = chains.verify_recover_pubkey(o.attestation, digest)
# we have extracted a valid pubkey from the sig, but is it
# a whitelisted pubkey or something else?
ver_addr = chain.pubkey_to_address(pubkey, addr_fmt)
@ -372,11 +374,11 @@ class ApprovalRule:
# check the self-transfer percentage
if self.min_pct_self_transfer:
own_in_value = sum([i.amount for i in psbt.inputs if i.sp_idxs])
own_in_value = sum([i.amount for i in psbt.inputs if i.num_our_keys])
own_out_value = 0
for idx, txo in psbt.output_iter():
o = psbt.outputs[idx]
if o.sp_idxs:
if o.num_our_keys:
own_out_value += txo.nValue
percentage = (float(own_out_value) / own_in_value) * 100.0
assert percentage >= self.min_pct_self_transfer, 'does not meet self transfer threshold, expected: %.2f, actual: %.2f' % (self.min_pct_self_transfer, percentage)
@ -387,8 +389,8 @@ class ApprovalRule:
assert len(psbt.inputs) == len(psbt.outputs), 'unequal number of inputs and outputs'
if "EQ_NUM_OWN_INS_OUTS" in self.patterns:
own_ins = sum([1 for i in psbt.inputs if i.sp_idxs])
own_outs = sum([1 for o in psbt.outputs if o.sp_idxs])
own_ins = sum([1 for i in psbt.inputs if i.num_our_keys])
own_outs = sum([1 for o in psbt.outputs if o.num_our_keys])
assert own_ins == own_outs, 'unequal number of own inputs and outputs'
if "EQ_OUT_AMOUNTS" in self.patterns:
@ -486,9 +488,9 @@ class HSMPolicy:
self.warnings_ok = pop_bool(j, 'warnings_ok')
# a list of paths we can accept for signing
self.msg_paths = pop_deriv_list(j, 'msg_paths', ['any'])
self.share_xpubs = pop_deriv_list(j, 'share_xpubs', ['any'])
self.share_addrs = pop_deriv_list(j, 'share_addrs', ['any', 'msas'])
self.msg_paths = pop_deriv_list(j, 'msg_paths')
self.share_xpubs = pop_deriv_list(j, 'share_xpubs')
self.share_addrs = pop_deriv_list(j, 'share_addrs', 'p2sh')
# free text shown at top
self.notes = pop_string(j, 'notes', 1, 80)
@ -573,7 +575,7 @@ class HSMPolicy:
fd.write('\n')
def plist(pl):
remap = {'any': '(any path)', 'msas': '(any miniscript)' }
remap = {'any': '(any path)', 'p2sh': '(any P2SH)' }
return ' OR '.join(remap.get(i, i) for i in pl)
fd.write('\nMessage signing:\n')
@ -654,6 +656,15 @@ class HSMPolicy:
assert not glob.hsm_active
glob.hsm_active = self
# HSM is the locked-down operating mode: shut down peripherals
# that enlarge the USB-stack interaction surface.
# - VDisk: MSC bulk OUT and HID OUT share the STM32 OTG_FS RX FIFO;
# under load this can wedge the HID OUT endpoint permanently
if glob.VD is not None:
glob.VD.shutdown()
if glob.NFC is not None:
glob.NFC.shutdown()
self.start_time = utime.ticks_ms()
if new_file:
@ -796,14 +807,14 @@ class HSMPolicy:
return match_deriv_path(self.share_xpubs, subpath)
def approve_address_share(self, subpath=None, miniscript=False):
def approve_address_share(self, subpath=None, is_p2sh=False):
# Are we allowing "show address" requests over USB?
if not self.share_addrs:
return False
if miniscript:
return ('msas' in self.share_addrs)
if is_p2sh:
return ('p2sh' in self.share_addrs)
return match_deriv_path(self.share_addrs, subpath)
@ -871,17 +882,39 @@ class HSMPolicy:
# do this super early so always cleared even if other issues
local_ok = self.consume_local_code(psbt_sha)
if not self.rules:
raise ValueError("no txn signing allowed")
# reject anything with warning, probably
if psbt.warnings:
print(psbt.warnings)
if self.warnings_ok:
log.info("Txn has warnings, but policy is to accept anyway.")
else:
raise ValueError("has %d warning(s)" % len(psbt.warnings))
if psbt.por322:
if not self.msg_paths:
raise ValueError("Message signing not permitted")
for inp in psbt.inputs:
if not inp.required_key:
continue
if inp.is_multisig:
paths = [
keypath_to_str(inp.subpaths[pk])
for pk in inp.required_key
if pk in inp.subpaths
]
else:
paths = [keypath_to_str(inp.subpaths[inp.required_key])]
if not any(match_deriv_path(self.msg_paths, p) for p in paths):
raise ValueError("Message signing not enabled for that path")
self.approve(log, "BIP-322 message signing allowed")
return 'y'
if not self.rules:
raise ValueError("no txn signing allowed")
# See who has entered creditials already (all must be valid).
users = []
for u, (token, counter) in auth.items():
@ -977,7 +1010,7 @@ def hsm_status_report():
rv['approval_wait'] = True
rv['users'] = Users.list()
rv['wallets'] = [msc.name for msc in MiniScriptWallet.iter_wallets()]
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()]
rv['chain'] = settings.get('chain', 'BTC')

View File

@ -59,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
msg = '''Last chance. You are defining a new policy which \
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
Policy hash:\n%s\n\n
Press %s to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
Press (%s) to save policy and enable HSM mode.''' % (self.policy.hash(), confirm_char)
ch = await ux_show_story(msg, title=self.title,
escape='x'+confirm_char, strict_escape=True)
@ -297,7 +297,7 @@ class hsmUxInteraction:
# replacements for display.py:Display functions
def hack_fullscreen(self, msg, percent=None, **kwargs):
def hack_fullscreen(self, msg, percent=None):
self.draw_busy(msg, percent)
def hack_progress_bar(self, percent):
self.draw_busy(None, percent)

View File

@ -134,7 +134,7 @@ class FullKeyboard(NumpadBase):
if self._history[kn] == NUM_SAMPLES:
self.is_pressed[kn] = 1
new_presses.add(kn)
elif self._history[i] == 0:
elif self._history[kn] == 0:
self.is_pressed[kn] = 0
self._history[kn] = 0

View File

@ -642,10 +642,10 @@ class Display:
def _draw_addr(self, y, addr, prev_x=None):
# Draw a single-line of an address
# - use prev_x=0 to start centered
# - use prev_x=0 to start centered
if prev_x is None:
# left justify (for stories)
prev_x = x = 1
prev_x = x = 1
elif prev_x == 0:
# center first line, following line(s) will be left-justified to match that
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
@ -734,7 +734,9 @@ class Display:
lines = self.handle_qr_msg(msg, max_lines=True)
self.draw_qr_lines(lines, False)
self.draw_qr_idx_hint(idx_hint)
if idx_hint:
self.draw_qr_idx_hint(idx_hint)
self.show()
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,

View File

@ -6,7 +6,7 @@ freeze_as_mpy('', [
'address_explorer.py',
'auth.py',
'backups.py',
'bsms.py',
'block_height.py',
'callgate.py',
'ccc.py',
'chains.py',
@ -14,9 +14,6 @@ freeze_as_mpy('', [
'compat7z.py',
'countdowns.py',
'descriptor.py',
'desc_utils.py',
'dev_helper.py',
'display.py',
'drv_entro.py',
'exceptions.py',
'export.py',
@ -29,7 +26,6 @@ freeze_as_mpy('', [
'login.py',
'main.py',
'menu.py',
"miniscript.py",
'mk4.py',
'msgsign.py',
'multisig.py',
@ -41,7 +37,6 @@ freeze_as_mpy('', [
'ownership.py',
'paper.py',
'pincodes.py',
'precomp_tag_hash.py',
'psbt.py',
'psram.py',
'pwsave.py',
@ -52,7 +47,6 @@ freeze_as_mpy('', [
'selftest.py',
'serializations.py',
'sffile.py',
'ssd1306.py',
'stash.py',
'tapsigner.py',
'trick_pins.py',

View File

@ -1,5 +1,6 @@
# Mk4 only files; would not be needed on Mk3 or earlier.
freeze_as_mpy('', [
'display.py',
'hsm.py',
'hsm_ux.py',
'mempad.py',

View File

@ -290,7 +290,7 @@ class MenuSystem:
dis.clear()
cursor_y = None
for n in range(self.ypos+PER_M+1):
for n in range(PER_M+1):
real_idx = n+self.ypos
if real_idx >= self.count: break

File diff suppressed because it is too large Load Diff

View File

@ -179,14 +179,16 @@ async def msg_sign_ux_get_subpath(addr_fmt):
purpose = chains.af_to_bip44_purpose(addr_fmt)
chain_n = chains.current_chain().b44_cointype
acct = await ux_enter_bip32_index('Account Number:') or 0
acct = await ux_enter_bip32_index('Account Number:')
if acct is None: return
ch = await ux_show_story(title="Change?",
msg="Press (0) to use internal/change address,"
" %s to use external/receive address." % OK, escape="0")
change = 1 if ch == '0' else 0
idx = await ux_enter_bip32_index('Index Number:') or 0
idx = await ux_enter_bip32_index('Index Number:')
if idx is None: return
return "m/%dh/%dh/%dh/%d/%d" % (purpose, chain_n, acct, change, idx)
@ -260,13 +262,13 @@ def write_sig_file(content_list, derive=None, addr_fmt=AF_CLASSIC, pk=None, sig_
return sig_nice
def validate_text_for_signing(text, only_printable=True):
def validate_text_for_signing(text, allow_tab_nl=False):
# Check for some UX/UI traps in the message itself.
# - messages must be short and ascii only. Our charset is limited
# - too many spaces, leading/trailing can be an issue
# MSG_MAX_SPACES = 4 # impt. compared to -=- positioning
result = to_ascii_printable(text, only_printable=only_printable)
text = str(text, "ascii") # handle memoryview coming from USB
result = to_ascii_printable(text, allow_tab_nl=allow_tab_nl)
length = len(result)
assert length >= 2, "msg too short (min. 2)"
@ -313,6 +315,7 @@ def parse_msg_sign_request(data):
if text is None:
raise AssertionError("MSG required")
subpath = data_dict.get("subpath", subpath)
assert isinstance(subpath, str), "subpath"
addr_fmt = data_dict.get("addr_fmt", addr_fmt)
is_json = True
except ValueError:
@ -331,11 +334,13 @@ def parse_msg_sign_request(data):
addr_fmt = addr_fmt_from_subpath(subpath)
if not subpath:
subpath = chains.STD_DERIVATIONS[addr_fmt]
subpath = subpath.format(
coin_type=chains.current_chain().b44_cointype,
account=0, change=0, idx=0
)
try:
subpath = chains.STD_DERIVATIONS[addr_fmt]
subpath = subpath.format(
coin_type=chains.current_chain().b44_cointype,
account=0, change=0, idx=0
)
except: pass
return text, subpath, addr_fmt, is_json
@ -408,9 +413,10 @@ async def ux_sign_msg(txt, approved_cb=None, kill_menu=True):
text, af = item.arg
subpath = await msg_sign_ux_get_subpath(af)
if subpath is None: return
await approve_msg_sign(text, subpath, af, approved_cb=approved_cb,
kill_menu=kill_menu, only_printable=False)
kill_menu=kill_menu, allow_tab_nl=True)
# pick address format
rv = [

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
#
# ndef.py -- NDEF records: making them and parsing them.
#
# - see ../docs/nfc-on-coldcard.md for background.
# - see ../docs/nfc-coldcard.md for background.
# - cross platform file
#
from struct import pack, unpack

View File

@ -226,10 +226,13 @@ class NFCHandler:
self.set_rf_disable(1)
async def share_loop(self, n, **kws):
# Keep one fully-written tag image live until the user exits. Some
# phones perform multiple probes while deciding if a tag is NDEF.
await self.big_write(n.bytes())
while 1:
done = await self.share_start(n, **kws)
if done:
# do not wipe if we are not done
aborted = await self.ux_animation(exit_after_activity=False, **kws)
if aborted:
await self.wipe(kws.get("is_secret", False))
break
@ -401,8 +404,9 @@ class NFCHandler:
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
self.read_dyn(IT_STS_Dyn) # clear interrupt
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None,
is_secret=False):
async def ux_animation(self, allow_enter=True, prompt=None, line2=None,
is_secret=False, exit_after_activity=True,
min_delay=1000):
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
# - similar when "read" and then removed from field
# - return T if aborted by user
@ -428,7 +432,6 @@ class NFCHandler:
# (ms) How long to wait after RF field comes and goes
# - user can press OK during this period if they know they are done
min_delay = (3000 if write_mode else 1000)
while 1:
if dis.has_lcd:
@ -467,7 +470,7 @@ class NFCHandler:
aborted = False
break
if last_activity:
if exit_after_activity and last_activity:
dt = utime.ticks_diff(utime.ticks_ms(), last_activity)
if dt >= min_delay:
# They acheived some RF activity and then nothing for some time, so
@ -484,14 +487,14 @@ class NFCHandler:
# - assumpting is people know what they are scanning
# - x key to abort early, but also self-clears
await self.big_write(ndef_obj.bytes())
return await self.ux_animation(False, **kws)
return await self.ux_animation(**kws)
async def start_nfc_rx(self, **kws):
# Pretend to be a big warm empty tag ready to be stuffed with data
await self.big_write(ndef.CC_WR_FILE)
# wait until something is written
aborted = await self.ux_animation(True, **kws)
aborted = await self.ux_animation(min_delay=3000, **kws)
if aborted: return
# read CCFILE area (header)
@ -519,7 +522,7 @@ class NFCHandler:
await self.wipe(False)
return rv
async def start_psbt_rx(self, miniscript_wallet=None):
async def start_psbt_rx(self):
from auth import psbt_encoding_taster, TXN_INPUT_OFFSET
from auth import UserAuthorizedAction, ApproveTransaction
from ux import the_ux
@ -567,7 +570,7 @@ class NFCHandler:
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, psbt_sha=psbt_sha, input_method="nfc",
output_encoder=output_encoder, miniscript_wallet=miniscript_wallet,
output_encoder=output_encoder
)
# kill any menu stack, and put our thing at the top
the_ux.push(UserAuthorizedAction.active_request)
@ -585,7 +588,7 @@ class NFCHandler:
aborted = await n.share_start(nn, allow_enter=False)
assert not aborted, "Aborted"
async def share_file(self):
# Pick file from SD card and share over NFC...
from actions import file_picker
@ -618,7 +621,7 @@ class NFCHandler:
# it's a txn, and we wrote as hex
data = a2b_hex(data)
else:
assert data[2:8] == bytes(6)
assert data[1:4] == bytes(3)
sha = ngu.hash.sha256s(data)
await self.share_signed_txn(txid, data, len(data), sha)
elif ext == 'psbt':
@ -631,6 +634,29 @@ class NFCHandler:
else:
raise ValueError(ext)
async def import_multisig_nfc(self, *a):
# user is pushing a file downloaded from another CC over NFC
# - would need an NFC app in between for the sneakernet step
# get some data
def f(m):
if len(m) < 70:
return
m = m.decode()
# multi( catches both multi( and sortedmulti(
if 'pub' in m or "multi(" in m:
return m
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
if winner:
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_ephemeral_seed_words_nfc(self, *a):
def f(m):
sm = m.decode().strip().split(" ")
@ -724,10 +750,11 @@ class NFCHandler:
async def verify_address_nfc(self):
# Get an address or complete bip-21 url even and search it... slow.
_, addr, args = await self.read_address()
if addr:
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(addr, args)
res = await self.read_address()
if not res: return
_, addr, args = res
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(addr, args)
async def read_extended_private_key(self):
f = lambda x: x.decode().strip() if b"prv" in x else None
@ -751,15 +778,17 @@ class NFCHandler:
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg)
try:
r = func(msg)
if r is not None:
winner = r
break
except:
pass
try:
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg)
try:
r = func(msg)
if r is not None:
winner = r
break
except:
pass
except Exception: pass # dont crash when given garbage
if not winner:
await ux_show_story(fail_msg)
@ -767,43 +796,4 @@ class NFCHandler:
return winner
async def read_bsms_token(self):
def f(m):
m = m.decode().strip()
try:
int(m, 16)
return m
except: pass
return await self._nfc_reader(f, 'Unable to find BSMS token in NDEF data')
async def read_bsms_data(self):
def f(m):
m = m.decode().strip() # from memory view
try:
if "BSMS" in m or int(m[:6], 16):
# unencrypted/encrypted case
return m
except: pass
return await self._nfc_reader(f, 'Unable to find BSMS data in NDEF data')
async def import_miniscript_nfc(self):
def f(m):
if len(m) < 70: return
m = m.decode()
# TODO this should be Descriptor.is_descriptor() ?
if 'pub' in m:
return m
winner = await self._nfc_reader(f, 'Unable to find miniscript descriptor expected in NDEF')
if not winner:
return
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
# EOF

View File

@ -10,10 +10,11 @@ from ux_q1 import QRScannerInteraction
from actions import goto_top_menu
from glob import settings, dis
from files import CardMissingError, needs_microsd, CardSlot
from public_constants import MSG_SIGNING_MAX_LENGTH
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from charcodes import KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6
from lcd_display import CHARS_W
from utils import problem_file_line, url_unquote, wipe_if_deltamode
from utils import problem_file_line, url_unquote, wipe_if_deltamode, is_printable
# title, username and such are limited that they fit on the one line both in
# text entry (W-2) and also in menu display (W-3)
@ -130,9 +131,7 @@ class NotesMenu(MenuSystem):
else:
wipe_if_deltamode()
rv = []
for note in NoteContent.get_all():
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), menu=note.make_menu))
rv = cls.construct_note_items(readonly=False)
rv.extend(news)
@ -150,16 +149,34 @@ class NotesMenu(MenuSystem):
# When only allowed to view, no export/add new/delete.
wipe_if_deltamode()
rv = []
for note in NoteContent.get_all():
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title),
menu=note.make_menu, arg=True)) # readonly=True
rv = cls.construct_note_items(readonly=True)
if not rv:
rv.append(MenuItem('(none saved yet)'))
return rv
@classmethod
def construct_note_items(cls, readonly=False):
rv = []
by_group = {}
for note in NoteContent.get_all():
item = MenuItem('%d: %s' % (note.idx+1, note.title),
menu=note.make_menu, arg=readonly)
group = note.group
if group:
if group not in by_group:
by_group[group] = []
by_group[group].append(item)
else:
rv.append(item)
for group in sorted(by_group):
rv.append(MenuItem('' + group, menu=NoteGroupMenu(group, readonly)))
return rv
@classmethod
async def export_all(cls, *a):
await start_export(NoteContent.get_all())
@ -211,7 +228,7 @@ class NotesMenu(MenuSystem):
@classmethod
async def disable_notes(cls, *a):
# they don't want feature anymore; already checked no notes in effect
# - no need for confirm, they aren't loosing anything
# - no need for confirm, they aren't losing anything
settings.remove_key('secnap')
settings.remove_key('notes')
settings.save()
@ -230,10 +247,28 @@ class NotesMenu(MenuSystem):
@classmethod
async def drill_to(cls, menu, item):
# make it so looks like we drilled down into the new note
menu.goto_idx(item.idx)
label = '%d: %s' % (item.idx+1, item.title)
group = item.group
if group:
cls.goto_exact_label(menu, '' + group)
gm = NoteGroupMenu(group)
cls.goto_exact_label(gm, label)
the_ux.push(gm)
else:
cls.goto_exact_label(menu, label)
m = await item._make_menu()
the_ux.push(MenuSystem(m))
@staticmethod
def goto_exact_label(menu, label):
for i, mi in enumerate(menu.items):
if mi.label == label:
menu.goto_idx(i)
return True
return False
class NoteContentBase:
def __init__(self, json={}, idx=-1):
@ -249,9 +284,15 @@ class NoteContentBase:
return PasswordContent(j, idx) if 'user' in j else NoteContent(j, idx)
def serialize(self):
return {fld:getattr(self, fld, '') for fld in self.flds}
res = {}
for fld in self.flds:
val = getattr(self, fld, '')
# user field is necessary for proper password identification in constructor
if not val and (fld != "user"):
continue
res[fld] = val
to_json = serialize
return res
@classmethod
def get_all(cls):
@ -277,6 +318,15 @@ class NoteContentBase:
settings.put('notes', [n.serialize() for n in notes])
settings.save()
@classmethod
def get_groups(cls):
groups = set()
for note in cls.get_all():
if note.group:
groups.add(note.group)
return sorted(groups)
async def delete(self, *a):
# Remove note
ok = await ux_confirm("Everything about this note/password will be lost.")
@ -297,6 +347,11 @@ class NoteContentBase:
the_ux.pop()
m = the_ux.top_of_stack()
m.update_contents()
parent = the_ux.parent_of(m)
if parent:
parent.update_contents()
if isinstance(m, NoteGroupMenu) and not m.has_notes():
the_ux.pop()
await ux_dramatic_pause('Deleted.', 3)
@ -334,6 +389,11 @@ class NoteContentBase:
# update parent
parent = the_ux.parent_of(menu)
parent.update_contents()
grandparent = the_ux.parent_of(parent)
if grandparent:
grandparent.update_contents()
if isinstance(parent, NoteGroupMenu) and not parent.has_notes():
the_ux.stack.remove(parent)
else:
menu.update_contents()
@ -363,12 +423,94 @@ class NoteContentBase:
await ux_sign_msg(txt, approved_cb=msg_signing_done, kill_menu=False)
def sign_misc_menu_item(self):
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc)
return MenuItem("Sign Note Text", f=self.sign_txt_msg, arg=self.misc,
predicate=2 <= len(self.misc) <= MSG_SIGNING_MAX_LENGTH)
@staticmethod
def is_b39pass_applicable(data, read_only):
from seed import MAX_PASS_LEN
from ccc import sssp_spending_policy
if read_only and not sssp_spending_policy('okeys'):
return False
return (len(data) <= MAX_PASS_LEN) and is_printable(data) and settings.get("words", True)
async def apply_as_b39_pass(self, a, b, item):
data, readonly = item.arg
# rstrip just trailing whitespaces/tabs/newlines
data = data.rstrip()
# do not allow any more tabs/newlines
assert self.is_b39pass_applicable(data, readonly)
from seed import apply_pass_value
await apply_pass_value(data)
class NoteGroupMenu(MenuSystem):
def __init__(self, group, readonly=False):
self.group = group
self.readonly = readonly
super().__init__(self.construct())
def construct(self):
items = []
for note in NoteContent.get_all():
if note.group == self.group:
items.append(MenuItem('%d: %s' % (note.idx+1, note.title),
menu=note.make_menu, arg=self.readonly))
return items or [MenuItem('(none)')]
def has_notes(self):
return any(note.group == self.group for note in NoteContent.get_all())
def update_contents(self):
self.replace_items(self.construct())
class GroupPickerMenu(MenuSystem):
def __init__(self, current=''):
self.result = None
self.current = current
groups = NoteContentBase.get_groups()
chosen = 0
items = [MenuItem('(none)', f=self.picked, arg='')]
for group in groups:
if group == self.current:
chosen = len(items)
items.append(MenuItem(group, f=self.picked, arg=group))
items.append(MenuItem('New Group', f=self.new_group))
super().__init__(items, chosen=chosen)
async def picked(self, menu, idx, mi):
assert menu == self
self.result = mi.arg
the_ux.pop()
async def new_group(self, menu, idx, mi):
group = await ux_input_text('', max_len=ONE_LINE, confirm_exit=False,
prompt='Group', placeholder='(optional)')
if group is None:
self.result = None
else:
self.result = group
the_ux.pop()
@classmethod
async def pick(cls, current=''):
m = cls(current)
the_ux.push(m)
await m.interact()
return current if m.result is None else m.result
class PasswordContent(NoteContentBase):
# "Passwords" have a few more fields and are more structured
flds = ['title', 'user', 'password', 'site', 'misc' ]
flds = ['title', 'user', 'password', 'site', 'misc', 'group']
type_label = 'password'
async def _make_menu(self, readonly=False):
@ -380,7 +522,7 @@ class PasswordContent(NoteContentBase):
# if self.misc: rv.append(MenuItem('↳ (notes)', f=self.view))
rv += [
MenuItem('View Password', f=self.view_pw),
MenuItem('Send Password', f=self.send_pw, predicate=lambda: settings.get('du', True)),
MenuItem('Send Password', f=self.send_pw, predicate=lambda: not settings.get('du', 0)),
]
if not readonly:
rv += [
@ -395,6 +537,12 @@ class PasswordContent(NoteContentBase):
ShortcutItem(KEY_NFC, f=self.share_nfc, arg=self.type_label),
]
# if password is less than MAX_PASS_LEN and only consist of printable ASCII characters
# and current seed (master or tmp) is word based - offer to apply pwd text as BIP-39 passphrase
if self.is_b39pass_applicable(self.password, readonly):
rv += [MenuItem('Apply as BIP-39 Passphrase',
f=self.apply_as_b39_pass, arg=(self.password, readonly))]
return rv
async def make_menu(self, a, b, item):
@ -468,7 +616,8 @@ class PasswordContent(NoteContentBase):
if self.idx == -1:
# prompt for password only on new records.
self.password = await get_a_password(self.password)
# can be None if CANCEL is pressed - handle, Send Password requires string
self.password = await get_a_password(self.password) or ""
site = await ux_input_text(self.site, max_len=ONE_LINE, scan_ok=True, confirm_exit=False,
prompt='Website', placeholder='(optional)')
@ -480,6 +629,8 @@ class PasswordContent(NoteContentBase):
if misc is None:
misc = self.misc
group = await GroupPickerMenu.pick(self.group)
if self.idx != -1:
# confirm changes, don't for new records
chgs = []
@ -491,6 +642,8 @@ class PasswordContent(NoteContentBase):
chgs.append('Username')
if self.misc != misc:
chgs.append('Other Notes')
if self.group != group:
chgs.append('Group')
if not chgs:
await ux_dramatic_pause('No changes.', 3)
@ -504,6 +657,7 @@ class PasswordContent(NoteContentBase):
self.user = user
self.site = site
self.misc = misc
self.group = group
await self._save_ux(menu)
return self
@ -511,11 +665,12 @@ class PasswordContent(NoteContentBase):
class NoteContent(NoteContentBase):
# Pure "notes" have just a title and free-form text
flds = ['title', 'misc']
flds = ['title', 'misc', 'group']
type_label = 'note'
async def _make_menu(self, readonly=False):
# Details and actions for this Note
rv = [
MenuItem('"%s"' % self.title, f=self.view),
MenuItem('View Note', f=self.view),
@ -526,11 +681,19 @@ class NoteContent(NoteContentBase):
MenuItem('Delete', f=self.delete),
MenuItem('Export', f=self.export),
]
rv += [
self.sign_misc_menu_item(),
ShortcutItem(KEY_QR, f=self.view_qr_menu, arg="misc"),
ShortcutItem(KEY_NFC, f=self.share_nfc, arg='misc'),
]
# if misc is less than MAX_PASS_LEN and only consist of printable ASCII characters
# and current seed (master or tmp) is word based - offer to apply note text as BIP-39 passphrase
if self.is_b39pass_applicable(self.misc, readonly):
rv += [MenuItem('Apply as BIP-39 Passphrase',
f=self.apply_as_b39_pass, arg=(self.misc, readonly))]
return rv
async def make_menu(self, a, b, item):
@ -557,6 +720,8 @@ class NoteContent(NoteContentBase):
if misc is None:
misc = self.misc
group = await GroupPickerMenu.pick(self.group)
if self.idx != -1:
# confirm changes, don't for new records
chgs = []
@ -564,6 +729,8 @@ class NoteContent(NoteContentBase):
chgs.append('Title')
if self.misc != misc:
chgs.append('Note Text')
if self.group != group:
chgs.append('Group')
if not chgs:
await ux_dramatic_pause('No changes.', 3)
@ -576,6 +743,7 @@ class NoteContent(NoteContentBase):
self.title = title
self.misc = misc
self.group = group
await self._save_ux(menu)
@ -664,11 +832,12 @@ async def import_from_other(menu, *a):
records = json.load(open(fn, 'rt'))
# We have some JSON, parsed now.
await import_from_json(records)
ok = await import_from_json(records)
if not ok: return
await ux_dramatic_pause('Saved.', 3)
menu.update_contents()
async def import_from_json(records):
# should dedup, but we aren't
try:
@ -683,6 +852,7 @@ async def import_from_json(records):
settings.set('notes', was)
settings.set('secnap', True)
settings.save()
return True
except Exception as e:
await ux_show_story(title="Failure", msg=str(e) + '\n\n' + problem_file_line(e))

View File

@ -32,8 +32,7 @@ from utils import call_later_ms
# batt_to = (when on battery only) idle timeout period
# _age = internal verison number for data (see below)
# tested = selftest has been completed successfully
# multisig = list of defined multisig wallets (complex) [before removal of MultisigWallet]
# miniscript = list of defined miniscript wallets, including multisig (complex)
# multisig = list of defined multisig wallets (complex)
# pms = trust/import/distrust xpubs found in PSBT files
# fee_limit = (int) percentage of tx value allowed as max fee
# axi = index of last selected address in explorer
@ -64,9 +63,10 @@ from utils import call_later_ms
# b85max = (bool) allow max BIP-32 int value in BIP-85 derivations
# ptxurl = (str) URL for PushTx feature, clear to disable feature
# hmx = (bool) Force display of current XFP in home menu, even w/o tmp seed active
# unsort_ms = (bool) Allow unsorted multisig with BIP-67 disabled
# msas = multisig address show (do not censor multisig addresses)
# ccc = (complex) If present, CCC feature is enabled and key details stored here.
# ktrx = (privkey) Key teleport Rx has been started, this will be our keypair
# aemscsv = (bool) opt-in enable more verbose CSV output for miniscript wallets with Derivations and Scripts
# sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled)
# lfr = (string) If present, the reason why Spending Policy blocked last transaction
# wifs = (list) List of tuples (public/private key)
@ -83,7 +83,7 @@ from utils import call_later_ms
# terms_ok = customer has signed-off on the terms of sale
# settings linked to seed
# LINKED_SETTINGS = ["miniscript", "tp", "ovc", "xfp", "xpub", "words"]
# LINKED_SETTINGS = ["multisig", "tp", "ovc", "xfp", "xpub", "words"]
# settings that does not make sense to copy to temporary secret
# LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"]
# prelogin settings - do not need to be part of other saved settings
@ -91,7 +91,7 @@ from utils import call_later_ms
# keep these settings only if unspecified on the other end
KEEP_IF_BLANK_SETTINGS = ["wa", "sighshchk", "emu", "rz", "b39skip",
"axskip", "del", "pms", "idle_to", "batt_to",
"bright"]
"bright", "msas"]
# key value pairs saved directly to master seed settings
# held in RAM for tmp seed sessions

View File

@ -82,7 +82,7 @@ OP_RETURN = const(106)
#OP_RSHIFT = const(153)
#OP_BOOLAND = const(154)
#OP_BOOLOR = const(155)
OP_NUMEQUAL = const(156)
#OP_NUMEQUAL = const(156)
#OP_NUMEQUALVERIFY = const(157)
#OP_NUMNOTEQUAL = const(158)
#OP_LESSTHAN = const(159)
@ -114,7 +114,6 @@ OP_CHECKMULTISIGVERIFY = const(175)
#OP_NOP8 = const(183)
#OP_NOP9 = const(184)
#OP_NOP10 = const(185)
OP_CHECKSIGADD = const(186)
#OP_NULLDATA = const(252)
#OP_PUBKEYHASH = const(253)
#OP_PUBKEY = const(254)

View File

@ -6,8 +6,9 @@ import os, chains, ngu, struct, version
from glob import settings
from ucollections import namedtuple
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from exceptions import UnknownAddressExplained
from utils import problem_file_line, show_single_address
from utils import problem_file_line, show_single_address, validate_own_address
from public_constants import AFC_SCRIPT, AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH_P2SH, AF_P2TR, AF_P2WSH
# Track many addresses, but in compressed form
@ -50,7 +51,7 @@ class AddressCacheFile:
def __init__(self, wallet, change_idx):
self.wallet = wallet
self.change_idx = change_idx
desc = wallet.to_descriptor().to_string(internal=False)
desc = wallet.to_descriptor().serialize()
h = b2a_hex(ngu.hash.sha256d(wallet.chain.ctype + desc))
self.fname = h[0:32] + '-%d.own' % change_idx
self.salt = h[32:]
@ -157,7 +158,8 @@ class AddressCacheFile:
self.setup(self.change_idx, start_idx)
bonus = None
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count, self.change_idx):
for idx,here,*_ in self.wallet.yield_addresses(start_idx, count,
change_idx=self.change_idx):
self.append(here)
self.count += 1
@ -215,8 +217,7 @@ class OwnershipCache:
def filter(cls, addr_fmt, args):
# Filter possible candidates!
# - if you start w/ testnet, we'll follow that
from wallet import MiniScriptWallet
from glob import dis
from multisig import MultisigWallet
args = args or {}
@ -224,7 +225,7 @@ class OwnershipCache:
named_wal = args.get("wallet", None)
if named_wal:
# quick search without deserialization
res = list(MiniScriptWallet.iter_wallets(name=named_wal))
res = list(MultisigWallet.iter_wallets(name=named_wal))
if not res:
raise UnknownAddressExplained("Wallet '%s' not defined." % named_wal)
@ -232,8 +233,6 @@ class OwnershipCache:
return res
possibles = []
if addr_fmt == AF_P2TR:
possibles.extend([w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2TR])
if addr_fmt & AFC_SCRIPT:
# multisig or script at least... must exist already
afs = [addr_fmt]
@ -247,7 +246,7 @@ class OwnershipCache:
# defined, assume that that's the only p2sh address source.
addr_fmt = AF_P2WPKH_P2SH
possibles.extend(MiniScriptWallet.iter_wallets(addr_fmts=afs))
possibles.extend(MultisigWallet.iter_wallets(addr_fmts=afs))
try:
# Construct possible single-signer wallets, always at least account=0 case
@ -267,7 +266,7 @@ class OwnershipCache:
if not possibles:
# can only happen w/ scripts; for single-signer we have things to check
raise UnknownAddressExplained(
"No suitable multisig/miniscript wallets are currently defined.")
"No suitable multisig wallets are currently defined.")
# ordering here
return possibles
@ -305,11 +304,10 @@ class OwnershipCache:
dis.fullscreen("Wait...")
ch = chains.current_chain()
addr_fmt = ch.possible_address_fmt(addr)
if not addr_fmt:
# might be valid address over on testnet vs mainnet
raise UnknownAddressExplained('That address is not valid on ' + ch.name)
try:
addr, addr_fmt = validate_own_address(addr)
except Exception as e:
raise UnknownAddressExplained('That address is not valid on ' + e.args[0])
matches = OWNERSHIP.filter(addr_fmt, args)
@ -340,11 +338,11 @@ class OwnershipCache:
# nothing found among singlesig & registered multisig wallets
# check WIF store (single sig only)
if addr_fmt not in [AF_P2WSH]:
if addr_fmt not in [AF_P2TR, AF_P2WSH]:
dis.fullscreen("WIF Store...")
from wif import iter_wif_store_addresses
target_af = AF_P2WPKH_P2SH if addr_fmt == AF_P2SH else addr_fmt
for i, store_addr in iter_wif_store_addresses(ch, target_af):
for i, store_addr in iter_wif_store_addresses(target_af):
if store_addr == addr:
return False, ("wif", target_af), i+1
@ -356,30 +354,23 @@ class OwnershipCache:
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
from ux import ux_show_story, show_qr_code
from charcodes import KEY_QR
from wallet import MiniScriptWallet
from multisig import MultisigWallet
from public_constants import AFC_BECH32, AFC_BECH32M
try:
_, wallet, subpath = cls.search(addr, args)
is_complex = isinstance(wallet, MiniScriptWallet)
is_ms = isinstance(wallet, MultisigWallet)
msg = show_single_address(addr)
esc = ""
if isinstance(wallet, tuple) and (wallet[0] == "wif"):
msg += '\n\nFound in WIF store at index %d' % subpath
addr_fmt = wallet[1]
else:
msg += '\n\nFound in wallet:\n' + wallet.name
msg += '\n\nDerivation path:\n'
sp = wallet.render_path(*subpath)
msg += '\n\nFound in wallet:\n ' + wallet.name
msg += '\nDerivation path:\n ' + sp
addr_fmt = wallet.addr_fmt
if hasattr(wallet, "render_path"):
sp = wallet.render_path(*subpath)
msg += sp
else:
sp = None
msg += ".../%d/%d" % subpath
if not is_complex:
if not is_ms:
esc = "0"
msg += "\n\nPress (0) to sign message with this key."
@ -395,12 +386,9 @@ class OwnershipCache:
while 1:
ch = await ux_show_story(msg, title=title, escape=esc, hint_icons=KEY_QR)
if ch in ("1"+KEY_QR):
await show_qr_code(
addr,
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)),
msg=addr, is_addrs=True
)
elif not is_complex and (ch == "0"): # only singlesig
await show_qr_code(addr, msg=addr, is_addrs=True,
is_alnum=(addr_fmt & (AFC_BECH32 | AFC_BECH32M)))
elif not is_ms and (ch == "0"): # only singlesig
from msgsign import sign_with_own_address
await sign_with_own_address(sp, addr_fmt)
else:

View File

@ -3,10 +3,10 @@
#
# paper.py - generate paper wallets, based on random values (not linked to wallet)
#
import ujson, ngu, chains
import ujson
from ubinascii import hexlify as b2a_hex
from utils import imported, problem_file_line
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
from utils import imported
from public_constants import AF_CLASSIC, AF_P2WPKH
from ux import ux_show_story, ux_dramatic_pause
from files import CardSlot, CardMissingError, needs_microsd
from actions import file_picker
@ -29,6 +29,10 @@ can still be made. Visit the Coldcard website to get some interesting templates.
SECP256K1_ORDER = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xaf\x48\xa0\x3b\xbf\xd2\x5e\x8c\xd0\x36\x41\x41"
# Aprox. time of this feature release (Nov 20/2019) so no need to scan
# blockchain earlier than this during "importmulti"
FEATURE_RELEASE_TIME = const(1574277000)
# These very-specific text values are matched on the Coldcard; cannot be changed.
class placeholders:
addr = b'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
@ -47,12 +51,6 @@ class PaperWalletMaker:
self.my_menu = my_menu
self.template_fn = None
self.is_segwit = False
self.is_taproot = False
def atype(self):
if self.is_taproot: return 2, 'Taproot P2TR'
if self.is_segwit: return 1, 'Segwit P2WPKH'
return 0, 'Classic P2PKH'
async def pick_template(self, *a):
fn = await file_picker(suffix='.pdf', min_size=20000, taster=template_taster,
@ -64,17 +62,17 @@ class PaperWalletMaker:
def addr_format_chooser(self, *a):
# simple bool choice
def set(idx, text):
self.is_segwit = idx == 1
self.is_taproot = idx == 2
self.is_segwit = bool(idx)
self.update_menu()
return self.atype()[0], ['Classic P2PKH', 'Segwit P2WPKH', 'Taproot P2TR'], set
return int(self.is_segwit), ['Classic P2PKH', 'Segwit P2WPKH'], set
def update_menu(self):
# Reconstruct the menu contents based on our state.
self.my_menu.replace_items([
MenuItem("Don't make PDF" if not self.template_fn else 'Making PDF',
f=self.pick_template),
MenuItem(self.atype()[1], chooser=self.addr_format_chooser),
MenuItem('Classic P2PKH' if not self.is_segwit else 'Segwit P2WPKH',
chooser=self.addr_format_chooser),
MenuItem('Use Dice', f=self.use_dice),
MenuItem('GENERATE WALLET', f=self.doit),
], keep_position=True)
@ -106,16 +104,12 @@ class PaperWalletMaker:
dis.fullscreen("Rendering...")
# make payment address
ch = chains.current_chain()
digest = hash160(pubkey)
ch = current_chain()
if self.is_segwit:
af = AF_P2WPKH
elif self.is_taproot:
af = AF_P2TR
pubkey = pubkey[1:]
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
else:
af = AF_CLASSIC
addr = ch.pubkey_to_address(pubkey, af)
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
@ -170,10 +164,8 @@ class PaperWalletMaker:
else:
nice_pdf = ''
nice_sig = None
if af != AF_P2TR:
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
nice_sig = write_sig_file(sig_cont, pk=privkey, sig_name=basename,
addr_fmt=AF_P2WPKH if self.is_segwit else AF_CLASSIC)
# Half-hearted attempt to cleanup secrets-contaminated memory
# - better would be force user to reboot
@ -186,14 +178,14 @@ class PaperWalletMaker:
await needs_microsd()
return
except Exception as e:
from utils import problem_file_line
await ux_show_story('Failed to write!\n\n'+problem_file_line(e))
return
story = "Done! Created file(s):\n\n%s" % nice_txt
if nice_pdf:
story += "\n\n%s" % nice_pdf
if nice_sig:
story += "\n\n%s" % nice_sig
story += "\n\n%s" % nice_sig
await ux_show_story(story)
async def use_dice(self, *a):
@ -222,17 +214,10 @@ class PaperWalletMaker:
fp.write('Bitcoin Core command:\n\n')
# new hotness: output descriptors
if self.is_taproot:
desc = 'tr(%s)'
elif self.is_segwit:
desc = 'wpkh(%s)'
else:
desc = 'pkh(%s)'
desc = desc % wif
descriptor = ujson.dumps(dict(timestamp="now", desc=append_checksum(desc)))
fp.write(" bitcoin-cli importdescriptors '[%s]'\n\n" % descriptor)
if not self.is_taproot:
fp.write('# OR (only supported with legacy wallets)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
desc = ('wpkh(%s)' if self.is_segwit else 'pkh(%s)') % wif
multi = ujson.dumps(dict(timestamp=FEATURE_RELEASE_TIME, desc=append_checksum(desc)))
fp.write(" bitcoin-cli importmulti '[%s]'\n\n" % multi)
fp.write('# OR (more compatible, but slower)\n\n bitcoin-cli importprivkey "%s"\n\n' % wif)
if qr_addr and qr_wif:
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')

View File

@ -410,13 +410,9 @@ class PinAttempt:
# Main secret has changed: reset the settings+their key,
# and capture xfp/xpub
# if None is provided as raw_secret -> restore to main seed
import glob
from glob import settings, dis
stash.SensitiveValues.clear_cache()
# invalidate descriptor cache - upon new secret load
glob.DESC_CACHE.clear()
bypass_tmp = False
stash.bip39_passphrase = bool(bip39pw)

View File

@ -1,12 +0,0 @@
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# taproot precomputed tag hashes
#
# SHA256(TapLeaf)
TAP_LEAF_H = b'\xae\xea\x8f\xdcB\x08\x981\x05sKX\x08\x1d\x1e&8\xd3_\x1c\xb5@\x08\xd4\xd3W\xca\x03\xbex\xe9\xee'
# SHA256(TapBranch)
TAP_BRANCH_H = b'\x19A\xa1\xf2\xe5n\xb9_\xa2\xa9\xf1\x94\xbe\\\x01\xf7!o3\xed\x82\xb0\x91F4\x90\xd0[\xf5\x16\xa0\x15'
# SHA256(TapTweak)
TAP_TWEAK_H = b'\xe8\x0f\xe1c\x9c\x9c\xa0P\xe3\xaf\x1b9\xc1C\xc6>B\x9c\xbc\xeb\x15\xd9@\xfb\xb5\xc5\xa1\xf4\xafW\xc5\xe9'
# SHA256(TapSighash)
TAP_SIGHASH_H = b'\xf4\nH\xdfK*p\xc8\xb4\x92K\xf2eFa\xed=\x95\xfdf\xa3\x13\xeb\x87#u\x97\xc6(\xe4\xa01'

File diff suppressed because it is too large Load Diff

View File

@ -25,10 +25,6 @@ class PSRAMWrapper:
return memoryview(self._wr)[offset:offset+ln]
def is_at(self, ptr, offset):
# is bytes() object really one we created at read_at
return uctypes.addressof(ptr) == self.base+offset
# Be compatible with SPIFlash class...
def read(self, address, buf, cmd=None):

View File

@ -57,9 +57,8 @@ SLOW_BAUD = const(9600)
FAST_BAUD = const(57600)
RX_BUF_SIZE = const(4350) # big enough for full v40 decoded
# TODO: constructor should leave it in reset for simple lower-power usage; then after
# login we can do full setup (2+ seconds) and then sleep again until needed.
# TODO: constructor should avoid full setup until after login; after setup,
# command sleep is the known low-power state.
class QRScanner:
def __init__(self):
@ -68,6 +67,8 @@ class QRScanner:
self.scan_light = False # is light on during scanning?
self.version = None
self.setup_done = False
self.needs_reinit = False
self.sleep_seq = 0
# hodl this lock when communicating w/ QR scanner
self.lock = asyncio.Lock()
@ -84,16 +85,21 @@ class QRScanner:
# setup hardware, reset scanner and return time to delay until ready
from machine import UART, Pin
self.serial = UART(2, SLOW_BAUD, rxbuf=RX_BUF_SIZE)
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=0)
self.reset = Pin('QR_RESET', Pin.OUT_OD, value=1)
self.trigger = Pin('QR_TRIG', Pin.OUT_OD, value=1) # wasn't needed
# NOTE: reset is active low (open drain)
self.pulse_reset()
# needs full 2 seconds of recovery time after reset
return 2
def pulse_reset(self):
# RESET is active low (open drain). Keep it as a pulse; module docs
# describe low on this pin as wake-up, so don't use it as parking state.
self.reset(0)
utime.sleep_ms(10)
self.reset(1)
# needs full 2 seconds of recovery time
return 2
self.needs_reinit = False
def set_baud(self, br):
# change serial port baud rate
@ -118,56 +124,104 @@ class QRScanner:
async def setup_task(self, start_delay):
# Task to setup device, and then die.
await asyncio.sleep(start_delay)
async with self.lock:
for attempt in range(3):
await asyncio.sleep(start_delay)
async with self.lock:
try:
await self._configure()
except Exception:
# a step failed or timed out (would have left scanner dead
# until next boot); reset module and start over
await self.blind_shutdown()
if attempt == 2:
break
start_delay = self.reset_stream()
continue
# might need to repeat a few time to get into right state
self.setup_done = True
await self.goto_sleep()
return
self.mark_needs_reinit()
def reset_stream(self):
self.sleep_seq += 1
start_delay = self.hardware_setup()
self.stream = asyncio.StreamReader(self.serial, {})
return start_delay
def mark_needs_reinit(self):
self.setup_done = False
self.version = None
self.needs_reinit = True
if hasattr(self, 'reset'):
self.reset(1)
async def blind_shutdown(self):
for baud in (SLOW_BAUD, FAST_BAUD):
self.set_baud(baud)
await self.tx('S_CMD_020D') # return to "Command mode"
await asyncio.sleep_ms(20)
await self.tx('S_CMD_03L0') # turn off bright light
await asyncio.sleep_ms(20)
await self.tx('SRDF0050') # sleep scanner
await asyncio.sleep_ms(150)
await self.tx('SRDF0050')
await asyncio.sleep_ms(20)
async def _configure(self):
# full config sequence; any step may raise on timeout/framing error
# might need to repeat a few time to get into right state
for retry in range(5):
baud = await self.probe_baud()
if baud: break
else:
#print("QR Scanner: missing")
raise RuntimeError('no contact')
try:
await self.txrx('S_CMD_FFFF') # factory reset of settings
except RuntimeError:
await asyncio.sleep_ms(1000)
for retry in range(5):
baud = await self.probe_baud()
if baud: break
else:
#print("QR Scanner: missing")
return
raise RuntimeError('no contact after S_CMD_FFFF')
await self.txrx('S_CMD_FFFF') # factory reset of settings
# go to high speed!
if baud != FAST_BAUD:
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
self.set_baud(FAST_BAUD)
# go to high speed!
if baud != FAST_BAUD:
await self.txrx('S_CMD_H3BR%d' % FAST_BAUD)
self.set_baud(FAST_BAUD)
# configure it like we want it
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
await self.txrx('S_CMD_MT30') # Same code reading without delay
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
await self.txrx('S_CMD_03L0') # light off all the time by default
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
# configure it like we want it
await self.txrx('S_CMD_MTRS5000') # 5s to read before fail (unused)
await self.txrx('S_CMD_MT11') # trigger is edge-based (not level)
await self.txrx('S_CMD_MT30') # Same code reading without delay
await self.txrx('S_CMD_MT20') # Enable automatic sleep when idle
await self.txrx('S_CMD_MTRF500') # Idle time: 500ms
await self.txrx('S_CMD_059A') # add CR LF after QR data (important)
await self.txrx('S_CMD_03L0') # light off all the time by default
await self.txrx('S_CMD_0407') # turn on signal for our yellow led
# settings under continuous scan mode
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
# settings under continuous scan mode
await self.txrx('S_CMD_MARS0000') # "Modify the duration of single code reading" (ms)
await self.txrx('S_CMD_MARR000') # "Modify the time of the reading interval 0ms"
await self.txrx('S_CMD_MA31') # Enable "Same code reading delay"
await self.txrx('S_CMD_MARI0050') # "Modify the same code reading delay 50ms"
# these aren't useful (yet?) and just make things harder to decode.
#await self.txrx('S_CMD_05F1') # add all information on
#await self.txrx('S_CMD_05L1') # output decoding length info on
#await self.txrx('S_CMD_05S1') # STX start char
#await self.txrx('S_CMD_05C1') # CodeID+prefix
#await self.txrx('S_CMD_0501') # prefix on
#await self.txrx('S_CMD_0506') # suffix
#await self.txrx('S_CMD_05D0') # tx total data
# these aren't useful (yet?) and just make things harder to decode.
#await self.txrx('S_CMD_05F1') # add all information on
#await self.txrx('S_CMD_05L1') # output decoding length info on
#await self.txrx('S_CMD_05S1') # STX start char
#await self.txrx('S_CMD_05C1') # CodeID+prefix
#await self.txrx('S_CMD_0501') # prefix on
#await self.txrx('S_CMD_0506') # suffix
#await self.txrx('S_CMD_05D0') # tx total data
# prevent scanning magic QR to affect settings
await self.txrx('S_CMD_0000') # close setting codes
self.setup_done = True
await self.goto_sleep()
# prevent scanning magic QR to affect settings
await self.txrx('S_CMD_0000') # close setting codes
async def scan_once(self):
# Blocks until something is scanned. Returns it as string
@ -176,6 +230,16 @@ class QRScanner:
# - returns a BBQr object at that point
self.scan_light = False
if self.needs_reinit:
try:
await self.setup_task(self.reset_stream())
if self.setup_done:
await asyncio.sleep_ms(200)
except asyncio.CancelledError:
await self.blind_shutdown()
self.mark_needs_reinit()
return None
# wait for reset process to complete (can be an issue right after boot)
# - few seconds of boot time needed
for retry in range(10):
@ -211,19 +275,22 @@ class QRScanner:
finally:
# Problem: another valid scan can come in just as we are trying
# to get out of scanner mode
for retry in range(10):
for retry in range(3):
try:
await self.txrx('S_CMD_020D') # return to "Command mode"
await self.txrx('S_CMD_03L0') # turn off bright light
await self.txrx('S_CMD_020D', timeout=1000) # return to "Command mode"
await self.txrx('S_CMD_03L0', timeout=1000) # turn off bright light
#print('rest after %d retries' % retry)
break
except: pass
await asyncio.sleep_ms(25)
except Exception:
pass
await asyncio.sleep_ms(50)
else:
pass
#print('reset failed')
await self.blind_shutdown()
self.mark_needs_reinit()
await self.goto_sleep()
if self.setup_done:
await self.goto_sleep()
self.busy_scanning = False
# return BBQr object or string if simple QR
@ -254,13 +321,14 @@ class QRScanner:
# send specific command until it responds
# - it will wake on any command, but not instant
# - first one seems to fail 100%
self.sleep_seq += 1
await self.tx('SRDF0051') # blindly at first
for retry in range(5):
try:
await self.txrx('SRDF0051', timeout=50) # 50 ok, 20 too short
return
except:
except Exception:
# first try usually fails, that's okay... its asleep and groggy
pass
@ -270,9 +338,13 @@ class QRScanner:
# - need blind retries here
# - might be two layers of sleep, and we need this second command after the first
# - helps to turn off the yellow LED, and save power as well
self.sleep_seq += 1
sleep_seq = self.sleep_seq
await self.tx('SRDF0050')
async def later():
await asyncio.sleep_ms(150)
if sleep_seq != self.sleep_seq or self.busy_scanning:
return
await self.tx('SRDF0050')
asyncio.create_task(later())
@ -290,6 +362,22 @@ class QRScanner:
#print('tx >> ' + msg)
self.serial.write(msg)
async def readexactly_timeout(self, num, timeout, msg=None):
# Avoid asyncio.wait_for_ms here: it can leave the scanner setup task
# stuck after a CancelledError. Convert scanner silence into a normal
# retryable command failure instead.
if timeout is None:
return await self.stream.readexactly(num)
start = utime.ticks_ms()
while self.stream.s.any() < num:
if utime.ticks_diff(utime.ticks_ms(), start) >= timeout:
#print("no rx after %s" % msg)
raise RuntimeError
await asyncio.sleep_ms(5)
return await self.stream.readexactly(num)
async def txrx(self, msg, timeout=250):
# Send a command, get the corresponding response.
# - has a long timeout, collects rx based on framing
@ -310,13 +398,8 @@ class QRScanner:
expect = LEN_OKAY
rx = b''
while 1:
try:
rx += await asyncio.wait_for_ms(self.stream.readexactly(expect), timeout)
except asyncio.TimeoutError:
if timeout is None:
continue
#print("no rx after %s" % msg)
raise RuntimeError
rx += await self.readexactly_timeout(expect, timeout, msg)
#print('txrx << ' + B2A(rx))

View File

@ -10,10 +10,10 @@
# - 'abandon' * 17 + 'agent'
# - 'abandon' * 11 + 'about'
#
import ngu, uctypes, bip39, random, version, ure, chains
import ngu, uctypes, bip39, random, version
from ucollections import OrderedDict
from menu import MenuItem, MenuSystem
from utils import xfp2str, swab32
from utils import xfp2str, parse_extended_key, swab32
from utils import deserialize_secret, problem_file_line, wipe_if_deltamode
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, OK, X
@ -33,6 +33,9 @@ from ucollections import namedtuple
# seed words lengths we support: 24=>256 bits, and recommended
VALID_LENGTHS = (24, 18, 12)
# maximum length for BIP-39 passphrase
MAX_PASS_LEN = 100
# bit flag that means "also include bare prefix as a valid word"
_PREFIX_MARKER = const(1<<26)
@ -49,7 +52,7 @@ def seed_vault_iter():
# raw vault entries are list type when json.loaded from flash
for lst in settings.master_get("seeds", []):
yield VaultEntry(*lst)
def letter_choices(sofar='', depth=0, thres=5):
# make a list of word completions based on indicated prefix
if not sofar:
@ -710,17 +713,9 @@ def seed_words_to_encoded_secret(words):
return nv
def xprv_to_encoded_secret(xprv):
# read an xprv/tprv/etc and return BIP-32 node and what chain it's on.
# - can handle any garbage line
# - returns (node, chain)
# - people are using SLIP132 so we need this
ln = xprv.strip()
pat = ure.compile('.prv[A-Za-z0-9]+')
found = pat.search(ln)
assert found, "not extended privkey"
# serialize, and note version code
node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
assert node, "wrong extended privkey"
node, chain, _ = parse_extended_key(xprv, private=True)
if node is None:
raise ValueError("Failed to parse extended private key.")
nv = SecretStash.encode(xprv=node)
node.blank()
return nv, chain # need to know chain
@ -1174,7 +1169,7 @@ class EphemeralSeedMenu(MenuSystem):
from actions import nfc_recv_ephemeral, import_xprv
from actions import restore_backup, scan_any_qr
from tapsigner import import_tapsigner_backup_file
from xor_seed import xor_restore_start
from xor_seed import xor_restore_temporary
from charcodes import KEY_QR
import_ephemeral_menu = [
@ -1198,7 +1193,7 @@ class EphemeralSeedMenu(MenuSystem):
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
MenuItem("Coldcard Backup", f=restore_backup, arg=True), # tmp=True
MenuItem("Restore Seed XOR", f=xor_restore_start),
MenuItem("Restore Seed XOR", f=xor_restore_temporary),
]
return rv
@ -1243,10 +1238,10 @@ the passphrase as well, it's okay to put them together.) There is no way for \
the Coldcard to know if your entry is correct, and if you have it wrong, \
you will be looking at an empty wallet.
Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only.
Limitations: %d characters max length, ASCII characters 32-126 (0x20-0x7e) only.
%s to continue or press (2) to hide this message forever.
''' % (howto if not version.has_qwerty else '', OK)
''' % (howto if not version.has_qwerty else '', MAX_PASS_LEN, OK)
ch = await ux_show_story(msg, escape='2')
if ch == '2':
@ -1256,8 +1251,8 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
if version.has_qwerty and not PassphraseSaver.has_file():
# no need for any menus if Q and no card present
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase",
b39_complete=True, scan_ok=True, max_len=100)
pp = await ux_input_text('', prompt="Your BIP-39 Passphrase", b39_complete=True,
scan_ok=True, max_len=MAX_PASS_LEN)
if not pp: return
await apply_pass_value(pp)
@ -1267,7 +1262,7 @@ Limitations: 100 characters max length, ASCII characters 32-126 (0x20-0x7e) only
class PassphraseMenu(MenuSystem):
# Collect up to 100 chars as a BIP-39 passphrase
# Collect up to MAX_PASS_LEN chars as a BIP-39 passphrase
# singleton (cls level) vars
done_cb = None
@ -1356,7 +1351,7 @@ class PassphraseMenu(MenuSystem):
async def view_edit_phrase(cls, *a):
# let them control each character
pw = await ux_input_text(cls.pp_sofar, prompt="Your BIP-39 Passphrase",
b39_complete=True, scan_ok=True, max_len=100)
b39_complete=True, scan_ok=True, max_len=MAX_PASS_LEN)
if pw is not None:
cls.pp_sofar = pw
cls.check_length()
@ -1367,8 +1362,8 @@ class PassphraseMenu(MenuSystem):
@classmethod
def check_length(cls):
# enforce a limit of 100 chars
cls.pp_sofar = cls.pp_sofar[0:100]
# enforce a limit of MAX_PASS_LEN chars
cls.pp_sofar = cls.pp_sofar[0:MAX_PASS_LEN]
@classmethod
async def add_text(cls, _1, _2, item):

View File

@ -19,7 +19,7 @@ from ubinascii import hexlify as b2a_hex
import ustruct as struct
import ngu
from opcodes import *
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_BARE_PK, AF_P2TR
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2SH, AF_P2WSH, AF_P2TR, AF_BARE_PK
# single-shot hash functions
sha256 = ngu.hash.sha256s
@ -27,7 +27,9 @@ ripemd160 = ngu.hash.ripemd160
hash256 = ngu.hash.sha256d
hash160 = ngu.hash.hash160
SIGHASH_DEFAULT = const(0) # in taproot meaning same as SIGHASH_ALL (over whole TX)
#def bytes_to_hex_str(s):
# return str(b2a_hex(s), 'ascii')
SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2)
SIGHASH_SINGLE = const(3)
@ -35,7 +37,6 @@ SIGHASH_ANYONECANPAY = const(0x80)
# list containing all flags that we support signing for
ALL_SIGHASH_FLAGS = [
SIGHASH_DEFAULT,
SIGHASH_ALL,
SIGHASH_NONE,
SIGHASH_SINGLE,
@ -55,23 +56,17 @@ def ser_compact_size(l):
else:
return struct.pack("<BQ", 255, l)
def deser_compact_size(f, ret_num_bytes=False):
def deser_compact_size(f):
nit = struct.unpack("<B", f.read(1))[0]
num_bytes = 1
if nit == 253:
nit = struct.unpack("<H", f.read(2))[0]
assert nit >= 253
num_bytes += 2
elif nit == 254:
nit = struct.unpack("<I", f.read(4))[0]
assert nit >= 0x1_0000
num_bytes += 4
elif nit == 255:
nit = struct.unpack("<Q", f.read(8))[0]
assert nit >= 0x1_0000_0000
num_bytes += 8
if ret_num_bytes:
return nit, num_bytes
return nit
def deser_string(f):
@ -200,47 +195,49 @@ def disassemble(script):
try:
offset = 0
slen = len(script)
while 1:
if offset >= len(script):
if offset >= slen:
#print('dis %d done' % offset)
return
c = script[offset]
offset += 1
if 1 <= c <= 75:
#print('dis %d: bytes=%s' % (offset, b2a_hex(script[offset:offset+c])))
yield (script[offset:offset+c], None)
offset += c
cnt = c
elif OP_1 <= c <= OP_16:
# OP_1 thru OP_16
#print('dis %d: number=%d' % (offset, (c - OP_1 + 1)))
yield (c - OP_1 + 1, None)
continue
elif c == OP_PUSHDATA1:
cnt = script[offset]
offset += 1
yield (script[offset:offset+cnt], None)
offset += cnt
elif c == OP_PUSHDATA2:
# up to 65535 bytes
cnt, = struct.unpack_from("H", script, offset)
offset += 2
yield (script[offset:offset+cnt], None)
offset += cnt
elif c == OP_PUSHDATA4:
# no where to put so much data
raise NotImplementedError
elif c == OP_1NEGATE:
yield (-1, None)
continue
else:
# OP_0 included here
#print('dis %d: opcode=%d' % (offset, c))
yield (None, c)
continue
# a data push of `cnt` bytes - reject if it runs off the end
if offset + cnt > slen:
raise ValueError
yield (script[offset:offset+cnt], None)
offset += cnt
except Exception as e:
# import sys;sys.print_exception(e)
raise ValueError("bad script")
def ser_sig_der(r, s, sighash_type=SIGHASH_ALL):
def ser_sig_der(r, s, sighash_type=1):
# Take R and S values from a signature and encode into DER format.
sig = b"\x30"
@ -356,36 +353,38 @@ class CTxOut(object):
return r
def get_address(self):
# Detect type of output from scriptPubKey, and return 2-tuple:
# (addr_type_code, pubkey/pubkeyhash/scripthash)
# Detect type of output from scriptPubKey, and return 3-tuple:
# (addr_type_code, addr, is_segwit)
# 'addr' is byte string, either 20 or 32 long
if self.is_p2tr():
return AF_P2TR, self.scriptPubKey[2:2+32]
return AF_P2TR, self.scriptPubKey[2:2+32], True
if self.is_p2wpkh():
return AF_P2WPKH, self.scriptPubKey[2:2+20]
return AF_P2WPKH, self.scriptPubKey[2:2+20], True
if self.is_p2wsh():
return AF_P2WSH, self.scriptPubKey[2:2+32]
return AF_P2WSH, self.scriptPubKey[2:2+32], True
if self.is_p2pkh():
return AF_CLASSIC, self.scriptPubKey[3:3+20]
return AF_CLASSIC, self.scriptPubKey[3:3+20], False
if self.is_p2sh():
# can be:
# * bare P2SH
# * P2SH-P2WPKH
# * P2SH-P2WSH
return AF_P2SH, self.scriptPubKey[2:2+20]
return AF_P2SH, self.scriptPubKey[2:2+20], False
if self.is_p2pk():
# rare, pay to full pubkey
return AF_BARE_PK, self.scriptPubKey[2:2+33]
# rare, pay to full pubkey: <push_op> <pubkey> OP_CHECKSIG
# push_op is 0x21 (33) for compressed, 0x41 (65) for uncompressed
pk_len = self.scriptPubKey[0]
return AF_BARE_PK, self.scriptPubKey[1:1+pk_len], False
if self.scriptPubKey[0] == OP_RETURN:
return OP_RETURN, self.scriptPubKey
if self.is_op_return():
return OP_RETURN, self.scriptPubKey, False
return None, self.scriptPubKey
return None, self.scriptPubKey, None
def is_p2tr(self):
return len(self.scriptPubKey) == 34 and \
@ -410,8 +409,11 @@ class CTxOut(object):
def is_p2pk(self):
return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) \
and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) \
and self.scriptPubKey[-1] == 0xac
and self.scriptPubKey[0] == len(self.scriptPubKey) - 2 \
and self.scriptPubKey[-1] == OP_CHECKSIG
def is_op_return(self):
return self.scriptPubKey and (self.scriptPubKey[0] == OP_RETURN)
#def __repr__(self):
# return "CTxOut(nValue=%d scriptPubKey=%s)" \

View File

@ -2,7 +2,7 @@
#
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
#
# - implements stream IO protoccol
# - implements stream IO protocol
# - random read, sequential write
# - only a few of these are possible
# - the offset is the file name

View File

@ -15,7 +15,7 @@ from bbqr import b32encode, b32decode
from menu import MenuItem, MenuSystem
from notes import NoteContentBase
from sffile import SFFile
from wallet import MiniScriptWallet
from multisig import MultisigWallet
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
@ -233,7 +233,6 @@ def pick_noid_key():
async def kt_decode_rx(is_psbt, payload):
# we are getting data back from a sender, decode it.
dis.fullscreen("Wait...")
prompt = 'Teleport Password (text)'
@ -252,11 +251,11 @@ async def kt_decode_rx(is_psbt, payload):
ses_key, body = decode_step1(pair, his_pubkey, body)
else:
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
if not MiniScriptWallet.exists():
await ux_show_story("Incoming PSBT requires miniscript wallet(s) to be already setup, but you have none.")
if not MultisigWallet.exists():
await ux_show_story("Incoming PSBT requires multisig wallet(s) to be already setup, but you have none.")
return
ses_key, body, sender_xfp = MiniScriptWallet.kt_search_rxkey(payload)
ses_key, body, sender_xfp = MultisigWallet.kt_search_rxkey(payload)
if sender_xfp is not None:
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)
@ -344,17 +343,20 @@ async def kt_accept_values(dtype, raw):
# This will take over UX w/ the signing process
# flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None)
sign_transaction(psbt_len, flags=None, input_method="kt")
return
elif dtype == 'b':
# full system backup, including master: text lines
from backups import text_bk_parser, restore_tmp_from_dict_ll, restore_from_dict, extract_raw_secret
vals = text_bk_parser(raw)
assert vals # empty?
raw_sec, _ = extract_raw_secret(vals)
try:
vals = text_bk_parser(raw)
assert vals # empty?
raw_sec, _ = extract_raw_secret(vals)
except Exception as e:
await ux_show_story("Invalid backup\n\n" + str(e), title='FAILED')
return
from flow import has_secrets
@ -589,10 +591,10 @@ class SecretPickerMenu(MenuSystem):
async def share_full_backup(self, *a):
# context, and warn them
ch = await ux_show_story("Sending complete backup, including master secret, "
"seed vault (if any), miniscript wallets, notes/passwords, and all settings! "
"seed vault (if any), multisig wallets, notes/passwords, and all settings! "
"The receiving "
"COLDCARD must already have the master seed wiped to be able to install "
"everything, otherwise only master secret and miniscripts are saved into a tmp seed. "
"everything, otherwise only master secret and multisig are saved into a tmp seed. "
"OK to proceed?")
if ch != 'y': return
@ -644,24 +646,18 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
# User wants to send to one or more other senders for them to complete signing.
# who remains to sign? look at inputs
# all_xfps is set, no need to list one master xfp more than once - assuming CC can sign it all
assert psbt.active_miniscript
ms = psbt.active_miniscript
all_xfps = {x for x,*p in ms.to_descriptor().xfp_paths(skip_unspend_ik=True)}
ms = psbt.active_multisig
all_xfps = [x for x,*p in ms.get_xfp_paths()]
need = [x for x in psbt.multisig_xfps_needed() if x in all_xfps]
# ignore -> keys to ignore, currently only musig aggregate keys
need, ignore = psbt.miniscript_xfps_needed()
need = [x for x in need if x in all_xfps]
# maybe it's not really a PSBT where we know the other signers? might be
# a weird coinjoin we don't fully understand
if not need:
await ux_show_story("No more signers?")
return
# move out of PSRAM
from auth import TXN_OUTPUT_OFFSET
with SFFile(TXN_OUTPUT_OFFSET, psbt_len) as fd:
# (TXN_OUTPUT_OFFSET after signing, TXN_INPUT_OFFSET for the file-teleport path)
with SFFile(psbt_offset, psbt_len) as fd:
bin_psbt = fd.read(psbt_len)
my_xfp = settings.get('xfp')
@ -681,7 +677,7 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
ci = []
next_signer = None
for idx, x in enumerate(all_xfps - ignore): # set diff
for idx, x in enumerate(all_xfps):
txt = '[%s] Co-signer #%d' % (xfp2str(x), idx+1)
f = done_cb
if x == my_xfp:
@ -694,7 +690,7 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
async def sign_now(*a):
# this will reset the UX stack:
# flags=None --> whether to finalize is decided based on psbt.is_complete
sign_transaction(psbt_len, flags=None, offset=psbt_offset)
sign_transaction(psbt_len, flags=None, input_method="kt", offset=psbt_offset)
f = sign_now
@ -717,16 +713,14 @@ async def kt_send_psbt(psbt, psbt_len, psbt_offset):
m.goto_idx(next_signer) # position cursor on next candidate
the_ux.push(m)
await m.interact()
if m.next_xfp:
assert m.next_xfp != my_xfp
ri, rx_pubkey, kp = ms.kt_make_rxkey(m.next_xfp)
await kt_do_send(rx_pubkey, 'p', raw=bin_psbt, prefix=ri, kp=kp,
rx_label='[%s] co-signer' % xfp2str(m.next_xfp))
return True
return None
return True, ms.M - (ms.N - len(need))
async def kt_send_file_psbt(*a):
# Menu item: choose a PSBT file from SD card, and send to co-signers.
@ -775,6 +769,8 @@ async def kt_send_file_psbt(*a):
psbt.consider_inputs()
dis.progress_sofar(3, 4)
psbt.consider_keys()
except Exception as exc:
# not going to do full reporting here, use our other code for that!
await ux_show_story("Cannot validate PSBT?\n\n"+str(exc), "PSBT Load Failed")
@ -782,8 +778,8 @@ async def kt_send_file_psbt(*a):
finally:
dis.progress_bar_show(1)
if not psbt.active_miniscript:
await ux_show_story("We are not part of this wallet.", "Cannot Teleport PSBT")
if not psbt.active_multisig:
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
return
await kt_send_psbt(psbt, psbt_len=psbt_len, psbt_offset=TXN_INPUT_OFFSET)

View File

@ -12,8 +12,6 @@ from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux
from stash import SecretStash
from drv_entro import bip85_derive
from glob import settings
from utils import node_from_privkey
# see from mk4-bootloader/se2.h
@ -101,6 +99,22 @@ class TrickPinMgmt:
def __init__(self):
assert uctypes.sizeof(TRICK_SLOT_LAYOUT) == 128
self.reload()
def reload(self):
# we track known PINS as a dictionary:
# pin (in ascii) => (slot_num, tc_flags, arg)
from glob import settings
self.tp = settings.get('tp', {})
def save_record(self):
# commit changes back to settings
from glob import settings
if self.tp:
settings.set('tp', self.tp)
else:
settings.remove_key('tp')
settings.save()
def roundtrip(self, method_num, slot_buf=None):
from pincodes import pa
@ -120,36 +134,26 @@ class TrickPinMgmt:
return rc
def get_all(self):
return settings.get("tp", {})
def commit(self, trick_pins):
settings.set("tp", trick_pins)
settings.save()
def clear_all(self):
# get rid of them all
self.roundtrip(0)
settings.remove_key('tp')
settings.save()
self.tp = {}
self.save_record()
def forget_pin(self, pin):
# forget about settings for a PIN
t_pins = self.get_all()
t_pins.pop(pin, None)
self.commit(t_pins)
self.tp.pop(pin, None)
self.save_record()
def restore_pin(self, new_pin):
# remember/restore PIN that we "forgot", return T if worked
b, slot = self.get_by_pin(new_pin)
b, slot = tp.get_by_pin(new_pin)
if slot is None: return False
record = (slot.slot_num, slot.tc_flags,
0xffff if slot.tc_flags & TC_DELTA_MODE else slot.tc_arg)
t_pins = self.get_all()
t_pins[new_pin] = record
self.commit(t_pins)
self.tp[new_pin] = record
self.save_record()
return True
@ -207,14 +211,14 @@ class TrickPinMgmt:
def update_slot(self, pin, new=False, new_pin=None, tc_flags=None, tc_arg=None, secret=None):
# create or update a trick pin
# - doesn't support wallet to no-wallet transitions
'''
>>> from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
'''
#
# from pincodes import pa; pa.setup(b'12-12'); pa.login(); from trick_pins import *
#
assert isinstance(pin, bytes)
b, slot = self.get_by_pin(pin)
if not slot:
if not new: raise KeyError("wrong pin")
assert new, "wrong pin"
# Making a new entry
b, slot = make_slot()
@ -228,12 +232,11 @@ class TrickPinMgmt:
slot.slot_num = sn
t_pins = self.get_all()
if new_pin is not None:
slot.pin_len = len(new_pin)
slot.pin[0:slot.pin_len] = new_pin
if new_pin != pin:
t_pins.pop(pin.decode(), None)
self.tp.pop(pin.decode(), None)
pin = new_pin
if tc_flags is not None:
@ -267,14 +270,14 @@ class TrickPinMgmt:
assert rc == 0
# record key details.
t_pins[pin.decode()] = record
self.commit(t_pins)
self.tp[pin.decode()] = record
self.save_record()
return b, slot
def all_tricks(self):
# put them in order, with "wrong" last
return sorted(self.get_all().keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
return sorted(self.tp.keys(), key=lambda i: i if (i != WRONG_PIN_CODE) else 'Z')
def define_unlock_pin(self, new_pin):
# user is setting the bypass PIN for first time.
@ -301,14 +304,16 @@ class TrickPinMgmt:
# if spending policy defined, this PIN allows adjustment
# - not TRICK bypass choices, like ones that wipe
# - could be multiple, but only first returned.
for k, (sn,flags,arg) in self.get_all().items():
self.reload()
for k, (sn,flags,arg) in self.tp.items():
if (flags == TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
return k
return None
def delete_sp_unlock_pins(self):
# remove all bypass pins, they are done w/ feature
for k, (sn,flags,arg) in self.get_all().items():
self.reload()
for k, (sn,flags,arg) in self.tp.items():
if (flags & TC_FW_DEFINED) and (arg == TCA_SP_UNLOCK):
self.clear_slots([sn])
self.forget_pin(k)
@ -316,13 +321,13 @@ class TrickPinMgmt:
def get_deltamode_pins(self):
# iterate over all delta-mode PIN's defined.
for k, (sn,flags,args) in self.get_all().items():
for k, (sn,flags,args) in self.tp.items():
if flags & TC_DELTA_MODE:
yield k
def get_duress_pins(self):
# iterate over all duress wallets
for k, (sn,flags,args) in self.get_all().items():
for k, (sn,flags,args) in self.tp.items():
if flags & (TC_WORD_WALLET | TC_XPRV_WALLET):
yield k
@ -333,7 +338,7 @@ class TrickPinMgmt:
# as checking only self.tp is not sufficient for hidden TPs or after fast wipe
# - return error msg or None
assert isinstance(pin, str)
b, slot = self.get_by_pin(pin)
b, slot = tp.get_by_pin(pin)
if slot is not None:
return 'That PIN is already in use as a Trick PIN.'
@ -352,9 +357,8 @@ class TrickPinMgmt:
def backup_duress_wallets(self, sv):
# for backup file, yield (label, path, pairs-of-data)
done = set()
t_pins = self.get_all()
for pin in self.get_duress_pins():
sn, flags, arg = t_pins[pin]
sn, flags, arg = self.tp[pin]
if (flags, arg) in done:
continue
@ -364,7 +368,7 @@ class TrickPinMgmt:
label = "Duress: BIP-85 Derived wallet"
nwords = 12 if ((arg // 1000) == 2) else 24
path = "BIP85(words=%d, index=%d)" % (nwords, arg)
b, slot = self.get_by_pin(pin)
b, slot = tp.get_by_pin(pin)
words = bip39.b2a_words(slot.xdata[0:(32 if nwords==24 else 16)])
d = [ ('duress_%d_words' % arg, words) ]
@ -394,17 +398,17 @@ class TrickPinMgmt:
continue
if flags & TC_DELTA_MODE:
prob = validate_delta_pin(true_pin, pin)
prob, arg = validate_delta_pin(true_pin, pin)
if prob:
# just forget it, no UI here to report issue
continue
continue
try:
# might need to construct a BIP-85 or XPRV secret to match
path, new_secret = construct_duress_secret(flags, arg)
self.update_slot(pin.encode(), new=True, tc_flags=flags,
tc_arg=arg, secret=new_secret)
tp.update_slot(pin.encode(), new=True, secret=new_secret,
tc_flags=flags, tc_arg=arg)
except: pass
@staticmethod
@ -440,6 +444,7 @@ class TrickPinMenu(MenuSystem):
if bool(pa.tmp_value):
return [MenuItem('Not Available')]
tp.reload()
tricks = tp.all_tricks()
if self.current_pin in tricks:
@ -836,7 +841,7 @@ so you may perform transactions with it.''')
# "arg" can be out-of-date, if they edited timer value after parent was
# rendered, where arg was captured into item.arg ... so don't use it.
cd_val = tp.get_all()[pin][2]
cd_val = tp.tp[pin][2]
msg = 'Shows login countdown (%s)' % lgto_map.get(cd_val, '???').strip()
if flags & TC_WIPE:
@ -852,13 +857,14 @@ so you may perform transactions with it.''')
def adjust_countdown_chooser():
# 'disabled' choice not appropriate for this case
ch = lgto_ch[1:]
va = lgto_va[1:]
def set_it(idx, text):
new_val = va[idx]
# save it
try:
tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
b, slot = tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
except: pass
return va.index(cd_val), lgto_ch[1:], set_it
@ -909,8 +915,7 @@ Wallet is XPRV-based and derived from a fixed path.''' % pin
# drill down into a sub-menu per existing PIN
# - data display only, no editing; just clear and redo
pin = item.arg
t_pins = tp.get_all()
slot_num, flags, arg = t_pins[pin] if (pin in t_pins) else (-1, 0, 0)
slot_num, flags, arg = tp.tp[pin] if (pin in tp.tp) else (-1, 0, 0)
rv = []

View File

@ -2,10 +2,10 @@
#
# usb.py - USB related things
#
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr, ujson
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr
from uasyncio import sleep_ms, core
from uhashlib import sha256
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN
from public_constants import MAX_MSG_LEN, MAX_BLK_LEN, AFC_SCRIPT
from public_constants import STXN_FLAGS_MASK
from ustruct import pack, unpack_from
from ckcc import watchpoint, is_simulator
@ -53,8 +53,8 @@ HSM_WHITELIST = frozenset({
'smsg', # limited by policy
'blkc', 'hsts', # report status values
'stok', 'smok', # completion check: sign txn or msg
'xpub', # quick status checks
'show', 'msas', # limited by HSM policy
'xpub', 'msck', # quick status checks
'p2sh', 'show', # limited by HSM policy
'user', # auth HSM user, other user cmds not allowed
'gslr', # read storage locker; hsm mode only, limited usage
})
@ -75,14 +75,7 @@ HOBBLED_CMDS = frozenset({
'enrl', # no new multisigs during policy enforcement
'back', # no backups
'bagi', 'dfu_', # just in case
"user", # same as HSM_DISABLE_CMDS
"rmur",
"nwur",
"gslr",
"hsts",
"hsms",
})
}) | HSM_DISABLE_CMDS
# singleton instance of USBHandler()
handler = None
@ -133,16 +126,6 @@ def is_vcp_active():
return cur and ('VCP' in cur) and en
def get_miniscript_by_name(name_bytes):
from wallet import MiniScriptWallet
for w in MiniScriptWallet.iter_wallets():
if w.name == str(name_bytes, 'ascii'):
return True, w
else:
return False, b'err_Miniscript wallet not found'
class USBHandler:
def __init__(self):
self.dev = pyb.USB_HID()
@ -433,10 +416,12 @@ class USBHandler:
if cmd == 'dwld':
offset, length, fileno = unpack_from('<III', args)
assert len(args) == 12, 'badlen'
return await self.handle_download(offset, length, fileno)
if cmd == 'ncry':
version, his_pubkey = unpack_from('<I64s', args)
assert len(args) == 68, 'badlen'
return self.handle_crypto_setup(version, his_pubkey)
@ -466,19 +451,54 @@ class USBHandler:
if cmd == 'smsg':
# sign message
addr_fmt, len_subpath, len_msg = unpack_from('<III', args)
assert len(args) == (12 + len_subpath + len_msg), 'badlen'
subpath = args[12:12+len_subpath]
msg = args[12+len_subpath:]
assert len(msg) == len_msg, "badlen"
from auth import sign_msg
sign_msg(msg, subpath, addr_fmt)
return None
if cmd == 'p2sh':
# show P2SH (probably multisig) address on screen (also provides it back)
# - must provide redeem script, and list of [xfp+path]
from auth import start_show_p2sh_address
if hsm_active and not hsm_active.approve_address_share(is_p2sh=True):
raise HSMDenied
# new multsig goodness, needs mapping from xfp->path and M values
addr_fmt, M, N, script_len = unpack_from('<IBBH', args)
assert addr_fmt & AFC_SCRIPT
assert 1 <= M <= N <= 20
assert 30 <= script_len <= 520
offset = 8
witdeem_script = args[offset:offset+script_len]
offset += script_len
assert len(witdeem_script) == script_len
xfp_paths = []
for i in range(N):
assert offset < len(args), 'badlen'
ln = args[offset]
assert 1 <= ln <= 16, 'badlen'
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
offset += (ln*4) + 1
assert offset == len(args)
return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths,
witdeem_script)
if cmd == 'show':
# simple cases, older code: text subpath
from auth import usb_show_address
addr_fmt, = unpack_from('<I', args)
assert len(args) >= 4, 'badlen'
# regression patch of AFC_BECH32M flag
# fixed here https://github.com/Coldcard/ckcc-protocol/commit/a6d901f9fca50755835eca895586ca74d0ca81ed
if addr_fmt == 0x17: # old P2TR
@ -490,9 +510,10 @@ class USBHandler:
# - text config file must already be uploaded
file_len, file_sha = unpack_from('<I32s', args)
assert len(args) == 36, 'badlen'
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 100 < file_len <= (32*200), "badlen"
assert 100 < file_len <= (20*200), "badlen"
# Start an UX interaction, return immediately here
from auth import maybe_enroll_xpub
@ -500,96 +521,24 @@ class USBHandler:
return None
if cmd == 'mins':
# Enroll new xpubkey to be involved in miniscript.
# - descriptor text config file must already be uploaded
file_len, file_sha = unpack_from('<I32s', args)
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 100 < file_len <= (100 * 200), "badlen"
# Start an UX interaction, return immediately here
from auth import maybe_enroll_xpub
maybe_enroll_xpub(sf_len=file_len, ux_reset=True)
return None
if cmd.startswith("ms"):
# miniscript related commands
assert self.encrypted_req, 'must encrypt'
if cmd == "msls":
# list all registered miniscript wallet names
from wallet import MiniScriptWallet
wallets = [w.name for w in MiniScriptWallet.iter_wallets()]
return b'asci' + ujson.dumps(wallets)
if cmd == "msas":
# get miniscript address based on int/ext index
if hsm_active and not hsm_active.approve_address_share(miniscript=True):
raise HSMDenied
change, idx, = unpack_from('<II', args)
assert change in (0, 1), "change not bool"
assert 0 <= idx < (2 ** 31), "child idx"
name = args[8:]
assert len(name) <= 32, "name len"
ok, w = get_miniscript_by_name(name)
if not ok:
return w
from auth import start_show_miniscript_address
return b'asci' + start_show_miniscript_address(w, change, idx)
assert len(args) <= 32, "name len"
ok, w = get_miniscript_by_name(args)
if not ok:
return w
if cmd == "msdl":
# delete miniscript wallet by its name (unique id)
from auth import maybe_delete_miniscript
maybe_delete_miniscript(w)
return None
if cmd == "msgt":
# takes name and returns descriptor + name json
# MiniscriptWallet.to_string only fills policy
return b'asci' + ujson.dumps({"name": w.name, "desc": w.to_string()})
if cmd == "mspl":
# takes name and returns BIP-388 Wallet Policy
return b'asci' + ujson.dumps({"name": w.name, "desc_template": w.desc_tmplt,
"keys_info": w.keys_info})
if cmd == 'msck':
# Quick check to test if we have a wallet already installed.
from multisig import MultisigWallet
M, N, xfp_xor = unpack_from('<3I', args)
assert len(args) == 12, 'badlen'
return int(MultisigWallet.quick_check(M, N, xfp_xor))
if cmd == 'stxn':
# sign transaction
txn_len, flags, txn_sha = unpack_from('<II32s', args)
assert len(args) == 40, 'badlen'
if txn_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 50 < txn_len <= MAX_TXN_LEN, "badlen"
# optional miniscript wallet name
try:
name_len = unpack_from('B', args[40:])[0]
name = str(args[41:41 + name_len], "ascii")
assert 1 <= len(name) <= 32, "name len"
except:
name = None
w = None
if name:
ok, w = get_miniscript_by_name(name)
if not ok:
return w
from auth import sign_transaction
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, miniscript_wallet=w)
sign_transaction(txn_len, (flags & STXN_FLAGS_MASK), txn_sha, input_method="usb")
return None
if cmd == 'stok' or cmd == 'bkok' or cmd == 'smok' or cmd == 'pwok':
@ -652,6 +601,8 @@ class USBHandler:
if cmd == 'rest':
# restore backup from what is already uploaded in PSRAM
file_len, file_sha, bf = unpack_from('<I32sB', args)
assert len(args) == 37, 'badlen'
assert 0 < file_len <= MAX_TXN_LEN, "badlen"
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
@ -674,6 +625,7 @@ class USBHandler:
# HSM mode "start" -- requires user approval
if args:
file_len, file_sha = unpack_from('<I32s', args)
assert len(args) == 36, 'badlen'
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 2 <= file_len <= (200*1000), "badlen"
@ -689,6 +641,7 @@ class USBHandler:
if cmd == 'hsts':
# can always query HSM mode
from hsm import hsm_status_report
import ujson
return b'asci' + ujson.dumps(hsm_status_report())
if cmd == 'gslr':
@ -700,6 +653,8 @@ class USBHandler:
if cmd == 'nwur': # new user
from users import Users
auth_mode, ul, sl = unpack_from('<BBB', args)
assert len(args) == (3 + ul + sl), 'badlen'
assert ul and sl, "badlen"
username = bytes(args[3:3+ul]).decode('ascii')
secret = bytes(args[3+ul:3+ul+sl])
@ -708,6 +663,8 @@ class USBHandler:
if cmd == 'rmur': # delete user
from users import Users
ul, = unpack_from('<B', args)
assert len(args) == (1 + ul), 'badlen'
assert ul, "badlen"
username = bytes(args[1:1+ul]).decode('ascii')
return Users.delete(username)
@ -715,6 +672,8 @@ class USBHandler:
if cmd == 'user': # auth user (HSM mode)
from users import Users
totp_time, ul, tl = unpack_from('<IBB', args)
assert len(args) == (6 + ul + tl), 'badlen'
assert ul and tl, "badlen"
username = bytes(args[6:6+ul]).decode('ascii')
token = bytes(args[6+ul:6+ul+tl])
@ -803,7 +762,8 @@ class USBHandler:
length = min(length, MAX_BLK_LEN)
assert 0 <= file_number < 2, 'bad fnum'
assert 0 <= offset <= MAX_TXN_LEN, "bad offset"
assert 0 <= offset < MAX_TXN_LEN, "bad offset"
assert offset + length <= MAX_TXN_LEN, "bad offset"
assert 1 <= length, 'len'
# maintain a running SHA256 over what's sent
@ -838,7 +798,8 @@ class USBHandler:
dis.progress_sofar(offset, total_size)
assert offset % 256 == 0, 'alignment'
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
assert 1 <= total_size <= MAX_UPLOAD_LEN, 'long'
assert offset + len(data) <= total_size, 'long'
if hsm_active or pa.hobbled_mode:
# additional restriction in HSM mode or hobbled: must be PSBT

View File

@ -78,7 +78,7 @@ KEY = 'usr'
UserInfo = namedtuple('UserInfo', 'auth_mode secret last_counter')
class Users:
'''Track users and thier TOTP secrets or hashed passwords'''
'''Track users and their TOTP secrets or hashed passwords'''
@classmethod
def get(cls):

View File

@ -8,7 +8,7 @@ from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64
from charcodes import OUT_CTRL_ADDRESS, OUT_CTRL_NOWRAP
from uhashlib import sha256
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC
from public_constants import MAX_PATH_DEPTH, AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2TR
B2A = lambda x: str(b2a_hex(x), 'ascii')
@ -193,34 +193,31 @@ def str2xfp(txt):
# Inverse of xfp2str
return ustruct.unpack('<I', a2b_hex(txt))[0]
def is_ascii(s):
if len(s) == len(s.encode()):
return True
return False
def is_printable(s):
PRINTABLE = range(32, 127)
for ch in s:
if ord(ch) not in PRINTABLE:
o = ord(ch)
if o < 32 or o > 126:
return False
return True
def to_ascii_printable(s, strip=False, only_printable=True):
def to_ascii_printable(s, allow_tab_nl=False):
try:
s = str(s, 'ascii')
if strip:
s = s.strip()
assert is_ascii(s)
if only_printable:
# s must be a string!
assert len(s) == len(s.encode())
if not allow_tab_nl:
assert is_printable(s)
else:
for ch in s:
o = ord(ch)
assert 32 <= o <= 126 or o == 9 or o == 10
return s
except:
raise AssertionError("must be ascii" + (" printable" if only_printable else ""))
err = "must be ascii printable" + (", tab, or newline" if allow_tab_nl else "")
raise AssertionError(err)
def problem_file_line(exc):
# return a string of just the filename.py and line number where
# an exception occured. Best used on AssertionError.
# an exception occurred. Best used on AssertionError.
tmp = uio.StringIO()
sys.print_exception(exc, tmp)
@ -252,7 +249,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# - do not assume /// is m/0/0/0
# - if allow_star, then final position can be * or *h (wildcard)
s = to_ascii_printable(bin_path, strip=True).lower()
s = to_ascii_printable(str(bin_path, "ascii").strip()).lower()
# empty string is valid
if s == '': return 'm'
@ -343,13 +340,6 @@ def match_deriv_path(patterns, path):
return False
def validate_derivation_path_length(length, allow_master=False):
# force them to use a derived key, never the master
if not allow_master:
assert length >= 4, 'too short key path'
assert (length % 4) == 0, 'corrupt key path'
assert (length // 4) <= MAX_PATH_DEPTH, 'too deep'
class DecodeStreamer:
def __init__(self):
self.runt = bytearray()
@ -463,6 +453,8 @@ def clean_shutdown(style=0):
callgate.show_logout(style)
def call_later_ms(delay, cb, *args, **kws):
import uasyncio
async def doit():
await uasyncio.sleep_ms(delay)
await cb(*args, **kws)
@ -533,6 +525,32 @@ def word_wrap(ln, w):
ln = ln[nsp:]
if not ln: return
def parse_extended_key(ln, private=False):
# read an xpub/ypub/etc and return BIP-32 node and what chain it's on.
# - can handle any garbage line
# - returns (node, chain, addr_fmt)
# - people are using SLIP132 so we need this
node, chain, addr_fmt = None, None, None
if ln is None:
return node, chain, addr_fmt
ln = ln.strip()
if private:
rgx = r'.prv[A-Za-z0-9]+'
else:
rgx = r'.pub[A-Za-z0-9]+'
pat = ure.compile(rgx)
found = pat.search(ln)
# serialize, and note version code
try:
node, chain, addr_fmt, is_private = chains.slip132_deserialize(found.group(0))
except:
pass
return node, chain, addr_fmt
def deserialize_secret(text_sec_str):
# Chip can hold 72-bytes as a secret
# - has 0th byte as marker, secret and zero padding to AE_SECRET_LEN
@ -582,7 +600,6 @@ def datetime_to_str(dt, fmt="%d-%02d-%02d %02d:%02d:%02d"):
dts = fmt % (y, mo, d, h, mi, s)
return dts + " UTC"
def txid_from_fname(fname):
if len(fname) >= 64:
txid = fname[:64]
@ -671,6 +688,35 @@ def decode_bip21_text(got):
raise ValueError('not bip-21')
def validate_own_address(addr):
ch = chains.current_chain()
addr_l = addr.lower()
if addr_l[:3] in ("bc1", "tb1") or addr_l[:5] == 'bcrt1':
try:
hrp, witver, data = ngu.codecs.segwit_decode(addr)
assert hrp == ch.bech32_hrp
assert witver == 0
if len(data) == 20:
return addr_l, AF_P2WPKH
if len(data) == 32:
return addr_l, AF_P2WSH
except: pass
# Bitcoin main/test/reg base58 address prefixes.
elif addr and addr[0] in '123mn':
try:
raw = ngu.codecs.b58_decode(addr)
assert len(raw) == 21
if raw[0] == ch.b58_addr[0]:
return addr, AF_CLASSIC
if raw[0] == ch.b58_script[0]:
return addr, AF_P2SH
except: pass
assert False, ch.name
def encode_seed_qr(words):
return ''.join('%04d' % bip39.get_word_index(w) for w in words)

View File

@ -349,7 +349,7 @@ async def show_qr_code(data, is_alnum=False, msg=None, **kw):
o = QRDisplaySingle([data], is_alnum, msg=msg, **kw)
await o.interact_bare()
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
async def ux_enter_bip32_index(prompt, can_cancel=True, unlimited=False):
if unlimited:
max_value = (2 ** 31) - 1 # we handle hardened
else:

View File

@ -60,7 +60,7 @@ class PressRelease:
return ch
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
# return the decimal number which the user has entered
# - default/blank value assumed to be zero
# - clamps large values to the max
@ -283,7 +283,7 @@ async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100, min_
ch = await press.wait()
if ch == 'y':
if len(pw) < min_len:
ch = await ux_show_story('Need %d character(s) at least. Press OK '
ch = await ux_show_story('Need %d characters at least. Press OK '
'to continue X to exit.' % min_len, escape="xy",
strict_escape=True)
if ch == "x": return

View File

@ -76,7 +76,7 @@ class PressRelease:
self.last_key = ch
return ch
async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
async def ux_enter_number(prompt, max_value, can_cancel=True, value=''):
# return the decimal number which the user has entered
# - default/blank value assumed to be zero
# - clamps large values to the max
@ -121,7 +121,7 @@ async def ux_enter_number(prompt, max_value, can_cancel=False, value=''):
dis.text(0, 4, ' '*CHARS_W)
elif ch == KEY_CANCEL:
if can_cancel:
# quit if they press X on empty screen
# quit if they press CANCEL on any screen
return None
elif '0' <= ch <= '9':
if len(value) == max_w:
@ -578,7 +578,7 @@ def ux_draw_words(y, num_words, words):
if num_words == 12:
# luxious space after colon
msg = ('%2d: ' % n) + word
x_off = 3
x_off = 4
else:
if n <= n_per_c:
# no space in front of 1: thru N: in leftmost column of 3
@ -667,7 +667,7 @@ async def seed_word_entry(prompt, num_words, has_checksum=True, done_cb=None, li
what, vals = decode_qr_result(got, expect_secret=True)
except QRDecodeExplained as e:
err_msg = str(e)
redraw_words()
redraw_words(words)
continue
if what != "words":
@ -825,7 +825,6 @@ class QRScannerInteraction:
while 1:
if task.done():
data = await task
#print("Scanned: %r" % data)
break
dis.image(None, 40, 'scan_%d' % frames[ph])
@ -838,7 +837,12 @@ class QRScannerInteraction:
data = None
break
task.cancel()
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# clear screen right away so user knows we got it
dis.clear()
@ -881,7 +885,7 @@ class QRScannerInteraction:
file_type, _, data = decode_qr_result(got, expect_bbqr=True)
if file_type == 'U':
data = data.strip()
if data[0] == '{' and data[-1] == '}':
if data[:1] == b'{' and data[-1:] == b'}':
file_type = 'J'
if file_type != 'J':
raise QRDecodeExplained('Expected JSON data')
@ -919,7 +923,7 @@ class QRScannerInteraction:
return await self.scan_general(prompt, addr_taster, line2=line2, enter_quits=True)
async def scan_anything(self, expect_secret=False, tmp=False, miniscript_wallet=None):
async def scan_anything(self, expect_secret=False, tmp=False):
# start a QR scan, and act on what we find, whatever it may be.
from ux import ux_show_story
from pincodes import pa
@ -978,7 +982,7 @@ class QRScannerInteraction:
if what == 'psbt':
decoder, psbt_len, got = vals
await qr_psbt_sign(decoder, psbt_len, got, miniscript_wallet)
await qr_psbt_sign(decoder, psbt_len, got)
elif what == 'txn':
bin_txn, = vals
@ -988,7 +992,7 @@ class QRScannerInteraction:
proto, addr, args = vals
await ux_visualize_bip21(proto, addr, args)
elif what == "minisc":
elif what == "multi":
from auth import maybe_enroll_xpub
ms_config, = vals
try:
@ -996,7 +1000,6 @@ class QRScannerInteraction:
except Exception as e:
await ux_show_story(
'Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
return
elif what == "wif":
data, = vals
@ -1030,7 +1033,7 @@ class QRScannerInteraction:
await ux_show_story(what, title='Unhandled')
async def qr_psbt_sign(decoder, psbt_len, raw, miniscript_wallet=None):
async def qr_psbt_sign(decoder, psbt_len, raw):
# Got a PSBT coming in from QR scanner. Sign it.
# - similar to auth.sign_psbt_file()
from auth import UserAuthorizedAction, ApproveTransaction
@ -1058,14 +1061,14 @@ async def qr_psbt_sign(decoder, psbt_len, raw, miniscript_wallet=None):
psbt_len = total
else:
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
with SFFile(TXN_INPUT_OFFSET, length=psbt_len) as out:
taste = out.read(10)
_, output_encoder, _ = psbt_encoding_taster(taste, psbt_len)
UserAuthorizedAction.cleanup()
UserAuthorizedAction.active_request = ApproveTransaction(
psbt_len, input_method="qr", output_encoder=output_encoder,
miniscript_wallet=miniscript_wallet,
psbt_len, input_method="qr",
output_encoder=output_encoder
)
the_ux.push(UserAuthorizedAction.active_request)
@ -1110,20 +1113,22 @@ async def ux_visualize_bip21(proto, addr, args):
# - imho, a bare address is a valid BIP-21 URL so we come here too
# - validate address ownership on request
from ux import ux_show_story
from chains import current_chain
msg = show_single_address(addr) + '\n\n'
args = args or {}
if 'amount' in args:
msg += 'Amount: '
try:
amt = args.pop('amount')
whole, frac = amt.split('.', 1)
frac = int(frac) if frac else 0
whole = int(whole) if whole else 0
msg += '%d.%08d BTC\n' % (whole, frac)
whole, _, frac = amt.partition('.')
assert whole.isdigit()
assert len(whole) <= 8
assert len(frac) <= 8
sats = int((whole or '0') + (frac + '00000000')[:8])
msg += 'Amount: %s %s\n' % current_chain().render_value(sats)
except:
msg += '(corrupt)\n'
msg += 'Amount: (corrupt)\n'
for fn in ['label', 'message', 'lightning']:
if fn in args:
@ -1200,10 +1205,6 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
from ux import ux_wait_keydown
import uqr
# put QR shenanigans at offset 1MB after TXN_OUTPUT_OFFSET
TMP_OFFSET = const(3 * 1024 * 1024)
assert not PSRAM.is_at(data, TMP_OFFSET) # output data would be overwritten with our work
assert type_code in TYPE_LABELS
dis.fullscreen('Generating BBQr...', .1)
@ -1214,6 +1215,11 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
else:
# default to Base32, because always best option
encoding = '2'
if isinstance(data, str):
# 'U'/'J' payloads are UTF-8 text; b32encode consumes the UTF-8
# bytes, so convert now to keep length/slicing consistent (else
# multi-byte chars overflow target_vers -> assert below trips)
data = data.encode()
data_len = len(data)
# try a few select resolutions (sizes) in order such that we use either single QR
@ -1260,7 +1266,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
else:
_, _, raw = qr_data.packed()
PSRAM.write_at(TMP_OFFSET + (qr_size * pkt), qr_size)[0:raw_qr_size] = raw
PSRAM.write_at(qr_size * pkt, qr_size)[0:raw_qr_size] = raw
del qr_data
@ -1276,7 +1282,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
ch = None
while not ch:
for pkt in range(num_parts):
buf = PSRAM.read_at(TMP_OFFSET + (qr_size * pkt), raw_qr_size)
buf = PSRAM.read_at(qr_size * pkt, raw_qr_size)
dis.draw_qr_display( (scan_w, w, buf), msg, True, None, None, False,
partial_bar=((pkt, num_parts) if num_parts else None))

View File

@ -131,9 +131,6 @@ def probe_system():
# what firmware signing key did we boot with? are we in dev mode?
is_devmode = get_is_devmode()
# newer, edge code in effect?
is_edge = (get_mpy_version()[1][-1] == 'X')
probe_system()
# EOF

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,10 @@ from utils import problem_file_line, show_single_address, node_from_pubkey
from files import CardSlot, CardMissingError, needs_microsd
from glob import settings
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
from public_constants import AF_P2WPKH
from public_constants import AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2SH
from msgsign import msg_signing_done
MAX_ITEMS = 30
def decode_wif(wif):
# Decode base58 encoded WIF string, return keypair and metadata
@ -33,7 +34,7 @@ def decode_wif(wif):
return kp, testnet, compressed
def iter_wif_store_addresses(chain, addr_fmt):
def iter_wif_store_addresses(addr_fmt):
# nothing found among singlesig & registered multisig wallets
# check WIF store
wifs = settings.get("wifs", [])
@ -41,7 +42,37 @@ def iter_wif_store_addresses(chain, addr_fmt):
for i, (pk, sk) in enumerate(wifs):
node = node_from_pubkey(a2b_hex(pk))
yield i, chain.address(node, addr_fmt)
yield i, chains.current_chain().address(node, addr_fmt)
def save_wif_store_items(new_wifs):
saved = settings.get("wifs", [])
len_saved = len(saved)
unique = []
dups = 0
for item in new_wifs:
if item in unique:
continue
if item not in saved:
unique.append(item)
else:
dups += 1
err = ("No valid WIF key found." + (" Contains duplicate WIF(s)" if dups else ""))
assert unique, err
err = ("Max %d items allowed in WIF Store.\n\nAttempted to import %d keys,"
" while remaining WIF store capacity is only %d. Please, make room"
" first." % (MAX_ITEMS, len(unique), MAX_ITEMS - len_saved))
assert (len_saved + len(unique)) <= MAX_ITEMS, err
saved.extend(unique)
settings.set('wifs', saved)
settings.save()
return len(unique)
async def ux_visualize_wif(wif_str, kp, compressed, testnet):
@ -58,27 +89,24 @@ async def ux_visualize_wif(wif_str, kp, compressed, testnet):
ch = await ux_show_story(msg, title="WIF Key", escape=esc)
if ch == "1":
saved = settings.get("wifs", [])
if (pk, sk) in saved:
await ux_show_story("Already saved in WIF Store.", title="Failure")
return
title = "Success"
try:
save_wif_store_items([(pk, sk)])
msg = "Saved to WIF Store."
except Exception as e:
title = "Failure"
msg = str(e)
saved.append((pk, sk))
settings.set('wifs', saved)
settings.save()
await ux_show_story("Saved to WIF Store.", title="Success")
await ux_show_story(msg, title=title)
class WIFStore(MenuSystem):
MAX_ITEMS = 30
class WIFStoreMenu(MenuSystem):
def __init__(self):
items = self.construct()
super().__init__(items)
@classmethod
async def make_menu(cls, *a):
async def make(cls, *a):
if not settings.get("wifs", None):
intro = ("Individual private keys, encoded as WIF (Wallet Import Format) keys"
" can be imported and used for signing. Any PSBT that uses a WIF stored here"
@ -104,7 +132,7 @@ class WIFStore(MenuSystem):
items = []
if len(wifs) < self.MAX_ITEMS:
if len(wifs) < MAX_ITEMS:
items.append(MenuItem('Import WIF', f=self.import_wif, predicate=not_hobbled_mode))
a_items = []
@ -115,6 +143,7 @@ class WIFStore(MenuSystem):
submenu = [
MenuItem("Detail", f=self.detail, arg=(wif,pk,sk)),
MenuItem("Descriptors", f=self.show_desc_step1, arg=pk),
MenuItem("Addresses", f=self.show_addr_step1, arg=pk),
MenuItem("Sign MSG", f=self.sign_msg_step1, arg=sk),
MenuItem('Delete', f=self.delete, arg=(i, pk), predicate=not_hobbled_mode),
@ -144,40 +173,51 @@ class WIFStore(MenuSystem):
await export_contents(title, wif, "wif.txt", None, None,
force_prompt=True, intro=msg, ux_title=title)
async def show_addr_step1(self, a, b, item):
pubkey = a2b_hex(item.arg)
async def show_desc_step1(self, a, b, item):
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.show_addr_step2, arg=(pubkey, af))
MenuItem(chains.addr_fmt_label(af), f=self.show_desc_step2, arg=(item.arg, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def show_desc_step2(self, a, b, item):
# allow to export pubkey, instead of main detail where WIF is exported
pk, af = item.arg
title = "Descriptor"
if af == AF_P2WPKH:
desc = "wpkh(%s)"
elif af == AF_CLASSIC:
desc = "pkh(%s)"
else:
assert af == AF_P2WPKH_P2SH
desc = "sh(wpkh(%s))"
from descriptor import append_checksum
desc = append_checksum(desc % pk)
from export import export_contents
await export_contents(title, desc, "wif_desc_%d.txt" % af, None, None,
force_prompt=True, intro=desc, ux_title=title)
async def show_addr_step1(self, a, b, item):
rv = [
MenuItem(chains.addr_fmt_label(af), f=self.show_addr_step2, arg=(item.arg, af))
for af in chains.SINGLESIG_AF
]
the_ux.push(MenuSystem(rv))
async def show_addr_step2(self, a, b, item):
from glob import NFC
pubkey, af = item.arg
node = node_from_pubkey(pubkey)
node = node_from_pubkey(a2b_hex(pubkey))
addr = chains.current_chain().address(node, af)
msg = show_single_address(addr) + "\n\n"
msg = show_single_address(addr)
escape = ""
# Q only hint keys
if not version.has_qwerty:
msg += "Press (1) to show address QR code."
escape += "1"
if NFC:
msg += "(3) to share via NFC."
escape += "3"
ux_title = chains.addr_fmt_label(af) if version.has_qwerty else None
title = chains.addr_fmt_label(af) if version.has_qwerty else None
while True:
ch = await ux_show_story(msg, title=title, escape=escape,
hint_icons=KEY_QR+(KEY_NFC if NFC else ''))
if ch == "x": return
if ch in "1"+KEY_QR:
await show_qr_code(addr, is_alnum=af == AF_P2WPKH)
elif NFC and (ch in "3"+KEY_NFC):
await NFC.share_text(addr)
from export import export_contents
await export_contents("Address", addr, "wif_addr.txt", None, None,
force_prompt=True, intro=msg, ux_title=ux_title)
async def sign_msg_step1(self, a, b, item):
privkey = a2b_hex(item.arg)
@ -224,16 +264,17 @@ class WIFStore(MenuSystem):
return
idx, pubkey = item.arg
wifs = settings.get('wifs', {})
wifs = settings.get('wifs', [])
if not wifs: return
try:
item = wifs[idx]
assert item[0] == pubkey
entry = wifs[idx]
assert entry[0] == pubkey
del wifs[idx]
settings.set('wifs', wifs)
settings.save()
except IndexError: pass
except IndexError:
return
the_ux.pop() # pop submenu
self.update_contents()
@ -298,12 +339,8 @@ class WIFStore(MenuSystem):
# allow commas, spaces, and newlines as separators
got = got.replace(',', ' ').split()
saved = settings.get("wifs", [])
len_saved = len(saved)
try:
new_wifs = []
dups = 0
for here in got:
here = here.strip()
@ -322,28 +359,10 @@ class WIFStore(MenuSystem):
sk = b2a_hex(kp.privkey()).decode()
pk = b2a_hex(kp.pubkey().to_bytes()).decode()
item = (pk, sk)
if item in new_wifs:
# duplicate in import content
continue
new_wifs.append((pk, sk))
if item in saved: # ignore dups
dups += 1
else:
new_wifs.append(item)
save_wif_store_items(new_wifs)
assert new_wifs, 'no valid WIF found' if not dups else 'duplicate WIF(s)'
if (len_saved + len(new_wifs)) > self.MAX_ITEMS:
await ux_show_story("Max %d items allowed in WIF Store.\n\nAttempted to import %d keys,"
" while remaining WIF store capacity is only %d. Please, make room"
" first." % (self.MAX_ITEMS, len(new_wifs), self.MAX_ITEMS - len_saved),
title="Failure")
return
saved.extend(new_wifs)
settings.set('wifs', saved)
settings.save()
self.update_contents()
except Exception as e:
@ -351,13 +370,54 @@ class WIFStore(MenuSystem):
title="Failure")
def init_wif_store():
# stored as hex strings, need load to bytes
wifs = settings.get('wifs', [])
if not wifs: return {}
res = {}
for pk, sk in wifs:
res[a2b_hex(pk)] = a2b_hex(sk)
return res
class WIFStore:
def __init__(self):
wifs = settings.get('wifs', [])
self.wifs = [] # max 30 items, each (pubkey, privkey)
for pk, sk in wifs:
self.wifs.append((a2b_hex(pk), a2b_hex(sk)))
# built lazily, on first match_address_hash() call
self._pkh = [] # hash160(pubkey) — P2PKH / P2WPKH
self._sh = [] # hash160(0014 || _pkh) — P2SH-P2WPKH
def __bool__(self):
return len(self.wifs) > 0
def __contains__(self, pubkey):
return self._privkey_for(pubkey) is not None
def __getitem__(self, pubkey):
sk = self._privkey_for(pubkey)
if sk is None: raise KeyError
return sk
def _privkey_for(self, pubkey):
for pk, sk in self.wifs:
if pk == pubkey:
return sk
def match_address_hash(self, addr_fmt, hash20):
if not self.wifs:
return None
if not self._pkh:
self._pkh = [ngu.hash.hash160(pk) for pk, _ in self.wifs]
if addr_fmt in (AF_P2WPKH, AF_CLASSIC):
table = self._pkh
elif addr_fmt == AF_P2SH:
if not self._sh:
self._sh = [ngu.hash.hash160(b'\x00\x14' + h) for h in self._pkh]
table = self._sh
else:
return None # AF_P2WSH / AF_P2TR / AF_BARE_PK / unknown — not us
try:
idx = table.index(hash20)
return idx, self.wifs[idx][0]
except ValueError:
return None
# EOF

View File

@ -124,7 +124,7 @@ You have confirmed the details of the new split.''')
# - stores encoded secret bytes (not word lists)
import_xor_parts = []
async def xor_all_done(data):
async def xor_all_done(data, force_tmp, done_cb):
# So we have another part, might be done or not.
global import_xor_parts
@ -178,9 +178,9 @@ async def xor_all_done(data):
if version.has_qwerty:
from ux_q1 import seed_word_entry
await seed_word_entry("Part %s Words" % chr(65+len(import_xor_parts)),
target_words, done_cb=xor_all_done)
target_words, done_cb=done_cb)
else:
nxt = XORWordNestMenu(num_words=target_words, done_cb=xor_all_done)
nxt = XORWordNestMenu(num_words=target_words, done_cb=done_cb)
the_ux.push(nxt)
elif ch == '2':
@ -190,7 +190,7 @@ async def xor_all_done(data):
enc = SecretStash.encode(seed_phrase=seed)
if pa.is_secret_blank():
if pa.is_secret_blank() and not force_tmp:
# save it since they have no other secret
set_seed_value(encoded=enc)
# update menu contents now that wallet defined
@ -239,7 +239,7 @@ async def show_n_parts(parts, chk_word):
return await ux_show_story(msg, title="Record these:", sensitive=True, escape="4",
hint_icons=KEY_QR)
async def xor_restore_start(*a):
async def xor_restore_start(*a, force_tmp=False):
# shown on import menu when no seed of any kind yet
# - or operational system
ch = await ux_show_story('''\
@ -261,6 +261,9 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
global import_xor_parts
import_xor_parts.clear()
async def done_cb(data):
return await xor_all_done(data, force_tmp=force_tmp, done_cb=done_cb)
from pincodes import pa
from glob import dis
@ -317,14 +320,17 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
if selected:
import_xor_parts += [opt[i][-1] for i in range(len(opt)) if i in selected]
return await xor_all_done(None)
return await done_cb(None)
if version.has_qwerty:
from ux_q1 import seed_word_entry
# if current loaded seed is added to xor - it is always A
await seed_word_entry("Part %s Words" % (chr(65+len(import_xor_parts))),
desired_num_words, done_cb=xor_all_done)
desired_num_words, done_cb=done_cb)
else:
return XORWordNestMenu(num_words=desired_num_words, done_cb=xor_all_done)
return XORWordNestMenu(num_words=desired_num_words, done_cb=done_cb)
async def xor_restore_temporary(*a):
return await xor_restore_start(*a, force_tmp=True)
# EOF

View File

@ -2,12 +2,12 @@
//
// AUTO-generated.
//
// built: 2026-03-23
// version: 6.5.0X
// built: 2026-03-05
// version: 5.5.0
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x5c7730a0UL;
return 0x5c6528a0UL;
}

View File

@ -2,12 +2,12 @@
//
// AUTO-generated.
//
// built: 2026-03-23
// version: 6.5.0QX
// built: 2026-03-05
// version: 1.4.0Q
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x5c7730a0UL;
return 0x5c650880UL;
}

View File

@ -19,7 +19,7 @@ LATEST_RELEASE = $(shell ls -t1 ../releases/*-mk-*.dfu ../releases/*-mk4-*.dfu |
# Our version for this release.
# - caution, the bootrom will not accept version < 3.0.0
VERSION_STRING = 6.5.0X
VERSION_STRING = 5.5.0
# keep near top, because defined default target (all)
include shared.mk

View File

@ -16,7 +16,7 @@ BOOTLOADER_DIR = q1-bootloader
LATEST_RELEASE = $(shell ls -t1 ../releases/*-q1-*.dfu | head -1)
# Our version for this release.
VERSION_STRING = 6.5.0QX
VERSION_STRING = 1.4.0Q
# Remove this closer to shipping.
#$(warning "Forcing debug build")

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
#
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Capture current (mainnet) block height for SSSP/CCC features
#
import sys, time, datetime
import urllib.request
FILE_NAME = "../shared/block_height.py"
def _get_block_height(url):
with urllib.request.urlopen(url) as response:
height_data = response.read().decode().strip()
return int(height_data)
def get_block_height(url):
try:
return _get_block_height(url)
except:
time.sleep(2)
return _get_block_height(url)
def parse_block_height_file():
with open(FILE_NAME, "r") as f:
for l in f.readlines():
if l.startswith("BLOCK_HEIGHT ="):
return int(l.split("=")[-1].strip())
return None
def write_block_height_file(block_height):
now = datetime.datetime.now(datetime.timezone.utc)
with open(FILE_NAME, "wt") as f:
f.write('''\
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# AUTO-generated.
#
# As of %s UTC
BLOCK_HEIGHT = %d
# EOF
''' % (now.strftime("%Y-%m-%d %H:%M:%S"), block_height))
def main():
current_height = None
for _ in range(2):
bh_a = get_block_height("https://mempool.space/api/blocks/tip/height")
bh_b = get_block_height("https://blockstream.info/api/blocks/tip/height")
if bh_a == bh_b:
current_height = bh_a
break
time.sleep(5)
if current_height is None:
raise RuntimeError("Could not get current block height")
file_block_height = parse_block_height_file()
if file_block_height is None:
raise RuntimeError("Could not parse block height from file")
if current_height > file_block_height:
write_block_height_file(current_height)
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
# EOF

View File

@ -5,14 +5,14 @@
# Capture build time and version number into a number used as the timestamp on
# all created files for that Coldcard version.
#
import os, sys, time, datetime
import os, sys, datetime
out_fname, version = sys.argv[1:]
assert out_fname.endswith('.c'), out_fname
if os.path.exists(out_fname):
# to help deterministic builds, don't replace the file from git if verison # is right
# to help deterministic builds, don't replace the file from git if version # is right
with open(out_fname, 'rt') as fd:
if ('// version: %s\n' % version) in fd.read():
print("==> %s already version %s; not changing it" % (out_fname, version))
@ -22,7 +22,7 @@ if os.path.exists(out_fname):
today = datetime.date.today()
value = ((today.year - 1980) << 25) | (today.month << 21) | (today.day << 16)
# only 2second resolution for times, so can only support minor verion up to x.x.5 and hard to see
# only 2second resolution for times, so can only support minor version up to x.x.5 and hard to see
# anyway, let's omit ... worst case, use the date instead
ver = ''.join(v for v in version if v in '0123456789.') # strip letter codes from end
h, m, _ = [int(x) for x in ver.split('b')[0].split('.')]

View File

@ -82,6 +82,18 @@ $(BOARD)/file_time.c: make_filetime.py *-Makefile shared.mk
./make_filetime.py $(BOARD)/file_time.c $(VERSION_STRING)
cp $(BOARD)/file_time.c .
.PHONY: block_height
block_height:
@python3 make_block_height.py; \
if [ $$? -eq 0 ]; then \
echo "Block Height file already up-to-date."; \
else \
echo "Block Height file updated."; \
git commit -m "update block height" ../shared/block_height.py; \
fi
# Make a factory release: using key #1
# - when executed in a repro w/o the required key, it defaults to key zero
# - and that's what happens inside the Docker build
@ -91,7 +103,7 @@ production.bin: firmware-signed.bin Makefile
SUBMAKE = $(MAKE) -f $(PARENT_MKFILE)
.PHONY: release
release: submods-match code-committed
release: submods-match code-committed block_height
$(SUBMAKE) clean
$(SUBMAKE) repro
test -f built/production.bin
@ -118,7 +130,7 @@ rc1:
rc2: RC2_TIMESTAMP = $(shell date "+%F_%H%M")
rc2: RC2_FNAME = ./RC2-$(RC2_TIMESTAMP)-$(HW_MODEL)-coldcard.dfu
rc2: RC2_FNAME_FACT = ./RC2-$(RC2_TIMESTAMP)-$(HW_MODEL)-factory.dfu
rc2: submods-match code-committed
rc2: submods-match code-committed block_height
$(SUBMAKE) clean
$(SUBMAKE) repro
test -f built/production.bin
@ -139,11 +151,9 @@ release-products: built/production.bin
-git commit $(BOARD)/file_time.c -m "For $(NEW_VERSION)"
$(SIGNIT) sign -m $(HW_MODEL) $(VERSION_STRING) -r built/production.bin $(PROD_KEYNUM) -o built/production.bin
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin $(RELEASE_FNAME)
ifeq ($(findstring X,$(NEW_VERSION)),)
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin \
-b $(BOOTLOADER_BASE):$(BOOTLOADER_DIR)/releases/$(BOOTLOADER_VERSION)/bootloader.bin \
$(RELEASE_FNAME:%.dfu=%-factory.dfu)
endif
@echo
@echo 'Made release: ' $(RELEASE_FNAME)
@echo

View File

@ -11,10 +11,6 @@ from ckcc.protocol import CCProtocolPacker
def find_bitcoind():
# search for the binary we need
# - should be in the path really
env_path = os.environ.get("CC_TEST_BITCOIND", None)
if env_path:
return env_path
easy = shutil.which('bitcoind')
if easy:
return easy
@ -64,10 +60,10 @@ class Bitcoind:
"-noprinttoconsole",
"-fallbackfee=0.0002",
"-server=1",
"-listen=0",
"-keypool=1",
"-listen=0",
f"-port={self.p2p_port}",
f"-rpcport={self.rpc_port}"
f"-rpcport={self.rpc_port}",
]
)
signal.signal(signal.SIGTERM, self.cleanup)

View File

@ -1,14 +1,13 @@
# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import hashlib, hmac, bech32, os
import hashlib, hmac, bech32
from typing import Union
from io import BytesIO
try:
from pysecp256k1 import (
ec_seckey_verify, ec_pubkey_create, ec_pubkey_serialize, ec_pubkey_parse,
ec_seckey_tweak_add, ec_pubkey_tweak_add, tagged_sha256
ec_seckey_tweak_add, ec_pubkey_tweak_add,
)
from pysecp256k1.extrakeys import xonly_pubkey_from_pubkey, xonly_pubkey_serialize, xonly_pubkey_tweak_add
except ImportError:
import ecdsa
SECP256k1 = ecdsa.curves.SECP256k1
@ -19,7 +18,6 @@ except ImportError:
from helpers import hash160, str_to_path
from base58 import encode_base58_checksum, decode_base58_checksum
from constants import BIP_341_H
HARDENED = 2 ** 31
@ -121,10 +119,6 @@ class PrivateKey(object):
tweaked = ec_seckey_tweak_add(self.k, tweak32)
return PrivateKey(sec_exp=tweaked)
def address(self, compressed: bool = True, chain: str = "BTC",
addr_fmt: str = "p2wpkh") -> str:
return self.K.address(compressed, chain, addr_fmt)
@classmethod
def from_wif(cls, wif_str: str) -> "PrivateKey":
"""
@ -199,17 +193,8 @@ class PublicKey(object):
return self.K.to_string(encoding="compressed" if compressed else "uncompressed")
def tweak_add(self, tweak32: bytes) -> "PublicKey":
assert len(tweak32) == 32
return PublicKey(pub_key=ec_pubkey_tweak_add(self.K, tweak32))
def taptweak(self, tweak32: bytes = None) -> "bytes":
xonly_key, _ = xonly_pubkey_from_pubkey(self.K)
tweak = tweak32 or xonly_pubkey_serialize(xonly_key)
tweak = tagged_sha256(b"TapTweak", tweak)
tweaked_pubkey = xonly_pubkey_tweak_add(xonly_key, tweak)
tweaked_xonly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey)
return xonly_pubkey_serialize(tweaked_xonly_pubkey)
@classmethod
def parse(cls, key_bytes: bytes) -> "PublicKey":
"""
@ -242,7 +227,7 @@ class PublicKey(object):
"""
return hash160(self.sec(compressed=compressed))
def address(self, compressed: bool = True, chain: str = "BTC",
def address(self, compressed: bool = True, testnet: bool = False,
addr_fmt: str = "p2wpkh") -> str:
"""
Generates bitcoin address from public key.
@ -255,33 +240,18 @@ class PublicKey(object):
3. p2wpkh (default)
:return: bitcoin address
"""
if chain == "BTC":
hrp = "bc"
pkh_prefix = b"\x00"
sh_prefix = b"\x05"
else:
pkh_prefix = b"\x6f"
sh_prefix = b"\xc4"
if chain == "XRT":
hrp = "bcrt"
elif chain == "XTN":
hrp = "tb"
else:
assert False
if addr_fmt == "p2tr":
tweaked_xonly = self.taptweak()
return bech32.encode(hrp=hrp, witver=1, witprog=tweaked_xonly)
h160 = self.h160(compressed=compressed)
if addr_fmt == "p2pkh":
return encode_base58_checksum(pkh_prefix + h160)
prefix = b"\x6f" if testnet else b"\x00"
return encode_base58_checksum(prefix + h160)
elif addr_fmt == "p2wpkh":
hrp = "tb" if testnet else "bc"
return bech32.encode(hrp=hrp, witver=0, witprog=h160)
elif addr_fmt == "p2sh-p2wpkh":
scr = b"\x00\x14" + h160 # witversion 0 + pubkey hash
h160 = hash160(scr)
return encode_base58_checksum(sh_prefix + h160)
prefix = b"\xc4" if testnet else b"\x05"
return encode_base58_checksum(prefix + h160)
raise ValueError("Unsupported address type.")
@ -738,12 +708,6 @@ class BIP32Node:
ek = PubKeyNode.parse(extended_key, testnet)
return cls(ek, netcode="XTN" if testnet else "BTC")
@classmethod
def from_chaincode_pubkey(cls, chain_code, pubkey, netcode="XTN"):
node = PubKeyNode(pubkey, chain_code, 0, 0,
False if netcode == "BTC" else True)
return cls(node, netcode=netcode)
def subkey_for_path(self, path):
path_list = str_to_path(path)
node = self.node
@ -766,9 +730,9 @@ class BIP32Node:
def hash160(self, compressed=True):
return self.node.public_key.h160(compressed)
def address(self, compressed=True, chain="XTN", addr_fmt="p2pkh"):
def address(self, compressed=True, netcode="XTN", addr_fmt="p2pkh"):
return self.node.public_key.address(compressed, addr_fmt=addr_fmt,
chain=chain)
testnet=False if netcode == "BTC" else True)
def sec(self, compressed=True):
return self.node.public_key.sec(compressed)
@ -788,21 +752,3 @@ class BIP32Node:
def parent_fingerprint(self):
return self.node.parent_fingerprint
def ranged_unspendable_internal_key(chain_code=32 * b"\x01", subderiv="/<0;1>/*"):
# provide ranged provably unspendable key in serialized extended key format for core to understand it
# core does NOT understand 'unspend('
pk = b"\x02" + bytes.fromhex(BIP_341_H)
node = BIP32Node.from_chaincode_pubkey(chain_code, pk)
return node.hwif() + subderiv
def random_keys(num_keys, path="86h/1h/0h"):
keys = []
for _ in range(num_keys):
k = BIP32Node.from_master_secret(os.urandom(32))
key = f"[{k.fingerprint().hex()}/{path}]{k.subkey_for_path(path).hwif()}"
keys.append(key)
return keys

View File

@ -2,14 +2,18 @@
#
# construct Proof of Reserves transaction according to BIP-322
#
import pytest, struct, hashlib
import struct, hashlib
from ckcc_protocol.protocol import MAX_TXN_LEN
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
from io import BytesIO
from helpers import hash160, taptweak, str_to_path
from helpers import hash160, str_to_path, taptweak
from bip32 import BIP32Node, PublicKey
from constants import simulator_fixed_tprv, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
from ctransaction import CTransaction, COutPoint, CTxIn, CTxOut, uint256_from_str
from sighash import legacy_sighash, segwit_v0_sighash, taproot_sighash, SIGHASH_DEFAULT, SIGHASH_ALL
from pysecp256k1 import ec_pubkey_parse, ecdsa_signature_parse_der, ecdsa_verify
from pysecp256k1.extrakeys import xonly_pubkey_parse
from pysecp256k1.schnorrsig import schnorrsig_verify
def bip322_msg_hash(msg):
@ -17,302 +21,402 @@ def bip322_msg_hash(msg):
return hashlib.sha256(tag_hash + tag_hash + msg).digest()
@pytest.fixture
def create_msg_file(sim_root_dir, garbage_collector):
def doit(msg, msg_hash):
# carelessly overwrites
fpath = f"{sim_root_dir}/MicroSD/{msg_hash.hex()}.txt"
with open(fpath, "w") as f:
f.write(msg.decode())
garbage_collector.append(fpath)
return doit
def ecdsa_verify_sig(pubkey, sig, digest):
if not sig or sig[-1] != SIGHASH_ALL:
return False
try:
parsed = ecdsa_signature_parse_der(sig[:-1])
return bool(ecdsa_verify(parsed, ec_pubkey_parse(pubkey), digest))
except Exception:
return False
@pytest.fixture
def bip322_txn(dev, pytestconfig, create_msg_file):
def bip322_verify(psbt_bytes):
"""Verify BIP-322 PSBT signatures without a full script interpreter.
def doit(inputs, msg=b"POR", addr_fmt="p2wpkh", input_amount=1E8, to_sign_lock_time=0,
sighash=None, psbt_hacker=None, witness_utxo=[], to_sign_nVersion=0):
Enforces the BIP-322 transaction shape, SIGHASH_ALL for ECDSA,
SIGHASH_DEFAULT/SIGHASH_ALL for taproot, and direct signature checks for
p2pkh, p2wpkh, p2sh-p2wpkh, sh, wsh, and p2tr key-path.
It intentionally omits consensus-level script evaluation rules such as
CLEANSTACK, MINIMALIF, NULLFAIL beyond empty CHECKMULTISIG dummy,
CODESEPARATOR/FindAndDelete handling, and NOP-upgrade checks; unsupported
scripts raise AssertionError.
"""
psbt = BasicPSBT().parse(psbt_bytes)
assert psbt.bip322_msg is not None
msg = psbt.bip322_msg
tx = CTransaction()
if psbt.txn:
tx.deserialize(BytesIO(psbt.txn))
else:
tx.nVersion = psbt.txn_version
tx.nLockTime = psbt.fallback_locktime or 0
for inp in psbt.inputs:
tx.vin.append(CTxIn(COutPoint(uint256_from_str(inp.previous_txid), inp.prevout_idx),
nSequence=inp.sequence if inp.sequence is not None else 0xffffffff))
for out in psbt.outputs:
tx.vout.append(CTxOut(out.amount, out.script))
msg_challenge = None
inp0 = psbt.inputs[0]
to_spend = None
if inp0.utxo:
to_spend = CTransaction()
to_spend.deserialize(BytesIO(inp0.utxo))
assert len(to_spend.vout) == 1
assert to_spend.vout[0].nValue == 0
script_pubkey = to_spend.vout[0].scriptPubKey
else:
assert inp0.witness_utxo
witness_utxo = CTxOut()
witness_utxo.deserialize(BytesIO(inp0.witness_utxo))
assert witness_utxo.nValue == 0
script_pubkey = witness_utxo.scriptPubKey
num_ins = len(inputs)
expected_to_spend = CTransaction()
expected_to_spend.nVersion = 0
expected_to_spend.nLockTime = 0
expected_to_spend.vin = [CTxIn(COutPoint(hash=0, n=0xffffffff),
scriptSig=b'\x00\x20' + bip322_msg_hash(msg),
nSequence=0)]
expected_to_spend.vout = [CTxOut(0, script_pubkey)]
expected_to_spend.calc_sha256()
if to_spend:
assert to_spend.serialize_without_witness() == expected_to_spend.serialize_without_witness()
to_spend = expected_to_spend
psbt = BasicPSBT()
assert tx.nVersion in (0, 2)
assert len(tx.vin) >= 1
assert tx.vin[0].prevout.hash == to_spend.sha256
assert tx.vin[0].prevout.n == 0
assert not (len(tx.vin) == 1 and (tx.vin[0].nSequence != 0 or tx.nLockTime != 0))
assert len(tx.vout) == 1
assert tx.vout[0].nValue == 0
assert tx.vout[0].scriptPubKey == b'\x6a'
to_sign = CTransaction()
to_sign.nLockTime = to_sign_lock_time
# must be set to 2 if BIP-68 is used (relative tx level lock)
to_sign.nVersion = to_sign_nVersion
master_xpub = dev.master_xpub or simulator_fixed_tprv
# we have a key; use it to provide "plausible" value inputs
mk = BIP32Node.from_wallet_key(master_xpub)
mfp = mk.fingerprint()
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
psbt.outputs = []
for i, inp in enumerate(inputs):
sp = f"0/{i}"
af = addr_fmt
ia = input_amount
pubkey = None # public key
try:
if inp[0] is not None:
af = inp[0]
if inp[1] is not None:
sp = inp[1]
if inp[2] is not None:
ia = inp[2]
if inp[3] is not None:
pubkey = inp[3]
except:
pass
if pubkey:
int_path = [0]
sec = pubkey
prevouts = []
for idx, txin in enumerate(tx.vin):
if idx == 0:
prevouts.append((0, script_pubkey))
else:
assert idx < len(psbt.inputs)
if psbt.inputs[idx].witness_utxo:
prev = CTxOut()
prev.deserialize(BytesIO(psbt.inputs[idx].witness_utxo))
else:
int_path = str_to_path(sp)
sec = mk.subkey_for_path(sp).sec()
prev_tx = CTransaction()
prev_tx.deserialize(BytesIO(psbt.inputs[idx].utxo))
prev = prev_tx.vout[txin.prevout.n]
prevouts.append((prev.nValue, prev.scriptPubKey))
subkey = PublicKey.parse(sec)
for idx, txin in enumerate(tx.vin):
amount, spk = prevouts[idx]
assert len(sec) == 33, "expect compressed"
inp = psbt.inputs[idx]
if len(spk) == 25 and spk[:3] == b'\x76\xa9\x14' and spk[-2:] == b'\x88\xac':
assert len(inp.part_sigs) == 1
pub, sig = next(iter(inp.part_sigs.items()))
assert hash160(pub) == spk[3:23]
assert ecdsa_verify_sig(pub, sig, legacy_sighash(tx, idx, spk))
continue
if af == "p2tr":
tweaked_xonly = taptweak(sec[1:])
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + mfp + struct.pack(f'<{"I" * len(int_path)}',
*int_path)
scr = bytes([81, 32]) + tweaked_xonly
if len(spk) == 22 and spk[:2] == b'\x00\x14':
assert len(inp.part_sigs) == 1
pub, sig = next(iter(inp.part_sigs.items()))
assert hash160(pub) == spk[2:22]
script_code = b'\x76\xa9\x14' + spk[2:22] + b'\x88\xac'
assert ecdsa_verify_sig(pub, sig, segwit_v0_sighash(tx, idx, script_code, amount))
continue
elif af in ("p2wpkh", "p2sh-p2wpkh", "p2wpkh-p2sh"):
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
scr = bytes([0x00, 0x14]) + subkey.h160()
if af != "p2wpkh":
# use classic p2wpkh (from above) as redeem script
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
elif af == "p2pkh":
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
scr = bytes([0x76, 0xa9, 0x14]) + subkey.h160() + bytes([0x88, 0xac])
if len(spk) == 34 and spk[:2] == b'\x00\x20':
assert inp.witness_script
assert hashlib.sha256(inp.witness_script).digest() == spk[2:34]
assert inp.part_sigs
sighash = segwit_v0_sighash(tx, idx, inp.witness_script, amount)
for pub, sig in inp.part_sigs.items():
assert ecdsa_verify_sig(pub, sig, sighash)
continue
if len(spk) == 34 and spk[:2] == b'\x51\x20':
assert inp.taproot_key_sig
if len(inp.taproot_key_sig) == 64:
sighash = SIGHASH_DEFAULT
sig = inp.taproot_key_sig
else:
raise ValueError("unknown addr_fmt %s" % af)
assert len(inp.taproot_key_sig) == 65
sighash = inp.taproot_key_sig[-1]
sig = inp.taproot_key_sig[:-1]
digest = taproot_sighash(tx, idx, prevouts, sighash)
assert schnorrsig_verify(sig, digest, xonly_pubkey_parse(spk[2:34]))
continue
if i == 0:
# first input always spends to_spend
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(hash=0, n=0xffffffff)
msg_hash = bip322_msg_hash(msg)
create_msg_file(msg, msg_hash)
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
to_spend.vout = [CTxOut(0, scr)] # always zero val
msg_challenge = scr
else:
# other outputs that we want to prove ownership
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
73
)
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
to_spend.vout.append(CTxOut(int(ia), scr))
if len(spk) == 23 and spk[:2] == b'\xa9\x14' and spk[-1:] == b'\x87':
assert inp.redeem_script
assert hash160(inp.redeem_script) == spk[2:22]
if len(inp.redeem_script) == 22 and inp.redeem_script[:2] == b'\x00\x14':
assert len(inp.part_sigs) == 1
pub, sig = next(iter(inp.part_sigs.items()))
assert hash160(pub) == inp.redeem_script[2:22]
script_code = b'\x76\xa9\x14' + inp.redeem_script[2:22] + b'\x88\xac'
assert ecdsa_verify_sig(pub, sig, segwit_v0_sighash(tx, idx, script_code, amount))
continue
if len(inp.redeem_script) == 34 and inp.redeem_script[:2] == b'\x00\x20':
assert inp.witness_script
assert inp.redeem_script == b'\x00\x20' + hashlib.sha256(inp.witness_script).digest()
assert inp.part_sigs
sighash = segwit_v0_sighash(tx, idx, inp.witness_script, amount)
for pub, sig in inp.part_sigs.items():
assert ecdsa_verify_sig(pub, sig, sighash)
continue
assert inp.part_sigs
sighash = legacy_sighash(tx, idx, inp.redeem_script)
for pub, sig in inp.part_sigs.items():
assert ecdsa_verify_sig(pub, sig, sighash)
continue
assert False, "unsupported script"
if sighash is not None:
psbt.inputs[i].sighash = sighash
def bip322_txn(inputs, msg=b"POR", addr_fmt="p2wpkh", input_amount=1E8, to_sign_lock_time=0,
sighash=None, psbt_hacker=None, witness_utxo=[], to_sign_nVersion=0,
psbt_v2=False, master_xpub=None):
to_spend.calc_sha256()
msg_challenge = None
if i in witness_utxo:
psbt.inputs[i].witness_utxo = to_spend.vout[-1].serialize()
else:
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
num_ins = len(inputs)
psbt = BasicPSBT()
psbt.bip322_msg = msg
to_sign = CTransaction()
to_sign.nLockTime = to_sign_lock_time
# must be set to 2 if BIP-68 is used (relative tx level lock)
to_sign.nVersion = to_sign_nVersion
master_xpub = master_xpub or simulator_fixed_tprv
# we have a key; use it to provide "plausible" value inputs
mk = BIP32Node.from_wallet_key(master_xpub)
mfp = mk.fingerprint()
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
psbt.outputs = []
for i, inp in enumerate(inputs):
sp = f"0/{i}"
af = addr_fmt
ia = input_amount
pubkey = None # public key
try:
if inp[0] is not None:
af = inp[0]
if inp[1] is not None:
sp = inp[1]
if inp[2] is not None:
ia = inp[2]
if inp[3] is not None:
pubkey = inp[3]
except:
pass
if pubkey:
int_path = [0]
sec = pubkey
else:
int_path = str_to_path(sp)
sec = mk.subkey_for_path(sp).sec()
subkey = PublicKey.parse(sec)
assert len(sec) == 33, "expect compressed"
if af == "p2tr":
tweaked_xonly = taptweak(sec[1:])
psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + mfp + struct.pack(f'<{"I" * len(int_path)}',
*int_path)
scr = bytes([81, 32]) + tweaked_xonly
elif af in ("p2wpkh", "p2sh-p2wpkh", "p2wpkh-p2sh"):
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
scr = bytes([0x00, 0x14]) + subkey.h160()
if af != "p2wpkh":
# use classic p2wpkh (from above) as redeem script
psbt.inputs[i].redeem_script = scr
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
elif af == "p2pkh":
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
scr = bytes([0x76, 0xa9, 0x14]) + subkey.h160() + bytes([0x88, 0xac])
else:
raise ValueError("unknown addr_fmt %s" % af)
if i == 0:
# first input always spends to_spend
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(hash=0, n=0xffffffff)
msg_hash = bip322_msg_hash(msg)
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
to_spend.vout = [CTxOut(0, scr)] # always zero val
msg_challenge = scr
else:
# other outputs that we want to prove ownership
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
73
)
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
to_spend.vout.append(CTxOut(int(ia), scr))
if sighash is not None:
psbt.inputs[i].sighash = sighash
to_spend.calc_sha256()
if i in witness_utxo:
psbt.inputs[i].witness_utxo = to_spend.vout[-1].serialize()
else:
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
if len(inputs) == 1:
# basic msg sign
seq = 0
else:
if to_sign_lock_time and not i:
seq = 0xfffffffd
else:
seq = 0xffffffff
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
to_sign.vin.append(spendable)
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
to_sign.vin.append(spendable)
# just one zero amount output with script null data OP_RETURN
op_ret_o = BasicPSBTOutput(idx=0)
op_return_out = CTxOut(0, b'\x6a')
to_sign.vout.append(op_return_out)
# just one zero amount output with script null data OP_RETURN
op_ret_o = BasicPSBTOutput(idx=0)
op_return_out = CTxOut(0, b'\x6a')
to_sign.vout.append(op_return_out)
psbt.outputs.append(op_ret_o)
psbt.outputs.append(op_ret_o)
psbt.txn = to_sign.serialize_with_witness()
psbt.txn = to_sign.serialize_with_witness()
# last minute chance to mod PSBT object
if psbt_hacker:
psbt_hacker(psbt)
# last minute chance to mod PSBT object
if psbt_hacker:
psbt_hacker(psbt)
rv = BytesIO()
psbt.serialize(rv)
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
if psbt_v2:
psbt.parsed_txn = CTransaction()
psbt.parsed_txn.deserialize(BytesIO(psbt.txn))
psbt.to_v2()
return rv.getvalue(), msg_challenge
rv = BytesIO()
psbt.serialize(rv)
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
return doit
return rv.getvalue(), msg_challenge
@pytest.fixture
def bip322_ms_txn(pytestconfig, create_msg_file):
def bip322_ms_txn(num_ins, M, keys, msg=b"POR", inp_af=AF_P2WSH, input_amount=1E8, path_mapper=None,
lock_time=0, with_sigs=False, sighash=None, hack_psbt=None, to_sign_nVersion=0,
psbt_v2=False):
from test_multisig import make_ms_address
def doit(num_ins, M, keys, msg=b"POR", inp_af=AF_P2WSH, input_amount=1E8, path_mapper=None,
lock_time=0, with_sigs=False, sighash=None, hack_psbt=None, to_sign_nVersion=0):
msg_challenge = None
msg_challenge = None
psbt = BasicPSBT()
psbt.bip322_msg = msg
psbt = BasicPSBT()
txn = CTransaction()
txn.nVersion = to_sign_nVersion
txn.nLockTime = lock_time
txn = CTransaction()
txn.nVersion = to_sign_nVersion
txn.nLockTime = lock_time
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
psbt.outputs = []
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
psbt.outputs = []
for i in range(num_ins):
# make a fake txn to supply each of the inputs
# - each input is 1BTC
for i in range(num_ins):
# make a fake txn to supply each of the inputs
# - each input is 1BTC
# addr where the fake money will be stored.
addr, scriptPubKey, script, details = make_ms_address(
M, keys, idx=i, addr_fmt=inp_af, path_mapper=path_mapper
)
# addr where the fake money will be stored.
addr, scriptPubKey, script, details = make_ms_address(
M, keys, idx=i, addr_fmt=inp_af, path_mapper=path_mapper
if inp_af == AF_P2WSH:
psbt.inputs[i].witness_script = script
elif inp_af == AF_P2SH:
psbt.inputs[i].redeem_script = script
else:
assert inp_af == AF_P2WSH_P2SH
psbt.inputs[i].witness_script = script
psbt.inputs[i].redeem_script = b'\0\x20' + hashlib.sha256(script).digest()
for pubkey, xfp_path in details:
psbt.inputs[i].bip32_paths[pubkey] = b''.join(struct.pack('<I', j) for j in xfp_path)
if with_sigs and (xfp_path[0] != keys[-1][0]) and len(psbt.inputs[i].part_sigs) < (M-1): # only cosigner signatures are added
psbt.inputs[i].part_sigs[pubkey] = b"\x30" + 70*b"a"
if i == 0:
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(hash=0, n=0xffffffff)
msg_hash = bip322_msg_hash(msg)
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
to_spend.vout.append(CTxOut(0, scriptPubKey))
msg_challenge = scriptPubKey
else:
# other outputs that we want to prove ownership
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
73
)
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
to_spend.vout.append(CTxOut(int(input_amount), scriptPubKey))
if inp_af == AF_P2WSH:
psbt.inputs[i].witness_script = script
elif inp_af == AF_P2SH:
psbt.inputs[i].redeem_script = script
else:
assert inp_af == AF_P2WSH_P2SH
psbt.inputs[i].witness_script = script
psbt.inputs[i].redeem_script = b'\0\x20' + hashlib.sha256(script).digest()
# always add whole txn as utxo
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
if sighash is not None and (i != 0):
psbt.inputs[i].sighash = sighash
for pubkey, xfp_path in details:
psbt.inputs[i].bip32_paths[pubkey] = b''.join(struct.pack('<I', j) for j in xfp_path)
if with_sigs and (xfp_path[0] != keys[-1][0]): # only cosigner signatures are added
psbt.inputs[i].part_sigs[pubkey] = b"\x30" + 70*b"a"
if i == 0:
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(hash=0, n=0xffffffff)
msg_hash = bip322_msg_hash(msg)
create_msg_file(msg, msg_hash)
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
to_spend.vout.append(CTxOut(0, scriptPubKey))
msg_challenge = scriptPubKey
else:
# other outputs that we want to prove ownership
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(
uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)),
73
)
to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)]
to_spend.vout.append(CTxOut(int(input_amount), scriptPubKey))
# always add whole txn as utxo
psbt.inputs[i].utxo = to_spend.serialize_with_witness()
if sighash is not None and (i != 0):
psbt.inputs[i].sighash = sighash
to_spend.calc_sha256()
to_spend.calc_sha256()
if num_ins == 1:
# basic msg sign
seq = 0
else:
if lock_time and not i:
seq = 0xfffffffd
else:
seq = 0xffffffff
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
txn.vin.append(spendable)
spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq)
txn.vin.append(spendable)
# just one zero amount output with script null data OP_RETURN
op_ret_o = BasicPSBTOutput(idx=0)
op_return_out = CTxOut(0, b'\x6a')
txn.vout.append(op_return_out)
# just one zero amount output with script null data OP_RETURN
op_ret_o = BasicPSBTOutput(idx=0)
op_return_out = CTxOut(0, b'\x6a')
txn.vout.append(op_return_out)
psbt.outputs.append(op_ret_o)
psbt.outputs.append(op_ret_o)
if hack_psbt:
hack_psbt(psbt)
if hack_psbt:
hack_psbt(psbt)
psbt.txn = txn.serialize_with_witness()
psbt.txn = txn.serialize_with_witness()
if psbt_v2:
psbt.parsed_txn = CTransaction()
psbt.parsed_txn.deserialize(BytesIO(psbt.txn))
psbt.to_v2()
rv = BytesIO()
psbt.serialize(rv)
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
return rv.getvalue(), msg_challenge
return doit
@pytest.fixture
def bip322_from_classic_tx(dev, create_msg_file):
def doit(psbt, msg=b"POR"):
# takes in any PSBT and creates BIP-322 PSBT with all inputs as POR
# ignores & drops all outputs and replaces with one 0 val OP_RETURN
# 0th input is adjusted as specified in BIP-322 (to_spend)
po = BasicPSBT().parse(psbt)
to_sign = CTransaction()
to_sign.deserialize(BytesIO(po.txn))
to_sign.nVersion = 0 # required
to_sign.vout = [] # drop all outputs
# just one zero amount output with script null data OP_RETURN
op_ret_o = BasicPSBTOutput(idx=0)
op_return_out = CTxOut(0, b'\x6a')
to_sign.vout.append(op_return_out)
po.outputs = [op_ret_o]
if po.inputs[0].utxo:
i0_utxo = CTransaction()
i0_utxo.deserialize(BytesIO(po.inputs[0].utxo))
scriptPubKey = i0_utxo.vout[to_sign.vin[0].prevout.n].scriptPubKey
else:
assert po.inputs[0].witness_utxo
i0_wutxo = CTxOut()
i0_wutxo.deserialize(BytesIO(po.inputs[0].witness_utxo))
scriptPubKey = i0_wutxo.scriptPubKey
to_spend = CTransaction()
to_spend.nVersion = 0
out_point = COutPoint(hash=0, n=0xffffffff)
msg_hash = bip322_msg_hash(msg)
create_msg_file(msg, msg_hash)
to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)]
to_spend.vout.append(CTxOut(0, scriptPubKey))
msg_challenge = scriptPubKey
to_spend.calc_sha256()
to_sign.vin[0] = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=0xffffffff)
po.inputs[0].utxo = to_spend.serialize_with_witness()
# if it has witness UTXO - get rid of it
po.inputs[0].witness_utxo = None
po.txn = to_sign.serialize_with_witness()
rv = BytesIO()
po.serialize(rv)
return rv.getvalue(), msg_challenge
return doit
rv = BytesIO()
psbt.serialize(rv)
assert rv.tell() <= MAX_TXN_LEN, 'too fat'
return rv.getvalue(), msg_challenge

View File

@ -2,10 +2,12 @@
#
import pytest, time, pdb, itertools
from charcodes import KEY_ENTER
from core_fixtures import _pick_menu_item, _cap_story, _press_select
from core_fixtures import _need_keypress, _cap_menu, _sim_exec
from core_fixtures import _pick_menu_item, _cap_story, _press_select, _word_menu_entry
from core_fixtures import _need_keypress, _cap_menu, _sim_exec, _pass_word_quiz
from run_sim_tests import ColdcardSimulator, clean_sim_data
from ckcc_protocol.cli import wait_and_download
from ckcc_protocol.client import ColdcardDevice
from ckcc_protocol.protocol import CCProtocolPacker
def _clone(source, target):
@ -45,7 +47,7 @@ def _clone(source, target):
assert f"Bring that card back and press {'ENTER' if target_is_Q else 'OK'} to complete clone process" in story
# SOURCE
# clone with miniscript wallet
# clone with multisig wallet
sim_source = ColdcardSimulator(args=[source_sim_arg, "--ms", "--p2wsh",
"--set", "nfc=1", "--set", "vidsk=1"])
sim_source.start(start_wait=6)
@ -104,10 +106,10 @@ def _clone(source, target):
sim_target.start(start_wait=6)
device = ColdcardDevice(is_simulator=True)
_pick_menu_item(device, target_is_Q, "Settings")
_pick_menu_item(device, target_is_Q, "Multisig/Miniscript")
_pick_menu_item(device, target_is_Q, "Multisig Wallets")
time.sleep(.1)
m = _cap_menu(device)
assert "P2WSH--2-of-4" in m
assert "2/4: P2WSH--2-of-4" in m
# check NFC/VDisk after clone - must be disabled
# USB enabled as we are on the simulator
@ -123,4 +125,99 @@ def test_clone(source, target):
_clone(source, target)
time.sleep(1)
def test_backup_restore_delta_pin():
# SOURCE
# clone with multisig wallet
clean_sim_data() # remove all from previous
sim_source = ColdcardSimulator(args=["--ms", "--p2wsh", "--set", "nfc=1", "--set", "vidsk=1"],
segregate=True) # in /tmp/cc-simulators
sim_source.start(start_wait=6)
device_source = ColdcardDevice(is_simulator=True, sn=sim_source.socket)
_pick_menu_item(device_source, False, "Settings")
time.sleep(.1)
_pick_menu_item(device_source, False, "Login Settings")
time.sleep(.1)
_pick_menu_item(device_source, False, "Trick PINs")
time.sleep(.1)
_pick_menu_item(device_source, False, "Add New Trick")
time.sleep(.1)
# twice, first select, then verify
for _ in range(2):
pin = "11-11"
pre, suff = pin.split("-")
for ch in pre:
_need_keypress(device_source, ch)
time.sleep(.1)
_press_select(device_source, False)
time.sleep(.2)
for ch in suff:
_need_keypress(device_source, ch)
time.sleep(.1)
_press_select(device_source, False)
time.sleep(.2)
_pick_menu_item(device_source, False, "Delta Mode")
time.sleep(.1)
title, story = _cap_story(device_source)
assert "trick PIN must be same length as true PIN and differ only in final 4 positions" in story
_press_select(device_source, False)
time.sleep(.1)
_press_select(device_source, False)
time.sleep(.1)
m = _cap_menu(device_source)
assert "11-11" in m[1]
ok = device_source.send_recv(CCProtocolPacker.start_backup())
assert ok is None
time.sleep(1)
title, story = _cap_story(device_source)
assert "backup file password" in story
word_list = [item.split()[-1] for item in story.split("\n")[1:-4]]
assert len(word_list) == 12
_pass_word_quiz(device_source, False, word_list)
_press_select(device_source, False) # bkpw
result, chk = wait_and_download(device_source, CCProtocolPacker.get_backup_file(), 0)
sim_source.stop()
# TARGET Q (empty)
sim_target = ColdcardSimulator(args=["--q1", "-l"])
sim_target.start(start_wait=6)
device_target = ColdcardDevice(is_simulator=True)
name = "backup-delta.7z"
path = f"../unix/work/MicroSD/{name}"
with open(path, "wb") as f:
f.write(result)
_pick_menu_item(device_target, True, "Import Existing")
_pick_menu_item(device_target, True, "Restore Backup")
_pick_menu_item(device_target, True, name)
time.sleep(.1)
_word_menu_entry(device_target, True, word_list, has_checksum=False)
_press_select(device_target, True) # allow backup restore
time.sleep(.1)
_press_select(device_target, True) # best security practices config
time.sleep(.1)
_press_select(device_target, True) # success
sim_target.stop()
time.sleep(1)
sim_target = ColdcardSimulator(args=["--q1"])
sim_target.start(start_wait=6)
device_target = ColdcardDevice(is_simulator=True, sn=sim_target.socket)
_pick_menu_item(device_target, True, "Settings")
time.sleep(.1)
_pick_menu_item(device_target, True, "Login Settings")
time.sleep(.1)
_pick_menu_item(device_target, True, "Trick PINs")
time.sleep(.1)
m = _cap_menu(device_target)
assert "11-11" in m[1]
# EOF

View File

@ -1,20 +1,22 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, bech32, pdb, base64
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, functools, io, math, pdb, base64
from subprocess import check_output
from ckcc.protocol import CCProtocolPacker
from helpers import B2A, U2SAT, hash160, taptweak, addr_from_display_format, seconds2human_readable
from helpers import B2A, U2SAT, hash160, addr_from_display_format, seconds2human_readable
from base58 import decode_base58_checksum
from bip32 import BIP32Node
from msg import verify_message
from api import bitcoind, match_key
from api import bitcoind_wallet, bitcoind_d_wallet, bitcoind_d_wallet_w_sk, bitcoind_d_sim_sign, bitcoind_d_dev_watch
from api import bitcoind_d_sim_watch, finalize_v2_v0_convert
from electrum import electrum
from binascii import b2a_hex, a2b_hex
from constants import *
from charcodes import *
from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen, _sim_eval
from core_fixtures import _press_select, _pick_menu_item, _enter_complex, _dev_hw_label
from core_fixtures import _do_keypresses
from txn import render_address
from bbqr import split_qrs
@ -207,14 +209,11 @@ def enter_pin(enter_number, press_select, cap_screen, is_q1):
@pytest.fixture
def do_keypresses(need_keypress):
def do_keypresses(dev):
# do a series of keypresses, any kind
def doit(value):
for ch in value:
need_keypress(ch)
f = functools.partial(_do_keypresses, dev)
return f
return doit
@pytest.fixture
def enter_text(need_keypress, is_q1):
@ -291,33 +290,29 @@ def get_setting(sim_execfile, sim_exec):
@pytest.fixture
def addr_vs_path(master_xpub):
def doit(given_addr, path=None, addr_fmt=None, script=None, chain="XTN"):
def doit(given_addr, path=None, addr_fmt=None, script=None, testnet=True):
from bip32 import BIP32Node
from ckcc_protocol.constants import AF_CLASSIC, AFC_PUBKEY, AF_P2WPKH, AFC_SCRIPT
from ckcc_protocol.constants import AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH
from bech32 import bech32_decode, convertbits, decode, Encoding
from bech32 import bech32_decode, convertbits, Encoding
from hashlib import sha256
if not script:
try:
# prefer using xpub if we can
mk = BIP32Node.from_wallet_key(master_xpub)
mk._netcode = chain
sk = mk.subkey_for_path(path)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
except:
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
mk._netcode = chain
sk = mk.subkey_for_path(path)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
if addr_fmt == AF_P2TR:
tweaked_xonly = taptweak(sk.sec()[1:])
decoded = decode(given_addr[:2], given_addr)
assert not given_addr.startswith("bcrt") # regtest
assert tweaked_xonly == bytes(decoded[1])
elif addr_fmt in {None, AF_CLASSIC}:
if addr_fmt in {None, AF_CLASSIC}:
# easy
assert sk.address(chain=chain) == given_addr
assert sk.address(netcode="XTN" if testnet else "BTC") == given_addr
elif addr_fmt & AFC_PUBKEY:
@ -365,6 +360,7 @@ def addr_vs_path(master_xpub):
return doit
@pytest.fixture(scope='module')
def capture_enabled(sim_eval):
# need to have sim_display imported early, see unix/frozen-modules/ckcc
@ -606,12 +602,15 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
qr = cap_screen_qr().decode('ascii')
if isinstance(addr_fmt, str):
try:
addr_fmt = unmap_addr_fmt[addr_fmt]
except KeyError:
addr_fmt = msg_sign_unmap_addr_fmt[addr_fmt]
if addr_fmt == "p2tr":
addr_fmt = AF_P2TR
else:
try:
addr_fmt = unmap_addr_fmt[addr_fmt]
except KeyError:
addr_fmt = msg_sign_unmap_addr_fmt[addr_fmt]
if (addr_fmt & AFC_BECH32) or (addr_fmt & AFC_BECH32M):
if addr_fmt & AFC_BECH32:
qr = qr.lower()
# check text --if any-- matches QR contents
@ -642,7 +641,7 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1):
for c, line in zip("XXXXXXBACK", full_split):
assert not line.endswith(c)
txt = None # most of the time there is no address
txt = None
else:
if is_change:
assert "CHANGE BACK" in full
@ -694,12 +693,6 @@ def get_secrets(sim_execfile):
return doit
@pytest.fixture
def clear_miniscript(unit_test):
def doit():
unit_test('devtest/wipe_miniscript.py')
return doit
@pytest.fixture
def press_select(dev, has_qwerty):
f = functools.partial(_press_select, dev, has_qwerty)
@ -1033,20 +1026,16 @@ def settings_set(sim_exec):
return doit
@pytest.fixture
def settings_append(sim_exec):
def doit(key, val):
x = sim_exec("x=settings.get('%s',[])\nx.append(%r)\nsettings.set('%s', x)" % (key, val, key))
assert x == ''
return doit
@pytest.fixture
def settings_get(sim_exec):
def doit(key, def_val=None, prelogin=False):
source = "from nvstore import SettingsObject;SettingsObject.prelogin()" if prelogin else "settings"
cmd = f"RV.write(repr({source}.get('{key}', {def_val!r})))"
resp = sim_exec(cmd)
if prelogin:
src = f"from nvstore import SettingsObject;RV.write(repr(SettingsObject.prelogin().get('{key}', {def_val!r})))"
else:
src = f"RV.write(repr(settings.get('{key}', {def_val!r})))"
resp = sim_exec(src)
assert 'Traceback' not in resp, resp
return eval(resp)
@ -1066,8 +1055,9 @@ def master_settings_get(sim_exec):
@pytest.fixture
def settings_remove(sim_exec):
def doit(key):
x = sim_exec("settings.remove_key('%s')" % key)
def doit(key, prelogin=False):
source = "from nvstore import SettingsObject;SettingsObject.prelogin()" if prelogin else "settings"
x = sim_exec("%s.remove_key('%s')" % (source, key))
assert x == ''
return doit
@ -1414,8 +1404,8 @@ def try_sign_microsd(open_microsd, cap_story, pick_menu_item, goto_home,
def try_sign(start_sign, end_sign):
def doit(filename_or_data, accept=True, finalize=False, accept_ms_import=False,
exit_export_loop=True, miniscript=None):
ip = start_sign(filename_or_data, finalize=finalize, miniscript=miniscript)
exit_export_loop=True):
ip = start_sign(filename_or_data, finalize=finalize)
return ip, end_sign(accept, finalize=finalize, accept_ms_import=accept_ms_import,
exit_export_loop=exit_export_loop)
@ -1424,7 +1414,7 @@ def try_sign(start_sign, end_sign):
@pytest.fixture
def start_sign(dev):
def doit(filename, finalize=False, stxn_flags=0x0, miniscript=None):
def doit(filename, finalize=False, stxn_flags=0x0):
if filename[0:5] == b'psbt\xff':
ip = filename
filename = 'memory'
@ -1436,8 +1426,7 @@ def start_sign(dev):
ll, sha = dev.upload_file(ip)
dev.send_recv(CCProtocolPacker.sign_transaction(ll, sha, finalize, flags=stxn_flags,
miniscript_name=miniscript))
dev.send_recv(CCProtocolPacker.sign_transaction(ll, sha, finalize, flags=stxn_flags))
return ip
@ -1696,9 +1685,6 @@ def nfc_read_url(nfc_read, press_cancel):
def nfc_write(request, needs_nfc, is_q1):
# WRITE data into NFC "chip"
def doit_usb(ccfile):
from ckcc.constants import MAX_MSG_LEN
if len(ccfile) >= MAX_MSG_LEN:
pytest.xfail("MAX_MSG_LEN")
sim_exec = request.getfixturevalue('sim_exec')
press_select = request.getfixturevalue('press_select')
rv = sim_exec('list(glob.NFC.big_write(%r))' % ccfile)
@ -1866,7 +1852,7 @@ def load_shared_mod():
return doit
@pytest.fixture
def verify_detached_signature_file(microsd_path, virtdisk_path, garbage_collector):
def verify_detached_signature_file(microsd_path, virtdisk_path):
def doit(fnames, sig_fname, way, addr_fmt=None):
fpaths = []
for fname in fnames:
@ -1875,7 +1861,6 @@ def verify_detached_signature_file(microsd_path, virtdisk_path, garbage_collecto
else:
path = virtdisk_path(fname)
fpaths.append(path)
garbage_collector.append(path)
if way == "sd":
sig_path = microsd_path(sig_fname)
@ -1916,7 +1901,9 @@ def verify_detached_signature_file(microsd_path, virtdisk_path, garbage_collecto
assert (hashlib.sha256(contents).digest().hex() + fn_addendum) in msg
assert verify_message(address, sig, msg) is True
garbage_collector.append(sig_path)
try:
os.unlink(sig_path)
except: pass
return fcontents[0], address
return doit
@ -1940,9 +1927,6 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache
if is_json:
assert fname.endswith(".json")
if addr_fmt == AF_P2TR:
addr_fmt = AF_CLASSIC
contents, address = verify_detached_signature_file([fname], sig_fn, way, addr_fmt)
if is_json:
@ -1979,7 +1963,7 @@ def file_tx_signing_done(virtdisk_path, microsd_path):
txid = None
for l in _split:
if "TXID" in l:
if "TXID:" in l:
txid = l.split("\n")[-1].strip()
assert len(txid) == 64, "wrong txid"
break
@ -1991,10 +1975,10 @@ def file_tx_signing_done(virtdisk_path, microsd_path):
@pytest.fixture
def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json,
load_export_and_verify_signature, is_q1, press_cancel, press_select, readback_bbqr,
cap_screen_qr, nfc_read_txn, file_tx_signing_done, garbage_collector):
cap_screen_qr, nfc_read_txn, file_tx_signing_done):
def doit(way, label, is_json, sig_check=True, addr_fmt=AF_CLASSIC, ret_sig_addr=False,
tail_check=None, sd_key=None, vdisk_key=None, nfc_key=None, ret_fname=False,
fpattern=None, qr_key=None, is_tx=False, encoding="base64", skip_query=False):
fpattern=None, qr_key=None, is_tx=False, encoding="base64"):
s_label = None
if label == "Address summary":
@ -2006,62 +1990,61 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
"nfc": nfc_key or (KEY_NFC if is_q1 else "3"),
"qr": qr_key or (KEY_QR if is_q1 else "4"),
}
if not skip_query:
time.sleep(0.2)
title, story = cap_story()
if way == "sd":
if (f"({key_map['sd']}) to save {s_label if s_label else label} "
f"{'' if is_tx else 'file '}to SD Card") in story:
need_keypress(key_map['sd'])
time.sleep(0.2)
title, story = cap_story()
if way == "sd":
if (f"({key_map['sd']}) to save {s_label if s_label else label} "
f"{'' if is_tx else 'file '}to SD Card") in story:
need_keypress(key_map['sd'])
elif way == "nfc":
if f"{key_map['nfc'] if is_q1 else '(3)'} to share via NFC" not in story:
pytest.skip("NFC disabled")
else:
need_keypress(key_map['nfc'])
time.sleep(0.2)
if is_tx:
nfc_export = nfc_read_txn()
return nfc_export[1:]
if is_json:
nfc_export = nfc_read_json()
else:
nfc_export = nfc_read_text()
time.sleep(0.3)
press_cancel() # exit NFC animation
return nfc_export
elif way == "qr":
if 'file written' in story:
assert not is_q1
# mk4 only does QR if fits in normal QR, becaise it can't do BBQr
pytest.skip('no BBQr on Mk4')
need_keypress(key_map["qr"])
time.sleep(0.3)
try:
assert is_q1
file_type, data = readback_bbqr()
if file_type == "J":
return json.loads(data)
elif file_type == "U":
return data.decode('utf-8') if not isinstance(data, str) else data
elif file_type in ("P", "T"):
return data
else:
raise NotImplementedError
except:
res = cap_screen_qr().decode('ascii')
try:
return json.loads(res)
except:
return res
elif way == "nfc":
if f"{key_map['nfc'] if is_q1 else '(3)'} to share via NFC" not in story:
pytest.skip("NFC disabled")
else:
# virtual disk
if f"({key_map['vdisk']}) to save to Virtual Disk" not in story:
pytest.skip("Vdisk disabled")
need_keypress(key_map['nfc'])
time.sleep(0.2)
if is_tx:
nfc_export = nfc_read_txn()
return nfc_export[1:]
if is_json:
nfc_export = nfc_read_json()
else:
need_keypress(key_map['vdisk'])
nfc_export = nfc_read_text()
time.sleep(0.3)
press_cancel() # exit NFC animation
return nfc_export
elif way == "qr":
if 'file written' in story:
assert not is_q1
# mk4 only does QR if fits in normal QR, becaise it can't do BBQr
pytest.skip('no BBQr on Mk4')
need_keypress(key_map["qr"])
time.sleep(0.3)
try:
assert is_q1
file_type, data = readback_bbqr()
if file_type == "J":
return json.loads(data)
elif file_type == "U":
return data.decode('utf-8') if not isinstance(data, str) else data
elif file_type in ("P", "T"):
return data
else:
raise NotImplementedError
except:
res = cap_screen_qr().decode('ascii')
try:
return json.loads(res)
except:
return res
else:
# virtual disk
if f"({key_map['vdisk']}) to save to Virtual Disk" not in story:
pytest.skip("Vdisk disabled")
else:
need_keypress(key_map['vdisk'])
time.sleep(0.2)
title, story = cap_story()
@ -2092,8 +2075,6 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
if is_json:
export = json.loads(export)
garbage_collector.append(path)
press_select()
if ret_sig_addr and sig_addr:
@ -2243,7 +2224,7 @@ def tapsigner_encrypted_backup(microsd_path, virtdisk_path):
return doit
@pytest.fixture
def choose_by_word_length(need_keypress, press_select):
def choose_by_word_length(need_keypress):
# for use in seed XOR menu system
def doit(num_words):
if num_words == 12:
@ -2251,7 +2232,7 @@ def choose_by_word_length(need_keypress, press_select):
elif num_words == 18:
need_keypress("2")
else:
press_select()
need_keypress("y")
return doit
# workaround: need these fixtures to be global so I can call test from a test
@ -2264,7 +2245,7 @@ def verify_backup_file(goto_home, pick_menu_item, cap_story, need_keypress):
# Check on-device verify UX works.
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Backup')
pick_menu_item('File Management')
pick_menu_item('Verify Backup')
time.sleep(0.1)
pick_menu_item(os.path.basename(fn))
@ -2616,7 +2597,7 @@ def explorer_input_check(cap_story, press_cancel, need_keypress, is_q1, verify_q
n = BIP32Node.from_wallet_key(simulator_fixed_xprv if chain == "BTC" else simulator_fixed_tprv)
for pk, der in parsed_our_keys.items():
assert bytes.fromhex(pk) == n.subkey_for_path(der.split("/", 1)[-1]).sec()[1 if af == "p2tr" else 0:]
assert bytes.fromhex(pk) == n.subkey_for_path(der.split("/", 1)[-1]).sec()
# MULTISIG
if parsed_multisig is None:
@ -2636,10 +2617,7 @@ def explorer_input_check(cap_story, press_cancel, need_keypress, is_q1, verify_q
# SIGHASH
if parsed_sighash is None:
if af == "p2tr":
assert sighash in [None, "DEFAULT"]
else:
assert sighash in [None, "ALL"]
assert sighash in [None, "ALL"]
else:
assert sighash == parsed_sighash
@ -2666,7 +2644,8 @@ def txin_explorer(cap_story, press_cancel, need_keypress, is_q1, cap_menu,
time.sleep(.1)
title, story = cap_story()
ss = story.split("\n\n")
assert "Press RIGHT to see next group" in ss[-1]
if i < (num_inputs - 1):
assert "RIGHT to see next group" in ss[-1]
if i:
assert " LEFT to go back" in ss[-1]
else:
@ -2741,7 +2720,8 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_addr
_, story = cap_story()
ss = story.split("\n\n")
assert len(ss) == (len(d) * 2) + 1
assert "Press RIGHT to see next group" in ss[-1]
if (i + n) < len(data):
assert "RIGHT to see next group" in ss[-1]
if i:
assert " LEFT to go back" in ss[-1]
else:
@ -2782,9 +2762,6 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_addr
elif af in ("p2wpkh", "p2wsh"):
target = "bc1q" if chain == "BTC" else "tb1q"
assert addr.startswith(target)
elif af == "p2tr":
target = "bc1p" if chain == "BTC" else "tb1p"
assert addr.startswith(target)
elif af in ("p2sh", "p2wpkh-p2sh", "p2wsh-p2sh"):
target = "3" if chain == "BTC" else "2"
assert addr.startswith(target)
@ -2833,30 +2810,6 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1, verify_qr_addr
return doit
@pytest.fixture
def validate_address():
# Check whether an address is covered by the given subkey
def doit(addr, sk):
if addr[0] in '1mn':
chain = "XTN" if addr[0] != "1" else "BTC"
assert addr == sk.address(addr_fmt="p2pkh", chain=chain)
elif addr[0:4] in {'bc1q', 'tb1q'}:
chain = "XTN" if addr[0:4] != 'bc1q' else "BTC"
assert addr == sk.address(addr_fmt="p2wpkh", chain=chain)
elif addr[0:6] == "bcrt1q":
assert addr == sk.address(addr_fmt="p2wpkh", chain="XRT")
elif addr[0:4] in {'bc1p', 'tb1p'}:
chain = "XTN" if addr[0:4] != 'bc1p' else "BTC"
assert addr == sk.address(addr_fmt="p2tr", chain=chain)
elif addr[0:6] == "bcrt1p":
assert addr == sk.address(addr_fmt="p2tr", chain="XRT")
elif addr[0] in '23':
chain = "XTN" if addr[0] != '3' else "BTC"
assert addr == sk.address(addr_fmt="p2sh-p2wpkh", chain=chain)
else:
raise ValueError(addr)
return doit
@pytest.fixture
def skip_if_useless_way(is_q1, nfc_disabled, vdisk_disabled):
@ -2885,8 +2838,7 @@ def dev_core_import_object(dev):
ders = [
("m/44h/1h/0h", AF_CLASSIC),
("m/49h/1h/0h", AF_P2WPKH_P2SH),
("m/84h/1h/0h", AF_P2WPKH),
("m/86h/1h/0h", AF_P2TR),
("m/84h/1h/0h", AF_P2WPKH)
]
descriptors = []
for idx, (path, addr_format) in enumerate(ders):
@ -3068,24 +3020,39 @@ def import_wif_to_store(goto_home, pick_menu_item, cap_story, press_select, cap_
return doit
@pytest.fixture
def bip322_txn(dev, pytestconfig):
from bip322 import bip322_txn
return functools.partial(bip322_txn, master_xpub=dev.master_xpub,
psbt_v2=pytestconfig.getoption('psbt2'))
@pytest.fixture
def bip322_ms_txn(pytestconfig):
from bip322 import bip322_ms_txn
return functools.partial(bip322_ms_txn, psbt_v2=pytestconfig.getoption('psbt2'))
@pytest.fixture
def bip322_verify():
from bip322 import bip322_verify
return bip322_verify
# useful fixtures
from test_backup import backup_system
from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll, try_sign_bbqr, split_scan_bbqr
from bip322 import bip322_txn, bip322_ms_txn, create_msg_file, bip322_from_classic_tx
from test_bip39pw import set_bip39_pw
from test_ccc import get_last_violation
from test_ccc import get_last_violation, setup_ccc, goto_ccc_menu, ccc_ms_setup, bitcoind_create_watch_only_wallet
from test_drv_entro import derive_bip85_secret, activate_bip85_ephemeral
from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto_eph_seed_menu
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
from test_ephemeral import verify_ephemeral_secret_ui, get_identity_story, get_seed_value_ux, seed_vault_enable
from test_hobble import set_hobble
from test_msg import verify_msg_sign_story, sign_msg_from_text, msg_sign_export, sign_msg_from_address
from test_multisig import import_ms_wallet, make_multisig, fake_ms_txn
from test_miniscript import (offer_minsc_import, get_cc_key, bitcoin_core_signer, import_miniscript, usb_miniscript_get,
usb_miniscript_addr, create_core_wallet, import_duplicate, address_explorer_check,
miniscript_descriptors, usb_miniscript_policy)
from test_multisig import make_ms_address, make_myself_wallet
from test_musig2 import build_musig_wallet
from test_multisig import import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn
from test_multisig import make_ms_address, clear_ms, make_myself_wallet, import_multisig
from test_notes import need_some_notes, need_some_passwords, goto_notes
from test_nfc import try_sign_nfc, ndef_parse_txn_psbt
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed

View File

@ -23,11 +23,9 @@ unmap_addr_fmt = {
'p2wsh': AF_P2WSH,
'p2wsh-p2sh': AF_P2WSH_P2SH,
'p2sh-p2wsh': AF_P2WSH_P2SH,
"p2tr": AF_P2TR,
}
msg_sign_unmap_addr_fmt = {
'p2tr': AF_P2TR, # not supported for msg signign tho
'p2pkh': AF_CLASSIC,
'p2wpkh': AF_P2WPKH,
'p2sh-p2wpkh': AF_P2WPKH_P2SH,
@ -35,28 +33,27 @@ msg_sign_unmap_addr_fmt = {
}
addr_fmt_names = {
AF_P2TR: 'p2tr',
AF_CLASSIC: 'p2pkh',
AF_P2SH: 'p2sh',
AF_P2WPKH: 'p2wpkh',
AF_P2WSH: 'p2wsh',
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
AF_P2WSH_P2SH: 'p2sh-p2wsh',
AF_P2WSH_P2SH: 'p2wsh-p2sh',
AF_P2TR: "p2tr",
}
# all possible addr types, including multisig/scripts
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2tr']
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh']
# single-signer
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh', 'p2tr']
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh']
# multi signer
ADDR_STYLES_MS = ['p2sh', 'p2wsh', 'p2wsh-p2sh']
# SIGHASH
SIGHASH_MAP = {
"DEFAULT": 0,
"ALL": 1,
"NONE": 2,
"SINGLE": 3,
@ -65,9 +62,5 @@ SIGHASH_MAP = {
"SINGLE|ANYONECANPAY": 3 | 0x80,
}
SIGHASH_MAP_NON_TAPROOT = {k:v for k, v in SIGHASH_MAP.items() if k != "DEFAULT"}
# (2**31) - 1 --> max unhardened, but we handle hardened via h elsewhere
MAX_BIP32_IDX = 2147483647
BIP_341_H = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
MAX_BIP32_IDX = 2147483647

View File

@ -7,7 +7,7 @@
# Below functions are injected with proper scoped `device` in conftest.py
# using funtools.partial.
#
import time
import time, re
from charcodes import *
from ckcc_protocol.client import CCProtocolPacker
@ -149,4 +149,116 @@ def _enter_complex(device, is_Q, target, apply=False, b39pass=True):
if apply:
_pick_menu_item(device, is_Q, "APPLY")
def _pass_word_quiz(device, is_Q, words, prefix='', preload=None):
if not preload:
_press_select(device, is_Q)
time.sleep(.01)
count = 0
last_title = None
while 1:
title, body = preload or _cap_story(device)
preload = None
if not title.startswith('Word ' + prefix): break
assert title.endswith(' is?')
assert not last_title or last_title != title, "gave wrong ans?"
wn = int(title.split()[1][len(prefix):])
assert 1 <= wn <= len(words)
wn -= 1
ans = [w[3:].strip() for w in body.split('\n') if w and w[2] == ':']
assert len(ans) == 3
correct = ans.index(words[wn])
assert 0 <= correct < 3
# print("Pick %d: %s" % (correct, ans[correct]))
_need_keypress(device, chr(49 + correct))
time.sleep(.1)
count += 1
last_title = title
return count, title, body
def _do_keypresses(device, value):
for ch in value:
_need_keypress(device, ch)
def _word_menu_entry(device, is_Q, words, has_checksum=True, q_accept=True):
if is_Q:
# easier for us on Q, but have to anticipate the autocomplete
for n, w in enumerate(words, start=1):
_do_keypresses(device, w[0:2])
time.sleep(0.1)
if 'Next key' in _cap_screen(device):
_do_keypresses(device, w[2])
time.sleep(.01)
if 'Next key' in _cap_screen(device):
if len(w) > 3:
_do_keypresses(device, w[3])
else:
_do_keypresses(device, KEY_DOWN)
time.sleep(.01)
pat = rf'{n}:\s?{w}'
for x in range(10):
if re.search(pat, _cap_screen(device)):
break
time.sleep(0.02)
else:
raise RuntimeError('timeout')
if len(words) == 23:
_do_keypresses(device, KEY_DOWN)
time.sleep(.03)
cap_scr = _cap_screen(device)
while 'Next key' in cap_scr:
target = cap_scr.split("\n")[-1].replace("Next key: ", "")
# picks first choice!?
_do_keypresses(device, target[0])
time.sleep(.03)
cap_scr = _cap_screen(device)
else:
cap_scr = _cap_screen(device)
if has_checksum:
assert 'Valid words' in cap_scr
else:
assert 'Press ENTER if all done' in cap_scr
if q_accept:
_do_keypresses(device, '\r')
return
# do the massive drilling-down to pick a specific pass phrase
assert len(words) in {1, 12, 18, 23, 24}
for word in words:
while 1:
menu = _cap_menu(device)
which = None
for m in menu:
if '-' not in m:
if m == word:
which = m
break
else:
assert m[-1] == '-'
if m == word[0:len(m)-1]+'-':
which = m
break
assert which, "cant find: " + word
_pick_menu_item(device, is_Q, which)
if '-' not in which:
break
# EOF

Some files were not shown because too many files have changed in this diff Show More