Compare commits

...

179 Commits
master ... edge

Author SHA1 Message Date
scgbckbone
158c8a772b remove out of date taproot entry from limitations - point to taproot doc instead 2024-03-18 10:55:54 -04:00
scgbckbone
5af5a9ae2f bugfix: bsms coordinator round 2 export data were not properly numbered when exporting via NFC 2024-03-18 10:55:32 -04:00
scgbckbone
12e54d379b bugfix: reads in final 3 byte of file could return incorrect data 2024-02-26 09:48:07 -05:00
scgbckbone
57f9b367d5 testing: add BIP86 vectors 2024-01-25 14:42:55 -05:00
Peter D. Gray
742ac8e67e
New release: 2024-01-18T1507-v6.2.2X 2024-01-18 10:07:51 -05:00
Peter D. Gray
837d181612
undo 2024-01-18 10:06:03 -05:00
Peter D. Gray
4b82668a6a
back to master 2024-01-18 09:55:45 -05:00
Peter D. Gray
1c0fc15ca9
New release: 2024-01-18T1443-v6.2.2X 2024-01-18 09:43:23 -05:00
Peter D. Gray
c11616d415
version bump and release notes tidy 2024-01-18 09:39:36 -05:00
scgbckbone
8df7b6ff3b HSM: 'wallet' rule enabled for miniscript; allow miniscript address show 2024-01-18 09:22:14 -05:00
scgbckbone
b1a7d2b1b2 pwsave improve exception handling 2024-01-18 09:00:18 -05:00
scgbckbone
2c46a22679 bugfix: fix pwsave froze
(cherry picked from commit f755947f7e)
2024-01-18 09:00:18 -05:00
scgbckbone
04d2099724 bugfix: address format not defined in ShowMiniscriptAddress 2024-01-12 08:34:07 -05:00
scgbckbone
d8dd62732c HW Accelerated AES CTR for BSMS and passphrase saver 2024-01-12 08:32:14 -05:00
scgbckbone
5e2e435583 always use SettingsObject class for master data 2024-01-04 11:52:47 -05:00
scgbckbone
6d8c1ee96a fix tests 2024-01-04 11:52:47 -05:00
Peter D. Gray
3ad4262fcf nits
(cherry picked from commit 55d5490852)
2024-01-04 11:52:47 -05:00
scgbckbone
06869c5304 bugfix: lockdown temporary seed was no-op
(cherry picked from commit d289bfc7c2)
2024-01-04 11:52:47 -05:00
Peter D. Gray
99230c185f spelling
(cherry picked from commit f6aec0ec2a)
2024-01-04 11:52:47 -05:00
scgbckbone
5a21760e2f xprv master seed with tmp seeds and bip39 passphrase
(cherry picked from commit 5bfdc4f45a)
2024-01-04 11:52:47 -05:00
Peter D. Gray
54192f7c50 more british & precise
(cherry picked from commit 46dc0b5b6d)
2024-01-04 11:52:47 -05:00
Peter D. Gray
6182bf33d5 Bump
(cherry picked from commit 979c27387e)
2024-01-04 11:52:47 -05:00
Peter D. Gray
d293d08f8d cleanup of ftux
(cherry picked from commit f6f977503e)
2024-01-04 11:52:47 -05:00
Peter D. Gray
b80dd72f2d Remove FTUX, add simple welcome screen
(cherry picked from commit d1c5b907c0)
2024-01-04 11:52:47 -05:00
Peter D. Gray
74d0a92cbc logout at end of menu
(cherry picked from commit 58f0adc560)
2024-01-04 11:52:47 -05:00
scgbckbone
f190edb302 fix tests
(cherry picked from commit 697b6e211d)
2024-01-04 11:52:47 -05:00
scgbckbone
4fefcb6dc6 HSM multisig 400 test
(cherry picked from commit 3977ae2ce0)
2024-01-04 11:52:47 -05:00
Peter D. Gray
b7e345515f edits, set target date
(cherry picked from commit 09b9065e10)
2024-01-04 11:52:47 -05:00
scgbckbone
2cd038c431 Improve BIP39 Passphrase UX if temporary seed active and passphrase applicable
(cherry picked from commit 4359a9735b)
2024-01-04 11:52:47 -05:00
scgbckbone
2bd2b361a8 fix change_pin test
(cherry picked from commit 9824e59ef9)
2024-01-04 11:52:47 -05:00
scgbckbone
eef6aaf95a bugfix: prevent yikes in clone coldcard - creating backup with bypass_tmp=True on master secret
(cherry picked from commit 9594efcf03)
2024-01-04 11:52:47 -05:00
scgbckbone
eb258f4b2a pwsave menu UX rework; do not allow empty bip39 passphrase
(cherry picked from commit 3e5fd573a6)
2024-01-04 11:52:47 -05:00
scgbckbone
d341ed3f57 Remove legacy Mk1-3 code from pin changing
(cherry picked from commit 79c143b7eb)
2024-01-04 11:52:47 -05:00
scgbckbone
8c519a5bf3 provide info about Tx level locktimes (nLocktime, nSequence) when signing
(cherry picked from commit af753c38be)
2024-01-04 11:52:47 -05:00
scgbckbone
a9c402cba3 allow passphrase via USB if passphrase already set (work on master seed in that case); show password over USB UX change
(cherry picked from commit 4a39fc82f1)
2024-01-04 11:52:47 -05:00
scgbckbone
b3480c3ea6 one instant retry on AE_FAIL
(cherry picked from commit 84215b1721)
2024-01-04 11:52:47 -05:00
scgbckbone
db93d769d7 IOError is deprecated, use OSError
(cherry picked from commit 8ec2c7f88c)
2024-01-04 11:52:47 -05:00
scgbckbone
408b9d0ef6 bugfix: do not allow to import master seed as temporary
(cherry picked from commit 9188c7faf2)
2024-01-04 11:52:47 -05:00
scgbckbone
2f70733007 Upgrade Firmware menu item is hidden if temporary seed is active
(cherry picked from commit f8ac8eda89)
2024-01-04 11:52:47 -05:00
scgbckbone
b78b30f0b1 bugfix: add missing ftux for extended key import (as master)
(cherry picked from commit 285c90999e)
2024-01-04 11:52:47 -05:00
scgbckbone
05bd7ab39c Export SeedQR
(cherry picked from commit a1f6743de2)
2024-01-04 11:52:47 -05:00
scgbckbone
5de8c9672b prevent yikes in settings._read_slot when loading settings
(cherry picked from commit e71d932b30)
2024-01-04 11:52:47 -05:00
scgbckbone
84dda56746 Add test to sign 400 different PSBTs in one session
(cherry picked from commit ced5f068b1)
2024-01-04 11:52:47 -05:00
scgbckbone
336bfbfe08 mpy bump to include MacOS btree build cope
(cherry picked from commit 883c79b14f)
2024-01-04 11:52:47 -05:00
scgbckbone
5d69d5c039 forgotten BDB cope
(cherry picked from commit 073e8cf98b)
2024-01-04 11:52:47 -05:00
Peter D. Gray
545b23f1b4 Editings
(cherry picked from commit 73b84862fb)
2024-01-04 11:52:47 -05:00
Peter D. Gray
ea92a049af dup import
(cherry picked from commit d5f5956187)
2024-01-04 11:52:47 -05:00
scgbckbone
aa4580adbe cope with future removal of BDB wallet from bitcoin client
(cherry picked from commit 1b90f240b1)
2024-01-04 11:52:47 -05:00
scgbckbone
80106f300e postpone Restore Saved MicroSD card reading
(cherry picked from commit 56e26d051e)
2024-01-04 11:52:47 -05:00
scgbckbone
422a3a2834 update to ckcc with miniscript USB support 2023-12-21 09:06:49 -05:00
scgbckbone
ddae5020df Miniscript USB interface 2023-12-21 08:49:22 -05:00
scgbckbone
478c421627 * miniscript wallet name is uniqe id
* allow multisig/miniscript descriptor import wrapped in json with name key
2023-12-21 08:49:22 -05:00
scgbckbone
ee38dbb28e filepicker can operate on multiple suffix values 2023-12-21 08:49:22 -05:00
scgbckbone
93b3bea43f Allow keys of the same origin in (miniscript) descriptor if subderivation differs in change index 2023-12-07 11:58:40 -05:00
scgbckbone
2388efa8d7 bugfix: do not allow to import duplicate miniscript wallets 2023-12-05 10:19:53 -05:00
Peter D. Gray
0c68d1a4f4
New release: 2023-10-26T1343-v6.2.1X 2023-10-26 09:43:33 -04:00
scgbckbone
e6cb8e9e8a use miniscript settings from backup in temporary seed - if available 2023-10-25 11:20:58 -04:00
scgbckbone
c2e76b3132 bump version to 6.2.1 2023-10-24 14:30:35 -04:00
scgbckbone
202c2cae16 changelog update 2023-10-24 14:30:35 -04:00
scgbckbone
8b7c91c949 test fixes 2023-10-24 14:30:35 -04:00
scgbckbone
6e134a2c9c temporary seed from encrypted COLDCARD backup
(cherry picked from commit a65b1fcc09)
2023-10-24 14:30:35 -04:00
scgbckbone
0dda18b04c move 12 Words mnemonic options at the top of the menus
(cherry picked from commit e3014390c4)
2023-10-24 14:30:35 -04:00
scgbckbone
6a53f3524e Add current tmp seed to Seed Vault via Seed Vault menu
(cherry picked from commit 81e8f3dee2)
2023-10-24 14:30:35 -04:00
scgbckbone
e6dda17124 regenerate menu-tree.txt
(cherry picked from commit 55fc81755f)
2023-10-24 14:30:35 -04:00
Peter D. Gray
280aa07d4d fixes after cherry pick 2023-10-24 14:30:35 -04:00
Peter D. Gray
91197bc90f edit
(cherry picked from commit ec7b54979e)
2023-10-24 14:30:35 -04:00
Peter D. Gray
c7936da8b2 this is it
(cherry picked from commit adb9939df0)
2023-10-24 14:30:35 -04:00
scgbckbone
976772b7be remove last "Ephemeral" UX string from test
(cherry picked from commit 8a4b4b3d7b)
2023-10-24 14:30:35 -04:00
scgbckbone
3bf5dc319c seed vault xfp collision
(cherry picked from commit c9cf5b7db8)
2023-10-24 14:30:35 -04:00
scgbckbone
4416c531e9 seed vault backup tests
(cherry picked from commit bb6fea731e)
2023-10-24 14:30:35 -04:00
Peter D. Gray
0f43069e6c seed vault optimizations
(cherry picked from commit 05a08b6ff8)
2023-10-24 14:30:35 -04:00
scgbckbone
28c0fb4516 UX: rename Ephemeral Seed to Temporary Seed
(cherry picked from commit 3784e1e007)
2023-10-24 14:30:35 -04:00
scgbckbone
73fa603200 Never store 'seeds' in ephemeral settings
(cherry picked from commit e1ac204e04)
2023-10-24 14:30:35 -04:00
Martin
cfe86f5363 Fix double 'before'
(cherry picked from commit 1a761d0daf)
2023-10-24 14:30:35 -04:00
Peter D. Gray
394edaadbc Ephemeral => Temporary Seeds
(cherry picked from commit 93860bab2e)
2023-10-24 14:30:35 -04:00
Peter D. Gray
b57cf3b629 more lang tweaks
(cherry picked from commit 09322abfd4)
2023-10-24 14:30:35 -04:00
scgbckbone
ccc0ac039c bugfix: incorrect xfp shown in meta for passphrase on top of eph secret
(cherry picked from commit 8d304f408a)
2023-10-24 14:30:35 -04:00
Peter D. Gray
fdbfc85fd4 Edits
(cherry picked from commit 52d71a615f)
2023-10-24 14:30:35 -04:00
Peter D. Gray
35c637804b Messaging and logic
(cherry picked from commit 5734b316d0)
2023-10-24 14:30:35 -04:00
Peter D. Gray
e3212ed1a8 add dfu upload option for dev.dfu
(cherry picked from commit cb6fef244d)
2023-10-24 14:30:35 -04:00
Peter D. Gray
7bf89568e6 Bugfix
(cherry picked from commit e635f5d9bc)
2023-10-24 14:30:35 -04:00
scgbckbone
b34a514006 UX renaming
(cherry picked from commit e798765056)
2023-10-24 14:30:35 -04:00
Peter D. Gray
731cd11841 cleanup
(cherry picked from commit ee9e01bc6b)
2023-10-24 14:30:35 -04:00
Peter D. Gray
a293c05a4e cleanup
(cherry picked from commit 75235b240e)
2023-10-24 14:30:35 -04:00
scgbckbone
4414783e1c remove forgotten debug print statements
(cherry picked from commit c2b8527f0c)
2023-10-24 14:30:35 -04:00
Peter D. Gray
abb1d5c2c3 edits
(cherry picked from commit 58ba4000b5)
2023-10-24 14:30:35 -04:00
scgbckbone
75d30c4229 update sim_se2.py 2023-10-24 14:30:35 -04:00
scgbckbone
8d810b107f Seed Vault
(cherry picked from commit bceaaf92e0)
2023-10-24 14:30:35 -04:00
Peter D. Gray
3f5e9789b6 edits
(cherry picked from commit f91925c187)
2023-10-24 14:30:35 -04:00
scgbckbone
56a755c6ec restore to main se2 secret without reboot; active ephemeral seeds have first home menu item [XFP]; tests reorg - created separate test_backup.py; add ability to remove ephemeral seed settings via Restore Seed
(cherry picked from commit fe63163c85)
2023-10-24 14:30:35 -04:00
scgbckbone
e1cdafb272 NUM_SLOTS in PSRAM increased from 64 to 100
(cherry picked from commit 325435b678)
2023-10-24 14:30:35 -04:00
scgbckbone
ea37bc0b69 bugfix: off by one bug in trick pin - login countdown
(cherry picked from commit 9f408c2167)
2023-10-24 14:30:35 -04:00
scgbckbone
4247b6427a PSBTv2
(cherry picked from commit efda6f84dd)
2023-10-24 14:30:35 -04:00
scgbckbone
22fd6b4010 BIP39 passphrase as ephemeral seed; Lock Down Seed for all ephemeral; BIP-39 wallet backup
(cherry picked from commit e5d1782b9d)
2023-10-24 14:30:35 -04:00
scgbckbone
342af8f78e remove obsolete Mk2/Mk3 code paths from firmware
(cherry picked from commit 6655238409)
2023-10-24 14:30:35 -04:00
scgbckbone
615c0a5064 se2 (duress wallet) tests need to run without --eff
(cherry picked from commit d497e5006e)
2023-10-24 14:30:35 -04:00
scgbckbone
bc18f3dd79 shortcut to Batch Sign
(cherry picked from commit 4fb9148cfd)
2023-10-24 14:30:35 -04:00
scgbckbone
931d26962c replace obsolete plausible deniability in nvstore.py
(cherry picked from commit 5782dafed2)
2023-10-24 14:30:35 -04:00
scgbckbone
99fc4d3d23 update EdgeChangeLog.md no2 2023-09-09 10:58:17 -04:00
Peter D. Gray
c6d6adcf4e Bugfix: firmware sizes must align with flash erase unit (4k)
(cherry picked from commit 5393e924ba)
2023-09-09 10:58:17 -04:00
scgbckbone
ecb4804c2f update EdgeChangeLog.md 2023-09-09 10:58:17 -04:00
scgbckbone
4e552fc6c3 Do NOT inherit linked and prelogin settings, do NOT backup words length setting; goto_top_menu after activating bip85 as ephemeral secret
(cherry picked from commit 79ce4ae115)
2023-09-09 10:58:17 -04:00
scgbckbone
503641d8bf Pillow textsize removed in 10.0.0
(cherry picked from commit 0c92824b46)
2023-09-09 10:58:17 -04:00
Peter D. Gray
e0615a13f0 editing
(cherry picked from commit 1122eaafa8)
2023-09-09 10:58:17 -04:00
Peter D. Gray
fa05c28501 cleanup/generalize
(cherry picked from commit 7e1460b501)
2023-09-09 10:58:17 -04:00
Peter D. Gray
760ef7f168 language change to be more clear
(cherry picked from commit fc06f73017)
2023-09-09 10:58:17 -04:00
scgbckbone
0f76b4c74a remove "Look Blank" option from "if wrong" trick pins as it is not supported by the bootrom, remove duplicate "Blank Coldcard" menu item from Duress Wallet option
(cherry picked from commit 519d4ef5f1)
2023-09-05 11:12:08 -04:00
Peter D. Gray
c89a8deef7 edits
(cherry picked from commit f4588ab6f1)
2023-09-05 11:12:08 -04:00
scgbckbone
804ec34bd3 fix broken tests after master port 2023-09-05 11:12:08 -04:00
scgbckbone
971f602f7d update EdgeChangeLog.md after cherry-pick from master 2023-09-05 11:12:08 -04:00
scgbckbone
3120267464 preserve defined order of chooser in Login Countdown
(cherry picked from commit 673d8ab3e2)
2023-09-05 11:12:08 -04:00
scgbckbone
f1059e0972 more UX elements to improve responsiveness
(cherry picked from commit 205a2713d4)
2023-09-05 11:12:08 -04:00
scgbckbone
0c4175aa33 accept TOS only presented if pa.is_blank=True and settings.terms_ok=False
(cherry picked from commit 5a0e423e5d)
2023-09-05 11:12:08 -04:00
scgbckbone
56bf11bd3e Sparrow export via named_generic_export
(cherry picked from commit fba7de33cf)
2023-09-05 11:12:08 -04:00
scgbckbone
6aa707074c cli: explicit sigheader declaration in setup.py
(cherry picked from commit b282b5598d)
2023-09-05 11:12:08 -04:00
scgbckbone
c2d0a7f26f fix msg signing previously broken by 836980d9ce
(cherry picked from commit 90dff02591)
2023-09-05 11:12:08 -04:00
scgbckbone
20b14b7da0 update bip85-passwords.md
(cherry picked from commit 263800d720)
2023-09-05 11:12:08 -04:00
scgbckbone
91f7aa9b2b sd2fa is NOT backed up and not restored from older backups
(cherry picked from commit 500f730265)
2023-09-05 11:12:08 -04:00
scgbckbone
5de1d72f9f update simulator README.md with link to WSL/Windows
(cherry picked from commit 0d271d9027)
2023-09-05 11:12:08 -04:00
scgbckbone
74c6dcc7f3 bug: fix paper wallet error when no secrets
(cherry picked from commit 836980d9ce)
2023-09-05 11:12:08 -04:00
scgbckbone
3f2ce08f2b bug: xfp shown as integer in signing UI when bip39 passphrase in effect
(cherry picked from commit 99eb4b0345)
2023-09-05 11:12:08 -04:00
Peter D. Gray
ba6fd7025e backport
(cherry picked from commit 6fd6684004)
2023-09-05 11:12:08 -04:00
@RandyMcMillan
0ac7c433e1 README.md: fix spelling: products
(cherry picked from commit dadc6bc452)
2023-09-05 11:12:08 -04:00
Peter D. Gray
33753cfebb Renamed
(cherry picked from commit 71b893f417)
2023-09-05 11:12:08 -04:00
Peter D. Gray
7a6442f96b bugfix on MacOS
(cherry picked from commit 689496f018)
2023-09-05 11:12:08 -04:00
Peter D. Gray
dd3f79dc80 rename file
(cherry picked from commit ffb4cd32e9)
2023-09-05 11:12:08 -04:00
scgbckbone
14350452f0 truncated address - add one more char to first part to view regtest segwit version
(cherry picked from commit a3b466fc71)
2023-09-05 11:12:08 -04:00
Peter D. Gray
7a21a37fb9 Make menu item 'Batch Sign PSBT'
(cherry picked from commit 0fe230e150)
2023-09-05 11:12:08 -04:00
scgbckbone
7193e284b2 Batch Sign
(cherry picked from commit 39a125a753)
2023-09-05 11:12:08 -04:00
Peter D. Gray
336f5f6e04 Edits
(cherry picked from commit cc043e2fdf)
2023-09-05 11:12:08 -04:00
scgbckbone
23ae4cb317 mainnet/testnet separation 2023-08-30 10:54:52 -04:00
scgbckbone
9b2019c466 miniscript USB enroll 2023-07-25 11:17:59 -04:00
scgbckbone
1aeb74ada5 linux_addr.patch fix/revert 2023-06-21 08:54:01 -04:00
Peter D. Gray
b35e04be39
New release: 2023-06-20T1506-v6.1.0X 2023-06-20 11:06:55 -04:00
Peter D. Gray
e3c5b32419
Merge branch 'edge' of github.com:Coldcard/firmware into edge 2023-06-20 11:04:14 -04:00
scgbckbone
2623e1cc88 libngu: patch bech32 in make setup 2023-06-20 11:04:07 -04:00
Peter D. Gray
c43cc7130f
as built 2023-06-20 10:10:27 -04:00
Peter D. Gray
5ea5d3f793
tweaks 2023-06-20 10:09:52 -04:00
Peter D. Gray
6fd2cb86e9 correct libngu submodule; remove Export XPUB option from miniscript 2023-06-20 09:28:26 -04:00
scgbckbone
594690d187 Miniscript 2023-06-20 08:36:58 -04:00
scgbckbone
3cd47cdc5c bugfix: empty b39 pwd number selection causes type error
(cherry picked from commit 0fa8c8b0d2)
2023-06-20 08:36:58 -04:00
scgbckbone
15005d307e rename "Unchained Capital" to "Unchained"
(cherry picked from commit 1667ee8369)
2023-06-20 08:36:58 -04:00
scgbckbone
4b1a13a199 B85 menu flow
(cherry picked from commit 2dedccb0fb)
2023-06-20 08:36:58 -04:00
scgbckbone
df4819f0b8 duplicate XFP limitations.md
(cherry picked from commit d0a318000f)
2023-06-20 08:36:58 -04:00
scgbckbone
950589215b remove label from bitcoin core export - in 24.1 label is no longer supported with ranged descriptors
(cherry picked from commit 0874324372)
2023-06-20 08:36:58 -04:00
KST ☩ WINTER HODLER
9411b4bb96 Update bitcoin-core-usage.md
(cherry picked from commit e406b27838)
2023-06-20 08:36:58 -04:00
scgbckbone
f5d5ee0620 remove buggy error msg
(cherry picked from commit 573456885f)
2023-06-20 08:36:58 -04:00
Vishal Menon
2c44856a84 Added notes from dochex.
(cherry picked from commit 762cfb3a86)
2023-06-20 08:36:58 -04:00
Vishal Menon
487cc78635 Created docs/notes-on-repro.md to explain how repro builds are checked and verified.
(cherry picked from commit 0e455fde27)
2023-06-20 08:36:58 -04:00
scgbckbone
e0b0b1f51f leaf_version is only 7 most significant bits 2023-05-17 10:30:19 -04:00
Peter D. Gray
e2876dcc94
Merge branch 'edge' of github.com:Coldcard/firmware into edge 2023-05-16 09:16:56 -04:00
Peter D. Gray
5023cd4517
Add middle-dots character to font, use arrows/dots in address explorer menu; tweaks. 2023-05-16 09:16:10 -04:00
scgbckbone
9401bcd31a fix test_multisig_descriptor_export 2023-05-15 08:47:29 -04:00
Peter D. Gray
6f4b4f6363
6.0.0X 2023-05-12 10:23:44 -04:00
Peter D. Gray
7d855ebe5f
updates 2023-05-12 09:25:10 -04:00
Peter D. Gray
47d1aac44e
New release: 2023-05-12T1317-v6.0.0X 2023-05-12 09:17:02 -04:00
Peter D. Gray
7e4ecbf9b0
today 2023-05-12 09:15:14 -04:00
Peter D. Gray
7fcfd55d3e
Merge branch 'edge' of github.com:Coldcard/firmware into edge 2023-05-12 09:02:39 -04:00
scgbckbone
0f18c1fc59 fix bech32 patching for libngu 2023-05-12 08:47:05 -04:00
scgbckbone
df39a9166a multisig test fix 2023-05-12 08:47:05 -04:00
Peter D. Gray
321ab3d836
needed 2023-05-11 13:28:11 -04:00
scgbckbone
95e943a32b test fixes after Address Explorer label changes 2023-05-11 08:13:51 -04:00
scgbckbone
448fae8bdd struct for unpacking int from bytes 2023-05-11 08:13:51 -04:00
scgbckbone
91c579fadd MAX_TR_SIGNERS from updated ckcc-protocol 2023-05-11 08:13:51 -04:00
scgbckbone
aa7d17bf8f unify address format labels; address explorer now uses both labels and shortened addresses; axi updated to track last 4 addr chars or MenuItem label (multisig)
(cherry picked from commit 63debfebaf)
2023-05-11 08:13:51 -04:00
scgbckbone
b05bd09998 force vdisk for From Virtdisk firmware upgrade
(cherry picked from commit eb0f22ec6b)
2023-05-11 08:13:51 -04:00
Peter D. Gray
a982c86d91 edits
(cherry picked from commit feca7268c2)
2023-05-11 08:13:51 -04:00
Peter D. Gray
25403b017c
re-enable screensaver, which SDL disables by default 2023-05-09 13:58:13 -04:00
Peter D. Gray
70aa6de8b9
building 6.0.0X 2023-05-09 12:13:00 -04:00
Peter D. Gray
824bbbc3b2
set MAX_TR_SIGNERS=32, docs 2023-05-09 11:39:36 -04:00
Peter D. Gray
ebc1832b91
remove warning for simulator 2023-05-09 11:21:39 -04:00
Peter D. Gray
306f4d31b8
move 44 down 2023-05-09 10:27:19 -04:00
Peter D. Gray
0883d6f347
version.is_edge value 2023-05-09 10:17:54 -04:00
Peter D. Gray
8d7f6dee7a
Better 2023-05-09 09:54:18 -04:00
Peter D. Gray
79e820fea1
edge version 2023-05-09 09:52:44 -04:00
scgbckbone
0a506ecec6 deprecte_test::test_iss6743::nested segwit signing is properly tested with test_bitcoind_MofN_tutorial 2023-05-09 08:59:39 -04:00
scgbckbone
0da4088c01 Taproot keyspend & Tapscript multisig sortedmulti_a (tree depth = 0) 2023-05-09 08:59:39 -04:00
scgbckbone
c7762eedf2 BSMS (BIP-129)
This reverts commit e4e1844f
2023-05-09 08:59:39 -04:00
scgbckbone
2749bc00fb reckless edge X 2023-05-09 08:59:39 -04:00
145 changed files with 18846 additions and 4478 deletions

View File

@ -28,6 +28,17 @@ has been automated using Docker. Steps are as follows:
4. At the end of the process a clear confirmation message is shown, or the differences.
5. Build products can be found `firmware/stm32/built`.
6. If you do not trust the results of `make repro` refer to `docs/notes-on-repro.md` which breaks down the process.
## Long-Lived Branches
We are now maintaining two branches: `master` and `edge`.
"Edge" will contain features that may not be ready for prime time,
such as Taproot or Miniscript. Our standards for releasing new Edge
versions are lower, so we can iterate faster and get these advancements
out to other developers.
## Check-out and Setup

View File

@ -11,7 +11,7 @@ from setuptools import setup
setup(
name='signit',
version='1.0',
py_modules=['signit'],
py_modules=['signit', 'sigheader'],
install_requires=[
'Click',
],

View File

@ -293,8 +293,12 @@ def doit(keydir, outfn=None, build_dir=None, high_water=False,
# bugfix: size must be non-page aligned, so extra bytes are erased past end
if (body_len % 4096) == 0:
body_len += 512
assert body_len % 512 == 0, body_len
assert body_len % 512 == 0, body_len
else:
# bugfix: PSRAM-based products (Mk4, Q1) need to erase 4k blocks, so
# trouble happens if final binary isn't aligned to that size.
body_len = align_to(body_len, 4096)
assert body_len % 4096 == 0, body_len
# pad out
vectors = pad_to(vectors, FW_HEADER_OFFSET)

View File

@ -38,6 +38,17 @@ a single file, which is a simple text file and
easy to read. Before version 4.0.0, this text file was always
called `ckcc-backup.txt`, but the filename is now picked randomly.
## BIP39 Passphrase
If BIP39 passphrase is active the default behavior is to back-up
main wallet - not BIP39 passphrase wallet. From version `5.2.0`
users can choose to back-up also BIP39 passphrase wallet.
## Ephemeral Seeds
If ephemeral seed is active the default behavior is to always
back-up ephemeral wallet instead of the main wallet.
## Limitations
- The archive file names are not encrypted. You can see there is a single

View File

@ -1,9 +1,7 @@
# BIP-85 Passwords
This feature derives a deterministic password according from your seed,
according to [BIP-85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki)
(with the recent changes
[proposed here](https://github.com/scgbckbone/bips/blob/passwords/bip-0085.mediawiki)).
This feature derives a deterministic password from your seed,
according to [BIP-85 PWD BASE64](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#pwd-base64).
Generated passwords can be sent as keystrokes via USB to the host computer,
effectively using Coldcard as specialized password manager.

View File

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

View File

@ -1,43 +0,0 @@
# Ephemeral Seeds
Ephemeral seed is temporary secret stored only in Coldcard volatile
memory (RAM). It only survives single boot, meaning after Coldcard
restart it is gone. Ephemeral seeds *completely* defeats the design
of Coldcard's security model, based on secure elements.
Make sure you know what you're doing!
## Usage
- go to `Advanced/Tools -> Ephemeral Seed`
- if ephemeral seed is already in use, top menu item `[<xfp>]` is visible
with fingerprint of ephemeral master secret
- an ephemeral seed can be Imported or Generated at random
- go to `Advanced/Tools -> Ephemeral Seed`
- `Generate Words`:
- same options as generating new seed words, dice rolls included
- Import words via NFC with `Import via NFC` option
- `Import Words`:
- same options as importing seed words
- `Import XPRV`:
- import extended private key
- `Tapsigner Backup`
- import TAPSIGNER encrypted backup
- an ephemeral seed can also be a BIP-85 derived value
## Trick PIN Notes
If you intend to use the ephemeral seed feature frequently, you can
define a "Trick PIN" which takes you to a "look blank" trick wallet
(ie. no seed set appears to be set). Then you may then safely
unlock your Coldcard, without revealing the true PIN, and perform
all your ephemeral seed work in that state.
## Purpose
This feature is intended for those one-off signings, like recovering
a lost seed from some other system or importing some seed as an
balance check. We do not recommend handing unencrypted seed material
on a regular basis!

View File

@ -70,7 +70,11 @@
- change outputs (indicated with paths, scripts in output section) must correspond to
the active multisig wallet, and cannot be used to describe an unrelated (multisig) wallet.
- derivation path for each cosigner must be known and consistent with PSBT
- fixed: XFP values (fingerprints) for each of the co-signers must be unique (limitation removed)
- XFP values (fingerprints) MUST be unique for each of the co-signers
# Taproot
- more background and detail in `docs/taproot.md`
# SIGHASH types
@ -120,6 +124,8 @@ 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

@ -2,16 +2,16 @@
Choose PIN Code
Advanced/Tools
View Identity
Ephemeral Seed
Temporary Seed
Generate Words
24 Words
12 Words
24 Word Dice Roll
24 Words
12 Word Dice Roll
24 Word Dice Roll
Import Words
24 Words
18 Words
12 Words
18 Words
24 Words
Import via NFC
Import XPRV
Tapsigner Backup
@ -29,16 +29,16 @@
[IF BLANK WALLET]
New Seed Words
24 Word (default)
12 Word
24 Word Dice Roll
12 Word Dice Roll
Import Existing
12 Words
24 Words
12 Word Dice Roll
24 Word Dice Roll
Import Existing
12 Words
[SEED WORD MENUS]
18 Words
[SEED WORD MENUS]
12 Words
24 Words
[SEED WORD MENUS]
Restore Backup
Clone Coldcard
@ -48,16 +48,16 @@
Help
Advanced/Tools
View Identity
Ephemeral Seed
Temporary Seed
Generate Words
24 Words
12 Words
24 Word Dice Roll
24 Words
12 Word Dice Roll
24 Word Dice Roll
Import Words
24 Words
18 Words
12 Words
18 Words
24 Words
Import via NFC
Import XPRV
Tapsigner Backup
@ -84,7 +84,6 @@
Settings
Login Settings
Change Main PIN
PIN Options
Trick PINs
Add New Trick
Add If Wrong
@ -94,21 +93,23 @@
Kill Key
Login Countdown
Disabled
5 minutes
15 minutes
30 minutes
4 hours
1 hour
5 minutes
2 hours
4 hours
8 hours
12 hours
2 hours
3 days
48 hours
1 week
24 hours
48 hours
3 days
1 week
28 days later
MicroSD 2FA [MAYBE]
Add Card
Check Card
Remove Card #1
Test Login Now
Hardware On/Off
USB Port
@ -133,7 +134,7 @@
Bitcoin Core
Electrum Wallet
Import from File
Import via NFC
Import via NFC [MAYBE]
Export XPUB
Create Airgapped
Trust PSBT?
@ -170,6 +171,21 @@
Start HSM Mode [IF HSM POLICY]
Address Explorer
Type Passwords
Seed Vault
(none saved yet)
Temporary Seed
Generate Words
12 Words
24 Words
12 Word Dice Roll
24 Word Dice Roll
Import Words
12 Words
18 Words
24 Words
Import via NFC
Import XPRV
Tapsigner Backup
Secure Logout
Advanced/Tools
Backup
@ -179,9 +195,10 @@
Clone Coldcard
Export Wallet
Bitcoin Core
Sparrow Wallet
Electrum Wallet
Wasabi Wallet
Unchained Capital
Unchained
Lily Wallet
Samourai Postmix
Samourai Premix
@ -204,9 +221,10 @@
Backup System
Export Wallet
Bitcoin Core
Sparrow Wallet
Electrum Wallet
Wasabi Wallet
Unchained Capital
Unchained
Lily Wallet
Samourai Postmix
Samourai Premix
@ -220,24 +238,25 @@
Current XFP
Dump Summary
Sign Text File
Clone Coldcard
Batch Sign PSBT
List Files
Verify Sig File
NFC File Share [IF NFC ENABLED]
Clone Coldcard
Format SD Card
Format RAM Disk [IF VIRTDISK ENABLED]
Derive Seed B85
View Identity
Ephemeral Seed
Temporary Seed
Generate Words
24 Words
12 Words
24 Word Dice Roll
24 Words
12 Word Dice Roll
24 Word Dice Roll
Import Words
24 Words
18 Words
12 Words
18 Words
24 Words
Import via NFC
Import XPRV
Tapsigner Backup
@ -267,6 +286,9 @@
Wipe LFS
Warm Reset
Restore Txt Bkup
Seed Vault
Default Off
Enable
Perform Selftest
Set High-Water
Wipe HSM Policy [IF HSM POLICY]
@ -283,7 +305,6 @@
Settings
Login Settings
Change Main PIN
PIN Options
Trick PINs
Add New Trick
Add If Wrong
@ -293,21 +314,23 @@
Kill Key
Login Countdown
Disabled
5 minutes
15 minutes
30 minutes
4 hours
1 hour
5 minutes
2 hours
4 hours
8 hours
12 hours
2 hours
3 days
48 hours
1 week
24 hours
48 hours
3 days
1 week
28 days later
MicroSD 2FA [MAYBE]
Add Card
Check Card
Remove Card #1
Test Login Now
Hardware On/Off
USB Port
@ -332,7 +355,7 @@
Bitcoin Core
Electrum Wallet
Import from File
Import via NFC
Import via NFC [MAYBE]
Export XPUB
Create Airgapped
Trust PSBT?

27
docs/miniscript.md Normal file
View File

@ -0,0 +1,27 @@
# Miniscript
**COLDCARD<sup>&reg;</sup>** Mk4 experimental `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
* only keys with key origin info `[xfp/p/a/t/h]xpub`
* maximum number of keys allowed in segwit v0 miniscript is 20
* check MiniTapscript limitations in `docs/taproot.md`

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

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

View File

@ -81,7 +81,7 @@ of your duress PIN.
The attackers could tell when the brick-me PIN has worked, but when
the brick-me PIN works, the Coldcard will immediately use it to
destroy the main pairing secret. This renders the security element
useless. This happens in about 50 milliseconds and is done long before
useless. This happens in about 50 milliseconds and is done long
before anyone gets an on-screen confirmation that it worked.
There is little time to interrupt this or jam the bus to stop it.

66
docs/taproot.md Normal file
View File

@ -0,0 +1,66 @@
# 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 few methods to provide/generate provably unspendable internal key, if users wish to only use script path
for multisig.
1. use provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs). This way is leaking the information that key path spending is not possible and therefore not recommended privacy-wise.
`tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))`
2. 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))`
3. 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))`
## 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 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. Currently MUST be of length 1 (only one script allowed)
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. Currently only one script is allowed.

122
docs/temporary-seeds.md Normal file
View File

@ -0,0 +1,122 @@
# Temporary Seeds
[_(new in v5.0.7, requires Mk4)_](upgrade.md)
Temporary seed (renamed in `5.2.0` from Ephemeral seed) is a temporary secret completely separate
from the master seed, typically held in **COLDCARD<sup>&reg;</sup>** RAM and
not persisted between reboots in the Secure Element.
Temporary seeds *completely* defeat the design
of Coldcard's security model, based on secure elements.
Enable the `Seed Vault` feature to store these secrets longer-term.
Read more about `Seed Vault` feature below.
!!! warning "Make sure you know what you're doing!"
This feature is intended for those one-off signings, like recovering
a lost seed from some other system or importing some seed as a
balance check. We do not recommend handing unencrypted seed material
on a regular basis!
## Usage
* if temporary seed is already in use, first home menu option `[<xfp>]` is visible with fingerprint of temporary master secret
* go to `Advanced/Tools > Temporary Seed`
* temporary seed words can be Generated with TRNG
- `Advanced/Tools > Temporary Seed > Generate Words`
* temporary seed words can be imported
- `Advanced/Tools > Temporary Seed > Import Words`
* importing extended private keys
- `Advanced/Tools > Temporary Seed > Import XPRV`
- `Advanced/Tools > Temporary Seed > Tapsigner Backup`
* temporary seed can be activated from BIP-85 derived secrets - go to `Advanced/Tools > Derive Seed B85` and pick types of secret. Keep in mind that only word based and xprv based secrets can be used as temporary seed.
- `12 words`
- `18 words`
- `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
* temporary seed can be activated from Duress Wallet
- go to `Settings -> Login Settings -> Trick Pins`
- add new Duress Wallet trick pin and save it
- choose newly created trick pin in trick pins menu and use `Activate Wallet` option
* temporary seed can be obtained from `SeedXOR`
- go to `Advanced/Tools -> Danger Zone -> Seed Functions -> SeedXOR`
- pick `Restore Seed XOR` option and provide all XOR parts
- Press (2) to activate restored seed as temporary seed
* BIP-39 passphrase is from version `5.2.0` handled internally as temporary seed
Ability to generate and use **Temporary seed** is available on Coldcard when:
1. no PIN chosen and no secret chosen (newly unpacked Coldcard)
2. PIN set up but no secret chosen yet
3. with both PIN and secret already picked
# Restore Master
[_(new in v5.2.0, requires Mk4)_](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
seed active, first item in home menu is `[xfp]` and is a clone of `Ready To Sign`.
Last item in home menu is `Restore Master`.
`Restore Master` offers two options. First, if user presses OK, COLDCARD wipes temporary seed settings
and switches back to master seed and its settings.
If user presses (1) temporary seed settings are preserved for later use and COLDCARD only switches
back to master seed and its settings.
If current temporary seed is also saved in Seed Vault, option to wipe settings is not available.
Seed Vault entries can only be deleted in Seed Vault menu.
# Seed Vault
[_(new in v5.2.0, requires Mk4)_](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).
Users can capture and hold master secret from any temporary seed source, including: TRNG, Dice Rolls,
SeedXOR, TAPSIGNER backups, BIP-85 derived values, BIP-39 passphrase wallets.
## Enable Seed Vault
Enable this functionality in `Advanced/Tools -> Danger Zone -> Seed Vault -> Enable`.
Once seed vault is enabled new menu item is visible in home menu `Seed Vault`.
To disable Seed Vault user needs to remove all entries from Seed Vault first.
## Add Seed to Vault
After `Seed Vault` is enabled, users will see a new prompt, after
creation of temporary seed, asking whether to save this temporary
seed to Seed Vault. Press (1) to save or any other key to ignore.
If option to save was chosen, confirmation prompt is shown - `Saved to seed vault.`
## Seed Vault menu
* if Seed Vault is empty `(none saved yet)` is the first menu item followed by shortcut to `Temporary Seed` menu.
* if not empty, saved seeds are listed in menu as `[xfp]`
* if current active temporary seed is stored in Seed Vault - it has checkmark next to it
* if temporary seed is active - last menu item of Seed Vault menu is `Restore Master`
## Seed Vault entry submenu
1. by default `[xfp]` but can be renamed to allow user labeling and leads to additional information about the seed
2. `Use This Seed` allows to switch to the saved temporary seed. If it is already active `In Use` is shown instead.
3. `Rename` allows to change 1. menu item to something personalized to user (limited to 40 characters)
4. `Delete` allows to remove temporary seed from Seed Vault and optionally to completely wipe its settings.

2
external/README.md vendored
View File

@ -1,7 +1,7 @@
## Background on Submodules
This project uses many submodules, and to build the final produts, you will
This project uses many submodules, and to build the final products, you will
have to get all the submodules into place and build them in appropriate orders.
A good resource, from an unrelated project, is:

@ -1 +1 @@
Subproject commit 2216e4db0e2dd17ca16bd8772d09c69f6296f6df
Subproject commit 11c711e929a090ec29ccd2a05d094aa3d2cbc113

2
external/libngu vendored

@ -1 +1 @@
Subproject commit 356b9137cf7ddf5de66ec4cdc0a4d757b2e42790
Subproject commit 7bdb03864630ff68b143e3e5b4521ca3ef6588cc

View File

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

View File

@ -1,9 +1,100 @@
## 5.1.3 - 2023-XX-XX
## 5.2.3 - 2024-XX-XX
- Bugfix: Saving passphrase on SD Card caused a freeze that required reboot
- Bugfix: Very obscure bug in low level code could cause txid to be miscalculated
if all the conditions occured just right
## 5.2.2 - 2023-12-21
- Bugfix: Re-enable `Lock Down Seed` feature which was disabled by accident
## 5.2.1 - 2023-12-19
- New Feature: Temporary Seed import from a COLDCARD encrypted backup.
- New Feature: Export seed words in SeedQR format (on screen QR).
- New Feature: Provide user with info about transaction level timelocks
([nLockTime](https://en.bitcoin.it/wiki/NLockTime),
[nSequence](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki))
when signing.
- Enhancement: New submenu for saved BIP-39 Passphrases allowing delete of saved entries.
- Enhancement: Add current temporary seed to Seed Vault from within Seed Vault menu.
If current seed is temporary and not saved yet, `Add current tmp` menu item is
shown in Seed Vault menu.
- Enhancement: Speed up opening `Passphrase` menu when MicroSD card is available, by
deferring card read (and decryption) until after `Restore Saved` menu item is selected.
- Enhancement: `12 Words` menu option preferred on the top of the menu in all the seed menus
(rather than 24 words).
- Enhancement: Allow passphrase via USB if passphrase already set - operates on master seed.
- Enhancement: Improve BIP39 Passphrase UX when temporary seed is active and applicable.
- Enhancement: Continuation of removal of obsolete Mk2/Mk3 code-paths from master branch.
- Bugfix: Confusing first-time UX replaced with simple welcome screen.
- Bugfix: One instant retry on SE1 communication failures
- Bugfix: Handle any failures in slot reading when loading settings
- Bugfix: Add missing "First Time UX" for extended key import as master seed
- Bugfix: Hide `Upgrade Firmware` menu item if temporary seed is active (it cannot work)
- Bugfix: Disallow using master seed as temporary seed
- Bugfix: Do not allow `APPLY` of empty BIP-39 passphrase. Use "Restore Master" instead.
- Bugfix: Fix yikes in `Clone Coldcard` (thanks to AnchorWatch)
## 5.2.0 - 2023-10-10
- New Feature: Seed Vault. Store multiple temporary secrets into encrypted settings for simple
recall and later use (AES-256-CTR encrypted by key based on the seed).
Enable this functionality in `Advanced/Tools -> Danger Zone -> Seed Vault -> Enable`.
Use stored seeds from Seed Vault with top-level `Seed Vault` menu choice (once enabled).
Can capture and hold master secret from any temporary (ephemeral) seed source,
including: TRNG, Dice Rolls, SeedXOR, TAPSIGNER backups, Duress Wallets, BIP-85 derived
values, BIP-39 passphrase wallets.
- New Feature: PSBTv2 support added! Enables new PSBT workflows and applications.
- New Feature: `Lock Down Seed` now works with every temporary secret (not just BIP39 passphrase)
- New Feature: BIP-39 Passphrase can now be added to any words-based temporary seed.
- New Feature: Add ability to back-up BIP39 Passphrase wallet (with passphrase encoded).
- New Feature: Return to main secret from temporary without need to reboot the device.
- Enhancement: Shortcut to `Batch Sign PSBT` via `Ready To Sign` -> `Press (9)`
- Enhancement: Waste less storage space by removing old plausible deniability code
which was only needed for Mk1 - Mk3 where SPI flash was an external chip.
- Enhancement: Remove obsolete Mk2/Mk3 code-paths from master branch.
- Enhancement: BIP39 Passphrase is now internally handled as an temporary secret.
Ability to see BIP-39 Passphrase after wallet is active via `View Seed Words`
was removed as a consequence of this change. Benefit: passphrase no longer held
in memory while in operation.
- Enhancement: Showing secrets now also displays extended private key (XPRV) for BIP-39
passphrase wallets.
- Enhancement: Increase number of slots in settings memory from 64 to 100.
- Bugfix: Fixed off by one bug in `Trick Pins -> Login Countdown` menu.
- Nomenclature: "Ephemeral Seed" will now be called "Temporary Seed".
## 5.1.4 - 2023-09-08
- Bugfix: Most users would see a red light after upgrade to 5.1.3 from 5.1.2. Fixed.
## 5.1.3 - 2023-09-07
- New Feature: Batch sign multiple PSBT files. `Advanced/Tools -> File Management -> Batch Sign PSBT`
- Enhancement: `Sparrow Wallet` added as an individual export option (same file contents)
- Enhancement: change key origin information export format in multisig `addresses.csv` to match
[BIP-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions)
was `(m=0F056943)/m/48'/1'/0'/2'/0/0` now `[0F056943/48'/1'/0'/2'/0/0]`
- Enhancement: Address explorer UX cosmetics, now with arrows and dots.
- Enhancement: Linked settings (multisig, trick pins, backup password, hsm users and utxo cache)
separation for new main secret.
- Rename `Unchained Capital` to `Unchained`
- Bugfix: Correct `scriptPubkey` parsing for segwit v1-v16
- Bugfix: Do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT.
- Bugfix: Remove label from Bitcoin Core `importdescriptors` export as it is no longer supported
with ranged descriptors in version `24.1` of Core.
- Bugfix: Empty number during BIP-39 passphrase entry could cause crash.
- Bugfix: Signing with BIP39 Passphrase showed master fingerprint as integer. Fixed to show hex.
- Bugfix: Fixed inability to generate paper wallet without secrets
- Bugfix: Activating trick pin duress wallet copied multisig settings from main wallet
- Bugfix: SD2FA setting is cleared when seed is wiped after failed login due to policy SD2FA enforce.
Prevents infinite seed wipe loop when restoring backup after 2FA MicroSD lost or damaged.
SD2FA is not backed up and also not restored from older backups. If SD2FA is set up,
it will not survive restore of backup.
- Bugfix: Terms only presented if main PIN was not chosen already.
- Bugfix: Preserve defined order of Login Countdown settings list.
- Bugfix: Remove unsupported trick pin option `Look Blank` from `if wrong` (not supported by bootrom).
- 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 `scrptPubkey` parsing for segwit v1-v16
- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT
## 5.1.2 - 2023-04-07

54
releases/EdgeChangeLog.md Normal file
View File

@ -0,0 +1,54 @@
## 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.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

@ -1,4 +1,14 @@
## 4.1.9 - Jun 26, 2023
- Bugfix: QR codes could not be rendered in 4.1.8 release due to a regression.
## 4.1.8 - Jun 19, 2023
- Bugfix: "Validating..." screen would be shown twice in some cases. Improves signing performance.
- Bugfix: Reproducible builds corrected.
## 4.1.7 - Nov 15, 2022
- Bugfix: Upgrades to 4.1.6 version using SD Card did not work due to an obscure alignment
bug. USB upgrade did work. A workaround to this issue has been added for this release.

View File

@ -2,45 +2,26 @@
Hash: SHA256
715a3ec7a91d2366788b14a243e7343de875714b647d1e2bc906ecc5b752d8d9 README.md
8f71336a78573ccbd19b782e4f6e5930a8f944517884fe02d26eb0f38bf5c3ac Mk3ChangeLog.md
a0c4d0ac3881a36704f0b620a13c72704531f656ee29368d5aac87dc5f21c7a1 History.md
b05ab997d09ebbd93a59b14b2be9574ab9a1facb176efcc3fee630ec562b5cf8 ChangeLog.md
ffdd44e985d5a2bdc0de6b98d93038304f46f9c942ae4a5ac053135ebf49374f ChangeLog-mk3.md
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
0a7b4a32af7bfcc3ce079461752697d9da787a2e7d7a36c156f1368e20847816 EdgeChangeLog.md
bf86232f94f40a3fef1e0e5e8dd1f184f7e2782606001a121f71a911c1eab95d ChangeLog.md
a9d0b416c3cb4f122f2826283fce82bbc5fe4464817b601a3a5787b1f8aaba20 2024-01-18T1507-v6.2.2X-mk4-coldcard.dfu
cc93209e800bc05386b5613969e62c27b9acd4388e3a922686525da90a505778 2024-01-18T1507-v6.2.2X-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
489e161f686a0c631fc605054f8e7271208b16191b669174b8a58f5af28b0f4a 2023-06-20T1506-v6.1.0X-mk4-coldcard.dfu
66c83c3f95fd3d0796b1e452d2e8ed8ac6a4abead53faf5ae793eceb6f7bbdb5 2023-06-20T1506-v6.1.0X-mk4-coldcard-factory.dfu
2e8ed970f518a476d0b34752ecbad75bab246669aa65de8f43801364c6f5753e 2023-05-12T1316-v6.0.0X-mk4-coldcard.dfu
8dd5ff029bb2b08c857604f0c9b5773931f6683ee331ecbc35d9ab4c460b745f 2023-05-12T1316-v6.0.0X-mk4-coldcard-factory.dfu
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmQwGu0ACgkQo6MbrVoq
WxDgdggAnuubC/F0g53PNOFunqbRe9pCQPAvg47JjwZT9XQfzNL9DWqARZ4R2YZP
AJPMlF4mXiNi1JJ+hJ5iwwLJaHwPWdaTFX1xr3M8tbIrgjIQ8i2UXPgFXfqlQcmt
lPrYemYKXdojGvtjFMBrDQavWRwuGIK9Rc2pp+gtAfrji7vCevEBus39W9eHod61
ZQKsyaJKDI3HqfaKTPaTyVVQYYQwSAoe9RVJxZSXoaW3kAxnfvKU2Xzy8B3cH3jE
y22o82rnIdRNUZowk9YjzfvSK/NcGLpisJv7MjxCme04qDV5QbKG2gvTZ3p/ysn6
0yrxUq2CuPHHky1j9bLkiEAlDke2AA==
=qpFw
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmWpPsYACgkQo6MbrVoq
WxBgpQf9EyG2cBaejQovqvDfywP0Mz7UA4gt0f6w7BO2cJ8wapfFC0rCv01Ya8l9
87GLP9fqkKi34l8m+ujvG9OpQYCIwKStgS3J7Uz10aBx5P13Zl7ZxIQQ5pD7keZN
tmowJKq5us2as15zs4CCqHqCsh7DWRHqpaGFdNoRjxQgTrJtVz32pJcqVN+IVIKO
9O0OikiMRGaEdnqdyaO6feCrDbWknTjYAaW7fNO1+OrbBEubK5n1PFVGxJsY0gZf
66TVKze0bxFyhYwueMWSi5/QV+pX6Rnt9twT8ebIl5Yq7O1UW5tgp9HYoQpZB47z
VV+afwiGGF26hK6bYm/c/WfF7ikcnw==
=70KC
-----END PGP SIGNATURE-----

File diff suppressed because it is too large Load Diff

View File

@ -7,18 +7,16 @@
import chains, stash
from ux import ux_show_story, the_ux, ux_enter_bip32_index
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from uasyncio import sleep_ms
from ucollections import OrderedDict
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from glob import settings
from auth import write_sig_file
from utils import addr_fmt_label, truncate_address
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
# - 16 chars screen width, so show 8 prefix, dash, and 7 of end of address
return addr[0:8] + '-' + addr[-7:]
class KeypathMenu(MenuSystem):
def __init__(self, path=None, nl=0):
@ -27,14 +25,15 @@ class KeypathMenu(MenuSystem):
if path is None:
# Top level menu; useful shortcuts, and special case just "m"
items = [
MenuItem("m/..", f=self.deeper),
MenuItem("m/49'/..", f=self.deeper),
MenuItem("m/84'/..", f=self.deeper),
MenuItem("m/44'/..", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done),
]
MenuItem("m/..", f=self.deeper),
MenuItem("m/44'/..", f=self.deeper),
MenuItem("m/49'/..", f=self.deeper),
MenuItem("m/84'/..", f=self.deeper),
MenuItem("m/86'/..", f=self.deeper),
MenuItem("m/0/{idx}", menu=self.done),
MenuItem("m/{idx}", menu=self.done),
MenuItem("m", f=self.done),
]
else:
# drill down one layer: (nl) is the current leaf
# - hardened choice first
@ -69,7 +68,7 @@ class KeypathMenu(MenuSystem):
dis.clear_rect(0, y, dis.WIDTH, 8)
dis.text(-1, y+4, self.prefix, FontTiny, invert=False)
def done(self, _1, menu_idx, item):
async def done(self, _1, menu_idx, item):
final_path = item.arg or item.label
self.chosen = menu_idx
self.show()
@ -86,7 +85,7 @@ class KeypathMenu(MenuSystem):
return PickAddrFmtMenu(final_path, top)
def deeper(self, _1, _2, item):
async def deeper(self, _1, _2, item):
val = item.arg or item.label
assert val.endswith('/..')
cpath = val[:-3]
@ -97,9 +96,10 @@ class PickAddrFmtMenu(MenuSystem):
def __init__(self, path, parent):
self.parent = parent
items = [
MenuItem("Classic P2PKH", f=self.done, arg=(path, AF_CLASSIC)),
MenuItem("Segwit P2WPKH", f=self.done, arg=(path, AF_P2WPKH)),
MenuItem("P2SH-P2WPKH", f=self.done, arg=(path, AF_P2WPKH_P2SH)),
MenuItem(addr_fmt_label(AF_CLASSIC), f=self.done, arg=(path, AF_CLASSIC)),
MenuItem(addr_fmt_label(AF_P2WPKH), f=self.done, arg=(path, AF_P2WPKH)),
MenuItem(addr_fmt_label(AF_P2WPKH_P2SH), f=self.done, arg=(path, AF_P2WPKH_P2SH)),
MenuItem(addr_fmt_label(AF_P2TR), f=self.done, arg=(path, AF_P2TR)),
]
super().__init__(items)
if path.startswith("m/84'"):
@ -107,7 +107,7 @@ class PickAddrFmtMenu(MenuSystem):
if path.startswith("m/49'"):
self.goto_idx(2)
def done(self, _1, _2, item):
async def done(self, _1, _2, item):
the_ux.pop()
await self.parent.got_custom_path(*item.arg)
@ -123,7 +123,7 @@ class ApplicationsMenu(MenuSystem):
]
super().__init__(items)
def done(self, _1, _2, item):
async def done(self, _1, _2, item):
path = item.arg[0]
addr_fmt = item.arg[1]
await self.parent.show_n_addresses(path, addr_fmt, None, n=10, allow_change=True)
@ -179,8 +179,13 @@ class AddressListMenu(MenuSystem):
stash.blank_object(node)
items = [MenuItem(address, f=self.pick_single, arg=(path, addr_fmt))
for i, (address, path, addr_fmt) in enumerate(choices)]
items = []
for i, (address, path, addr_fmt) in enumerate(choices):
axi = address[-4:] # last 4 address characters
items.append(MenuItem(addr_fmt_label(addr_fmt), f=self.pick_single,
arg=(path, addr_fmt, axi)))
items.append(MenuItem(''+address, f=self.pick_single,
arg=(path, addr_fmt, axi)))
# some other choices
if self.account_num == 0:
@ -191,27 +196,36 @@ class AddressListMenu(MenuSystem):
# 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))
items.append(MenuItem(ms.name, f=self.pick_miniscript, arg=ms))
# if they have miniscript wallets, add those next
for msc in MiniScriptWallet.iter_wallets():
items.append(MenuItem(msc.name, f=self.pick_miniscript, arg=msc))
else:
items.append(MenuItem("Account: %d" % self.account_num, f=self.change_account))
self.goto_idx(settings.get('axi', 0)) # weak
self.replace_items(items)
axi = settings.get('axi', 0)
if isinstance(axi, str):
ok = self.goto_label(axi)
if not ok:
self.goto_idx(0)
else:
self.goto_idx(axi)
async def change_account(self, *a):
self.account_num = await ux_enter_bip32_index('Account Number:') or 0
await self.render()
async def pick_single(self, _1, menu_idx, item):
settings.put('axi', menu_idx) # update last clicked address
path, addr_fmt = item.arg
async def pick_single(self, _1, _2, item):
path, addr_fmt, axi = item.arg
settings.put('axi', axi) # update last clicked address
await self.show_n_addresses(path, addr_fmt, None)
async def pick_multisig(self, _1, menu_idx, item):
ms_wallet = item.arg
settings.put('axi', menu_idx) # update last clicked address
await self.show_n_addresses(None, None, ms_wallet)
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, None, msc_wallet)
async def make_custom(self, *a):
# picking a custom derivation path: makes a tree of menus, with chance
@ -239,11 +253,18 @@ Press (3) if you really understand and accept these risks.
# - also for other {account} numbers
# - or multisig case
from glob import dis, NFC, VD
import version
def make_msg(change=0):
# speed up UI (do not recalculate addresses in while loop)
# cache can only have 2 values external, internal (0,1)
_cache = OrderedDict()
def make_msg(change=0, start=start, n=n):
nonlocal _cache
if (change, start, n) in _cache:
return _cache[(change, start, n)]
export_msg = "Press (1) to save Address summary file to SD Card."
if version.has_fatram and not ms_wallet:
if not ms_wallet:
export_msg += " Press (2) to view QR Codes."
if NFC:
export_msg += " Press (3) to share via NFC."
@ -273,16 +294,8 @@ Press (3) if you really understand and accept these risks.
# but show enough they can verify addrs shown elsewhere.
# - makes a redeem script
# - converts into addr
# - assumes 0/0 is first address.
for (i, paths, addr, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
if i == 0 and ms_wallet.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '.../%d/%d =>\n' % (change, i)
addrs.append(addr)
msg += truncate_address(addr) + '\n\n'
dis.progress_bar_show(i/n)
# - assumes <0;1>/0 is first address.
msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change)
else:
# single-singer wallets
@ -304,10 +317,15 @@ Press (3) if you really understand and accept these risks.
if n > 1:
msg += "Press (9) to see next group, (7) to go back. X to quit."
if len(_cache) < 4:
_cache[(change, start, n)] = (msg, addrs)
else:
# LIFO
_cache = OrderedDict(list(_cache.items())[:-1])
return msg, addrs
msg, addrs = make_msg()
change = 0
msg, addrs = make_msg(change, start)
while 1:
ch = await ux_show_story(msg, escape='1234679')
@ -327,8 +345,7 @@ Press (3) if you really understand and accept these risks.
elif ch == '2':
# switch into a mode that shows them as QR codes
if not version.has_fatram or ms_wallet:
# requires mk3 and not multisig
if ms_wallet:
continue
from ux import show_qr_codes
@ -355,23 +372,13 @@ Press (3) if you really understand and accept these risks.
else:
continue # 3 in non-NFC mode
msg, addrs = make_msg(change)
msg, addrs = make_msg(change, start, n)
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0, change=0):
# Produce CSV file contents as a generator
if ms_wallet:
# For multisig, include redeem script and derivation for each signer
yield '"' + '","'.join(['Index', 'Payment Address',
'Redeem Script (%d of %d)' % (ms_wallet.M, ms_wallet.N)]
+ (['Derivation'] * ms_wallet.N)) + '"\n'
for (idx, derivs, addr, script) in ms_wallet.yield_addresses(start, n, change_idx=change):
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
ln += '"\n'
yield ln
for line in ms_wallet.generate_address_csv(start, n, change):
yield line
return
@ -420,7 +427,7 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num,
dis.progress_bar_show(idx / count)
sig_nice = None
if not ms_wallet:
if not ms_wallet and addr_fmt != AF_P2TR:
derive = path.format(account=account_num, change=change, idx=0) # first addr
sig_nice = write_sig_file([(h.digest(), fname)], derive, addr_fmt)

View File

@ -3,12 +3,12 @@
# Operations that require user authorization, like our core features: signing messages
# and signing bitcoin transactions.
#
import stash, ure, ux, chains, sys, gc, uio, version, ngu
import stash, ure, chains, sys, gc, uio, ngu, ujson
from ubinascii import b2a_base64, a2b_base64
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from public_constants import MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P2WPKH_P2SH
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
from public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
from sffile import SFFile
from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys
@ -18,7 +18,7 @@ from utils import HexWriter, xfp2str, problem_file_line, cleanup_deriv_path
from utils import B2A, parse_addr_fmt_str
from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput
from exceptions import HSMDenied
from version import has_psram, has_fatram, MAX_TXN_LEN
from version import MAX_TXN_LEN
# Where in SPI flash/PSRAM the two PSBT files are (in and out)
TXN_INPUT_OFFSET = 0
@ -153,31 +153,33 @@ def sign_message_digest(digest, subpath, prompt, addr_fmt=AF_CLASSIC, pk=None):
# do the signature itself!
from glob import dis
ch = chains.current_chain()
if prompt:
dis.fullscreen(prompt, percent=.25)
with stash.SensitiveValues() as sv:
dis.progress_bar_show(.50)
if pk is None:
# if private key is provided, derivation subpath is ignored
# and provided private key is used for signing
if pk is None:
with stash.SensitiveValues() as sv:
# if private key is provided, derivation subpath is ignored
# and provided private key is used for signing
node = sv.derive_path(subpath)
dis.progress_bar_show(.50)
pk = node.privkey()
else:
node = ngu.hdnode.HDNode().from_chaincode_privkey(bytes(32), pk)
addr = ch.address(node, addr_fmt)
else:
node = ngu.hdnode.HDNode().from_chaincode_privkey(bytes(32), pk)
dis.progress_bar_show(.50)
addr = ch.address(node, addr_fmt)
addr = chains.current_chain().address(node, addr_fmt)
sv.register(pk)
dis.progress_bar_show(.75)
rv = ngu.secp256k1.sign(pk, digest, 0).to_bytes()
# AF_CLASSIC header byte base 31 is returned by default from ngu - NOOP
if addr_fmt != AF_CLASSIC:
header_byte, rs = rv[0], rv[1:]
# ngu only produces header base for compressed p2pkh, anyways get only rec_id
rec_id = (header_byte - 27) & 0x03
new_header_byte = rec_id + sv.chain.sig_hdr_base(addr_fmt=addr_fmt)
rv = bytes([new_header_byte]) + rs
dis.progress_bar_show(.75)
rv = ngu.secp256k1.sign(pk, digest, 0).to_bytes()
# AF_CLASSIC header byte base 31 is returned by default from ngu - NOOP
if addr_fmt != AF_CLASSIC:
header_byte, rs = rv[0], rv[1:]
# ngu only produces header base for compressed p2pkh, anyways get only rec_id
rec_id = (header_byte - 27) & 0x03
new_header_byte = rec_id + ch.sig_hdr_base(addr_fmt=addr_fmt)
rv = bytes([new_header_byte]) + rs
dis.progress_bar_show(1)
@ -310,6 +312,9 @@ class ApproveMessageSign(UserAuthorizedAction):
self.text = validate_text_for_signing(text)
self.subpath = cleanup_deriv_path(subpath)
self.addr_fmt = parse_addr_fmt_str(addr_fmt)
# temporary - no p2tr support
if self.addr_fmt == AF_P2TR:
raise ValueError("Unsupported address format: 'p2tr'")
self.approved_cb = approved_cb
from glob import dis
@ -360,7 +365,7 @@ def sign_msg(text, subpath, addr_fmt):
abort_and_goto(UserAuthorizedAction.active_request)
def sign_txt_file(filename):
async def sign_txt_file(filename):
# sign a one-line text file found on a MicroSD card
# - not yet clear how to do address types other than 'classic'
from files import CardSlot, CardMissingError
@ -383,7 +388,7 @@ def sign_txt_file(filename):
if not addr_fmt:
addr_fmt = AF_CLASSIC
def done(signature, address, text):
async def done(signature, address, text):
# complete. write out result
from glob import dis
@ -629,7 +634,7 @@ class ApproveTransaction(UserAuthorizedAction):
# Prompt user w/ details and get approval
from glob import dis, hsm_active
# step 1: parse PSBT from sflash into in-memory objects.
# step 1: parse PSBT from PSRAM into in-memory objects.
try:
with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd:
@ -709,6 +714,13 @@ class ApproveTransaction(UserAuthorizedAction):
self.output_change_text(msg)
gc.collect()
if self.psbt.ux_notes:
# currently we only have locktimes in ux_notes
msg.write('\nTX LOCKTIMES\n\n')
for label, m in self.psbt.ux_notes:
msg.write('- %s: %s\n\n' % (label, m))
if self.psbt.warnings:
msg.write('\n---WARNING---\n\n')
@ -796,16 +808,14 @@ class ApproveTransaction(UserAuthorizedAction):
while 1:
# Show txid when we can; advisory
# - maybe even as QR, hex-encoded in alnum mode
tmsg = txid + '\n\n'
tmsg = txid + '\n\nPress (1) for QR Code of TXID. '
if has_fatram:
tmsg += 'Press (1) for QR Code of TXID. '
if NFC:
tmsg += 'Press (3) to share signed txn via NFC.'
ch = await ux_show_story(tmsg, "Final TXID", escape='13')
if ch=='1' and has_fatram:
if ch == '1':
await show_qr_code(txid, True)
continue
@ -819,7 +829,7 @@ class ApproveTransaction(UserAuthorizedAction):
#if NFC:
#NFC.share_signed_psbt(TXN_OUTPUT_OFFSET, self.result[0], self.result[1])
def save_visualization(self, msg, sign_text=False):
async def save_visualization(self, msg, sign_text=False):
# write text into spi flash, maybe signing it as we go
# - return length and checksum
txt_len = msg.seek(0, 2)
@ -838,7 +848,6 @@ class ApproveTransaction(UserAuthorizedAction):
fd.write(blk)
if chk:
from ubinascii import b2a_base64
# append the signature
digest = ngu.hash.sha256s(chk.digest())
sig = sign_message_digest(digest, 'm', None, AF_CLASSIC)[0]
@ -951,7 +960,7 @@ class ApproveTransaction(UserAuthorizedAction):
def sign_transaction(psbt_len, flags=0x0, psbt_sha=None):
# transaction (binary) loaded into sflash/PSRAM already, checksum checked
# transaction (binary) loaded into PSRAM already, checksum checked
UserAuthorizedAction.check_busy(ApproveTransaction)
UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, flags, psbt_sha=psbt_sha)
@ -985,9 +994,10 @@ async def sign_psbt_file(filename, force_vdisk=False):
from files import CardSlot, CardMissingError
from glob import dis
from ux import the_ux
from sram2 import tmp_buf
# copy file into our spiflash
tmp_buf = bytearray(1024)
# copy file into PSRAM
# - can't work in-place on the card because we want to support writing out to different card
# - accepts hex or base64 encoding, but binary prefered
with CardSlot(force_vdisk, readonly=True) as card:
@ -1191,25 +1201,38 @@ class NewPassphrase(UserAuthorizedAction):
async def interact(self):
# prompt them
from glob import settings
from pincodes import pa
showit = False
title = "Passphrase"
bypass_tmp = True
escape = "x2"
while 1:
if showit:
ch = await ux_show_story('''Given:\n\n%s\n\nShould we switch to that wallet now?
msg = ('BIP-39 passphrase (%d chars long) has been provided over '
'USB connection. Should we switch to that wallet now?\n\n')
if pa.tmp_value and settings.get("words", True):
escape += "1"
msg += "Press (1) to add passphrase to currently active temporary seed. "
OK to continue, X to cancel.''' % self._pw, title="Passphrase")
else:
ch = await ux_show_story('''BIP-39 passphrase (%d chars long) has been provided over USB connection. Should we switch to that wallet now?
if settings.master_get("words", True):
escape += "y"
msg += "Press OK to add passphrase to master seed. "
Press (2) to view the provided passphrase.\n\nOK to continue, X to cancel.''' % len(self._pw), title="Passphrase", escape='2')
msg += ('Press (2) to view the provided passphrase.\n\n'
'X to cancel.')
ch = await ux_show_story(msg=msg % len(self._pw), title=title,
escape=escape, strict_escape=True)
if ch == '2':
showit = True
await ux_show_story('Provided:\n\n%s\n\n' % self._pw, title=title)
continue
break
else:
if ch == '1':
bypass_tmp = False
break
try:
if ch != 'y':
if ch not in 'y1':
# they don't want to!
self.refused = True
await ux_dramatic_pause("Refused.", 1)
@ -1217,11 +1240,11 @@ Press (2) to view the provided passphrase.\n\nOK to continue, X to cancel.''' %
from seed import set_bip39_passphrase
# full screen message shown: "Working..."
set_bip39_passphrase(self._pw)
await set_bip39_passphrase(self._pw, bypass_tmp=bypass_tmp,
summarize_ux=False)
self.result = settings.get('xpub')
except BaseException as exc:
self.failed = "Exception"
sys.print_exception(exc)
@ -1230,8 +1253,9 @@ Press (2) to view the provided passphrase.\n\nOK to continue, X to cancel.''' %
if self.result:
new_xfp = settings.get('xfp')
await ux_show_story('''Above is the master key fingerprint of the current wallet.''',
title="[%s]" % xfp2str(new_xfp))
await ux_show_story('Above is the master key fingerprint '
'of the current wallet.',
title="[%s]" % xfp2str(new_xfp))
def start_bip39_passphrase(pw):
@ -1266,13 +1290,13 @@ class ShowAddressBase(UserAuthorizedAction):
msg += '\n\nCompare this payment address to the one shown on your other, less-trusted, software.'
if NFC:
msg += ' Press (3) to share via NFC.'
if has_fatram:
msg += ' Press (4) to view QR Code.'
msg += ' Press (4) to view QR Code.'
while 1:
ch = await ux_show_story(msg, title=self.title, escape='34')
if ch == '4' and has_fatram:
if ch == '4':
await show_qr_code(self.address, (self.addr_fmt & AFC_BECH32))
continue
if ch == '3' and NFC:
@ -1313,8 +1337,7 @@ class ShowP2SHAddress(ShowAddressBase):
# 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)
self.address = chains.current_chain().p2sh_address(addr_fmt, witdeem_script)
def get_msg(self):
return '''\
@ -1330,6 +1353,43 @@ Paths:
{sp}'''.format(addr=self.address, name=self.ms.name,
M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help))
class ShowMiniscriptAddress(ShowAddressBase):
def setup(self, msc, change, idx):
self.msc = msc
self.change = change
self.idx = idx
d = self.msc.desc.derive(None, change=change).derive(idx)
self.address = chains.current_chain().render_address(d.script_pubkey())
self.addr_fmt = self.msc.af
def get_msg(self):
return '''\
{addr}
Wallet:
{name}
Index:
{idx}
Change:
{change}'''.format(addr=self.address, name=self.msc.name, idx=self.idx, change=bool(self.change))
def start_show_miniscript_address(msc, change, index):
UserAuthorizedAction.check_busy(ShowAddressBase)
UserAuthorizedAction.active_request = ShowMiniscriptAddress(msc, change, index)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
# provide the value back to attached desktop
return UserAuthorizedAction.active_request.address
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
@ -1388,26 +1448,48 @@ def usb_show_address(addr_format, subpath):
return active_request.address
class NewEnrollRequest(UserAuthorizedAction):
def __init__(self, ms, auto_export=False):
class MiniscriptDeleteRequest(UserAuthorizedAction):
def __init__(self, msc):
super().__init__()
self.wallet = ms
self.auto_export = auto_export
# self.result ... will be re-serialized xpub
self.wallet = msc
async def interact(self):
from multisig import MultisigOutOfSpace
from miniscript 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, auto_export=False, bsms_index=None):
super().__init__()
self.wallet = msc
self.auto_export = auto_export
self.bsms_index = bsms_index
async def interact(self):
from wallet_base import WalletOutOfSpace
ms = self.wallet
try:
ch = await ms.confirm_import()
if ch == 'y':
if 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)
if self.auto_export:
# save cosigner details now too
await ms.export_wallet_file('created on',
"\n\nImport that file onto the other Coldcards involved with this multisig wallet.")
# save cosigner details now too
await ms.export_wallet_file('created on',
"\n\nImport that file onto the other Coldcards involved with this multisig wallet.")
await ms.export_electrum()
else:
@ -1415,18 +1497,32 @@ class NewEnrollRequest(UserAuthorizedAction):
self.refused = True
await ux_dramatic_pause("Refused.", 2)
except MultisigOutOfSpace:
except WalletOutOfSpace:
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
self.pop_menu()
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 multisig import MultisigMenu
while 1:
top = the_ux.top_of_stack()
if not top: break
if not isinstance(top, MultisigMenu):
the_ux.pop()
continue
break
restore_menu()
else:
self.pop_menu()
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.
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None, miniscript=False):
# Offer to import (enroll) a new multisig/miniscript wallet. Allow reject by user.
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
UserAuthorizedAction.cleanup()
@ -1434,11 +1530,29 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False):
with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd:
config = fd.read(sf_len).decode()
try:
j_conf = ujson.loads(config)
assert "desc" in j_conf, "'desc' key required"
config = j_conf["desc"]
assert isinstance(config, str), "'desc' value not a str"
assert config, "'desc' empty"
if "name" in j_conf:
# name from json has preference over filenames and desc checksum
name = j_conf["name"]
assert isinstance(name, str), "'name' value not a str"
assert len(name) >= 2, "'name' too short"
assert len(name) <= 40, "'name' too long (max 40)"
except ValueError: pass
# 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)
if miniscript:
msc = MiniScriptWallet.from_file(config, name=name)
else:
msc = MultisigWallet.from_file(config, name=name)
UserAuthorizedAction.active_request = NewEnrollRequest(ms)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(msc, bsms_index=bsms_index)
if ux_reset:
# for USB case, and import from PSBT
@ -1494,26 +1608,15 @@ Binary checksum and signature will be further verified before any changes are ma
# - reboot to start process
from glob import dis
dis.fullscreen('Upgrading...', percent=1)
if not has_psram:
from sflash import SF
import callgate
SF.write(self.length, self.hdr)
callgate.show_logout(2)
else:
# Mk4 copies from PSRAM to flash inside bootrom, we have
# nothing to do here except start that process.
from pincodes import pa
pa.firmware_upgrade(self.psram_offset, self.length)
# not reached, unless issue?
raise RuntimeError("bootrom fail")
# Mk4 copies from PSRAM to flash inside bootrom, we have
# nothing to do here except start that process.
from pincodes import pa
pa.firmware_upgrade(self.psram_offset, self.length)
# not reached, unless issue?
raise RuntimeError("bootrom fail")
else:
# they don't want to!
self.refused = True
if not has_psram:
from sflash import SF
SF.block_erase(0) # just in case, but not required
await ux_dramatic_pause("Refused.", 2)
except BaseException as exc:

View File

@ -5,13 +5,13 @@
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from utils import imported, xfp2str
from utils import pad_raw_secret
from ux import ux_show_story, ux_confirm, ux_dramatic_pause
import version, ujson
from uio import StringIO
import seed
from glob import settings
from pincodes import pa, AE_SECRET_LEN
from pincodes import pa
# we make passwords with this number of words
num_pw_words = const(12)
@ -19,12 +19,12 @@ num_pw_words = const(12)
# max size we expect for a backup data file (encrypted or cleartext)
MAX_BACKUP_FILE_SIZE = const(10000) # bytes
def render_backup_contents():
def render_backup_contents(bypass_tmp=False):
# simple text format:
# key = value
# or #comments
# but value is JSON
current_tmp = None
rv = StringIO()
def COMMENT(val=None):
@ -42,7 +42,7 @@ def render_backup_contents():
COMMENT('Private key details: ' + chain.name)
with stash.SensitiveValues(bypass_pw=True) as sv:
with stash.SensitiveValues(bypass_tmp=bypass_tmp) as sv:
if sv.deltamode:
# die rather than give up our secrets
import callgate
@ -66,21 +66,22 @@ def render_backup_contents():
ADD('long_secret', b2a_hex(pa.ls_fetch()))
# Duress wallets (somewhat optional, since derived)
if version.mk_num <= 3:
if pa.has_duress_pin():
COMMENT('Duress Wallet (informational)')
dpk, p = sv.duress_root()
COMMENT('path = %s' % p)
ADD('duress_xprv', chain.serialize_private(dpk))
ADD('duress_xpub', chain.serialize_public(dpk))
else:
from trick_pins import tp
for label, path, pairs in tp.backup_duress_wallets(sv):
COMMENT()
COMMENT(label + ' (informational)')
COMMENT(path)
for k,v in pairs:
ADD(k, v)
from trick_pins import tp
for label, path, pairs in tp.backup_duress_wallets(sv):
COMMENT()
COMMENT(label + ' (informational)')
COMMENT(path)
for k,v in pairs:
ADD(k, v)
if bypass_tmp and pa.tmp_value:
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)
settings.set_key(nv)
settings.load()
stash.blank_object(nv)
COMMENT('Firmware version (informational)')
date, vers, timestamp = version.get_mpy_version()[0:3]
@ -97,17 +98,57 @@ def render_backup_contents():
if k == 'xpub': continue # redundant, and wrong if bip39pw
if k == 'xfp': continue # redundant, and wrong if bip39pw
if k == 'bkpw': continue # confusing/circular
if k == 'sd2fa': continue # do NOT backup SD 2FA (card can be lost or damaged)
if k == 'words': continue # words length is recalculated from secret
if k == 'seedvault' and not v: continue
if k == 'seeds' and not v: continue
ADD('setting.' + k, v)
if version.has_fatram:
import hsm
if hsm.hsm_policy_available():
ADD('hsm_policy', hsm.capture_backup())
import hsm
if hsm.hsm_policy_available():
ADD('hsm_policy', hsm.capture_backup())
rv.write('\n# EOF\n')
if bypass_tmp and current_tmp:
# go back to tmp secret and its settings
stash.SensitiveValues.clear_cache()
pa.tmp_value = current_tmp
settings.set_key()
settings.load()
return rv.getvalue()
def extract_raw_secret(chain, vals):
# step1: the private key
# - prefer raw_secret over other values
# - TODO: fail back to other values
assert 'raw_secret' in vals
rs = vals.pop('raw_secret')
raw = pad_raw_secret(rs)
# check we can decode this right (might be different firmare)
opmode, bits, node = stash.SecretStash.decode(raw)
assert node
# verify against xprv value (if we have it)
if 'xprv' in vals:
check_xprv = chain.serialize_private(node)
assert check_xprv == vals['xprv'], 'xprv mismatch'
return raw
def extract_long_secret(vals):
ls = None
if ('long_secret' in vals) and version.has_608:
try:
ls = a2b_hex(vals.pop('long_secret'))
except Exception as exc:
sys.print_exception(exc)
# but keep going.
return ls
def restore_from_dict_ll(vals):
# Restore from a dict of values. Already JSON decoded.
# Need a Reboot on success, return string on failure
@ -115,42 +156,14 @@ def restore_from_dict_ll(vals):
from glob import dis
#print("Restoring from: %r" % vals)
chain = chains.get_chain(vals.get('chain', 'BTC'))
# step1: the private key
# - prefer raw_secret over other values
# - TODO: fail back to other values
try:
chain = chains.get_chain(vals.get('chain', 'BTC'))
assert 'raw_secret' in vals
raw = bytearray(AE_SECRET_LEN)
rs = vals.pop('raw_secret')
if len(rs) % 2:
rs += '0'
x = a2b_hex(rs)
raw[0:len(x)] = x
# check we can decode this right (might be different firmare)
opmode, bits, node = stash.SecretStash.decode(raw)
assert node
# verify against xprv value (if we have it)
if 'xprv' in vals:
check_xprv = chain.serialize_private(node)
assert check_xprv == vals['xprv'], 'xprv mismatch'
raw = extract_raw_secret(chain, vals)
except Exception as e:
return ('Unable to decode raw_secret and '
'restore the seed value!\n\n\n'+str(e))
ls = None
if ('long_secret' in vals) and version.has_608:
try:
ls = a2b_hex(vals.pop('long_secret'))
except Exception as exc:
sys.print_exception(exc)
# but keep going.
dis.fullscreen("Saving...")
dis.progress_bar_show(.25)
@ -161,9 +174,8 @@ def restore_from_dict_ll(vals):
# force the right chain
pa.new_main_secret(raw, chain) # updates xfp/xpub
# NOTE: don't fail after this point... they can muddle thru w/ just right seed
ls = extract_long_secret(vals)
if ls is not None:
try:
pa.ls_change(ls)
@ -171,34 +183,69 @@ def restore_from_dict_ll(vals):
sys.print_exception(exc)
# but keep going
# restore settings from backup file
# if sd2fa is encountered during backup restore - purge it
settings.remove_key("sd2fa")
for idx, k in enumerate(vals):
dis.progress_bar_show(idx / len(vals))
if not k.startswith('setting.'):
# restore settings from backup file
vals_len = len(vals)
for idx, key in enumerate(vals):
dis.progress_bar_show(idx / vals_len)
if not key[:8] == "setting.":
continue
if k == 'xfp' or k == 'xpub': continue
k = key[8:]
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
# old backups need this to function properly
continue
if k == 'tp':
# restore trick pins, which may involve many ops
if version.mk_num >= 4:
from trick_pins import tp
try:
tp.restore_backup(vals[k])
except Exception as exc:
sys.print_exception(exc)
from trick_pins import tp
try:
tp.restore_backup(vals[key])
except Exception as exc:
sys.print_exception(exc)
# continue as `tp.restore_backup` handles
# saving into settings
continue
settings.set(k[8:], vals[k])
settings.set(k, vals[key])
# write out
settings.save()
if version.has_fatram and ('hsm_policy' in vals):
if 'hsm_policy' in vals:
import hsm
hsm.restore_backup(vals['hsm_policy'])
async def restore_tmp_from_dict_ll(vals):
from glob import dis
chain = chains.get_chain(vals.get('chain', 'BTC'))
try:
raw = extract_raw_secret(chain, vals)
except Exception as e:
return ('Unable to decode raw_secret and '
'restore the seed value!\n\n\n' + str(e))
dis.fullscreen("Applying...")
from seed import set_ephemeral_seed
from actions import goto_top_menu
await set_ephemeral_seed(raw, chain, meta="Coldcard Backup")
for k, v in vals.items():
if not k[:8] == "setting.":
continue
key = k[8:]
if key in ["multisig", "miniscript"]:
# whitelist
settings.set(k, v)
goto_top_menu()
async def restore_from_dict(vals):
# Restore from a dict of values. Already JSON decoded (ie. dict object).
@ -216,11 +263,26 @@ async def restore_from_dict(vals):
async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
from stash import bip39_passphrase
words = None
skip_quiz = False
bypass_tmp = False
if pa.tmp_value:
if not await ux_confirm("An ephemeral seed is in effect, so backup will be of that seed."):
if bip39_passphrase and pa.tmp_value:
# this is a BIP39 password ephemeral wallet
msg = ("BIP39 passphrase is in effect. Backup ignores passphrases "
"and produces backup of main seed. Press OK to back-up main wallet,"
" press (2) to back-up BIP39 passphrase wallet "
"(extended private key created via seed + pass)")
ch = await ux_show_story(msg, escape="2")
if ch == "x": return
if ch == "y":
bypass_tmp = True
elif pa.tmp_value:
if not await ux_confirm("A temporary seed is in effect, "
"so backup will be of that seed."):
return
stored_words = settings.get('bkpw', None)
@ -277,16 +339,18 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
settings.remove_key('bkpw')
settings.save()
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash)
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash,
bypass_tmp=bypass_tmp)
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True):
async def write_complete_backup(words, fname_pattern, write_sflash=False,
allow_copies=True, bypass_tmp=False):
# Just do the writing
from glob import dis
from files import CardSlot, CardMissingError
from files import CardSlot
# Show progress:
dis.fullscreen('Encrypting...' if words else 'Generating...')
body = render_backup_contents().encode()
body = render_backup_contents(bypass_tmp=bypass_tmp).encode()
gc.collect()
@ -364,11 +428,11 @@ async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_
while 1:
msg = '''Backup file written:\n\n%s\n\n\
To view or restore the file, you must have the full password.\n\n\
Insert another SD card and press 2 to make another copy.''' % (nice)
Insert another SD card and press 2 to make another copy.''' % nice
ch = await ux_show_story(msg, escape='2')
if ch == 'y': return
if ch in 'xy': return
if ch == '2': break
else:
@ -419,14 +483,15 @@ async def verify_backup_file(fname):
await ux_show_story("Backup file CRC checks out okay.\n\nPlease note this is only a check against accidental truncation and similar. Targeted modifications can still pass this test.")
async def restore_complete(fname_or_fd):
async def restore_complete(fname_or_fd, temporary=False):
from ux import the_ux
async def done(words):
# remove all pw-picking from menu stack
seed.WordNestMenu.pop_all()
prob = await restore_complete_doit(fname_or_fd, words)
prob = await restore_complete_doit(fname_or_fd, words,
temporary=temporary)
if prob:
await ux_show_story(prob, title='FAILED')
@ -436,7 +501,7 @@ async def restore_complete(fname_or_fd):
the_ux.push(m)
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None):
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None, temporary=False):
# Open file, read it, maybe decrypt it; return string if any error
# - some errors will be shown, None return in that case
# - no return if successful (due to reboot)
@ -507,7 +572,10 @@ async def restore_complete_doit(fname_or_fd, words, file_cleanup=None):
# but keep going!
# this leads to reboot if it works, else errors shown, etc.
return await restore_from_dict(vals)
if temporary:
return await restore_tmp_from_dict_ll(vals)
else:
return await restore_from_dict(vals)
async def clone_start(*a):
# Begins cloning process, on target device.
@ -525,7 +593,6 @@ file with an ephemeral public key will be written.''')
try:
with CardSlot() as card:
fname, nice = card.pick_filename('ccbk-start.json', overwrite=True)
with card.open(fname, 'wb') as fd:
fd.write(ujson.dumps(dict(pubkey=b2a_hex(my_pubkey))))
@ -636,7 +703,7 @@ async def clone_write_data(*a):
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
await write_complete_backup(words, fname, allow_copies=False)
await write_complete_backup(words, fname, allow_copies=False, bypass_tmp=True)
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")

1084
shared/bsms.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,10 @@
import ngu
from uhashlib import sha256
from ubinascii import hexlify as b2a_hex
from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH, AF_P2TR
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT
from serializations import hash160, ser_compact_size, disassemble
from public_constants import TAPROOT_LEAF_TAPSCRIPT, TAPROOT_LEAF_MASK
from serializations import hash160, ser_compact_size, disassemble, ser_string
from ucollections import namedtuple
from opcodes import OP_RETURN, OP_1, OP_16
@ -25,6 +26,28 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint'))
# - from <https://github.com/Bit-Wasp/bitcoin-php/issues/576>
# - also electrum source: electrum/lib/constants.py
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.secp256k1.tagged_sha256(b"TapTweak", actual_tweak)
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.secp256k1.tagged_sha256(b"TapLeaf",
tapscript_serialize(script, leaf_version))
class ChainsBase:
curve = 'secp256k1'
@ -108,24 +131,30 @@ class ChainsBase:
# - renders a pubkey to an address
# - works only with single-key addresses
assert not addr_fmt & AFC_SCRIPT
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
if addr_fmt == AF_P2TR:
assert len(pubkey) == 32 # internal
script = b'\x51\x20' + taptweak(pubkey)
else:
raise ValueError('bad address template: %s' % addr_fmt)
keyhash = ngu.hash.hash160(pubkey)
if addr_fmt == AF_CLASSIC:
script = b'\x76\xA9\x14' + keyhash + b'\x88\xAC'
elif addr_fmt == AF_P2WPKH_P2SH:
redeem_script = b'\x00\x14' + keyhash
scripthash = ngu.hash.hash160(redeem_script)
script = b'\xA9\x14' + scripthash + b'\x87'
elif addr_fmt == AF_P2WPKH:
script = b'\x00\x14' + keyhash
else:
raise ValueError('bad address template: %s' % addr_fmt)
return cls.render_address(script)
@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
@ -268,6 +297,7 @@ 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'
@ -289,6 +319,7 @@ class BitcoinTestnet(BitcoinMain):
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'
@ -311,6 +342,7 @@ class BitcoinRegtest(BitcoinMain):
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 = 'bcrt'
@ -324,6 +356,8 @@ class BitcoinRegtest(BitcoinMain):
def get_chain(short_name):
# lookup object from name: 'BTC' or 'XTN'
if short_name is None:
return BitcoinMain
if short_name == 'BTC':
return BitcoinMain
elif short_name == 'XTN':
@ -343,6 +377,13 @@ 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]
@ -368,9 +409,10 @@ CommonDerivations = [
( 'BIP-44 / Electrum', "m/44'/{coin_type}'/{account}'/{change}/{idx}", AF_CLASSIC ),
( 'BIP-49 (P2WPKH-nested-in-P2SH)', "m/49'/{coin_type}'/{account}'/{change}/{idx}",
AF_P2WPKH_P2SH ), # generates 3xxx/2xxx p2sh-looking addresses
( 'BIP-84 (Native Segwit P2WPKH)', "m/84'/{coin_type}'/{account}'/{change}/{idx}",
AF_P2WPKH ), # generates bc1 bech32 addresses
AF_P2WPKH ), # generates bc1q bech32 addresses
('BIP-86 (Taproot Segwit P2TR)', "m/86'/{coin_type}'/{account}'/{change}/{idx}",
AF_P2TR), # generates bc1p bech32m addresses
]

View File

@ -67,7 +67,7 @@ def value_resolution_chooser():
def scramble_keypad_chooser():
# rngk = randomize keypad for PIN entry
s = SettingsObject()
s = SettingsObject.prelogin()
which = s.get('rngk', 0)
del s
@ -75,7 +75,7 @@ def scramble_keypad_chooser():
def set(idx, text):
# save it, but "outside" of login PIN
s = SettingsObject()
s = SettingsObject.prelogin()
s.set('rngk', idx)
s.save()
del s
@ -85,7 +85,7 @@ def scramble_keypad_chooser():
def kill_key_chooser():
# kbtn = single keypress after anti-phishing words will wipe seed
s = SettingsObject()
s = SettingsObject.prelogin()
which = s.get('kbtn', -1)
del s
which = int(which) + 1
@ -94,7 +94,7 @@ def kill_key_chooser():
def set(idx, text):
# save it, but "outside" of login PIN
s = SettingsObject()
s = SettingsObject.prelogin()
if idx == 0:
s.remove_key('kbtn')
else:

View File

@ -114,13 +114,14 @@ def check_file_headers(f):
raise ValueError("Second header too big")
# capture this spot
# TODO 'data_start' unused
data_start = f.tell() # expect 0x20
try:
f.seek(sh.offset, 1)
th = f.read(sh.size)
if len(th) != sh.size:
raise IndexError("Truncated file? %s" % e.message)
raise IndexError("Truncated file?")
# Look for properties about compression. this could be
# faked-out but good enough for now

View File

@ -2,28 +2,28 @@
#
# countdowns.py - various details and chooser menus for setting/showing countdown times
#
from glob import settings
from ucollections import OrderedDict
from nvstore import SettingsObject
from menu import MenuItem
from ux import ux_show_story, ux_dramatic_pause
# Login countdown length, stored in minutes
#
lgto_map = {
0: 'Disabled',
5: ' 5 minutes',
15: '15 minutes',
30: '30 minutes',
60: ' 1 hour',
2*60: ' 2 hours',
4*60: ' 4 hours',
8*60: ' 8 hours',
12*60: '12 hours',
24*60: '24 hours',
48*60: '48 hours',
3*24*60: ' 3 days',
7*24*60: ' 1 week',
28*24*60: '28 days later' }
lgto_map = OrderedDict([
(0, 'Disabled'),
(5, ' 5 minutes'),
(15, '15 minutes'),
(30, '30 minutes'),
(60, ' 1 hour'),
(2*60, ' 2 hours'),
(4*60, ' 4 hours'),
(8*60, ' 8 hours'),
(12*60, '12 hours'),
(24*60, '24 hours'),
(48*60, '48 hours'),
(3*24*60, ' 3 days'),
(7*24*60, ' 1 week'),
(28*24*60, '28 days later'),
])
lgto_va = list(lgto_map.keys())
lgto_ch = list(lgto_map.values())
@ -33,7 +33,7 @@ def real_countdown_chooser(tag, offset, def_to):
ch = lgto_ch[offset:]
va = lgto_va[offset:]
s = SettingsObject()
s = SettingsObject.prelogin()
timeout = s.get(tag, def_to) # in minutes
try:
which = va.index(timeout)
@ -42,7 +42,7 @@ def real_countdown_chooser(tag, offset, def_to):
def set_it(idx, text):
# save on key0, not normal settings
s = SettingsObject()
s = SettingsObject.prelogin()
s.set(tag, va[idx])
s.save()
del s
@ -51,81 +51,5 @@ def real_countdown_chooser(tag, offset, def_to):
def countdown_chooser():
return real_countdown_chooser('lgto', 0, 0)
def cd_countdown_chooser():
return real_countdown_chooser('cd_lgto', 1, 60)
# Mk3 only
async def set_countdown_pin(_1, _2, menu_item):
# Accept a new PIN to be used to enable this feature
from login import LoginUX
lll = LoginUX()
lll.reset()
lll.subtitle = "Countdown PIN"
pin = await lll.get_new_pin(None, allow_clear=True) # a string
s = SettingsObject()
from pincodes import pa
if pin == pa.pin.decode():
# can't compare to others like duress/brickme but will override them
await ux_show_story("Must be a unique PIN value!")
return
elif not pin:
# X on first screen does this (better than CLEAR_PIN thing)
s.remove_key('cd_pin')
msg = 'PIN Cleared.'
menu_item.label = "Enable Feature"
else:
s.set('cd_pin', pin)
msg = 'PIN Set.'
menu_item.label = "PIN is Set!"
s.save()
await ux_dramatic_pause(msg, 3)
# Mk3 only
def set_countdown_pin_mode():
# cd_mode = various harm levels
s = SettingsObject()
which = s.get('cd_mode', 0) # default is brick
del s
ch = ['Brick', 'Final PIN', 'Test Mode']
def set(idx, text):
# save it, but "outside" of login PIN
s = SettingsObject()
s.set('cd_mode', idx)
s.save()
del s
return which, ch, set
# Mk3 only
async def countdown_pin_submenu(*a):
# Background and settings for duress-countdown pin
s = SettingsObject()
pin_set = bool(s.get('cd_pin', 0))
if not pin_set:
ok = await ux_show_story('''\
This special PIN will immediately and silently brick the Coldcard, \
but as it does that, it shows a normal-looking countdown timer for login. \
At the end of the countdown, the Coldcard crashes with a vague error. \
Instead of complete brick, you may select a test mode (no harm done) or \
to consume all but the final PIN attempt.\
''')
if not ok: return
return [
MenuItem('PIN is Set!' if pin_set else 'Enable Feature', f=set_countdown_pin),
MenuItem('Countdown Time', chooser=cd_countdown_chooser),
MenuItem('Brick Mode', chooser=set_countdown_pin_mode),
]
# EOF

519
shared/desc_utils.py Normal file
View File

@ -0,0 +1,519 @@
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Copyright (c) 2020 Stepan Snigirev MIT License embit/arguments.py
#
import ngu, chains
from io import BytesIO
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_CLASSIC, AF_P2TR
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 = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
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 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,...))"
elif addr_fmt == AF_P2TR:
# provably unspendable BIP-0341
descriptor_template = "tr(" + PROVABLY_UNSPENDABLE + ",sortedmulti_a(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
def read_until(s, chars=b",)(#"):
# TODO potential infinite loop
# what is the longest possible element? (proly some raw( but that is unsupported)
#
res = b""
chunk = b""
char = None
while True:
chunk = s.read(1)
if len(chunk) == 0:
return res, None
if chunk in chars:
return res, chunk
res += chunk
return res, None
class KeyOriginInfo:
def __init__(self, fingerprint: bytes, derivation: list):
self.fingerprint = fingerprint
self.derivation = derivation
self.cc_fp = swab32(int(b2a_hex(self.fingerprint).decode(), 16))
def __eq__(self, other):
return self.psbt_derivation() == other.psbt_derivation()
def __hash__(self):
return hash(tuple(self.psbt_derivation()))
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
return cls(xfp, derivation)
def __str__(self):
return "%s/%s" % (b2a_hex(self.fingerprint).decode(),
keypath_to_str(self.derivation, prefix='', skip=0).replace("'", "h"))
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
@property
def is_int_ext(self):
if self.multi_path_index is not None:
return True
return False
@property
def is_external(self):
if self.is_int_ext:
return True
elif self.indexes[-2] % 2 == 0:
return True
return False
@property
def branches(self):
if self.is_int_ext:
return self.indexes[self.multi_path_index]
else:
return [self.indexes[-2]]
@classmethod
def from_string(cls, s):
fail_msg = "Cannot use hardened sub derivation path"
if not s:
return cls()
res = []
mp = 0
mpi = None
for idx, i in enumerate(s.split("/")):
start_i = i.find("<")
if start_i != -1:
end_i = s.find(">")
assert end_i
inner = s[start_i+1:end_i]
assert ";" in inner
inner_split = inner.split(";")
assert len(inner_split) == 2, "wrong multipath"
res.append([int(i) for i in inner_split])
mp += 1
mpi = idx
else:
if i == WILDCARD:
res.append(WILDCARD)
else:
assert "'" not in i, fail_msg
assert "h" not in i, fail_msg
res.append(int(i))
# only one <x;y> allowed in subderivation
assert mp <= 1, "too many multipaths (%d)" % mp
if res == [0, WILDCARD]:
obj = cls()
else:
assert len(res) == 2, "Key derivation too long"
assert res[-1] == WILDCARD, "All keys must be ranged"
obj = cls(res)
obj.multi_path_index = mpi
return obj
def to_string(self, external=True, internal=True):
res = []
for i in self.indexes:
if isinstance(i, list):
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 to_int_list(self, branch_idx, idx):
assert branch_idx in self.indexes[0]
return [branch_idx, idx]
class Key:
def __init__(self, node, origin, derivation=None, taproot=False, chain_type=None):
self.origin = origin
self.node = node
self.derivation = derivation
self.taproot = taproot
self.chain_type = chain_type
if not isinstance(self.node, bytes):
assert self.origin, "Key origin info is required"
def __eq__(self, other):
return self.origin.psbt_derivation() == other.origin.psbt_derivation() \
and self.derivation.indexes == other.derivation.indexes
def __hash__(self):
orig = tuple(self.origin.psbt_derivation())
der = self.derivation.indexes.copy()
if self.derivation.multi_path_index is not None:
der[self.derivation.multi_path_index] = tuple(der[self.derivation.multi_path_index])
der = tuple(der)
return hash(orig+der)
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(cls, s):
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",)/")
der = b""
if char == b"/":
der, char = read_until(s, b"<,)")
if char == b"<":
der += b"<"
branch, char = read_until(s, b">")
if char is None:
raise ValueError("Failed reading the key, missing >")
der += branch + b">"
rest, char = read_until(s, b",)")
der += rest
if char is not None:
s.seek(-1, 1)
# parse key
node, chain_type = cls.parse_key(k)
der = KeyDerivationInfo.from_string(der.decode())
return cls(node, origin, der, chain_type=chain_type)
@classmethod
def parse_key(cls, key_str):
chain_type = None
if key_str[1:4].lower() == b"pub":
# 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"
else:
assert hint == b"t", "no slip"
chain_type = "XTN"
node = ngu.hdnode.HDNode()
node.deserialize(key_str)
else:
# only unspendable keys can be bare pubkeys - for now
# TODO
# if b"unspend(" in key_str:
# node = ngu.hdnode.HDNode()
# chain_code = key_str.replace(b"unspend(", b"").replace(b")", b"")
# node.chaincode = a2b_hex(chain_code)
# node.pubkey = a2b_hex("02" + PROVABLY_UNSPENDABLE)
H = a2b_hex(PROVABLY_UNSPENDABLE)
if b"r=" in key_str:
_, r = key_str.split(b"=")
if r == b"@":
# pick a fresh integer r in the range 0...n-1 uniformly at random and use H + rG
kp = ngu.secp256k1.keypair()
else:
# H + rG where r is provided from user
r = a2b_hex(r)
assert len(r) == 32, "r != 32"
kp = ngu.secp256k1.keypair(r)
H_xo = ngu.secp256k1.xonly_pubkey(H)
node = H_xo.tweak_add(kp.xonly_pubkey().to_bytes()).to_bytes()
elif a2b_hex(key_str) == H:
node = H
else:
node = a2b_hex(key_str)
assert len(node) == 32, "invalid pk %d %s" % (len(node), node)
return node, chain_type
def derive(self, idx=None, change=False):
if isinstance(self.node, bytes):
return self
if isinstance(idx, list):
for i in idx:
mp_i = self.derivation.multi_path_index or 0
if i in self.derivation.indexes[mp_i]:
idx = i
break
else:
assert False
elif idx is None:
# derive according to key subderivation if any
if self.derivation is None:
idx = 1 if change else 0
else:
if self.derivation.multi_path_index is not None:
ext, inter = self.derivation.indexes[self.derivation.multi_path_index]
idx = inter if change else ext
new_node = self.node.copy()
new_node.derive(idx, False)
if self.origin:
origin = KeyOriginInfo(self.origin.fingerprint, self.origin.derivation + [idx])
else:
origin = KeyOriginInfo(self.node.my_fp(), [idx])
# empty derivation
derivation = None
return type(self)(new_node, origin, derivation, taproot=self.taproot)
@classmethod
def read_from(cls, s, taproot=False):
return cls.parse(s)
@classmethod
def from_cc_data(cls, xfp, deriv, xpub):
koi = KeyOriginInfo.from_string("%s/%s" % (xfp2str(xfp), deriv.replace("m/", "")))
node = ngu.hdnode.HDNode()
node.deserialize(xpub)
return cls(node, koi, KeyDerivationInfo())
def to_cc_data(self):
ch = chains.current_chain()
return (self.origin.cc_fp,
self.origin.str_derivation(),
ch.serialize_public(self.node, AF_CLASSIC))
@property
def is_provably_unspendable(self):
if isinstance(self.node, bytes):
return True
return False
@property
def prefix(self):
if self.origin:
return "[%s]" % self.origin
return ""
def key_bytes(self):
kb = self.node
if not isinstance(kb, bytes):
kb = self.node.pubkey()
if self.taproot:
if len(kb) == 33:
kb = kb[1:]
assert len(kb) == 32
return kb
def extended_public_key(self):
return chains.current_chain().serialize_public(self.node)
def to_string(self, external=True, internal=True, subderiv=True):
key = self.prefix
if isinstance(self.node, ngu.hdnode.HDNode):
key += self.extended_public_key()
if self.derivation and subderiv:
key += "/" + self.derivation.to_string(external, internal)
else:
key += b2a_hex(self.node).decode()
return key
@classmethod
def from_string(cls, s):
s = BytesIO(s.encode())
return cls.parse(s)
def fill_policy(policy, keys, external=True, internal=True):
keys_len = len(keys)
for i in range(keys_len - 1, -1, -1):
k = keys[i]
ph = "@%d" % i
ph_len = len(ph)
while True:
subderiv = True
ix = policy.find(ph)
if ix == -1:
break
if policy[ix+ph_len] == "/":
# subderivation is part of the policy
subderiv = False
x = ix + ph_len
substr = policy[x:x+26] # 26 is longest possible subderivation allowed "/<2147483647;2147483646>/*"
mp_start = substr.find("<")
assert mp_start != -1
mp_end = substr.find(">")
mp = substr[mp_start:mp_end + 1]
_ext, _int = mp[1:-1].split(";")
if external and not internal:
sub = _ext
elif internal and not external:
sub = _int
else:
sub = None
if sub is not None:
policy = policy[:x + mp_start] + sub + policy[x + mp_end + 1:]
if not isinstance(k, str):
k_str = k.to_string(external, internal, subderiv=subderiv)
else:
k_str = k
if not subderiv:
k_str = "/".join(k_str.split("/")[:-2])
mp_start = k_str.find("<")
if mp_start != -1:
mp_end = k_str.find(">")
mp = k_str[mp_start:mp_end+1]
ext, int = mp[1:-1].split(";")
if external and not internal:
k_str = k_str.replace(mp, ext)
if internal and not external:
k_str = k_str.replace(mp, int)
x = policy[ix:ix + ph_len]
assert x == ph
policy = policy[:ix] + k_str + policy[ix + ph_len:]
return policy
def taproot_tree_helper(scripts):
from miniscript import Miniscript
if isinstance(scripts, Miniscript):
script = scripts.compile()
assert isinstance(script, bytes)
h = ngu.secp256k1.tagged_sha256(b"TapLeaf", chains.tapscript_serialize(script))
return [(chains.TAPROOT_LEAF_TAPSCRIPT, script, bytes())], h
if len(scripts) == 1:
return taproot_tree_helper(scripts[0])
split_pos = len(scripts) // 2
left, left_h = taproot_tree_helper(scripts[0:split_pos])
right, right_h = taproot_tree_helper(scripts[split_pos:])
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
h = ngu.secp256k1.tagged_sha256(b"TapBranch", left_h + right_h)
return left + right, h

View File

@ -1,356 +1,619 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
# (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.
# Copyright (c) 2020 Stepan Snigirev MIT License embit/descriptor.py
#
# 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
import ngu, chains
from io import BytesIO
from collections import OrderedDict
from binascii import hexlify as b2a_hex
from utils import cleanup_deriv_path, check_xpub, xfp2str
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_SIGNERS, MAX_TR_SIGNERS
from desc_utils import parse_desc_str, append_checksum, descriptor_checksum, Key
from desc_utils import taproot_tree_helper, fill_policy
from miniscript import Miniscript
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 DescriptorException(ValueError):
pass
class WrongCheckSumError(Exception):
pass
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
class Tapscript:
def __init__(self, tree=None, keys=None, policy=None):
self.tree = tree
self.keys = keys
self.policy = policy
self._merkle_root = None
return c
@staticmethod
def iter_leaves(tree):
if isinstance(tree, Miniscript):
yield tree
else:
assert isinstance(tree, list)
for lv in tree:
yield from Tapscript.iter_leaves(lv)
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)
@property
def merkle_root(self):
if not self._merkle_root:
self.process_tree()
return self._merkle_root
c = polymod(c, pos & 31)
cls = cls * 3 + (pos >> 5)
clscount += 1
if clscount == 3:
c = polymod(c, cls)
cls = 0
clscount = 0
@staticmethod
def _derive(tree, idx, key_map, change=False):
if isinstance(tree, Miniscript):
return tree.derive(idx, key_map, change=change)
else:
if len(tree) == 1 and isinstance(tree[0], Miniscript):
return tree[0].derive(idx, key_map, change=change)
l, r = tree
return [Tapscript._derive(l, idx, key_map, change=change),
Tapscript._derive(r, idx, key_map, change=change)]
if clscount > 0:
c = polymod(c, cls)
for j in range(0, 8):
c = polymod(c, 0)
c ^= 1
def derive(self, idx=None, change=False):
derived_keys = OrderedDict()
for k in self.keys:
derived_keys[k] = k.derive(idx, change=change)
tree = Tapscript._derive(self.tree, idx, derived_keys, change=change)
return type(self)(tree, policy=self.policy, keys=list(derived_keys.values()))
rv = ''
for j in range(0, 8):
rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
def process_tree(self):
info, mr = taproot_tree_helper(self.tree)
self._merkle_root = mr
return info, mr
return rv
@classmethod
def read_from(cls, s):
num_leafs = 0
depth = 0
tapscript = []
p0 = s.read(1)
if p0 != b"{":
# depth zero
s.seek(-1, 1)
alone = Miniscript.read_from(s, taproot=True)
alone.is_sane(taproot=True)
alone.verify()
tapscript.append(alone)
num_leafs += 1
else:
assert p0 == b"{"
depth += 1
itmp = None
itmp_p = None
while True:
p1 = s.read(1)
if p1 == b'':
break
elif p1 == b")":
s.seek(-1, 1)
break
elif p1 == b",":
continue
elif p1 == b"{":
if itmp is None:
itmp = []
else:
if itmp_p:
itmp[itmp_p].append([])
else:
itmp.append(([]))
itmp_p = -1
def append_checksum(desc):
return desc + "#" + descriptor_checksum(desc)
depth += 1
continue
elif p1 == b"}":
depth -= 1
if depth == 1:
tapscript.append(itmp)
itmp = None
if depth <= 2:
itmp_p = None
continue
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)
item = Miniscript.read_from(s, taproot=True)
item.is_sane(taproot=True)
item.verify()
num_leafs += 1
if itmp is None:
tapscript.append(item)
else:
if itmp_p and depth == 4:
itmp[itmp_p][itmp_p].append(item)
elif itmp_p:
itmp[itmp_p].append(item)
else:
itmp.append(item)
assert num_leafs <= 8, "num_leafs > 8"
ts = cls(tapscript)
ts.parse_policy()
return ts
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
def parse_policy(self):
self.policy, self.keys = self._parse_policy(self.tree, [])
orig_keys = OrderedDict()
for k in self.keys:
if k.origin not in orig_keys:
orig_keys[k.origin] = []
orig_keys[k.origin].append(k)
for i, k_lst in enumerate(orig_keys.values()):
subderiv = True if len(k_lst) == 1 else False
self.policy = self.policy.replace(k_lst[0].to_string(subderiv=subderiv), chr(64) + str(i))
@staticmethod
def _parse_policy(tree, all_keys):
if isinstance(tree, Miniscript):
keys, leaf_str = tree.keys, tree.to_string()
for k in keys:
if k not in all_keys:
all_keys.append(k)
return leaf_str, all_keys
else:
assert isinstance(tree, list)
if len(tree) == 1 and isinstance(tree[0], Miniscript):
keys, leaf_str = tree[0].keys, tree[0].to_string()
for k in keys:
if k not in all_keys:
all_keys.append(k)
return leaf_str, all_keys
else:
l, r = tree
ll, all_keys = Tapscript._parse_policy(l, all_keys)
rr, all_keys = Tapscript._parse_policy(r, all_keys)
return "{" + ll + "," + rr + "}", all_keys
@staticmethod
def script_tree(tree):
if isinstance(tree, Miniscript):
return b2a_hex(chains.tapscript_serialize(tree.compile())).decode()
else:
assert isinstance(tree, list)
if len(tree) == 1 and isinstance(tree[0], Miniscript):
return b2a_hex(chains.tapscript_serialize(tree[0].compile())).decode()
else:
l, r = tree
ll = Tapscript.script_tree(l)
rr = Tapscript.script_tree(r)
return "{" + ll + "," + rr + "}"
def to_string(self, external=True, internal=True):
return fill_policy(self.policy, self.keys, external, internal)
class Descriptor:
__slots__ = (
"keys",
"addr_fmt",
)
def __init__(self, miniscript=None, sh=False, wsh=True, key=None, wpkh=True,
taproot=False, tapscript=None):
if key is None and miniscript is None:
raise DescriptorException("Provide either miniscript or a key")
def __init__(self, keys, addr_fmt):
self.keys = keys
self.addr_fmt = addr_fmt
self.sh = sh
self.wsh = wsh
self.key = key
self.miniscript = miniscript
self.wpkh = wpkh
self.taproot = taproot
self.tapscript = tapscript
if taproot:
if self.key:
self.key.taproot = True
for k in self.keys:
k.taproot = taproot
def legacy_ms_compat(self):
if not (self.is_sortedmulti and self.addr_fmt in (AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH)):
raise ValueError("Unsupported descriptor. Supported: sh(, sh(wsh(, wsh(. "
"MUST be sortedmulti.")
def validate(self):
from glob import settings
if self.miniscript:
if self.is_basic_multisig:
assert len(self.keys) <= MAX_SIGNERS
else:
assert len(self.keys) <= 20
self.miniscript.verify()
if self.miniscript.type != "B":
raise DescriptorException("Top level miniscript should be 'B'")
has_mine = 0
my_xfp = settings.get('xfp')
to_check = self.keys.copy()
if self.tapscript:
assert len(self.keys) <= MAX_TR_SIGNERS
assert self.key # internal key (would fail during parse)
if not isinstance(self.key.node, bytes):
to_check += [self.key]
else:
assert self.key is None and self.miniscript, "not miniscript"
c = chains.current_key_chain().ctype
for k in to_check:
assert k.chain_type == c, "wrong chain"
xfp = k.origin.cc_fp
deriv = k.origin.str_derivation()
xpub = k.extended_public_key()
deriv = cleanup_deriv_path(deriv)
is_mine, _ = check_xpub(xfp, xpub, deriv, c, my_xfp, False)
if is_mine:
has_mine += 1
assert has_mine != 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
def storage_policy(self):
if self.tapscript:
return self.tapscript.policy
s = self.miniscript.to_string()
orig_keys = OrderedDict()
for k in self.keys:
if k.origin not in orig_keys:
orig_keys[k.origin] = []
orig_keys[k.origin].append(k)
for i, k_lst in enumerate(orig_keys.values()):
subderiv = True if len(k_lst) == 1 else False
s = s.replace(k_lst[0].to_string(subderiv=subderiv), chr(64) + str(i))
return s
def ux_policy(self):
if self.tapscript:
return "Taproot tree keys:\n\n" + self.tapscript.policy
return self.storage_policy()
@property
def script_len(self):
if self.taproot:
return 34 # OP_1 <32:xonly>
if self.miniscript:
return len(self.miniscript)
if self.wpkh:
return 22 # 00 <20:pkh>
return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG
def xfp_paths(self):
keys = self.keys
if self.taproot and self.key.origin:
# ignore provably unspendable
keys += [self.key]
return [
key.origin.psbt_derivation()
for key in keys
if key.origin
]
@property
def is_wrapped(self):
return self.sh and self.is_segwit
@property
def is_legacy(self):
return not (self.is_segwit or self.is_taproot)
@property
def is_segwit(self):
return (self.wsh and self.miniscript) or (self.wpkh and self.key) or self.taproot
@property
def is_pkh(self):
return self.key is not None and not self.taproot
@property
def is_taproot(self):
return self.taproot
@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.tapscript:
return self.tapscript.keys
elif self.key:
return [self.key]
return self.miniscript.keys
@property
def addr_fmt(self):
if self.sh and not self.wsh:
af = AF_P2SH
elif self.wsh and not self.sh:
af = AF_P2WSH
elif self.sh and self.wsh:
af = AF_P2WSH_P2SH
elif self.taproot:
af = AF_P2TR
elif self.sh and self.wpkh:
af = AF_P2WPKH_P2SH
elif self.wpkh and not self.sh:
af = AF_P2WPKH
else:
af = AF_CLASSIC
return af
def set_from_addr_fmt(self, addr_fmt):
self.taproot = False
self.wsh = False
self.wpkh = False
self.sh = False
if addr_fmt == AF_P2TR:
self.taproot = True
assert self.key
elif addr_fmt == AF_P2WPKH:
self.wpkh = True
self.miniscript = None
assert self.key
elif addr_fmt == AF_P2WPKH_P2SH:
self.wpkh = True
self.sh = True
self.miniscript = None
assert self.key
elif addr_fmt == AF_P2SH:
self.sh = True
assert self.miniscript
assert not self.key
elif addr_fmt == AF_P2WSH:
self.wsh = True
assert self.miniscript
assert not self.key
elif addr_fmt == AF_P2WSH_P2SH:
self.wsh = True
self.sh = True
assert self.miniscript
assert not self.key
else:
# AF_CLASSIC
assert self.key
assert not self.miniscript
def scriptpubkey_type(self):
if self.is_taproot:
return "p2tr"
if self.sh:
return "p2sh"
if self.is_pkh:
if self.is_legacy:
return "p2pkh"
if self.is_segwit:
return "p2wpkh"
else:
return "p2wsh"
def derive(self, idx=None, change=False):
if self.taproot:
return type(self)(
None,
self.sh,
self.wsh,
self.key.derive(idx, change=change),
self.wpkh,
self.taproot,
tapscript=self.tapscript.derive(idx, change=change),
)
if self.miniscript:
return type(self)(
self.miniscript.derive(idx, change=change),
self.sh,
self.wsh,
None,
self.wpkh,
self.taproot,
tapscript=None,
)
else:
return type(self)(
None, self.sh, self.wsh,
self.key.derive(idx, change=change),
self.wpkh, self.taproot, tapscript=None
)
def witness_script(self):
if self.wsh and self.miniscript is not None:
return self.miniscript.compile()
def redeem_script(self):
if not self.sh:
return None
if self.miniscript:
if self.wsh:
return b"\x00\x20" + ngu.hash.sha256s(self.miniscript.compile())
else:
return self.miniscript.compile()
else:
return b"\x00\x14" + ngu.hash.hash160(self.key.node.pubkey())
def script_pubkey(self):
if self.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.sh:
return b"\xa9\x14" + ngu.hash.hash160(self.redeem_script()) + b"\x87"
if self.wsh:
return b"\x00\x20" + ngu.hash.sha256s(self.witness_script())
if self.miniscript:
return self.miniscript.compile()
if self.wpkh:
return b"\x00\x14" + ngu.hash.hash160(self.key.serialize())
return b"\x76\xa9\x14" + ngu.hash.hash160(self.key.serialize()) + b"\x88\xac"
@classmethod
def is_descriptor(cls, desc_str):
"""Quick method to guess whether this is a descriptor"""
try:
temp = parse_desc_str(desc_str)
except:
return False
for prefix in ("pk(", "pkh(", "wpkh(", "tr(", "addr(", "raw(", "rawtr(", "combo(",
"sh(", "wsh(", "multi(", "sortedmulti(", "multi_a(", "sortedmulti_a("):
if temp.startswith(prefix):
return True
return False
@staticmethod
def checksum_check(desc_w_checksum: str):
def checksum_check(desc_w_checksum, csum_required=False):
try:
desc, checksum = desc_w_checksum.split("#")
except ValueError:
raise ValueError("Missing descriptor checksum")
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
@staticmethod
def parse_key_orig_info(key: str):
# 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:]
assert "/" in key_orig_info, "Malformed key derivation info"
return key_orig_info, key
@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
@staticmethod
def parse_key_derivation_info(key: str):
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")
def checksum(self):
return descriptor_checksum(self._serialize())
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>" + "/" + "*"
@classmethod
def read_from(cls, s, taproot=False):
start = s.read(7)
sh = False
wsh = False
wpkh = False
is_miniscript = True
internal_key = None
tapscript = None
if start.startswith(b"tr("):
is_miniscript = False # miniscript vs. tapscript (that can contain miniscripts in tree)
taproot = True
s.seek(-4, 1)
internal_key = Key.parse(s) # internal key is a must
internal_key.taproot = True
sep = s.read(1)
if sep == b")":
s.seek(-1, 1)
else:
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
result.append(key_str.replace("'", "h"))
return result
def _serialize(self, internal=False, int_ext=False) -> str:
"""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)
def serialize(self, internal=False, int_ext=False) -> str:
"""Serialize with checksum"""
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
@classmethod
def parse(cls, desc_w_checksum: str) -> "Descriptor":
# 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(")")
# native segwit
elif desc.startswith("wpkh("):
addr_fmt = AF_P2WPKH
tmp_desc = desc.replace("wpkh(", "")
tmp_desc = tmp_desc.rstrip(")")
# wrapped segwit
elif desc.startswith("sh(wpkh("):
addr_fmt = AF_P2WPKH_P2SH
tmp_desc = desc.replace("sh(wpkh(", "")
tmp_desc = tmp_desc.rstrip("))")
assert sep == b","
tapscript = Tapscript.read_from(s)
elif start.startswith(b"sh(wsh("):
sh = True
wsh = True
elif start.startswith(b"wsh("):
sh = False
wsh = True
s.seek(-3, 1)
elif start.startswith(b"sh(wpkh"):
is_miniscript = False
sh = True
wpkh = True
assert s.read(1) == b"("
elif start.startswith(b"wpkh("):
is_miniscript = False
wpkh = True
s.seek(-2, 1)
elif start.startswith(b"pkh("):
is_miniscript = False
s.seek(-3, 1)
elif start.startswith(b"sh("):
sh = True
wsh = False
s.seek(-4, 1)
else:
raise ValueError("Unsupported descriptor. Supported: pkh(, wpkh(, sh(wpkh(.")
raise ValueError("Invalid descriptor")
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")
if is_miniscript:
miniscript = Miniscript.read_from(s)
miniscript.is_sane(taproot=False)
key = internal_key
nbrackets = int(sh) + int(wsh)
elif taproot:
miniscript = None
key = internal_key
nbrackets = 1
else:
miniscript = None
key = Key.parse(s)
nbrackets = 1 + int(sh)
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
end = s.read(nbrackets)
if end != b")" * nbrackets:
raise ValueError("Invalid descriptor")
o = cls(miniscript, sh=sh, wsh=wsh, key=key, wpkh=wpkh,
taproot=taproot, tapscript=tapscript)
o.validate()
return o
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
def to_string(self, external=True, internal=True, checksum=True):
if self.taproot:
desc = "tr(%s" % self.key.to_string(external, internal)
if self.tapscript:
desc += ","
tree = self.tapscript.to_string(external, internal)
desc += tree
@classmethod
def is_descriptor(cls, desc_str):
"""Method to guess whether this can be a descriptor"""
try:
temp = parse_desc_str(desc_str)
desc, checksum = temp.split("#")
assert desc[-1] == ")"
desc = desc + ")"
return append_checksum(desc)
return True
except:
return False
if self.miniscript is not None:
res = self.miniscript.to_string(external, internal)
if self.wsh:
res = "wsh(%s)" % res
else:
if self.wpkh:
res = "wpkh(%s)" % self.key.to_string(external, internal)
else:
res = "pkh(%s)" % self.key.to_string(external, internal)
if self.sh:
res = "sh(%s)" % res
def bitcoin_core_serialize(self, external_label=None):
if checksum:
res = append_checksum(res)
return res
def bitcoin_core_serialize(self):
# this will become legacy one day
# instead use <0;1> descriptor format
res = []
for internal in [False, True]:
for external, internal in [(True, False), (False, True)]:
desc_obj = {
"desc": self.serialize(internal=internal),
"desc": self.to_string(external, internal),
"active": True,
"timestamp": "now",
"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",
)
def __init__(self, M, N, keys, addr_fmt):
self.M = M
self.N = N
super().__init__(keys, addr_fmt)
@classmethod
def parse(cls, desc_w_checksum: str) -> "MultisigDescriptor":
# 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("sh(sortedmulti("):
addr_fmt = AF_P2SH
tmp_desc = desc.replace("sh(sortedmulti(", "")
tmp_desc = tmp_desc.rstrip("))")
# native segwit
elif desc.startswith("wsh(sortedmulti("):
addr_fmt = AF_P2WSH
tmp_desc = desc.replace("wsh(sortedmulti(", "")
tmp_desc = tmp_desc.rstrip("))")
# wrapped segwit
elif desc.startswith("sh(wsh(sortedmulti("):
addr_fmt = AF_P2WSH_P2SH
tmp_desc = desc.replace("sh(wsh(sortedmulti(", "")
tmp_desc = tmp_desc.rstrip(")))")
else:
raise ValueError("Unsupported descriptor. Supported: sh(, sh(wsh(, wsh(. All have to be sortedmulti.")
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)
def _serialize(self, internal=False, int_ext=False) -> str:
"""Serialize without checksum"""
desc_base = MULTI_FMT_TO_SCRIPT[self.addr_fmt]
desc_base = desc_base % ("sortedmulti(%s)")
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) -> str:
def pretty_serialize(self):
# TODO not enabled
"""Serialize in pretty and human-readable format"""
inner_ident = 1
res = "# Coldcard descriptor export\n"
res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n"
if self.addr_fmt == AF_P2SH:
@ -365,23 +628,36 @@ class MultisigDescriptor(Descriptor):
elif self.addr_fmt == AF_P2WSH_P2SH:
res += "# wrapped segwit - p2sh-p2wsh\n"
res += "sh(wsh(sortedmulti(\n%s\n)))"
elif self.addr_fmt == AF_P2TR:
inner_ident = 2
res += "# taproot multisig - p2tr\n"
res += "tr(\n"
if isinstance(self.internal_key, str):
res += "\t" + "# internal key (provably unspendable)\n"
res += "\t" + self.internal_key + ",\n"
res += "\t" + "sortedmulti_a(\n%s\n))"
else:
ik_ser = self.serialize_keys(keys=[self.internal_key])[0]
res += "\t" + "# internal key\n"
res += "\t" + ik_ser + ",\n"
res += "\t" + "sortedmulti_a(\n%s\n))"
else:
raise ValueError("Malformed descriptor")
assert len(self.keys) == self.N
inner = "\t" + "# %d of %d (%s)\n" % (
inner = ("\t" * inner_ident) + "# %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"
inner += ("\t" * inner_ident) + 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
inner += ("\t" * inner_ident) + key_str
else:
inner += "\t" + key_str + ",\n"
inner += ("\t" * inner_ident) + key_str + ",\n"
checksum = self.serialize().split("#")[1]
return (res % inner) + "#" + checksum
# EOF

View File

@ -2,18 +2,17 @@
#
# display.py - OLED rendering
#
import machine, ssd1306, uzlib, ckcc, utime
import machine, uzlib, ckcc, utime
from ssd1306 import SSD1306_SPI
from version import is_devmode
from version import is_devmode, is_edge
import framebuf
import uasyncio
from uasyncio import sleep_ms
from graphics import Graphics
from sram2 import display2_buf
# we support 4 fonts
from zevvpeep import FontSmall, FontLarge, FontTiny
FontFixed = object() # ugly 8x8 PET font
display2_buf = bytearray(1024)
class Display:
@ -134,10 +133,16 @@ class Display:
self.dis.fill_rect(128-2, pos, 1, 8, 1)
if is_devmode and not ckcc.is_simulator():
self.dis.fill_rect(128-6, 20, 5, 21, 1)
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)
self.dis.fill_rect(128-5, 20, 5, 21, 1)
self.text(-1, 21, 'D', font=FontTiny, invert=1)
self.text(-1, 28, 'E', font=FontTiny, invert=1)
self.text(-1, 35, 'V', font=FontTiny, invert=1)
elif is_edge:
self.dis.fill_rect(128-5, 19, 5, 26, 1)
self.text(-1, 20, 'E', font=FontTiny, invert=1)
self.text(-1, 27, 'D', font=FontTiny, invert=1)
self.text(-1, 33, 'G', font=FontTiny, invert=1)
self.text(-1, 39, 'E', font=FontTiny, invert=1)
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".

View File

@ -12,12 +12,13 @@ from menu import MenuItem, MenuSystem
from ubinascii import hexlify as b2a_hex
from ubinascii import b2a_base64
from auth import write_sig_file
from utils import chunk_writer
from utils import chunk_writer, xfp2str
BIP85_PWD_LEN = 21
def drv_entro_start(*a):
async def drv_entro_start(*a):
from pincodes import pa
# UX entry
ch = await ux_show_story('''\
@ -36,8 +37,14 @@ so the other wallet is effectively segregated from the Coldcard and yet \
still backed-up.''')
if ch != 'y': return
if stash.bip39_passphrase:
if not await ux_confirm('''You have a BIP-39 passphrase set right now and so that will become wrapped into the new secret.'''):
if pa.tmp_value:
if stash.bip39_passphrase:
msg = ('You have a BIP-39 passphrase set right now '
'and so it will be wrapped into the new secret.')
else:
msg = 'You have a temporary seed active - deriving from temporary.'
if not await ux_confirm(msg):
return
choices = [ '12 words', '18 words', '24 words', 'WIF (privkey)',
@ -109,7 +116,6 @@ async def drv_entro_step2(_1, picked, _2):
from glob import dis
from files import CardSlot, CardMissingError, needs_microsd
the_ux.pop()
msg = "Index Number?"
if picked == 7:
# Passwords
@ -189,7 +195,7 @@ async def drv_entro_step2(_1, picked, _2):
prompt += ', (2) to switch to derived secret'
elif s_mode == 'pw':
prompt += ', (2) to type password over USB'
if (qr is not None) and version.has_fatram:
if qr is not None:
prompt += ', (3) to view as QR code'
if glob.NFC:
prompt += ', (4) to share via NFC'
@ -227,7 +233,7 @@ async def drv_entro_step2(_1, picked, _2):
story = "Filename is:\n\n%s" % out_fn
story += "\n\nSignature filename is:\n\n%s" % sig_nice
await ux_show_story(story, title='Saved')
elif ch == '3' and version.has_fatram:
elif ch == '3':
from ux import show_qr_code
await show_qr_code(qr, qr_alnum)
continue
@ -247,12 +253,16 @@ async def drv_entro_step2(_1, picked, _2):
stash.blank_object(msg)
if ch == '2' and (encoded is not None):
from glob import dis
from pincodes import pa
# switch over to new secret!
dis.fullscreen("Applying...")
await seed.set_ephemeral_seed(encoded)
from actions import goto_top_menu
from glob import settings
xfp_str = xfp2str(settings.get("xfp", 0))
await seed.set_ephemeral_seed(
encoded,
meta='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
)
goto_top_menu()
if encoded is not None:
stash.blank_object(encoded)

View File

@ -8,8 +8,8 @@ from ucollections import OrderedDict
from utils import xfp2str, swab32, export_prompt_builder, chunk_writer
from ux import ux_show_story
from glob import settings
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2TR, AF_P2SH
from auth import write_sig_file
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
def generate_public_contents():
@ -84,7 +84,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 show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132):
if show_slip132 and addr_fmt not in (AF_CLASSIC, AF_P2TR) and (addr_fmt in chain.slip132):
yield ("%s => %s ##SLIP-132##\n" % (
hard_sub, chain.serialize_public(node, addr_fmt)))
@ -102,7 +102,8 @@ be needed for different systems.
yield ('\n\n')
from multisig import MultisigWallet
if MultisigWallet.exists():
exists, exists_other_chain = MultisigWallet.exists()
if exists:
yield '\n# Your Multisig Wallets\n\n'
for ms in MultisigWallet.get_all():
@ -143,8 +144,10 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
with open(fname, 'wb') as fd:
chunk_writer(fd, body)
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
sig_nice = None
if addr_fmt != AF_P2TR:
h = ngu.hash.sha256s(body.encode())
sig_nice = write_sig_file([(h, fname)], derive, addr_fmt)
except CardMissingError:
await needs_microsd()
@ -153,8 +156,9 @@ async def write_text_file(fname_pattern, body, title, derive, addr_fmt):
await ux_show_story('Failed to write!\n\n\n'+str(e))
return
msg = '%s file written:\n\n%s\n\n%s signature file written:\n\n%s' % (title, nice, title,
sig_nice)
msg = '%s file written:\n\n%s' % (title, nice)
if sig_nice:
msg += '\n\n%s signature file written:\n\n%s' % (title, sig_nice)
await ux_show_story(msg)
async def make_summary_file(fname_pattern='public.txt'):
@ -177,10 +181,11 @@ async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.tx
# make the data
examples = []
imp_multi, imp_desc = generate_bitcoin_core_wallet(account_num, examples)
imp_multi, imp_desc, imp_desc_tr = 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
@ -196,7 +201,11 @@ Wallet operates on blockchain: {nb}
The following command can be entered after opening Window -> Console
in Bitcoin Core, or using bitcoin-cli:
importdescriptors '{imp_desc}'
p2wpkh:
importdescriptors '{imp_desc}'
p2tr:
importdescriptors '{imp_desc_tr}'
> **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.
@ -211,7 +220,8 @@ importmulti '{imp_multi}'
## Resulting Addresses (first 3)
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, xfp=xfp, nb=chains.current_chain().name)
'''.format(imp_multi=imp_multi, imp_desc=imp_desc, imp_desc_tr=imp_desc_tr,
xfp=xfp, nb=chains.current_chain().name)
body += '\n'.join('%s => %s' % t for t in examples)
@ -224,42 +234,57 @@ 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
from descriptor import Descriptor, Key
chain = chains.current_chain()
derive = "84'/{coin_type}'/{account}'".format(account=account_num, coin_type=chain.b44_cointype)
derive_v0 = "84'/{coin_type}'/{account}'".format(account=account_num, coin_type=chain.b44_cointype)
derive_v1 = "86'/{coin_type}'/{account}'".format(account=account_num, coin_type=chain.b44_cointype)
with stash.SensitiveValues() as sv:
prefix = sv.derive_path(derive)
xpub = chain.serialize_public(prefix)
prefix = sv.derive_path(derive_v0)
xpub_v0 = 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, sp), a) )
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))
xfp = settings.get('xfp')
txt_xfp = xfp2str(xfp).lower()
_, vers, _ = version.get_mpy_version()
key0 = Key.from_cc_data(xfp, derive_v0, xpub_v0)
desc_v0 = Descriptor(key=key0)
desc_v0.set_from_addr_fmt(AF_P2WPKH)
desc_obj = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=AF_P2WPKH)
key1 = Key.from_cc_data(xfp, derive_v1, xpub_v1)
desc_v1 = Descriptor(key=key1)
desc_v1.set_from_addr_fmt(AF_P2TR)
# for importmulti
imm_list = [
{
'desc': desc_obj.serialize(internal=internal),
'desc': desc_v0.to_string(external, internal),
'range': [0, 1000],
'timestamp': 'now',
'internal': internal,
'keypool': True,
'watchonly': True
}
for internal in [False, True]
for external, internal in [(True, False), (False, True)]
]
# for importdescriptors
imd_list = desc_obj.bitcoin_core_serialize(external_label="Coldcard %s" % txt_xfp)
return imm_list, imd_list
imd_list = desc_v0.bitcoin_core_serialize()
imd_list_v1 = desc_v1.bitcoin_core_serialize()
return imm_list, imd_list, imd_list_v1
def generate_wasabi_wallet():
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
@ -323,7 +348,8 @@ 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, multisig_descriptor_template
from descriptor import Descriptor, Key
from desc_utils import multisig_descriptor_template
chain = chains.current_chain()
master_xfp = settings.get("xfp")
@ -340,8 +366,10 @@ def generate_generic_export(account_num=0):
( 'bip44', "m/44'/{ct}'/{acc}'", AF_CLASSIC, 'p2pkh', False ),
( 'bip49', "m/49'/{ct}'/{acc}'", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh"
( 'bip84', "m/84'/{ct}'/{acc}'", AF_P2WPKH, 'p2wpkh', False ),
( 'bip86', "m/86'/{ct}'/{acc}'", AF_P2TR, 'p2tr', False ),
( 'bip48_1', "m/48'/{ct}'/{acc}'/1'", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ),
( 'bip48_2', "m/48'/{ct}'/{acc}'/2'", AF_P2WSH, 'p2wsh', True ),
( 'bip48_3', "m/48'/{ct}'/{acc}'/3'", AF_P2TR, 'p2tr', True ),
( 'bip45', "m/45'", AF_P2SH, 'p2sh', True ),
]:
if fmt == AF_P2SH and account_num:
@ -351,11 +379,14 @@ 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 != AF_CLASSIC else None
zp = chain.serialize_public(node, fmt) if fmt not in (AF_CLASSIC, AF_P2TR) 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)
key = Key.from_cc_data(master_xfp, dd, xp)
desc_obj = Descriptor(key=key)
desc_obj.set_from_addr_fmt(fmt)
desc = desc_obj.to_string()
rv[name] = OrderedDict(name=atype,
xfp=xfp,
@ -472,7 +503,7 @@ async def make_json_wallet(label, func, fname_pattern='new-wallet.json'):
async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int_ext=True,
fname_pattern="descriptor.txt"):
from descriptor import Descriptor
from descriptor import Descriptor, Key
from glob import dis
dis.fullscreen('Generating...')
@ -487,28 +518,33 @@ async def make_descriptor_wallet_export(addr_type, account_num=0, mode=None, int
mode = 84
elif addr_type == AF_P2WPKH_P2SH:
mode = 49
elif addr_type == AF_P2TR:
mode = 86
else:
raise ValueError(addr_type)
derive = "m/{mode}'/{coin_type}'/{account}'".format(mode=mode,
account=account_num, coin_type=chain.b44_cointype)
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)
desc = Descriptor(keys=[(xfp, derive, xpub)], addr_fmt=addr_type)
key = Key.from_cc_data(xfp, derive, xpub)
desc = Descriptor(key=key)
desc.set_from_addr_fmt(addr_type)
dis.progress_bar_show(0.8)
if int_ext:
# with <0;1> notation
body = desc.serialize(int_ext=True)
body = desc.to_string()
else:
# external descriptor
# internal descriptor
body = "%s\n%s" % (
desc.serialize(internal=False),
desc.serialize(internal=True),
desc.to_string(internal=False),
desc.to_string(external=False),
)
dis.progress_bar_show(1)

View File

@ -56,8 +56,7 @@ def wipe_flash_filesystem():
# erase and re-format the flash filesystem (/flash/**)
import ckcc, pyb
from glob import dis
from version import mk_num
dis.fullscreen('Erasing...')
os.umount('/flash')
@ -81,16 +80,10 @@ def wipe_flash_filesystem():
# rebuild and mount /flash
dis.fullscreen('Rebuilding...')
if mk_num == 4:
# no need to erase, we just put new FS on top
import mk4
mk4.make_flash_fs()
else:
ckcc.wipe_fs()
# remount it
os.mount(fl, '/flash')
# no need to erase, we just put new FS on top
import mk4
mk4.make_flash_fs()
# re-store current settings
from glob import settings

View File

@ -3,75 +3,32 @@
# flow.py - Menu structure
#
from menu import MenuItem, ToggleMenuItem
import version
from glob import settings
from actions import *
from choosers import *
from mk4 import dev_enable_repl
from multisig import make_multisig_menu, import_multisig_nfc
from seed import make_ephemeral_seed_menu
from miniscript import make_miniscript_menu
from seed import make_ephemeral_seed_menu, make_seed_vault_menu
from address_explorer import address_explore
from users import make_users_menu
from drv_entro import drv_entro_start, password_entry
from backups import clone_start, clone_write_data
from xor_seed import xor_split_start, xor_restore_start
from countdowns import countdown_pin_submenu, countdown_chooser
from countdowns import countdown_chooser
from hsm import hsm_policy_available
from paper import make_paper_wallet
from trick_pins import TrickPinMenu
# Optional feature: HSM
if version.has_fatram:
from hsm import hsm_policy_available
else:
hsm_policy_available = lambda: False
# Optional feature: Paper Wallets
try:
from paper import make_paper_wallet
except:
make_paper_wallet = None
if version.mk_num >= 4:
from trick_pins import TrickPinMenu
trick_pin_menu = TrickPinMenu.make_menu
else:
trick_pin_menu = None
trick_pin_menu = TrickPinMenu.make_menu
#
# NOTE: "Always In Title Case"
#
# - try to keep harmless things as first item: so double-tap of OK does no harm
# Mk3 and earlier: see Trick Pins for Mk4
PinChangesMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Change Main PIN', f=pin_changer, arg='main'),
MenuItem('Second Wallet', f=pin_changer, arg='secondary',
predicate=lambda: not version.has_608),
MenuItem('Duress PIN', f=pin_changer, arg='duress'),
MenuItem('Brick Me PIN', f=pin_changer, arg='brickme'),
MenuItem('Countdown PIN', menu=countdown_pin_submenu, predicate=lambda: version.has_608),
MenuItem('Login Now', f=login_now, arg=1),
]
# Not reachable on Mark3 hardware
if not version.has_608:
SecondaryPinChangesMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Second Wallet', f=pin_changer, arg='secondary'),
MenuItem('Duress PIN', f=pin_changer, arg='duress'),
MenuItem('Countdown PIN', menu=countdown_pin_submenu),
MenuItem('Login Now', f=login_now, arg=1),
]
async def which_pin_menu(_1,_2, item):
assert version.mk_num < 4
if version.has_608:
# mk3
return PinChangesMenu
else:
# mk2 only
from pincodes import pa
return PinChangesMenu if not pa.is_secondary else SecondaryPinChangesMenu
#
# Predicates
#
@ -87,9 +44,18 @@ def nfc_enabled():
def vdisk_enabled():
return bool(settings.get('vidsk', 0))
def is_not_tmp():
from pincodes import pa
return not bool(pa.tmp_value)
def se2_and_real_secret():
from pincodes import pa
return version.has_se2 and (not pa.is_secret_blank()) and (not pa.tmp_value)
return (not pa.is_secret_blank()) and (not pa.tmp_value)
def bip39_passphrase_active():
import stash
return settings.get('words', True) \
or (settings.master_get('words', True) and stash.bip39_passphrase)
HWTogglesMenu = [
@ -97,7 +63,7 @@ HWTogglesMenu = [
on_change=change_usb_disable, story='''\
Blocks any data over USB port. Useful when your plan is air-gap usage.'''),
ToggleMenuItem('Virtual Disk', 'vidsk', ['Default Off', 'Enable', 'Enable & Auto'],
predicate=lambda: version.has_psram, on_change=change_virtdisk_enable,
on_change=change_virtdisk_enable,
story='''Coldcard can emulate a virtual disk drive (4MB) where new PSBT files \
can be saved. Signed PSBT files (transactions) will also be saved here. \n\
In "auto" mode, selects PSBT as soon as written.'''),
@ -108,15 +74,14 @@ with the Coldcard.''',
predicate=lambda: version.has_nfc),
]
# all pre-login values
# Mostly pre-login values here.
LoginPrefsMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Change Main PIN', f=pin_changer, arg='main'),
MenuItem('PIN Options', predicate=lambda: not version.has_se2, menu=which_pin_menu),
MenuItem('Trick PINs', predicate=lambda: version.has_se2, menu=trick_pin_menu),
MenuItem('Change Main PIN', f=main_pin_changer),
MenuItem('Trick PINs', menu=trick_pin_menu),
MenuItem('Set Nickname', f=pick_nickname),
MenuItem('Scramble Keypad', f=pick_scramble),
MenuItem('Kill Key', f=pick_killkey, predicate=lambda: version.has_se2),
MenuItem('Kill Key', f=pick_killkey),
MenuItem('Login Countdown', chooser=countdown_chooser),
MenuItem('MicroSD 2FA', menu=microsd_2fa, predicate=se2_and_real_secret),
MenuItem('Test Login Now', f=login_now, arg=1),
@ -127,6 +92,7 @@ SettingsMenu = [
MenuItem('Login Settings', menu=LoginPrefsMenu),
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
MenuItem('Multisig Wallets', menu=make_multisig_menu),
MenuItem('Miniscript', menu=make_miniscript_menu),
MenuItem('Display Units', chooser=value_resolution_chooser),
MenuItem('Max Network Fee', chooser=max_fee_chooser),
MenuItem('Idle Timeout', chooser=idle_timeout_chooser),
@ -153,6 +119,7 @@ XpubExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84),
MenuItem("Classic (BIP-44)", f=export_xpub, arg=44),
MenuItem("Taproot/P2TR(86)", f=export_xpub, arg=86),
MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49),
MenuItem("Master XPUB", f=export_xpub, arg=0),
MenuItem("Current XFP", f=export_xpub, arg=-1),
@ -161,10 +128,11 @@ XpubExportMenu = [
WalletExportMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Bitcoin Core", f=bitcoin_core_skeleton),
MenuItem("Sparrow Wallet", f=named_generic_skeleton, arg="Sparrow"),
MenuItem("Electrum Wallet", f=electrum_skeleton),
MenuItem("Wasabi Wallet", f=wasabi_skeleton),
MenuItem("Unchained Capital", f=unchained_capital_export),
MenuItem("Lily Wallet", f=lily_skeleton),
MenuItem("Unchained", f=unchained_capital_export),
MenuItem("Lily Wallet", f=named_generic_skeleton, arg="Lily"),
MenuItem("Samourai Postmix", f=samourai_post_mix_descriptor_export),
MenuItem("Samourai Premix", f=samourai_pre_mix_descriptor_export),
# MenuItem("Samourai BadBank", f=samourai_bad_bank_descriptor_export), # not released yet
@ -181,11 +149,11 @@ FileMgmtMenu = [
MenuItem("Backup System", predicate=has_secrets, f=backup_everything),
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), #dup elsewhere
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
#MenuItem('Upgrade Firmware', f=microsd_upgrade),
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
MenuItem('Batch Sign PSBT', predicate=has_secrets, f=batch_sign),
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),
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
MenuItem('Format SD Card', f=wipe_sd_card),
MenuItem('Format RAM Disk', predicate=vdisk_enabled, f=wipe_vdisk),
]
@ -193,37 +161,24 @@ FileMgmtMenu = [
UpgradeMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Show Version', f=show_version),
MenuItem('From MicroSD', f=microsd_upgrade), # mk4: misnomer, could be vdisk too
MenuItem('From VirtDisk', predicate=vdisk_enabled, f=microsd_upgrade),
MenuItem('From MicroSD', f=microsd_upgrade, arg=False),
MenuItem('From VirtDisk', predicate=vdisk_enabled, f=microsd_upgrade, arg=True), # force_vdisk=True
MenuItem('Bless Firmware', f=bless_flash),
]
if version.mk_num < 4:
DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Normal USB Mode", f=dev_enable_protocol),
MenuItem("Enable USB REPL", f=dev_enable_vcp),
MenuItem("Enable USB Disk", f=dev_enable_disk),
MenuItem("Wipe Patch Area", f=wipe_filesystem), # needs better label
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
]
else:
# Mk4 and later
from mk4 import dev_enable_repl
DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Serial REPL", f=dev_enable_repl),
MenuItem("Wipe LFS", f=wipe_filesystem), # kills settings, HSM stuff
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
]
DevelopersMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Serial REPL", f=dev_enable_repl),
MenuItem("Wipe LFS", f=wipe_filesystem), # kills settings, HSM stuff
MenuItem('Warm Reset', f=reset_self),
MenuItem("Restore Txt Bkup", f=restore_everything_cleartext),
]
AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
# xxxxxxxxxxxxxxxx
MenuItem("View Identity", f=view_ident),
MenuItem("Ephemeral Seed", menu=make_ephemeral_seed_menu),
MenuItem('Upgrade Firmware', menu=UpgradeMenu),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem('Upgrade Firmware', menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
MenuItem('Perform Selftest', f=start_selftest),
MenuItem('Secure Logout', f=logout_now),
@ -232,8 +187,8 @@ AdvancedVirginMenu = [ # No PIN, no secrets yet (factory fresh)
AdvancedPinnedVirginMenu = [ # Has PIN but no secrets yet
# xxxxxxxxxxxxxxxx
MenuItem("View Identity", f=view_ident),
MenuItem("Ephemeral Seed", menu=make_ephemeral_seed_menu),
MenuItem("Upgrade Firmware", menu=UpgradeMenu),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem("File Management", menu=FileMgmtMenu),
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
MenuItem('Perform Selftest', f=start_selftest),
@ -260,7 +215,9 @@ SeedFunctionsMenu = [
MenuItem('View Seed Words', f=view_seed_words), # text is a little wrong sometimes, rare
MenuItem('Seed XOR', menu=SeedXORMenu),
MenuItem("Destroy Seed", f=clear_seed),
MenuItem('Lock Down Seed', f=convert_bip39_to_bip32),
MenuItem('Lock Down Seed', f=convert_ephemeral_to_master),
MenuItem('Export SeedQR', f=export_seedqr,
predicate=lambda: settings.get('words', True)),
]
DangerZoneMenu = [
@ -268,6 +225,14 @@ DangerZoneMenu = [
MenuItem("Debug Functions", menu=DebugFunctionsMenu), # actually harmless
MenuItem("Seed Functions", menu=SeedFunctionsMenu),
MenuItem("I Am Developer.", menu=maybe_dev_menu),
ToggleMenuItem('Seed Vault', 'seedvault', ['Default Off', 'Enable'],
on_change=change_seed_vault,
story=("Enable Seed Vault? Adds prompt to store temporary seeds "
"into Seed Vault, where they can easily be reused later.\n\n"
"WARNING: Seed Vault is encrypted (AES-256-CTR) by your seed,"
" but not held directly inside secure elements. Backups are required"
" after any change to vault! Recommended for experiments or temporary use."),
predicate=has_secrets),
MenuItem('Perform Selftest', f=start_selftest), # little harmful
MenuItem("Set High-Water", f=set_highwater),
MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available),
@ -284,7 +249,7 @@ Keep blocked unless you intend to sign special transactions.'''),
story="Testnet must only be used by developers because \
correctly- crafted transactions signed on Testnet could be broadcast on Mainnet."),
MenuItem('Settings Space', f=show_settings_space),
MenuItem('MCU Key Slots', predicate=lambda: version.has_se2, f=show_mcu_keys_left),
MenuItem('MCU Key Slots', f=show_mcu_keys_left),
]
BackupStuffMenu = [
@ -307,17 +272,16 @@ AdvancedNormalMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("Backup", menu=BackupStuffMenu),
MenuItem('Export Wallet', predicate=has_secrets, menu=WalletExportMenu), # also inside FileMgmt
MenuItem("Upgrade Firmware", menu=UpgradeMenu),
MenuItem("Upgrade Firmware", menu=UpgradeMenu, predicate=is_not_tmp),
MenuItem("File Management", menu=FileMgmtMenu),
MenuItem('Derive Seed B85', f=drv_entro_start),
MenuItem("View Identity", f=view_ident),
MenuItem("Ephemeral Seed", menu=make_ephemeral_seed_menu),
MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu),
MenuItem('Paper Wallets', f=make_paper_wallet, predicate=lambda: make_paper_wallet),
ToggleMenuItem('Enable HSM', 'hsmcmd', ['Default Off', 'Enable'],
story="Enable HSM? Enables all user management commands, and other HSM-only USB commands. \
By default these commands are disabled.",
predicate=lambda: version.has_fatram),
MenuItem('User Management', menu=make_users_menu, predicate=lambda: version.has_fatram),
By default these commands are disabled."),
MenuItem('User Management', menu=make_users_menu),
MenuItem('NFC Tools', predicate=nfc_enabled, menu=NFCToolsMenu),
MenuItem("Danger Zone", menu=DangerZoneMenu),
]
@ -332,10 +296,9 @@ VirginSystem = [
]
ImportWallet = [
# xxxxxxxxxxxxxxxx
MenuItem("24 Words", menu=start_seed_import, arg=24),
MenuItem("18 Words", menu=start_seed_import, arg=18),
MenuItem("12 Words", menu=start_seed_import, arg=12),
MenuItem("18 Words", menu=start_seed_import, arg=18),
MenuItem("24 Words", menu=start_seed_import, arg=24),
MenuItem("Restore Backup", f=restore_everything),
MenuItem("Clone Coldcard", menu=clone_start),
MenuItem("Import XPRV", f=import_xprv, arg=False), # ephemeral=False
@ -343,13 +306,11 @@ ImportWallet = [
MenuItem("Seed XOR", f=xor_restore_start),
]
NewSeedMenu = [
# xxxxxxxxxxxxxxxx
MenuItem("24 Word (default)", f=pick_new_seed, arg=24),
MenuItem("12 Word", f=pick_new_seed, arg=12),
MenuItem("24 Word Dice Roll", f=new_from_dice, arg=24),
MenuItem("12 Words", f=pick_new_seed, arg=12),
MenuItem("24 Words", f=pick_new_seed, arg=24),
MenuItem("12 Word Dice Roll", f=new_from_dice, arg=12),
MenuItem("24 Word Dice Roll", f=new_from_dice, arg=24),
]
# has PIN, but no secret seed yet
@ -362,21 +323,22 @@ EmptyWallet = [
MenuItem('Settings', menu=SettingsMenu),
]
# In operation, normal system, after a good PIN received.
NormalSystem = [
# xxxxxxxxxxxxxxxx
MenuItem('Ready To Sign', f=ready2sign),
MenuItem('Passphrase', f=start_b39_pw, predicate=lambda: settings.get('words', True)),
MenuItem('Passphrase', f=start_b39_pw, predicate=bip39_passphrase_active),
MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available),
MenuItem("Address Explorer", f=address_explore),
MenuItem('Type Passwords', f=password_entry, predicate=lambda: settings.get("emu", False) and has_secrets()),
MenuItem('Secure Logout', f=logout_now),
MenuItem('Type Passwords', f=password_entry,
predicate=lambda: settings.get("emu", False) and has_secrets()),
MenuItem('Seed Vault', menu=make_seed_vault_menu,
predicate=lambda: settings.master_get('seedvault') and has_secrets()),
MenuItem('Advanced/Tools', menu=AdvancedNormalMenu),
MenuItem('Settings', menu=SettingsMenu),
MenuItem('Secure Logout', f=logout_now),
]
# Shown until unit is put into a numbered bag
FactoryMenu = [
MenuItem('Bag Me Now'), # nice to have NOP at top of menu

View File

@ -2,57 +2,35 @@
#
# ftux.py - First Time User Experience! A new ride at the waterpark.
#
import version
import version, ckcc
from glob import settings
from ux import ux_show_story, the_ux, ux_dramatic_pause
from actions import change_nfc_enable, change_virtdisk_enable, change_usb_disable
COMMON = '''\
\n
You can change this later under Settings > Hardware On/Off.'''
from ux import ux_show_story, the_ux
from actions import change_usb_disable
class FirstTimeUX:
async def interact(self):
# Help them enable the good stuff.
# - they might have already enabled things
# - some features not on mk3
# Force USB to be disabled by default, but also warn/tell user
# how to enable it, plus NFC and VirtDisk (already disabled by default)
if settings.get('du', None) is None:
if version.has_nfc and not settings.get('nfc', 0):
msg = '''Enable NFC/Tap?\n\n\
Lets you Tap your mobile phone on the COLDCARD and \
transfer data easily via NFC.''' + COMMON
ch = await ux_show_story(msg)
if ch == 'y':
settings.set('nfc', 1)
await change_nfc_enable(1)
await ux_dramatic_pause('Enabled.', 1)
# Disabled for now, because limited audience and
# extra barrier to "just getting started"
if 0: # version.has_psram and not settings.get('vidsk', 0):
msg = '''Enable USB Drive?\n\n\
Connect your COLDCARD directly as a USB flash drive \
to your phone or desktop. You will be able to drag-n-drop or \
save PSBT files like other drives/volumes.''' + COMMON
ch = await ux_show_story(msg)
if ch == 'y':
# put them into full-auto mode: 2
settings.set('vidsk', 2)
await change_virtdisk_enable(2)
await ux_dramatic_pause('Enabled.', 1)
if not settings.get('vidsk', 0) and not settings.get('du', 0):
msg = '''Disable USB port?\n\n\
If you intend to operate in Air-Gap mode, where this COLDCARD \
is never connected to anything but power, then this will disable the USB port.''' + COMMON
ch = await ux_show_story(msg)
if ch == 'y':
settings.set('du', 1)
if not ckcc.is_simulator():
settings.set('du', 1) # disable USB
await change_usb_disable(1)
await ux_dramatic_pause('Disabled.', 1)
# done
#settings.set('nfc', 0) # default already
#settings.set('vidsk', 0) # same as default
await ux_show_story('''
Your COLDCARD has been configured for \
best security practices:
- USB disabled
- NFC disabled
- VDisk disabled
You can change these under Settings > Hardware On/Off.''', title="Welcome!")
# done, clear UX
the_ux.pop()
# EOF

View File

@ -3,7 +3,6 @@
# history.py - store some history about past transactions and/or outputs they involved
#
import gc, chains
from utils import B2A
from uhashlib import sha256
from ustruct import pack, unpack
from exceptions import IncorrectUTXOAmount
@ -125,8 +124,6 @@ class OutptValueCache:
# memory management: can't store very much, so trim as needed
depth = HISTORY_SAVED
if settings.capacity > 0.8:
depth //= 2
# also limit in-memory use
cls.load_cache()

View File

@ -4,16 +4,15 @@
#
# Unattended signing of transactions and messages, subject to a set of rules.
#
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu, version
from sffile import SFFile
import ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
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 multisig import MultisigWallet
from miniscript import MiniScriptWallet
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
@ -88,13 +87,13 @@ def pop_list(j, fld_name, cleanup_fcn=None):
else:
return []
def pop_deriv_list(j, fld_name, extra_val=None):
def pop_deriv_list(j, fld_name, extra_vals=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 s.lower() == 'any': return s.lower()
if extra_val and s.lower() == extra_val: return s.lower()
if extra_vals and s.lower() in extra_vals:
return s.lower()
try:
return cleanup_deriv_path(s, allow_star=True)
except:
@ -195,7 +194,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 multisig wallet to restrict to, or '1' for single signer only
# - wallet: which multisig/miniscript 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
@ -212,6 +211,7 @@ class ApprovalRule:
return u
self.index = idx+1
self.ms_type = "multisig"
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
self.users = pop_list(j, 'users', check_user)
@ -238,8 +238,11 @@ class ApprovalRule:
# if specified, 'wallet' must be an existing multisig wallet's name
if self.wallet and self.wallet != '1':
names = [ms.name for ms in MultisigWallet.get_all()]
assert self.wallet in names, "unknown MS wallet: "+self.wallet
ms_names = [ms.name for ms in MultisigWallet.get_all()]
msc_names = [msc.name for msc in MiniScriptWallet.get_all()]
assert self.wallet in (ms_names+msc_names), "unknown wallet: "+self.wallet
if self.wallet in msc_names:
self.ms_type = "miniscript"
# patterns must be valid
for p in self.patterns:
@ -283,9 +286,9 @@ class ApprovalRule:
rv = 'Any amount'
if self.wallet == '1':
rv += ' (non multisig)'
rv += ' (singlesig only)'
elif self.wallet:
rv += ' from multisig wallet "%s"' % self.wallet
rv += ' from %s wallet "%s"' % (self.ms_type, self.wallet)
if self.users:
rv += ' may be authorized by '
@ -328,7 +331,9 @@ class ApprovalRule:
# rule limited to one wallet
if psbt.active_multisig:
# if multisig signing, might need to match specific wallet name
assert self.wallet == psbt.active_multisig.name, 'wrong wallet'
assert self.wallet == psbt.active_multisig.name, 'wrong multisig wallet'
elif psbt.active_miniscript:
assert self.wallet == psbt.active_miniscript.name, 'wrong miniscript wallet'
else:
# non multisig, but does this rule apply to all wallets or single-singers
assert self.wallet == '1', 'not multisig'
@ -430,7 +435,7 @@ class AuditLogger:
self.card = CardSlot().__enter__()
d = self.card.get_sd_root() + '/' + self.dirname
d = self.card.get_sd_root() + '/' + self.dirname
# mkdir if needed
try: uos.stat(d)
@ -504,9 +509,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')
self.share_xpubs = pop_deriv_list(j, 'share_xpubs')
self.share_addrs = pop_deriv_list(j, 'share_addrs', 'p2sh')
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', ['p2sh', 'any', 'msas'])
# free text shown at top
self.notes = pop_string(j, 'notes', 1, 80)
@ -667,7 +672,6 @@ class HSMPolicy:
def activate(self, new_file):
# user approved the HSM activation, so apply it.
from glob import dis
from pincodes import pa
import glob
assert not glob.hsm_active
@ -682,12 +686,6 @@ class HSMPolicy:
with open(POLICY_FNAME, 'w+t') as f:
ujson.dump(self.save(), f)
if version.mk_num <= 3:
# that changes the flash, so need to update
# the hash stored in SE (Mk3 and earlier)
pa.greenlight_firmware()
dis.show()
if self.set_sl:
self.save_storage_locker()
@ -821,12 +819,15 @@ class HSMPolicy:
return match_deriv_path(self.share_xpubs, subpath)
def approve_address_share(self, subpath=None, is_p2sh=False):
def approve_address_share(self, subpath=None, is_p2sh=False, miniscript=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)
@ -1001,7 +1002,8 @@ def hsm_status_report():
rv['approval_wait'] = True
rv['users'] = Users.list()
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()]
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()] \
+ [msc.name for msc in MiniScriptWallet.get_all()]
rv['chain'] = settings.get('chain', 'BTC')

View File

@ -39,21 +39,17 @@ glob.dis = dis
# slowish imports, some with side-effects
import ckcc, uasyncio
if version.mk_num >= 4:
# early setup code needed on Mk4
try:
import mk4
mk4.init0()
# early setup code needed on Mk4
try:
import mk4
mk4.init0()
from psram import PSRAMWrapper
glob.PSRAM = PSRAMWrapper()
from psram import PSRAMWrapper
glob.PSRAM = PSRAMWrapper()
except BaseException as exc:
sys.print_exception(exc)
# continue tho
else:
# Serial Flash memory
from sflash import SF
except BaseException as exc:
sys.print_exception(exc)
# continue tho
# Setup membrane numpad (mark 2+)
from mempad import MembraneNumpad
@ -62,14 +58,15 @@ glob.numpad = numpad
# NV settings
from nvstore import SettingsObject
settings = SettingsObject(glob.dis)
settings = SettingsObject()
settings.load(glob.dis)
glob.settings = settings
async def more_setup():
# Boot up code; splash screen is being shown
# MAYBE: check if we're a brick and die again? Or show msg?
try:
from files import CardSlot
CardSlot.setup()
@ -78,7 +75,9 @@ async def more_setup():
try:
from pincodes import pa
pa.setup(b'') # just to see where we stand.
is_blank = pa.is_blank()
except RuntimeError as e:
is_blank = True
print("Problem: %r" % e)
if version.is_factory_mode:
@ -92,8 +91,9 @@ async def more_setup():
from actions import start_selftest
await start_selftest()
else:
# force them to accept terms (unless marked as already done)
elif is_blank:
# force them to accept terms (unless marked as already done in settings)
# only if no main PIN chosen
from actions import accept_terms
await accept_terms()

View File

@ -6,12 +6,14 @@ freeze_as_mpy('', [
'address_explorer.py',
'auth.py',
'backups.py',
'bsms.py',
'callgate.py',
'chains.py',
'choosers.py',
'compat7z.py',
'countdowns.py',
'descriptor.py',
'desc_utils.py',
'dev_helper.py',
'display.py',
'drv_entro.py',
@ -19,6 +21,7 @@ freeze_as_mpy('', [
'export.py',
'files.py',
'flow.py',
'ftux.py',
'glob.py',
'history.py',
'hsm.py',
@ -28,6 +31,7 @@ freeze_as_mpy('', [
'main.py',
'mempad.py',
'menu.py',
'miniscript.py',
'multisig.py',
'numpad.py',
'nvstore.py',
@ -42,8 +46,8 @@ freeze_as_mpy('', [
'seed.py',
'selftest.py',
'serializations.py',
'wallet_base.py',
'sffile.py',
'sram2.py',
'ssd1306.py',
'stash.py',
'usb.py',
@ -52,7 +56,6 @@ freeze_as_mpy('', [
'ux.py',
'version.py',
'xor_seed.py',
'ftux.py',
], opt=0)
# Optimize data-like files, since no need to debug them.

View File

@ -1,5 +0,0 @@
# Mk3 and earlier only files; would not be needed on Mk4 or later
freeze_as_mpy('', [
'sflash.py',
], opt=0)

View File

@ -10,4 +10,4 @@ freeze_as_mpy('', [
freeze_as_mpy('', [
'graphics_mk4.py',
], opt=3)
], opt=3)

View File

@ -166,9 +166,18 @@ class MenuSystem:
if not keep_position:
self.cursor = 0
self.ypos = 0
self.items = [m for m in menu_items if not getattr(m, 'predicate', None) or m.predicate()]
self.count = len(self.items)
def goto_label(self, label):
# pick menu item based on label text
for i, m in enumerate(self.items):
if m.label.endswith(label):
self.goto_idx(i)
return True
return False
def show(self):
#
# Redraw the menu.

1857
shared/miniscript.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -96,7 +96,6 @@ def wipe_if_deltamode():
if not pa.is_deltamode():
return
import callgate
callgate.fast_wipe()
# EOF

View File

@ -3,16 +3,22 @@
# multisig.py - support code for multisig signing and p2sh in general.
#
import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson
from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str
from utils import str_to_keypath, problem_file_line, export_prompt_builder, parse_extended_key
from ubinascii import hexlify as b2a_hex
from utils import xfp2str, str2xfp, cleanup_deriv_path, keypath_to_str, truncate_address
from utils import str_to_keypath, problem_file_line, export_prompt_builder, check_xpub
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys, ux_enter_bip32_index
from files import CardSlot, CardMissingError, needs_microsd
from descriptor import MultisigDescriptor, multisig_descriptor_template
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS
from descriptor import Descriptor
from miniscript import Key, Sortedmulti, Number
from desc_utils import multisig_descriptor_template
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, AF_P2TR
from public_constants import MAX_SIGNERS
from menu import MenuSystem, MenuItem
from opcodes import OP_CHECKMULTISIG
from opcodes import OP_CHECKMULTISIG, OP_CHECKSIG, OP_NUMEQUAL, OP_CHECKSIGADD
from exceptions import FatalPSBTIssue
from glob import settings
from wallet_base import BaseWallet
from serializations import disassemble
# PSBT Xpub trust policies
@ -21,14 +27,11 @@ TRUST_OFFER = const(1)
TRUST_PSBT = const(2)
class MultisigOutOfSpace(RuntimeError):
pass
def disassemble_multisig_mn(redeem_script):
# pull out just M and N from script. Simple, faster, no memory.
# Pull out just M and N from script. Simple, faster, no memory.
assert MAX_SIGNERS == 15
assert redeem_script[-1] == OP_CHECKMULTISIG, 'need CHECKMULTISIG'
if redeem_script[-1] != OP_CHECKMULTISIG:
return None, None
M = redeem_script[0] - 80
N = redeem_script[-2] - 80
@ -40,8 +43,7 @@ def disassemble_multisig(redeem_script):
# - only for multisig scripts, not general purpose
# - expect OP_1 (pk1) (pk2) (pk3) OP_3 OP_CHECKMULTISIG for 1 of 3 case
# - returns M, N, (list of pubkeys)
# - for very unlikely/impossible asserts, dont document reason; otherwise do.
from serializations import disassemble
# - for very unlikely/impossible asserts, don't document reason; otherwise do.
M, N = disassemble_multisig_mn(redeem_script)
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
@ -104,7 +106,8 @@ def make_redeem_script(M, nodes, subkey_idx):
return b''.join(pubkeys)
class MultisigWallet:
class MultisigWallet(BaseWallet):
# Capture the info we need to store long-term in order to participate in a
# multisig wallet as a co-signer.
# - can be saved to nvram
@ -113,25 +116,25 @@ class MultisigWallet:
# - required during signing to verify change outputs
# - can reconstruct any redeem script from this
# Challenges:
# - can be big, taking big % of 4k storage in nvram
# - can be big, taking big % of storage in nvram
# - complex object, want to have flexibility going forward
FORMAT_NAMES = [
(AF_P2SH, 'p2sh'),
(AF_P2WSH, 'p2wsh'),
(AF_P2WSH_P2SH, 'p2sh-p2wsh'), # preferred
(AF_P2TR, 'p2tr'),
(AF_P2WSH_P2SH, 'p2wsh-p2sh'), # obsolete (now an alias)
]
# optional: user can short-circuit many checks (system wide, one power-cycle only)
disable_checks = False
key_name = "multisig"
def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, chain_type='BTC'):
self.storage_idx = -1
def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, chain_type=None):
super().__init__(chain_type=chain_type)
self.name = name
assert len(m_of_n) == 2
self.M, self.N = m_of_n
self.chain_type = chain_type or 'BTC'
assert len(xpubs[0]) == 3
self.xpubs = xpubs # list of (xfp(int), deriv, xpub(str))
self.addr_fmt = addr_fmt # address format for wallet
@ -150,17 +153,12 @@ class MultisigWallet:
return v.upper()
return '?'
@property
def chain(self):
return chains.get_chain(self.chain_type)
@classmethod
def get_trust_policy(cls):
which = settings.get('pms', None)
exists, _ = cls.exists()
if which is None:
which = TRUST_VERIFY if cls.exists() else TRUST_OFFER
which = TRUST_VERIFY if exists else TRUST_OFFER
return which
@ -215,16 +213,27 @@ class MultisigWallet:
return rv
@classmethod
def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmt=None):
def is_correct_chain(cls, o, curr_chain):
if "ch" not in o[-1]:
# mainnet
ch = "BTC"
else:
ch = o[-1]["ch"]
if ch == curr_chain.ctype:
return True
return False
@classmethod
def iter_wallets(cls, M=None, N=None, addr_fmt=None):
# yield MS wallets we know about, that match at least right M,N if known.
# - this is only place we should be searching this list, please!!
lst = settings.get('multisig', [])
lst = settings.get(cls.key_name, [])
c = chains.current_key_chain()
for idx, rec in enumerate(lst):
if idx == not_idx:
# ignore one by index
if not cls.is_correct_chain(rec, c):
continue
if M or N:
# peek at M/N
has_m, has_n = tuple(rec[1])
@ -319,57 +328,6 @@ class MultisigWallet:
return False
@classmethod
def get_all(cls):
# return them all, as a generator
return cls.iter_wallets()
@classmethod
def exists(cls):
# are there any wallets defined?
return bool(settings.get('multisig', False))
@classmethod
def get_by_idx(cls, nth):
# instance from index number (used in menu)
lst = settings.get('multisig', [])
try:
obj = lst[nth]
except IndexError:
return None
return cls.deserialize(obj, nth)
def commit(self):
# data to save
# - important that this fails immediately when nvram overflows
obj = self.serialize()
v = settings.get('multisig', [])
orig = v.copy()
if not v or self.storage_idx == -1:
# create
self.storage_idx = len(v)
v.append(obj)
else:
# update in place
v[self.storage_idx] = obj
settings.set('multisig', v)
# save now, rather than in background, so we can recover
# from out-of-space situation
try:
settings.save()
except:
# back out change; no longer sure of NVRAM state
try:
settings.set('multisig', orig)
settings.save()
except: pass # give up on recovery
raise MultisigOutOfSpace
def has_similar(self):
# check if we already have a saved duplicate to this proposed wallet
# - return (name_change, diff_items, count_similar) where:
@ -421,9 +379,9 @@ class MultisigWallet:
else:
raise IndexError # consistency bug
lst = settings.get('multisig', [])
lst = settings.get(self.key_name, [])
del lst[self.storage_idx]
settings.set('multisig', lst)
settings.set(self.key_name, lst)
settings.save()
self.storage_idx = -1
@ -437,9 +395,9 @@ class MultisigWallet:
# Assuming a suffix of /0/0 on the defined prefix's, yield
# possible deposit addresses for this wallet. Never show
# user the resulting addresses because we cannot be certain
# they are valid and could be signed. And yet, dont blank too many
# they are valid and could be signed. And yet, don't blank too many
# spots or else an attacker could grid out a suitable replacement.
ch = self.chain
ch = chains.current_chain()
assert self.addr_fmt, 'no addr fmt known'
@ -460,6 +418,7 @@ class MultisigWallet:
# make the redeem script, convert into address
script = make_redeem_script(self.M, nodes, idx)
addr = ch.p2sh_address(self.addr_fmt, script)
addr = addr[0:12] + '___' + addr[12+3:]
yield idx, [p.format(idx=idx) for p in paths], addr, script
@ -467,6 +426,35 @@ class MultisigWallet:
idx += 1
count -= 1
def make_addresses_msg(self, msg, start, n, change=0):
from glob import dis
addrs = []
for (i, paths, addr, script) in self.yield_addresses(start, n, change_idx=change):
if i == 0 and self.N <= 4:
msg += '\n'.join(paths) + '\n =>\n'
else:
msg += '.../%d/%d =>\n' % (change, i)
addrs.append(addr)
msg += truncate_address(addr) + '\n\n'
dis.progress_bar_show(i / n)
return msg, addrs
def generate_address_csv(self, start, n, change):
yield '"' + '","'.join(['Index', 'Payment Address',
'Redeem Script (%d of %d)' % (self.M, self.N)]
+ (['Derivation'] * self.N)) + '"\n'
for (idx, derivs, addr, script) in self.yield_addresses(start, n, change_idx=change):
ln = '%d,"%s","%s","' % (idx, addr, b2a_hex(script).decode())
ln += '","'.join(derivs)
ln += '"\n'
yield ln
def validate_script(self, redeem_script, subpaths=None, xfp_paths=None):
# Check we can generate all pubkeys in the redeem script, raise on errors.
# - working from pubkeys in the script, because duplicate XFP can happen
@ -483,7 +471,8 @@ class MultisigWallet:
M, N, pubkeys = disassemble_multisig(redeem_script)
assert M==self.M and N == self.N, 'wrong M/N in script'
if self.disable_checks: return ['UNVERIFIED']
if self.disable_checks:
return ['UNVERIFIED']
for pk_order, pubkey in enumerate(pubkeys):
check_these = []
@ -535,7 +524,7 @@ class MultisigWallet:
found_pk = node.pubkey()
# Document path(s) used. Not sure this is useful info to user tho.
# - Do not show what we can't verify: we don't really know the hardeneded
# - Do not show what we can't verify: we don't really know the hardened
# part of the path from fingerprint to here.
here = '[%s]' % xfp2str(xfp)
if dp != len(path):
@ -581,6 +570,7 @@ class MultisigWallet:
xpubs = []
addr_fmt = AF_P2SH
my_xfp = settings.get('xfp')
for ln in lines:
# remove comments
comm = ln.find('#')
@ -646,9 +636,12 @@ class MultisigWallet:
continue
# deserialize, update list and lots of checks
is_mine = cls.check_xpub(xfp, value, deriv, chains.current_chain().ctype, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, value, deriv, chains.current_key_chain().ctype,
my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
return name, addr_fmt, xpubs, has_mine, M, N
@classmethod
@ -658,20 +651,34 @@ class MultisigWallet:
my_xfp = settings.get('xfp')
xpubs = []
desc = MultisigDescriptor.parse(descriptor)
for xfp, deriv, xpub in desc.keys:
descriptor = Descriptor.from_string(descriptor)
descriptor.legacy_ms_compat() # raises
addr_fmt = descriptor.addr_fmt
M, N = descriptor.miniscript.m_n()
for key in descriptor.miniscript.keys:
assert key.derivation.is_external, "Invalid subderivation path - only 0/* or <0;1>/* allowed"
xfp = key.origin.cc_fp
deriv = key.origin.str_derivation()
xpub = key.extended_public_key()
deriv = cleanup_deriv_path(deriv)
is_mine = cls.check_xpub(xfp, xpub, deriv, chains.current_chain().ctype, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, xpub, deriv, chains.current_key_chain().ctype,
my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
return None, desc.addr_fmt, xpubs, has_mine, desc.M, desc.N
return None, addr_fmt, xpubs, has_mine, M, N
def to_descriptor(self):
return MultisigDescriptor(
M=self.M, N=self.N,
keys=self.xpubs,
addr_fmt=self.addr_fmt,
)
keys = [
Key.from_cc_data(xfp, deriv, xpub)
for xfp, deriv, xpub in self.xpubs
]
miniscript = Sortedmulti(Number(self.M), *keys)
desc = Descriptor(miniscript=miniscript)
desc.set_from_addr_fmt(self.addr_fmt)
return desc
@classmethod
def from_file(cls, config, name=None):
@ -692,8 +699,8 @@ class MultisigWallet:
# - M of N line (assume N of N if not spec'd)
# - xpub: any bip32 serialization we understand, but be consistent
#
expect_chain = chains.current_chain().ctype
if "sortedmulti(" in config or MultisigDescriptor.is_descriptor(config):
expect_chain = chains.current_key_chain().ctype
if Descriptor.is_descriptor(config):
# assume descriptor, classic config should not contain sertedmulti( and check for checksum separator
# ignore name
_, addr_fmt, xpubs, has_mine, M, N = cls.from_descriptor(config)
@ -721,9 +728,7 @@ class MultisigWallet:
except:
raise AssertionError('name must be ascii, 1..20 long')
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N
assert addr_fmt & AFC_SCRIPT, 'script style addr fmt'
# check we're included... do not insert ourselves, even tho we
# have enough info, simply because other signers need to know my xpubkey anyway
@ -733,83 +738,6 @@ class MultisigWallet:
# done. have all the parts
return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, chain_type=expect_chain)
@classmethod
def check_xpub(cls, xfp, xpub, deriv, expect_chain, my_xfp, xpubs):
# Shared code: consider an xpub for inclusion into a wallet, if ok, append
# to list: xpubs with a tuple: (xfp, deriv, xpub)
# return T if it's our own key
# - deriv can be None, and in very limited cases can recover derivation path
# - could enforce all same depth, and/or all depth >= 1, but
# seems like more restrictive than needed, so "m" is allowed
try:
# Note: addr fmt detected here via SLIP-132 isn't useful
node, chain, _ = parse_extended_key(xpub)
except:
raise AssertionError('unable to parse xpub')
try:
assert node.privkey() == None # 'no privkeys plz'
except ValueError:
pass
if expect_chain == "XRT":
# HACK but there is no difference extended_keys - just bech32 hrp
assert chain.ctype == "XTN"
else:
assert chain.ctype == expect_chain # 'wrong chain'
depth = node.depth()
if depth == 1:
if not xfp:
# allow a shortcut: zero/omit xfp => use observed parent value
xfp = swab32(node.parent_fp())
else:
# generally cannot check fingerprint values, but if we can, do so.
if not cls.disable_checks:
assert swab32(node.parent_fp()) == xfp, 'xfp depth=1 wrong'
assert xfp, 'need fingerprint' # happens if bare xpub given
# In most cases, we cannot verify the derivation path because it's hardened
# and we know none of the private keys involved.
if depth == 1:
# but derivation is implied at depth==1
kn, is_hard = node.child_number()
if is_hard: kn |= 0x80000000
guess = keypath_to_str([kn], skip=0)
if deriv:
if not cls.disable_checks:
assert guess == deriv, '%s != %s' % (guess, deriv)
else:
deriv = guess # reachable? doubt it
assert deriv, 'empty deriv' # or force to be 'm'?
assert deriv[0] == 'm'
# path length of derivation given needs to match xpub's depth
if not cls.disable_checks:
p_len = deriv.count('/')
assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % (
p_len, depth, xfp2str(xfp))
if xfp == my_xfp:
# its 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
with stash.SensitiveValues() as sv:
chk_node = sv.derive_path(deriv)
assert node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
# serialize xpub w/ BIP-32 standard now.
# - this has effect of stripping SLIP-132 confusion away
xpubs.append((xfp, deriv, chain.serialize_public(node, AF_P2SH)))
return (xfp == my_xfp)
def make_fname(self, prefix, suffix='txt'):
rv = '%s-%s.%s' % (prefix, self.name, suffix)
return rv.replace(' ', '_')
@ -909,7 +837,7 @@ class MultisigWallet:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to write!\n\n\n'+str(e))
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
return
def render_export(self, fp, hdr_comment=None, descriptor=False, core=False, desc_pretty=True):
@ -922,9 +850,10 @@ class MultisigWallet:
print("importdescriptors '%s'\n" % core_str, file=fp)
else:
if desc_pretty:
desc = desc_obj.pretty_serialize()
# TODO pretty serialize
desc = desc_obj.to_string(internal=False)
else:
desc = desc_obj.serialize()
desc = desc_obj.to_string(internal=False)
print("%s\n" % desc, file=fp)
else:
if hdr_comment:
@ -996,8 +925,9 @@ class MultisigWallet:
for k, v in xpubs_list:
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
xpub = ngu.codecs.b58_encode(v)
is_mine = cls.check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
expect_chain, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
expect_chain, my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
addr_fmt = cls.guess_addr_fmt(path)
@ -1005,7 +935,7 @@ class MultisigWallet:
assert has_mine == 1 # 'my key not included'
name = 'PSBT-%d-of-%d' % (M, N)
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH)
ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH) # TODO why legacy
# may just keep just in-memory version, no approval required, if we are
# trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet
@ -1029,7 +959,9 @@ class MultisigWallet:
# cleanup and normalize xpub
tmp = []
self.check_xpub(xfp, xpub, keypath_to_str(path, skip=0), self.chain_type, 0, tmp)
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
self.chain_type, 0, self.disable_checks)
tmp.append(item)
(_, deriv, xpub_reserialized) = tmp[0]
assert deriv # because given as arg
@ -1122,7 +1054,7 @@ Press (1) to see extended public keys, '''.format(M=M, N=N, name=self.name, exp=
continue
if ch == 'y' and not is_dup:
# save to nvram, may raise MultisigOutOfSpace
# save to nvram, may raise WalletOutOfSpace
if name_change:
name_change.delete()
@ -1146,13 +1078,15 @@ Addresses:
at=self.render_addr_fmt(self.addr_fmt)))
# concern: the order of keys here is non-deterministic
# or order is taken from descriptor order (multi) but we do not support it
# or order is determined by BIP (sortedmulti)
for idx, (xfp, deriv, xpub) in enumerate(self.xpubs):
if idx:
msg.write('\n---===---\n\n')
msg.write('%s:\n %s\n\n%s\n' % (xfp2str(xfp), deriv, xpub))
if self.addr_fmt != AF_P2SH:
if self.addr_fmt not in (AF_P2SH, AF_P2TR):
# SLIP-132 format [yz]pubs here when not p2sh mode.
# - has same info as proper bitcoin serialization, but looks much different
node = self.chain.deserialize_node(xpub, AF_P2SH)
@ -1238,9 +1172,11 @@ class MultisigMenu(MenuSystem):
@classmethod
def construct(cls):
# Dynamic menu with user-defined names of wallets shown
from bsms import make_ms_wallet_bsms_menu
if not MultisigWallet.exists():
rv = [MenuItem('(none setup yet)', f=no_ms_yet)]
exists, exists_other_chain = MultisigWallet.exists()
if not exists:
rv = [MenuItem(MultisigWallet.none_setup_yet(exists_other_chain), f=no_ms_yet)]
else:
rv = []
for ms in MultisigWallet.get_all():
@ -1250,6 +1186,7 @@ class MultisigMenu(MenuSystem):
rv.append(MenuItem('Import from File', f=import_multisig))
rv.append(MenuItem('Import via NFC', f=import_multisig_nfc, predicate=lambda: NFC is not None))
rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs))
rv.append(MenuItem('BSMS (BIP-129)', menu=make_ms_wallet_bsms_menu))
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
rv.append(MenuItem('Skip Checks?', f=disable_checks_menu))
@ -1279,15 +1216,14 @@ async def make_ms_wallet_menu(menu, label, item):
ms = MultisigWallet.get_by_idx(item.arg)
if not ms: return
rv = [
return [
MenuItem('"%s"' % ms.name, f=ms_wallet_detail, arg=ms),
MenuItem('View Details', f=ms_wallet_detail, arg=ms),
MenuItem('Descriptors', menu=make_ms_wallet_descriptor_menu, arg=ms),
MenuItem('Delete', f=ms_wallet_delete, arg=ms),
MenuItem('Coldcard Export', f=ms_wallet_ckcc_export, arg=(ms, {})),
MenuItem('Descriptors', menu=make_ms_wallet_descriptor_menu, arg=ms),
MenuItem('Electrum Wallet', f=ms_wallet_electrum_export, arg=ms),
]
return rv
async def make_ms_wallet_descriptor_menu(menu, label, item):
# descriptor menu
@ -1333,7 +1269,7 @@ async def ms_wallet_ckcc_export(menu, label, item):
async def ms_wallet_show_descriptor(menu, label, item):
ms = item.arg
desc = ms.to_descriptor()
desc_str = desc.serialize()
desc_str = desc.to_string(internal=False)
ch = await ux_show_story("Press (1) to export in pretty human readable format.\n\n" + desc_str, escape="1")
if ch == "1":
await ms.export_wallet_file(descriptor=True, desc_pretty=True)
@ -1378,7 +1314,7 @@ async def export_multisig_xpubs(*a):
# Consumer for this file is supposed to be ourselves, when we build on-device multisig.
# - however some 3rd parties are making use of it as well.
#
from glob import NFC
from glob import NFC, dis
xfp = xfp2str(settings.get('xfp', 0))
chain = chains.current_chain()
@ -1397,6 +1333,8 @@ P2SH-P2WSH:
m/48'/{coin}'/acct'/1'
P2WSH:
m/48'/{coin}'/acct'/2'
P2TR:
m/48'/{coin}'/acct'/3'
OK to continue. X to abort.'''.format(coin=chain.b44_cointype)
@ -1414,10 +1352,13 @@ OK to continue. X to abort.'''.format(coin=chain.b44_cointype)
force_vdisk = True
if ch not in escape: return
dis.fullscreen('Generating...')
todo = [
( "m/45'", 'p2sh', AF_P2SH), # iff acct_num == 0
( "m/48'/{coin}'/{acct_num}'/1'", 'p2sh_p2wsh', AF_P2WSH_P2SH ),
( "m/48'/{coin}'/{acct_num}'/2'", 'p2wsh', AF_P2WSH ),
( "m/48'/{coin}'/{acct_num}'/3'", 'p2tr', AF_P2TR ),
]
def render(fp):
@ -1503,7 +1444,7 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, force_vdisk=
# sigh, OS/filesystem variations
file_size = var[1] if len(var) == 2 else get_filesize(full_fname)
if not (0 <= file_size <= 1100):
if not (0 <= file_size <= 1500):
# out of range size
continue
@ -1521,8 +1462,9 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, force_vdisk=
assert deriv == vals[mode+'_deriv'], "wrong derivation: %s != %s"%(
deriv, vals[mode+'_deriv'])
is_mine = MultisigWallet.check_xpub(xfp, ln, deriv,
chain.ctype, my_xfp, xpubs)
is_mine, item = check_xpub(xfp, ln, deriv, chain.ctype,
my_xfp, MultisigWallet.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
@ -1605,9 +1547,9 @@ Coldcard multisig setup file and an Electrum wallet file will be created automat
name = 'CC-%d-of-%d' % (M, N)
ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, addr_fmt=addr_fmt)
from auth import NewEnrollRequest, UserAuthorizedAction
from auth import NewMiniscriptEnrollRequest, UserAuthorizedAction
UserAuthorizedAction.active_request = NewEnrollRequest(ms, auto_export=True)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(ms, auto_export=True)
# menu item case: add to stack
from ux import the_ux
@ -1637,14 +1579,14 @@ async def import_multisig_nfc(*a):
from glob import NFC
# this menu option should not be available if NFC is disabled
try:
return await NFC.import_multisig_nfc()
return await NFC.import_miniscript_nfc(legacy_multisig=True)
except Exception as e:
await ux_show_story(title="ERROR", msg="Failed to import multisig. %s" % str(e))
async def import_multisig(*a):
# pick text file from SD card, import as multisig setup file
from actions import file_picker
from glob import VD
from glob import VD, dis
force_vdisk = False
if VD:
@ -1671,8 +1613,9 @@ async def import_multisig(*a):
if 'pub' in ln:
return True
fn = await file_picker('Pick multisig wallet file to import (.txt)', suffix='.txt', min_size=100,
max_size=20*200, taster=possible, force_vdisk=force_vdisk)
fn = await file_picker('Pick multisig wallet file to import (.txt,.json)',
suffix=['.txt', '.json'], min_size=100, max_size=350*200,
taster=possible, force_vdisk=force_vdisk)
if not fn: return
try:
@ -1683,6 +1626,7 @@ async def import_multisig(*a):
await needs_microsd()
return
dis.fullscreen('Wait...')
from auth import maybe_enroll_xpub
try:
possible_name = (fn.split('/')[-1].split('.'))[0]

View File

@ -7,7 +7,7 @@
# - has GPIO signal "??" which is multipurpose on its own pin
# - this chip chosen because it can disable RF interaction
#
import ngu, utime, ngu, ndef
import utime, ngu, ndef
from uasyncio import sleep_ms
from ustruct import pack, unpack
from ubinascii import unhexlify as a2b_hex
@ -535,32 +535,6 @@ class NFCHandler:
else:
raise ValueError(ctype)
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
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
if len(msg) < 70: continue
msg = bytes(msg).decode() # from memory view
if 'pub' in msg or 'sortedmulti(' in msg:
winner = msg
break
if not winner:
await ux_show_story('Unable to find data expected in NDEF')
return
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):
data = await self.start_nfc_rx()
if not data: return
@ -579,7 +553,7 @@ class NFCHandler:
try:
from seed import set_ephemeral_seed_words
await set_ephemeral_seed_words(winner)
await set_ephemeral_seed_words(winner, meta='NFC Import')
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)))
@ -739,4 +713,76 @@ class NFCHandler:
return winner
async def read_bsms_token(self):
data = await self.start_nfc_rx()
if not data:
await ux_show_story('Unable to find data expected in NDEF')
return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode().strip() # from memory view
try:
int(msg, 16)
winner = msg
break
except: pass
if not winner:
await ux_show_story('Unable to find BSMS token in NDEF data')
return
return winner
async def read_bsms_data(self):
data = await self.start_nfc_rx()
if not data:
await ux_show_story('Unable to find data expected in NDEF')
return
winner = None
for urn, msg, meta in ndef.record_parser(data):
msg = bytes(msg).decode().strip() # from memory view
try:
if "BSMS" in msg:
# unencrypted case
winner = msg
break
elif int(msg[:6], 16):
# encrypted hex case
winner = msg
break
else:
continue
except: pass
if not winner:
await ux_show_story('Unable to find BSMS data in NDEF data')
return
return winner
async def import_miniscript_nfc(self, legacy_multisig=False):
data = await self.start_nfc_rx()
if not data: return
winner = None
for urn, msg, meta in ndef.record_parser(data):
if len(msg) < 70: continue
msg = bytes(msg).decode() # from memory view
if 'pub' in msg:
winner = msg
break
if not winner:
await ux_show_story('Unable to find miniscript descriptor expected in NDEF')
return
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner, miniscript=not legacy_multisig)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
# EOF

View File

@ -12,19 +12,15 @@
# - encrypted and stored in SPI flash, in last 128k area
# - AES encryption key is derived from actual wallet secret
# - if logged out, then use fixed key instead (ie. it's public)
# - to support multiple wallets and plausible deniablity, we
# will preserve any noise already there, and only replace our own stuff
# - you cannot move data between slots because AES-CTR with CTR seed based on slot #
# - SHA-256 check on decrypted data
# - (Mk4) each slot is a file on /flash/settings
#
import os, sys, ujson, ustruct, ckcc, gc, ngu, aes256ctr
from uio import BytesIO
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr
from uhashlib import sha256
from random import shuffle, randbelow
from random import randbelow
from utils import call_later_ms
from version import mk_num, is_devmode
from glob import PSRAM
# TODO fs.sync
@ -36,9 +32,9 @@ from glob import PSRAM
# b39skip = (bool) skip discussion about use of BIP-39 passphrase
# idle_to = idle timeout period (seconds)
# _age = internal verison number for data (see below)
# terms_ok = customer has signed-off on the terms of sale
# tested = selftest has been completed successfully
# multisig = list of defined multisig wallets (complex)
# miniscript = list of defined miniscript wallets (complex)
# pms = trust/import/distrust xpubs found in PSBT files
# axi = index of last selected address in explorer
# lgto = (minutes) how long to wait for Login Countdown feature [pre v4.0.2]
@ -58,48 +54,61 @@ from glob import PSRAM
# sd2fa = (list of strings): track which SD card is needed for login
# bkpw = (string): last backup password, so can be re-used easily
# sighshchk = (bool) set if sighash checks are disabled
# seedvault = (bool) opt-in enable seed vault feature
# seeds = list of stored secrets for seedvault feature
# Stored w/ key=00 for access before login
# _skip_pin = hard code a PIN value (dangerous, only for debug)
# nick = optional nickname for this coldcard (personalization)
# rngk = randomize keypad for PIN entry
# delay_left = [REMOVED-obsolete] seconds remaining on login countdown, if defined
# lgto = (minutes) how long to wait for Login Countdown feature [in v4.0.2+]
# cd_lgto = [<=mk3] minutes to show in countdown (in countdown-to-brick mode)
# cd_mode = [<=mk3] set to enable some less-destructive modes
# cd_pin = [<=mk3] pin code which enables "countdown to brick" mode
# kbtn = (1 char) '1'-'9' that will wipe seed during login process (mk4+)
# terms_ok = customer has signed-off on the terms of sale
# settings linked to seed
# LINKED_SETTINGS = ["multisig", "miniscript", "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
# PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"]
# keep these settings only if unspecified on the other end
KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz",
"axskip", "del", "pms", "idle_to", "b39skip"]
SEEDVAULT_FIELDS = ['seeds', 'seedvault', 'xfp', 'words']
NUM_SLOTS = const(100)
SLOTS = range(NUM_SLOTS)
MK4_WORKDIR = '/flash/settings/'
if mk_num <= 3:
# where in SPI Flash we work (last 128k)
SLOTS = range((1024-128)*1024, 1024*1024, 4096)
NUM_SLOTS = 32
# for mk4: we store binary files on LFS2 filesystem
def MK4_FILENAME(slot):
return MK4_WORKDIR + ('%03x.aes' % slot)
from sffile import SFFile
from sflash import SF
else:
# work in LFS2 filesystem instead, but same terminology
SLOTS = range(0, 64)
NUM_SLOTS = 64
MK4_WORKDIR = '/flash/settings/'
# for mk4: we store binary files on LFS2 filesystem
def MK4_FILENAME(slot):
return MK4_WORKDIR + ('%03x.aes' % slot)
class SettingsObject:
# class vars: track a few values from master seed settings
master_sv_data = {}
master_nvram_key = None
def __init__(self, dis=None):
def __init__(self, nvram_key=None):
# NOTE: constructor no longer loads the values by default (too slow).
self.is_dirty = 0
self.my_pos = None
self.nvram_key = b'\0'*32
self.capacity = 0
self.nvram_key = nvram_key or b'\0'*32
self.current = self.default_values()
self.overrides = {} # volatile overide values
self.load(dis)
@classmethod
def prelogin(cls):
# make an instance of the pre-login settings (ie. w/o key)
rv = cls()
rv.load()
return rv
def get_aes(self, pos):
# Build AES object for en/decrypt of specific block.
@ -107,12 +116,25 @@ class SettingsObject:
ctr = ustruct.pack('<4I', 4, 3, 2, pos)
return aes256ctr.new(self.nvram_key, ctr)
@staticmethod
def hash_key(secret):
# hash up the secret... without decoding it or similar
assert len(secret) >= 32
s = sha256(secret)
for round in range(5):
s.update('pad')
s = sha256(s.digest())
return s.digest()
def set_key(self, new_secret=None):
# System settings (not secrets) are stored in flash, encrypted with this
# key that is derived from main wallet secret. Call this method when the secret
# is first loaded, or changes for some reason.
from pincodes import pa
from stash import blank_object, SensitiveValues
from stash import blank_object
key = None
mine = False
@ -125,47 +147,28 @@ class SettingsObject:
# read secret and use it.
new_secret = pa.fetch()
mine = True
SensitiveValues.cache_secret(new_secret)
if new_secret:
# hash up the secret... without decoding it or similar
assert len(new_secret) >= 32
s = sha256(new_secret)
for round in range(5):
s.update('pad')
s = sha256(s.digest())
key = s.digest()
key = self.hash_key(new_secret)
if mine:
blank_object(new_secret)
# for restore from backup case, or when changing (created) the seed
# save value for use in self.get_aes()
self.nvram_key = key
def get_capacity(self):
# percent space used (0.0=>empty)
if mk_num <= 3:
return self.capacity
# could use whole filesystem, so use that as imprecise proxy
_, _, blocks, bfree, *_ = os.statvfs(MK4_WORKDIR)
return (blocks-bfree) / blocks
def _open_file(self, pos, mode='rb'):
return open(MK4_FILENAME(pos), mode)
def _slot_is_blank(self, pos, buf):
# read a few bytes from start of slot
if mk_num <= 3:
SF.read(pos, buf)
return (buf[0] == buf[1] == buf[2] == buf[3] == 0xff)
try:
with self._open_file(pos) as fd:
fd.readinto(buf)
@ -175,73 +178,38 @@ class SettingsObject:
def _wipe_slot(self, pos):
# blank out a slot
if mk_num <= 3:
SF.wait_done()
SF.sector_erase(pos)
SF.wait_done()
else:
fn = MK4_FILENAME(pos)
try:
os.remove(fn)
except BaseException as exc:
# Error (ENOENT) expected here when saving first time, because the
# "old" slot was not in use
pass
fn = MK4_FILENAME(pos)
try:
os.remove(fn)
except Exception:
# Error (ENOENT) expected here when saving first time, because the
# "old" slot was not in use
pass
def _deny_slot(self, pos):
# write garbage to look legit in a slot
if mk_num <= 3:
with self._open_file(pos, 'wb') as fd:
for i in range(0, 4096, 256):
h = ngu.random.bytes(256)
SF.wait_done()
SF.write(pos+i, h)
else:
with self._open_file(pos, 'wb') as fd:
for i in range(0, 4096, 256):
h = ngu.random.bytes(256)
fd.write(h)
fd.write(h)
def _read_slot(self, pos, decryptor):
# return decrypted (json text) and last 32-bytes which is SHA-256 over that
if mk_num <= 3:
chk = sha256()
# Mk4 is just reading a binary file and decrypt as we go.
with self._open_file(pos) as fd:
# missing ftell(), so emulate
ln = fd.seek(0, 2)
fd.seek(0, 0)
from sram2 import nvstore_buf as _tmp
buf = fd.read(ln - 32)
assert len(buf) == ln-32
with SFFile(pos, length=4096, pre_erased=True) as fd:
for i in range(4096/32):
b = decryptor(fd.read(32))
if i != 127:
_tmp[i*32:(i*32)+32] = b
chk.update(b)
else:
expect = b
rv = decryptor(buf)
digest = ngu.hash.sha256s(rv)
# check how much space was used for encoded JSON
try:
end = bytes(_tmp).index(b'\0')
self.capacity = end / (4096-32)
except ValueError:
self.capacity = 1.0
expect = decryptor(fd.read(32))
assert len(expect) == 32
return _tmp, expect, chk.digest()
else:
# Mk4 is just reading a binary file and decrypt as we go.
with self._open_file(pos) as fd:
# missing ftell(), so emulate
ln = fd.seek(0, 2)
fd.seek(0, 0)
buf = fd.read(ln - 32)
assert len(buf) == ln-32
rv = decryptor(buf)
digest = ngu.hash.sha256s(rv)
expect = decryptor(fd.read(32))
assert len(expect) == 32
return rv, expect, digest
return rv, expect, digest
def _write_slot(self, pos, aes):
# SHA-256 over plaintext
@ -249,52 +217,26 @@ class SettingsObject:
# serialize the data into JSON
d = ujson.dumps(self.current)
with self._open_file(pos, 'wb') as fd:
# pad w/ zeros at least to 4k, but allow larger
dat_len = len(d)
pad_len = (4096-32) - dat_len
if mk_num <= 3:
with SFFile(pos, max_size=4096, pre_erased=True) as fd:
# pad w/ zeros
dat_len = len(d)
pad_len = (4096-32) - dat_len
assert pad_len >= 0, 'too big'
fd.write(aes(d))
assert fd.tell() == dat_len
chk.update(d)
del d
self.capacity = dat_len / 4096
while pad_len > 0:
here = min(32, pad_len)
fd.write(aes(d))
chk.update(d)
del d
pad = bytes(here)
fd.write(aes(pad))
chk.update(pad)
while pad_len > 0:
here = min(32, pad_len)
pad_len -= here
pad = bytes(here)
fd.write(aes(pad))
chk.update(pad)
pad_len -= here
fd.write(aes(chk.digest()))
assert fd.tell() == 4096
else:
with self._open_file(pos, 'wb') as fd:
# pad w/ zeros at least to 4k, but allow larger
dat_len = len(d)
pad_len = (4096-32) - dat_len
fd.write(aes(d))
assert fd.tell() == dat_len
chk.update(d)
del d
while pad_len > 0:
here = min(32, pad_len)
pad = bytes(here)
fd.write(aes(pad))
chk.update(pad)
pad_len -= here
fd.write(aes(chk.digest()))
fd.write(aes(chk.digest()))
def _used_slots(self):
# mk4: faster list of slots in use; doesn't open them
@ -304,45 +246,85 @@ class SettingsObject:
def _nonempty_slots(self, dis=None):
# generate slots that are non-empty
taste = bytearray(4)
# use directory listing
files = self._used_slots()
self.num_empty = NUM_SLOTS - len(files)
if mk_num <= 3:
self.num_empty = 0
for i, pos in enumerate(files):
if dis:
dis.progress_bar_show(i / len(files))
for pos in SLOTS:
if dis:
dis.progress_bar_show((pos-SLOTS.start) / (SLOTS.stop-SLOTS.start))
gc.collect()
if self._slot_is_blank(pos, taste):
# unlikely case, but easy to handle
continue
if self._slot_is_blank(pos, taste):
# erased (probably)
self.num_empty += 1
continue
yield pos, taste
yield pos, taste
def leaving_master_seed(self):
# going from master seed to a tmp seed, so capture a few values we need.
SettingsObject.master_nvram_key = self.nvram_key
for fn in SEEDVAULT_FIELDS:
curr = self.current.get(fn, None)
if curr is not None:
SettingsObject.master_sv_data[fn] = curr
def return_to_master_seed(self):
# switching from a tmp seed to the normal master seed
# - we already kept the key needed, so just re-read
assert SettingsObject.master_nvram_key
self.nvram_key = SettingsObject.master_nvram_key
self.load()
# these value no longer required, and might become stale
SettingsObject.master_sv_data.clear()
SettingsObject.master_nvram_key = None
def master_set(self, key, value):
# Set a value, and it must be saved under the master seed's
# Concern is we may be changing a setting from a tmp seed mode
# - always does a save
from glob import settings as self
if not SettingsObject.master_nvram_key:
# simple, we are already on master seed
self.set(key, value)
self.save()
else:
# use directory listing
files = self._used_slots()
self.num_empty = NUM_SLOTS - len(files)
# harder, slower: have to load, change and write
master = SettingsObject(nvram_key=SettingsObject.master_nvram_key)
master.load()
master.set(key, value)
master.save()
del master
for i, pos in enumerate(files):
if dis:
dis.progress_bar_show(i / len(files))
# track our copies
if key in SEEDVAULT_FIELDS:
SettingsObject.master_sv_data[key] = value
if self._slot_is_blank(pos, taste):
# unlikely case, but easy to handle
continue
def master_get(self, kn, default=None):
# Read a value from master seed's settings, perhaps from within context of tmp seed
from glob import settings as self
yield pos, taste
if not SettingsObject.master_nvram_key:
# simple, we are already on master seed
return self.get(kn, default)
# LIMITATION: only supporting a few values we know we will need
assert kn in SEEDVAULT_FIELDS
res = SettingsObject.master_sv_data.get(kn, default)
if res is None:
return default
return res
def load(self, dis=None):
# Search all slots for any we can read, decrypt that,
# and pick the newest one (in unlikely case of dups)
# reset
self.current.clear()
self.overrides.clear()
self.my_pos = None
self.is_dirty = 0
self.capacity = 0
nonempty = set()
for pos, taste in self._nonempty_slots(dis):
@ -356,10 +338,8 @@ class SettingsObject:
continue
# probably good, read it
aes = aes.cipher
json_data, expect, actual = self._read_slot(pos, aes)
try:
json_data, expect, actual = self._read_slot(pos, aes.cipher)
# verify checksum in last 32 bytes
assert expect == actual
@ -373,10 +353,8 @@ class SettingsObject:
# likely winner
self.current = d
self.my_pos = pos
#print("NV: data @ 0x%x w/ age=%d" % (pos, got_age))
else:
# stale data seen; clean it up.
#print("NV: cleanup @ 0x%x" % pos)
assert self.current['_age'] > 0
self._wipe_slot(pos)
@ -385,31 +363,18 @@ class SettingsObject:
# done, if we found something
if self.my_pos is not None:
#print("NV: load done")
return
return
# nothing found, use defaults
self.current = self.default_values()
# pick a (new) random home
self.my_pos = self.find_spot(-1)
#print("NV: empty")
if is_devmode:
self.current['chain'] = 'XTN'
if self.num_empty == NUM_SLOTS:
# Whole thing is blank. Bad for plausible deniability. Write 3 slots
# with white noise. They will be wasted space until it fills up.
for _ in range(4):
pos = self.find_spot(-1)
self._deny_slot(pos)
def get(self, kn, default=None):
if kn in self.overrides:
return self.overrides.get(kn)
else:
return self.current.get(kn, default)
return self.current.get(kn, default)
def changed(self):
self.is_dirty += 1
@ -425,24 +390,57 @@ class SettingsObject:
self.current[kn] = v
self.changed()
def put_volatile(self, kn, v):
self.overrides[kn] = v
set = put
def remove_key(self, kn):
self.current.pop(kn, None)
self.changed()
def merge_previous_active(self, previous):
if previous:
for k in KEEP_IF_BLANK_SETTINGS:
if previous.get(k, None) and not self.current.get(k, None):
self.current[k] = previous[k]
# nfc, usb, vidsk handling
self.update_interface_state()
def update_interface_state(self):
# update current settings based on actual state
# settings that need to be copied to any newly loaded settings
# as they describe state as is (a.k.a current state)
import pyb
from glob import NFC, VD
to_update = []
nfc_on = int(bool(NFC))
if nfc_on != self.get("nfc", 0):
to_update.append(("nfc", nfc_on))
vidsk_on = int(bool(VD))
vidsk_setting = self.get("vidsk", 0)
if vidsk_on != vidsk_setting:
# state no2 is auto
if not (vidsk_on and (vidsk_setting == 2)):
to_update.append(("vidsk", vidsk_on))
usb_on = int(bool(pyb.usb_mode()))
du_setting = self.get("du", 0)
if usb_on == du_setting:
to_update.append(("du", int(not du_setting)))
# actual setting
for k, v in to_update:
self.put(k, v)
def clear(self):
# could be just:
# self.current = {}
# but accomidating the simulator here
# but accommodating the simulator here
rk = [k for k in self.current if k[0] != '_']
for k in rk:
del self.current[k]
self.overrides.clear()
self.changed()
async def write_out(self):
@ -459,43 +457,26 @@ class SettingsObject:
call_later_ms(250, self.write_out)
def find_spot(self, not_here=0):
# search for a blank sector to use
# search for a blank sector to use
# - check randomly and pick first blank one (wear leveling, deniability)
# - we will write and then erase old slot
# - if "full", blow away a random one
if mk_num <= 3:
options = [s for s in SLOTS if s != not_here]
shuffle(options)
# on mk4, use the filesystem to see what's already taken
avail = set(SLOTS) - set(self._used_slots())
avail.discard(not_here)
buf = bytearray(4)
for pos in options:
if self._slot_is_blank(pos, buf):
# found a blank area
return pos
if avail:
return avail.pop()
# No-where to write! (probably a bug because we have lots of slots)
# ... so pick a random slot and kill what it had
victim = options[0]
else:
# on mk4, use the filesystem to see what's already taken
avail = set(SLOTS) - set(self._used_slots())
avail.discard(not_here)
if avail:
return avail.pop()
victim = randbelow(NUM_SLOTS)
#print("ERROR: nvram full")
# TODO destructive
victim = randbelow(NUM_SLOTS)
self._wipe_slot(victim)
return victim
def save(self):
# render as JSON, encrypt and write it.
self.current['_age'] = self.current.get('_age', 1) + 1
pos = self.find_spot(self.my_pos)
aes = self.get_aes(pos).cipher
@ -509,10 +490,6 @@ class SettingsObject:
self.my_pos = pos
self.is_dirty = 0
def merge(self, prev):
# take a dict of previous values and merge them into what we have
self.current.update(prev)
def blank(self):
# erase current copy of values in nvram; older ones may exist still
# - use when clearing the seed value
@ -522,9 +499,7 @@ class SettingsObject:
# act blank too, just in case.
self.current.clear()
self.overrides.clear()
self.is_dirty = 0
self.capacity = 0
@staticmethod
def default_values():

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,6 +114,7 @@ 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

@ -3,14 +3,17 @@
#
# paper.py - generate paper wallets, based on random values (not linked to wallet)
#
import ujson
import ujson, ngu, chains
from ubinascii import hexlify as b2a_hex
from utils import imported
from public_constants import AF_CLASSIC, AF_P2WPKH
from auth import write_sig_file
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2TR
from ux import ux_show_story, ux_dramatic_pause
from files import CardSlot, CardMissingError, needs_microsd
from actions import file_picker
from menu import MenuSystem, MenuItem
from stash import blank_object
background_msg = '''\
Coldcard will pick a random private key (which has no relation to your seed words), \
@ -29,10 +32,6 @@ 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
@ -51,6 +50,12 @@ 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('Pick PDF template to use, or X for none.',
@ -63,22 +68,17 @@ class PaperWalletMaker:
def addr_format_chooser(self, *a):
# simple bool choice
def set(idx, text):
self.is_segwit = bool(idx)
self.is_segwit = idx == 1
self.is_taproot = idx == 2
self.update_menu()
return int(self.is_segwit), ['Classic', 'Segwit/Bech32'], set
@staticmethod
def can_do_qr():
import version
return version.has_fatram
return self.atype()[0], ['Classic P2PKH', 'Segwit P2WPKH', 'Taproot P2TR'], 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, predicate=self.can_do_qr),
MenuItem('Classic Address' if not self.is_segwit else 'Segwit Address',
chooser=self.addr_format_chooser),
f=self.pick_template),
MenuItem(self.atype()[1], chooser=self.addr_format_chooser),
MenuItem('Use Dice', f=self.use_dice),
MenuItem('GENERATE WALLET', f=self.doit),
], keep_position=True)
@ -88,12 +88,6 @@ class PaperWalletMaker:
from glob import dis, VD
try:
import ngu
from auth import write_sig_file
from chains import current_chain
from serializations import hash160
from stash import blank_object
if not have_key:
# get some random bytes
await ux_dramatic_pause("Picking key...", 2)
@ -110,27 +104,27 @@ class PaperWalletMaker:
dis.fullscreen("Rendering...")
# make payment address
digest = hash160(pubkey)
ch = current_chain()
ch = chains.current_chain()
if self.is_segwit:
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
af = AF_P2WPKH
elif self.is_taproot:
af = AF_P2TR
pubkey = pubkey[1:]
else:
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
af = AF_CLASSIC
addr = ch.pubkey_to_address(pubkey, af)
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
if self.can_do_qr():
with imported('uqr') as uqr:
# make the QR's now, since it's slow
is_alnum = self.is_segwit
qr_addr = uqr.make(addr if not is_alnum else addr.upper(),
min_version=4, max_version=4,
encoding=(uqr.Mode_ALPHANUMERIC if is_alnum else 0))
with imported('uqr') as uqr:
# make the QR's now, since it's slow
is_alnum = self.is_segwit
qr_addr = uqr.make(addr if not is_alnum else addr.upper(),
min_version=4, max_version=4,
encoding=(uqr.Mode_ALPHANUMERIC if is_alnum else 0))
qr_wif = uqr.make(wif, min_version=4, max_version=4, encoding=uqr.Mode_BYTE)
else:
qr_addr = None
qr_wif = None
qr_wif = uqr.make(wif, min_version=4, max_version=4, encoding=uqr.Mode_BYTE)
# Use address as filename. clearly will be unique, but perhaps a bit
# awkward to work with.
@ -174,8 +168,10 @@ class PaperWalletMaker:
else:
nice_pdf = ''
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 = 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)
# Half-hearted attempt to cleanup secrets-contaminated memory
# - better would be force user to reboot
@ -195,7 +191,8 @@ class PaperWalletMaker:
story = "Done! Created file(s):\n\n%s" % nice_txt
if nice_pdf:
story += "\n\n%s" % nice_pdf
story += "\n\n%s" % nice_sig
if nice_sig:
story += "\n\n%s" % nice_sig
await ux_show_story(story)
async def use_dice(self, *a):
@ -224,10 +221,17 @@ class PaperWalletMaker:
fp.write('Bitcoin Core command:\n\n')
# new hotness: output descriptors
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 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)
if qr_addr and qr_wif:
fp.write('\n\n--- QR Codes --- (requires UTF-8, unicode, white background)\n\n\n\n')
@ -295,7 +299,8 @@ class PaperWalletMaker:
async def make_paper_wallet(*a):
msg = background_msg.format(can_qr=('\nIf you have a special PDF template file, it can also make a pretty version of the same data.' if PaperWalletMaker.can_do_qr() else ''))
msg = background_msg.format(can_qr='\nIf you have a special PDF template file, '
'it can also make a pretty version of the same data.')
if await ux_show_story(msg) != 'y':
return

View File

@ -2,8 +2,8 @@
#
# pincodes.py - manage PIN code (which map to wallet seeds)
#
import ustruct, ckcc, version
from ubinascii import hexlify as b2a_hex
import ustruct, ckcc, version, chains, stash
# from ubinascii import hexlify as b2a_hex
from callgate import enter_dfu
from bip39 import wordlist_en
@ -100,6 +100,12 @@ PIN_ATTEMPT_SIZE = const(248+32)
# small cache of pin-prefix to words, for 608a based systems
_word_cache = []
def retry_ae_fail(*args):
err = ckcc.gate(*args)
if err == -106: # AE_FAIL
err = ckcc.gate(*args)
return err
class BootloaderError(RuntimeError):
pass
@ -123,8 +129,7 @@ class PinAttempt:
assert MAX_PIN_LEN == 32 # update FMT otherwise
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1, \
ustruct.calcsize(PIN_ATTEMPT_FMT)
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
# check for bricked system early
@ -149,7 +154,6 @@ class PinAttempt:
if new_secret is not None:
change_flags |= CHANGE_SECRET if not is_duress else CHANGE_DURESS_SECRET
assert len(new_secret) in (32, AE_SECRET_LEN)
import stash
stash.SensitiveValues.clear_cache()
else:
new_secret = bytes(AE_SECRET_LEN)
@ -254,7 +258,7 @@ class PinAttempt:
#print("> tx: %s" % b2a_hex(buf))
err = ckcc.gate(18, buf, method_num)
err = retry_ae_fail(18, buf, method_num)
#print("[%d] rx: %s" % (err, b2a_hex(buf)))
@ -319,7 +323,7 @@ class PinAttempt:
return bool(self.state_flags & PA_SUCCESSFUL)
def is_secret_blank(self):
assert self.state_flags & PA_SUCCESSFUL
assert self.is_successful()
return bool(self.state_flags & PA_ZERO_SECRET)
# Mk1/2/3 concepts, not used in Mk4
@ -362,7 +366,8 @@ class PinAttempt:
def change(self, **kws):
# change various values, stored in secure element
if self.tmp_value: return
if not kws.pop("tmp_lockdown", False):
if self.tmp_value: return
self.roundtrip(3, **kws)
@ -370,8 +375,8 @@ class PinAttempt:
# - call new_main_secret() when main secret changes!
# - is_secret_blank and is_successful may be wrong now, re-login to get again
def fetch(self, duress_pin=None, spare_num=0):
if self.tmp_value:
def fetch(self, duress_pin=None, spare_num=0, bypass_tmp=False):
if self.tmp_value and not bypass_tmp:
# must make a copy here, and must be mutable instance so not reused
if spare_num:
return bytearray(AE_SECRET_LEN)
@ -391,15 +396,8 @@ class PinAttempt:
if self.tmp_value:
return bytes(AE_LONG_SECRET_LEN)
if version.mk_num < 4:
secret = b''
for n in range(13):
secret += self.roundtrip(6, ls_offset=n)[0:32]
return secret
else:
# faster method for Mk4
return self.roundtrip(8, after_buf=bytes(AE_LONG_SECRET_LEN))
# faster method for Mk4
return self.roundtrip(8, after_buf=bytes(AE_LONG_SECRET_LEN))
def ls_change(self, new_long_secret):
# set the "long secret"
@ -421,45 +419,101 @@ class PinAttempt:
self.roundtrip(7, fw_upgrade=(start, length))
# not-reached
def new_main_secret(self, raw_secret, chain=None):
def new_main_secret(self, raw_secret=None, chain=None, bip39pw='', blank=False,
target_nvram_key=None):
# 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 nvstore
from glob import settings
import stash
stash.SensitiveValues.clear_cache()
bypass_tmp = False
stash.bip39_passphrase = bool(bip39pw)
# capture values we have already
old_values = dict(settings.current)
settings.set_key(raw_secret)
settings.load()
if chain is None:
chain = chains.get_chain(old_values.get("chain", None))
# merge in settings, including what chain to use, timeout, etc.
settings.merge(old_values)
if raw_secret is None:
assert pa.tmp_value
bypass_tmp = True
pa.tmp_value = None
if blank:
# wipe current ephemeral secret settings slot
settings.blank()
old_values = None
else:
if target_nvram_key is None:
settings.set_key(raw_secret)
else:
# we already have hashed nvram key calculated
# from self.tmp_secret - use it
settings.nvram_key = target_nvram_key
settings.load()
# Recalculate xfp/xpub values (depends both on secret and chain)
with stash.SensitiveValues(raw_secret) as sv:
if chain is not None:
sv.chain = chain
sv.capture_xpub()
try:
with stash.SensitiveValues(raw_secret, bypass_tmp=bypass_tmp) as sv:
if chain is not None:
sv.chain = chain
# does not call settings.save() but caller should!
if raw_secret is None:
# restore to main wallet's settings
settings.return_to_master_seed()
else:
sv.capture_xpub()
def tmp_secret(self, encoded, chain=None):
settings.merge_previous_active(old_values)
except stash.ZeroSecretException:
# secret is zero - using ephemeral secrets in CC
# with no se2 secret
settings.nvram_key = b'\0'*32
settings.load()
self.state_flags |= PA_ZERO_SECRET
def tmp_secret(self, encoded, chain=None, bip39pw=''):
# Use indicated secret and stop using the SE; operate like this until reboot
self.tmp_value = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
from glob import settings
from nvstore import SettingsObject
val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded)))
if self.tmp_value == val:
# noop - already enabled
return False, "Temporary master key already in use."
target_nvram_key = None
if encoded is not None:
# disallow using master seed as temporary
master_err = "Cannot use master seed as temporary."
target_nvram_key = settings.hash_key(encoded)
if SettingsObject.master_nvram_key:
assert self.tmp_value
if target_nvram_key == SettingsObject.master_nvram_key:
return False, master_err
else:
if target_nvram_key == settings.nvram_key:
return False, master_err
if not self.tmp_value:
# leaving from master seed, might capture some useful values
settings.leaving_master_seed()
self.tmp_value = val
# We're no longer blank. hard to say about duress secret and stuff tho
self.state_flags = PA_SUCCESSFUL
# Clear bip-39 secret, not applicable anymore.
import stash
stash.bip39_passphrase = ''
stash.SensitiveValues.clear_cache()
# Copies system settings to new encrypted-key value, calculates
# XFP, XPUB and saves into that, and starts using them.
self.new_main_secret(self.tmp_value, chain=chain)
self.new_main_secret(self.tmp_value, chain=chain, bip39pw=bip39pw,
target_nvram_key=target_nvram_key)
return True, None
def trick_request(self, method_num, data):
# send/recv a trick-pin related request (mk4 only)
@ -478,9 +532,6 @@ class PinAttempt:
def is_deltamode(self):
# (mk4 only) are we operating w/ a slightly wrong PIN code?
if version.mk_num < 4:
return False
from trick_pins import TC_DELTA_MODE
return bool(self.delay_required & TC_DELTA_MODE)

File diff suppressed because it is too large Load Diff

View File

@ -55,6 +55,7 @@ class PSRAMWrapper:
def wipe_all(self):
# works, but code in bootrom is much faster and better (rng values used)
from glob import dis
z = bytes(16384)
for pos in range(0, self.length, len(z)):

View File

@ -2,111 +2,190 @@
#
# pwsave.py - Save bip39 passphrases into encrypted file on MicroSD (if desired)
#
import sys, stash, ujson, os, ngu, pyb
import stash, ujson, ngu, pyb, os, aes256ctr
from files import CardSlot, CardMissingError, needs_microsd
from ux import ux_dramatic_pause, ux_confirm, ux_show_story
from utils import xfp2str, problem_file_line
from menu import MenuItem, MenuSystem
class PassphraseSaver:
# Encrypts BIP-39 passphrase very carefully, and appends
# to a file on MicroSD card. Order is preserved.
# AES-256 CTR with key=SHA256(SHA256(salt + derived key off master + salt))
# where: salt=sha256(microSD serial # details)
def __init__(self):
self.key = None
def filename(self, card):
@staticmethod
def filename(card):
# Construct actual filename to use.
# - some very minor obscurity, but we aren't relying on that.
return card.get_sd_root() + '/.tmp.tmp'
def _calc_key(self, card, force=False):
# calculate the key to be used.
if not force and getattr(self, 'key', None):
if not force and self.key:
return
try:
salt = card.get_id_hash()
salt = card.get_id_hash()
with stash.SensitiveValues(bypass_pw=True) as sv:
self.key = bytearray(sv.encryption_key(salt))
except:
self.key = None
with stash.SensitiveValues(bypass_tmp=True) as sv:
self.key = bytearray(sv.encryption_key(salt))
def _read(self, card):
# Return a list of saved passphrases, or empty list if fail.
# Fail silently in all cases. Expect to see lots of noise here.
decrypt = ngu.aes.CTR(self.key)
assert self.key
decrypt = aes256ctr.new(self.key)
try:
fname = self.filename(card)
msg = open(fname, 'rb').read()
with open(fname, 'rb') as f:
msg = f.read()
txt = decrypt.cipher(msg)
return ujson.loads(txt)
except OSError:
#print('missing? ' + fname)
return []
except:
return []
async def _save(self, card, data):
assert self.key
encrypt = ngu.aes.CTR(self.key)
msg = encrypt.cipher(ujson.dumps(data))
async def append(self, xfp, bip39pw):
# encrypt and save; always appends.
from glob import dis
# overwrites whatever already there
with open(self.filename(card), 'wb') as fd:
fd.write(msg)
while 1:
dis.fullscreen('Saving...')
async def delete(self, idx):
with CardSlot() as card:
self._calc_key(card)
data = self._read(card)
try:
with CardSlot() as card:
self._calc_key(card)
del data[idx]
except IndexError: pass
data = self._read(card) if self.key else []
await self._save(card, data)
if not data:
return True # is empty
data.append(dict(xfp=xfp, pw=bip39pw))
async def append(self, xfp, bip39pw):
from glob import dis
dis.fullscreen('Reading...')
with CardSlot() as card:
self._calc_key(card)
data = self._read(card)
encrypt = ngu.aes.CTR(self.key)
to_add = dict(xfp=xfp, pw=bip39pw)
if to_add not in data:
dis.fullscreen('Saving...')
data.append(to_add)
await self._save(card, data)
msg = encrypt.cipher(ujson.dumps(data))
with open(self.filename(card), 'wb') as fd:
fd.write(msg)
class PassphraseSaverMenu(MenuSystem):
await ux_dramatic_pause("Saved.", 1)
return
def update_contents(self):
tmp = PassphraseSaverMenu.construct()
self.replace_items(tmp)
except CardMissingError:
ch = await needs_microsd()
if ch == 'x': # undocumented, but needs escape route
break
def make_menu(self):
from menu import MenuItem, MenuSystem
@staticmethod
async def apply(menu, idx, item):
# apply the password immediately and drop them at top menu
from actions import goto_top_menu
from ux import ux_show_story
from seed import set_bip39_passphrase
from pincodes import pa
from glob import settings
# Very quick check for card not present case.
if not pyb.SDCard().present():
return None
bypass_tmp = True
pw, expect_xfp = item.arg
if pa.tmp_value and settings.get("words", True):
xfp = settings.get("xfp", 0)
title = "[%s]" % xfp2str(xfp)
msg = (
"Temporary seed is active. Press (1)"
" to add passphrase to the current active"
" temporary seed."
)
escape = "1x"
if settings.master_get("words", True):
escape += "y"
msg += " Press OK to add to master seed."
msg += "Press X to exit."
ch = await ux_show_story(msg, title=title, escape=escape,
strict_escape=True)
if ch == "x": return
if ch == '1':
bypass_tmp = False
applied = await set_bip39_passphrase(pw, bypass_tmp=bypass_tmp,
summarize_ux=False)
if not applied:
return
xfp = settings.get('xfp', 0)
# verification step
if xfp == expect_xfp:
# feedback that it worked
await ux_show_story("Passphrase restored.", title="[%s]" % xfp2str(xfp))
else:
got = xfp2str(xfp)
exp = xfp2str(expect_xfp)
await ux_show_story("XFP verification failed. Restored wallet XFP [%s] "
"does not match expected XFP [%s] from "
"saved passphrase file." % (got, exp))
return
goto_top_menu()
@staticmethod
async def delete_entry(menu, idx, item):
from ux import the_ux
from glob import dis
pw_saver, i = item.arg
if await ux_confirm("Delete saved passphrase?"):
dis.fullscreen("Wait...")
try:
is_empty = await pw_saver.delete(i)
the_ux.pop()
if not is_empty:
m = the_ux.top_of_stack()
m.update_contents()
else:
# remove .tmp.tmp file after last passphrase
# is deleted
with CardSlot() as card:
f_path = pw_saver.filename(card)
os.remove(f_path)
the_ux.pop()
m = the_ux.top_of_stack()
m.update_contents()
except CardMissingError:
await needs_microsd()
except Exception as e:
await ux_show_story(
title="ERROR",
msg='Delete failed!\n\n%s\n%s' % (e, problem_file_line(e))
)
@classmethod
def construct(cls):
# We have a list of xfp+pw fields. Make a menu.
# Read file, decrypt and make a menu to show; OR return None
# if any error hit.
try:
with CardSlot() as card:
pw_saver = PassphraseSaver()
with CardSlot() as card:
pw_saver._calc_key(card)
data = pw_saver._read(card)
self._calc_key(card)
if not self.key: return None
data = self._read(card)
if not data: return None
except CardMissingError:
# not an error: they just aren't using feature
return None
# We have a list of xfp+pw fields. Make a menu.
if not data: return None
# Challenge: we need to hint at which is which, but don't want to
# show the password on-screen.
@ -129,25 +208,16 @@ class PassphraseSaver:
# give up: show it all!
parts = [i for i,_ in pws]
async def doit(menu, idx, item):
# apply the password immediately and drop them at top menu
pw, expect_xfp = item.arg
set_bip39_passphrase(pw)
from glob import settings
from utils import xfp2str
xfp = settings.get('xfp')
# verification step; I don't see any way for this to go wrong
assert xfp == expect_xfp
# feedback that it worked
await ux_show_story("Passphrase restored.", title="[%s]" % xfp2str(xfp))
goto_top_menu()
return MenuSystem((MenuItem(label or '(empty)', f=doit, arg=pw) for pw, label in zip(pws, parts)))
items = []
for i, (pw, label) in enumerate(zip(pws, parts)):
xfp_ui = "[%s]" % xfp2str(pw[1])
submenu = MenuSystem([
MenuItem(xfp_ui),
MenuItem("Restore", f=cls.apply, arg=pw),
MenuItem("Delete", f=cls.delete_entry, arg=(pw_saver, i)),
])
items.append(MenuItem(label or "(empty)", menu=submenu))
return items
#
# Support for using MicroSD as second factor to the login PIN.
@ -207,6 +277,9 @@ class MicroSD2FA(PassphraseSaver):
except:
# die. wrong
import callgate
from glob import settings
settings.remove_key("sd2fa")
settings.save()
callgate.fast_wipe(silent=False)
# proceed w/o any notice
@ -251,13 +324,14 @@ class MicroSD2FA(PassphraseSaver):
data = dict(nonce=nonce)
encrypt = ngu.aes.CTR(self.key)
encrypt = aes256ctr.new(self.key)
msg = encrypt.cipher(ujson.dumps(data))
with open(self.filename(card), 'wb') as fd:
fd.write(msg)
# update setting as well
# TODO use general method that handles memory overflow
v.append(nonce)
settings.set('sd2fa', v)
settings.save()
@ -324,13 +398,15 @@ class MicroSD2FA(PassphraseSaver):
ok = cls.authorized_card_present(cls.get_nonces())
if ok:
await ux_show_story("Need a different MicroSD card. "
"This card would already be accepted.")
"This card would already be accepted.")
return
ctx = 'this card or one of the others' if count >= 1 else 'it'
ok = await ux_confirm("Add this card to authorized set? Going forward %s must be present during login process or the seed will be wiped!" % ctx)
ok = await ux_confirm("Add this card to authorized set? Going forward %s must be "
"present during login process or the seed will be wiped!" % ctx)
if not ok:
return
await cls().enroll()

View File

@ -2,11 +2,10 @@
#
# qrs.py - QR Display related UX
#
import framebuf, math, uqr
import framebuf, uqr
from ux import UserInteraction, ux_wait_keyup, the_ux
from utils import word_wrap
from version import has_fatram
from ubinascii import hexlify as b2a_hex
class QRDisplaySingle(UserInteraction):
# Show a single QR code for (typically) a list of addresses, or a single value.
@ -30,14 +29,14 @@ class QRDisplaySingle(UserInteraction):
# - inverted QR (black/white swap) still readable by scanners, altho wrong
if self.is_alnum:
# targeting 'alpha numeric' mode, nice and dense; caps only tho
enc = uqr.Mode_ALPHANUMERIC
enc = uqr.Mode_ALPHANUMERIC if not msg.isdigit() else uqr.Mode_NUMERIC
msg = msg.upper()
else:
# has to be 'binary' mode, altho shorter msg, typical 34-36
enc = uqr.Mode_BYTE
# can fail if not enough space in QR
self.qr_data = uqr.make(msg, min_version=3, max_version=11, encoding=enc)
self.qr_data = uqr.make(msg, min_version=2, max_version=11, encoding=enc)
def redraw(self):
# Redraw screen.

View File

@ -10,20 +10,21 @@
# - 'abandon' * 17 + 'agent'
# - 'abandon' * 11 + 'about'
#
import ngu, uctypes, bip39, random, stash, pyb
from menu import MenuItem, MenuSystem
from utils import xfp2str, parse_extended_key
import ngu, uctypes, bip39, random, version
from utils import xfp2str, parse_extended_key, swab32, pad_raw_secret, problem_file_line
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, show_qr_code
from ux import PressRelease, ux_input_numbers, ux_input_text
from pincodes import AE_SECRET_LEN, AE_LONG_SECRET_LEN
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm
from ux import PressRelease, ux_input_numbers, ux_input_text, show_qr_code
from actions import goto_top_menu
from stash import SecretStash, SensitiveValues
from stash import SecretStash
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from pwsave import PassphraseSaver
from pwsave import PassphraseSaver, PassphraseSaverMenu
from glob import settings, dis
from pincodes import pa
from nvstore import SettingsObject
from files import CardMissingError, needs_microsd, CardSlot
# seed words lengths we support: 24=>256 bits, and recommended
VALID_LENGTHS = (24, 18, 12)
@ -276,10 +277,8 @@ async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False)
# user can skip quiz for ephemeral secrets
msg += " There will be a test!"
if version.has_fatram:
escape = (escape or '') + '1'
extra += 'Press (1) to view as QR Code. '
escape = (escape or '') + '1'
extra += 'Press (1) to view as QR Code. '
if extra:
msg += '\n\n'
@ -294,6 +293,7 @@ async def show_words(words, prompt=None, escape=None, extra='', ephemeral=False)
return ch
async def add_dice_rolls(count, seed, judge_them, nwords=None, enforce=False):
from glob import dis
from display import FontTiny, FontLarge
@ -407,19 +407,90 @@ async def new_from_dice(nwords):
# send them to home menu, now with a wallet enabled
goto_top_menu(first_time=True)
async def set_ephemeral_seed(encoded, chain=None):
pa.tmp_secret(encoded, chain=chain)
dis.progress_bar_show(1)
xfp = settings.get("xfp", "")
if xfp:
xfp = "[" + xfp2str(xfp) + "]\n"
await ux_show_story("%sNew ephemeral master key in effect until next power down.\n\nIt is NOT stored anywhere." % xfp)
def in_seed_vault(encoded):
# Test if indicated xfp (or currently active XFP) is in the seed vault already.
seeds = settings.master_get("seeds", [])
if seeds:
ss = stash.SecretStash.storage_serialize(encoded)
if ss in [s[1] for s in seeds]:
return True
return False
async def set_ephemeral_seed_words(words):
async def add_seed_to_vault(encoded, meta=None):
if not settings.master_get("seedvault", False):
# seed vault disabled
return
if pa.is_secret_blank():
# do not save anything if no secrets yet
return
# do not offer to store secrets that are already in vault
if in_seed_vault(encoded):
return
main_xfp = settings.master_get("xfp", 0)
# parse encoded
_,_,node = SecretStash.decode(encoded)
new_xfp = swab32(node.my_fp())
new_xfp_str = xfp2str(new_xfp)
# do not offer to store main seed
if new_xfp == main_xfp:
return
seeds = settings.master_get("seeds", [])
xfp_ui = "[%s]" % new_xfp_str
story = ("Press (1) to "
"store temporary seed into Seed Vault. This way you can easily switch "
"to this secret and use it as temporary seed in future.\n\nPress OK "
"to continue without saving.")
ch = await ux_show_story(story, escape="1")
if ch != "1":
# didn't want to save
return
# Save it into master settings
seeds.append((new_xfp_str,
stash.SecretStash.storage_serialize(encoded),
xfp_ui,
meta))
settings.master_set("seeds", seeds)
await ux_show_story(xfp_ui + "\nSaved to Seed Vault")
return True
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
is_restore=False, meta=None):
if not is_restore:
await add_seed_to_vault(encoded, meta=meta)
dis.fullscreen("Wait...")
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
dis.progress_bar_show(1)
if not applied:
await ux_show_story(title="FAILED", msg=err_msg)
return
xfp = "[" + xfp2str(settings.get("xfp", 0)) + "]"
if summarize_ux:
await ux_show_story(title=xfp, msg="New temporary master key is in effect now.")
return applied
async def set_ephemeral_seed_words(words, meta):
dis.progress_bar_show(0.1)
encoded = seed_words_to_encoded_secret(words)
dis.progress_bar_show(0.5)
await set_ephemeral_seed(encoded)
await set_ephemeral_seed(encoded, meta=meta)
goto_top_menu()
async def ephemeral_seed_generate_from_dice(nwords):
@ -436,7 +507,7 @@ async def ephemeral_seed_generate_from_dice(nwords):
words = await approve_word_list(seed, nwords, ephemeral=True)
if words:
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words)
await set_ephemeral_seed_words(words, meta='Dice')
def generate_seed():
seed = random.bytes(32)
@ -455,12 +526,12 @@ async def make_new_wallet(nwords):
# send them to home menu, now with a wallet enabled
goto_top_menu(first_time=True)
async def ephemeral_seed_import_done_cb(words):
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words)
async def ephemeral_seed_import(nwords):
return WordNestMenu(nwords, done_cb=ephemeral_seed_import_done_cb)
async def import_done_cb(words):
await set_ephemeral_seed_words(words, meta='Imported')
return WordNestMenu(nwords, done_cb=import_done_cb)
async def ephemeral_seed_generate(nwords):
await ux_dramatic_pause('Generating...', 3)
@ -468,16 +539,17 @@ async def ephemeral_seed_generate(nwords):
words = await approve_word_list(seed, nwords, ephemeral=True)
if words:
dis.fullscreen("Applying...")
await set_ephemeral_seed_words(words)
await set_ephemeral_seed_words(words, meta="TRNG Words")
async def set_seed_extended_key(extended_key):
encoded, chain = xprv_to_encoded_secret(extended_key)
set_seed_value(encoded=encoded, chain=chain)
goto_top_menu()
goto_top_menu(first_time=True)
async def set_ephemeral_seed_extended_key(extended_key):
async def set_ephemeral_seed_extended_key(extended_key, meta=None):
encoded, chain = xprv_to_encoded_secret(extended_key)
await set_ephemeral_seed(encoded=encoded, chain=chain)
dis.fullscreen("Applying...")
await set_ephemeral_seed(encoded=encoded, chain=chain, meta=meta)
goto_top_menu()
async def approve_word_list(seed, nwords, ephemeral=False):
@ -577,52 +649,71 @@ def set_seed_value(words=None, encoded=None, chain=None):
finally:
dis.busy_bar(False)
def set_bip39_passphrase(pw):
# apply bip39 passphrase for now (volatile)
# takes a bit, so show something
from glob import dis
async def calc_bip39_passphrase(pw, bypass_tmp=False):
from glob import dis, settings
from pincodes import pa
dis.fullscreen("Working...")
# get xfp of parent reliably - cannot go to settings for this if in ephemeral
if pa.tmp_value:
with stash.SensitiveValues(bypass_tmp=bypass_tmp) as sv:
assert sv.mode == 'words', sv.mode
current_xfp = swab32(sv.node.my_fp())
else:
current_xfp = settings.get("xfp", 0)
# set passphrase
import stash
stash.bip39_passphrase = pw
# capture updated XFP
with stash.SensitiveValues() as sv:
with stash.SensitiveValues(bip39pw=pw, bypass_tmp=bypass_tmp) as sv:
# can't do it without original seed words (late, but caller has checked)
assert sv.mode == 'words'
assert sv.mode == 'words', sv.mode
nv = SecretStash.encode(xprv=sv.node)
xfp = swab32(sv.node.my_fp())
sv.capture_xpub()
return nv, xfp, current_xfp
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
nv, _, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp)
return await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
meta="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
# Might need to bounce the USB connection, because our pubkey has changed,
# altho if they have already picked a shared session key, no need, and
# would only affect MitM test, which has already been done.
async def remember_bip39_passphrase():
async def remember_ephemeral_seed():
# Compute current xprv and switch to using that as root secret.
import stash
from glob import dis
from nvstore import SettingsObject
from glob import dis, settings
dis.fullscreen('Check...')
# we are already at temporary seed, with correct
# settings in use - no need to call new_main_secret
# at the end
with stash.SensitiveValues() as sv:
nv = SecretStash.encode(xprv=sv.node)
# Important: won't write new XFP to nvram if pw still set
stash.bip39_passphrase = ''
# locking down temporary as new master
# old master settings are destroyed
dis.fullscreen("Cleanup...")
assert pa.tmp_value, "no tmp"
assert SettingsObject.master_nvram_key, "master nvram k"
old_master = SettingsObject(SettingsObject.master_nvram_key)
old_master.load()
old_master.blank()
del old_master
dis.fullscreen('Saving...')
pa.change(new_secret=nv)
pa.change(new_secret=pa.tmp_value, tmp_lockdown=True)
# re-read settings since key is now different
# - also captures xfp, xpub at this point
pa.new_main_secret(nv)
# not needed - will be handled by reboot
SettingsObject.master_nvram_key = None
SettingsObject.master_sv_data = {}
# check and reload secret
pa.reset()
pa.login()
async def restore_to_main_secret(preserve_settings=False):
# go back to main se2 secret
pa.new_main_secret(raw_secret=None, blank=not preserve_settings)
def clear_seed():
from glob import dis
import utime, callgate
@ -633,21 +724,9 @@ def clear_seed():
# clear settings associated with this key, since it will be no more
settings.blank()
if version.mk_num >= 4:
callgate.fast_wipe(True)
# NOT REACHED
else:
# save a blank secret (all zeros is a special case, detected by bootloader)
nv = bytes(AE_SECRET_LEN)
pa.change(new_secret=nv)
callgate.fast_wipe(True)
# NOT REACHED
if version.has_608:
# wipe the long secret too
nv = bytes(AE_LONG_SECRET_LEN)
pa.ls_change(nv)
dis.busy_bar(False)
dis.fullscreen('Reboot...')
utime.sleep(1)
# security: need to reboot to really be sure to clear the secrets from main memory.
@ -715,6 +794,222 @@ async def word_quiz(words, limited=None, title='Word %d is?'):
return
async def make_seed_vault_menu(*a):
rv = SeedVaultMenu.construct()
return SeedVaultMenu(rv)
class SeedVaultMenu(MenuSystem):
@staticmethod
async def _set(menu, label, item):
from glob import dis
dis.fullscreen("Applying...")
xfp, encoded = item.arg
await set_ephemeral_seed(encoded, is_restore=True)
goto_top_menu()
@staticmethod
async def _remove(menu, label, item):
from glob import dis, settings
idx, xfp_str, encoded = item.arg
msg = ("Remove seed from seed vault and delete its "
"settings?\n\nPress OK to continue, press (1) to "
"only remove from seed vault and keep "
"encrypted settings for later use.\n\n"
"WARNING: Funds will be lost if wallet is"
" not backed-up elsewhere.")
ch = await ux_show_story(title="[" + xfp_str + "]", msg=msg, escape="1")
if ch == "x": return
dis.fullscreen("Saving...")
wipe_slot = (ch != "1")
tmp_val = False
if pa.tmp_value:
tmp_val = True
if wipe_slot:
# are we deleting current active ephemeral wallet
# and its settings ?
# slot wiping
if tmp_val:
# wipe current settings
settings.blank()
pa.tmp_value = False
settings.return_to_master_seed()
else:
# in main settings
xs = SettingsObject()
xs.set_key(encoded)
xs.load()
xs.blank()
del xs
# CAUTION: will get shadow copy if in tmp seed mode already
seeds = settings.master_get("seeds", [])
try:
del seeds[idx]
except IndexError:
pass
# need to load and work on master secrets, will be slow if on tmp seed
settings.master_set("seeds", seeds)
if tmp_val and wipe_slot:
goto_top_menu()
# pop menu stack
the_ux.pop()
m = the_ux.top_of_stack()
m.update_contents()
@staticmethod
async def _detail(menu, label, item):
xfp_str, encoded, name, meta = item.arg
# - first byte represents type of secret (internal encoding flag)
txt = SecretStash.summary(encoded[0])
detail = "Name:\n%s\n\nMaster XFP:\n%s\n\nOrigin:\n%s\n\nSecret Type:\n%s" \
% (name, xfp_str, meta, txt)
await ux_show_story(detail)
@staticmethod
async def _rename(menu, label, item):
# let them edit the name
from glob import dis
from ux import ux_input_text
idx, xfp_str = item.arg
seeds = settings.master_get("seeds", [])
chk_xfp, encoded, old_name, meta = seeds[idx]
assert chk_xfp == xfp_str
new_name = await ux_input_text(old_name, confirm_exit=False, max_len=40)
if not new_name:
return
dis.fullscreen("Saving...")
# save it
seeds[idx] = (chk_xfp, encoded, new_name, meta)
# need to load and work on master secrets, will be slow if on tmp seed
settings.master_set("seeds", seeds)
# update label in sub-menu
menu.items[0].label = new_name
menu.items[0].arg = menu.items[0].arg[0:2] + (new_name,) + menu.items[0].arg[3:]
# .. and name in parent menu too
parent = the_ux.parent_of(menu)
if parent:
parent.update_contents()
@staticmethod
async def _add_current_tmp(*a):
from pincodes import pa
assert pa.tmp_value
main_xfp = settings.master_get("xfp", 0)
new_xfp = settings.get("xfp", 0)
new_xfp_str = xfp2str(new_xfp)
# do not offer to store main seed
if new_xfp == main_xfp:
return
xfp_ui = "[%s]" % new_xfp_str
ch = await ux_show_story(title=xfp_ui, msg="Add to Seed Vault?")
if ch != "y":
return
seeds = settings.master_get("seeds", [])
# Save it into master settings
seeds.append((new_xfp_str,
stash.SecretStash.storage_serialize(pa.tmp_value),
xfp_ui,
"unknown origin"))
settings.master_set("seeds", seeds)
await ux_show_story(xfp_ui + "\nSaved to Seed Vault")
m = the_ux.top_of_stack()
m.update_contents()
@classmethod
def construct(cls):
# Dynamic menu with user-defined names of seeds shown
from glob import settings
from pincodes import pa
rv = []
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)
seeds = settings.master_get("seeds", [])
if not seeds:
rv.append(MenuItem('(none saved yet)'))
if pa.tmp_value:
rv.append(add_current_tmp)
rv.append(MenuItem("Temporary Seed", menu=make_ephemeral_seed_menu))
else:
tmp_in_sv = False
for i, (xfp_str, encoded, name, meta) in enumerate(seeds):
is_active = False
encoded = pad_raw_secret(encoded)
if encoded == pa.tmp_value:
is_active = tmp_in_sv = True
submenu = [
MenuItem(name, f=cls._detail, arg=(xfp_str, encoded, name, meta)),
MenuItem('Use This Seed', f=cls._set, arg=(xfp_str, encoded)),
MenuItem('Rename', f=cls._rename, arg=(i, xfp_str)),
MenuItem('Delete', f=cls._remove, arg=(i, xfp_str, encoded)),
]
if is_active:
submenu[1] = MenuItem("Seed In Use")
submenu[1].is_chosen = lambda: True
if pa.tmp_value and (not is_active):
# if different ephemeral wallet active
# DO NOT offer any modification api (rename/delete)
submenu = submenu[:2]
item = MenuItem('%d: %s' % (i+1, name), menu=MenuSystem(submenu))
if is_active:
item.is_chosen = lambda: True
rv.append(item)
if pa.tmp_value:
if seeds and (not tmp_in_sv):
# give em chance to store current active
rv.append(add_current_tmp)
from actions import restore_main_secret
rv.append(MenuItem("Restore Master", f=restore_main_secret))
return rv
def update_contents(self):
# Reconstruct the list of wallets on this dynamic menu, because
# we added or changed them and are showing that same menu again.
tmp = self.construct()
self.replace_items(tmp)
class EphemeralSeedMenu(MenuSystem):
@ -732,44 +1027,43 @@ class EphemeralSeedMenu(MenuSystem):
@classmethod
def construct(cls):
from glob import NFC, settings
from actions import nfc_recv_ephemeral, import_tapsigner_backup_file, import_xprv
from glob import NFC
from actions import nfc_recv_ephemeral, import_tapsigner_backup_file, import_xprv, restore_temporary
import_ephemeral_menu = [
MenuItem("24 Words", f=cls.ephemeral_seed_import, arg=24),
MenuItem("18 Words", f=cls.ephemeral_seed_import, arg=18),
MenuItem("12 Words", f=cls.ephemeral_seed_import, arg=12),
MenuItem("18 Words", f=cls.ephemeral_seed_import, arg=18),
MenuItem("24 Words", f=cls.ephemeral_seed_import, arg=24),
MenuItem("Import via NFC", f=nfc_recv_ephemeral, predicate=lambda: NFC is not None),
]
gen_ephemeral_menu = [
MenuItem("24 Words", f=cls.ephemeral_seed_generate, arg=24),
MenuItem("12 Words", f=cls.ephemeral_seed_generate, arg=12),
MenuItem("24 Word Dice Roll", f=cls.ephemeral_seed_generate_from_dice, arg=24),
MenuItem("24 Words", f=cls.ephemeral_seed_generate, arg=24),
MenuItem("12 Word Dice Roll", f=cls.ephemeral_seed_generate_from_dice, arg=12),
MenuItem("24 Word Dice Roll", f=cls.ephemeral_seed_generate_from_dice, arg=24),
]
rv = [
MenuItem("Generate Words", menu=gen_ephemeral_menu),
MenuItem("Import Words", menu=import_ephemeral_menu),
MenuItem("Import XPRV", f=import_xprv, arg=True), # ephemeral=True
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
MenuItem("Tapsigner Backup", f=import_tapsigner_backup_file, arg=True), # ephemeral=True
MenuItem("Coldcard Backup", f=restore_temporary),
]
if pa.tmp_value:
xfp = settings.get("xfp", "")
if xfp:
rv.insert(0, MenuItem("[%s]" % xfp2str(xfp)))
else:
rv.insert(0, MenuItem("[Active]"))
return rv
async def make_ephemeral_seed_menu(*a):
if not pa.tmp_value:
if (not pa.tmp_value) and (not settings.master_get("seedvault", False)):
# force a warning on them, unless they are already doing it.
ch = await ux_show_story(
"Ephemeral seed is a temporary secret stored solely in device RAM, persisted for only a single boot. "
"This defeats all of the benefits of Coldcard's secure element design."
"\n\nPress (4) to prove you read to the end of this message and accept all consequences.",
"Temporary seed is a secret completely separate "
"from the master seed, typically held in device RAM and "
"not persisted between reboots in the Secure Element. "
"Enable the Seed Vault feature to store these secrets longer-term."
"\n\nPress (4) to prove you read to the end"
" of this message and accept all consequences.",
title="WARNING",
escape="4"
)
@ -788,10 +1082,18 @@ class PassphraseMenu(MenuSystem):
# singleton (cls level) vars
done_cb = None
def __init__(self, done_cb=None, items=None):
def __init__(self):
global pp_sofar
pp_sofar = ''
items = self.construct()
super(PassphraseMenu, self).__init__(items)
def update_contents(self):
tmp = self.construct()
self.replace_items(tmp)
def construct(self):
items = [
# xxxxxxxxxxxxxxxx
MenuItem('Edit Phrase', f=self.view_edit_phrase),
@ -801,16 +1103,36 @@ class PassphraseMenu(MenuSystem):
MenuItem('APPLY', f=self.done_apply),
MenuItem('CANCEL', f=self.done_cancel),
]
# quick SD card check
if pyb.SDCard().present():
try:
with CardSlot() as card:
# check if passphrases file exists on SD
# if yes add menu item
if card.exists(PassphraseSaver.filename(card)):
items.insert(0, MenuItem('Restore Saved', menu=self.restore_saved))
except: pass
return items
@staticmethod
async def restore_saved(*a):
dis.fullscreen("Decrypting...")
try:
saved = PassphraseSaver().make_menu()
if saved:
items.insert(0, MenuItem('Restore Saved', menu=saved))
except:
# don't want bugs/corrupt files to make rest of menu inaccessible
pass
items = PassphraseSaverMenu.construct()
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story(title="Failure", msg=str(e) + problem_file_line(e))
return
super(PassphraseMenu, self).__init__(items)
if not items:
await ux_show_story("Nothing found")
return
return PassphraseSaverMenu(items)
def on_cancel(self):
# zip to cancel item when they fail to exit via X button
@ -821,12 +1143,15 @@ class PassphraseMenu(MenuSystem):
async def add_numbers(self, *a):
global pp_sofar
pp_sofar = await ux_input_numbers(pp_sofar, self.check_length)
pw = await ux_input_numbers(pp_sofar, self.check_length)
if pw is not None:
pp_sofar = pw
self.check_length()
async def empty_phrase(self, *a):
global pp_sofar
if pp_sofar and len(pp_sofar) >= 3:
if len(pp_sofar) >= 3:
if not await ux_confirm("Press OK to clear passphrase. X to cancel."):
return
@ -871,27 +1196,84 @@ class PassphraseMenu(MenuSystem):
goto_top_menu()
async def done_apply(self, *a):
# apply the passphrase.
# - important to work on empty string here too.
from stash import bip39_passphrase
old_pw = str(bip39_passphrase)
# apply the passphrase
import stash
from glob import settings
from pincodes import pa
set_bip39_passphrase(pp_sofar)
xfp = settings.get('xfp')
msg = '''Above is the master key fingerprint of the new wallet.
Press X to abort and keep editing passphrase, OK to use the new wallet, or 1 to use and save to MicroSD'''
ch = await ux_show_story(msg, title="[%s]" % xfp2str(xfp), escape='1')
if ch == 'x':
# go back!
set_bip39_passphrase(old_pw)
if not pp_sofar:
# empty string here - noop
return
mdata = None
tdata = None
try:
m_nv, m_xfp, m_parent_xfp = await calc_bip39_passphrase(pp_sofar,
bypass_tmp=True)
m_parent_xfp_str = xfp2str(m_parent_xfp)
m_xfp_str = xfp2str(m_xfp)
mdata = (
m_nv, m_xfp, m_xfp_str, m_parent_xfp_str,
"master seed [%s]" % m_parent_xfp_str,
"(1) master+pass:\n%s%s\n\n" % (m_parent_xfp_str, m_xfp_str),
)
except AssertionError: pass
if pa.tmp_value and settings.get("words", True):
# we have ephemeral seed - can add passphrase to it as it is word based
t_nv, t_xfp, t_parent_xfp = await calc_bip39_passphrase(pp_sofar,
bypass_tmp=False)
t_parent_xfp_str = xfp2str(t_parent_xfp)
t_xfp_str = xfp2str(t_xfp)
tdata = (
t_nv, t_xfp, t_xfp_str, t_parent_xfp_str,
"current active temporary seed [%s]" % t_parent_xfp_str,
"(2) tmp+pass:\n%s%s\n\n" % (t_parent_xfp_str, t_xfp_str),
)
if tdata is None and mdata is None:
# if master is not word based, temporary has to be, otherwise "Passphrase"
# not offered in menu
# should never be seen by user because flow.py::bip39_passphrase_active
await ux_show_story(title="FAILED", msg="Need word based secret")
return
tmp = False
if tdata and mdata:
ch = await ux_show_story(mdata[-1] + tdata[-1], escape='12x',
strict_escape=True, scrollbar=False)
if ch == "x": return # exit
if ch == "2":
tmp = True
elif tdata:
tmp = True
data = tdata if tmp else mdata
nv, xfp, xfp_str, parent_xfp_str, msg, _ = data
msg = ('Above is the master key fingerprint of the new wallet'
' created by adding passphrase to %s.'
' Press X to abort and keep editing passphrase,'
' OK to use the new wallet, (1) to use'
' and save to MicroSD') % msg
ch = await ux_show_story(msg, title="[%s]" % xfp_str, escape='1')
if ch == 'x':
return
await set_ephemeral_seed(nv, summarize_ux=False, bip39pw=pp_sofar,
meta="BIP-39 Passphrase on [%s]" % parent_xfp_str)
if ch == '1':
await PassphraseSaver().append(xfp, pp_sofar)
try:
await PassphraseSaver().append(xfp, pp_sofar)
except CardMissingError:
await needs_microsd()
except Exception as e:
await ux_show_story(
title="ERROR",
msg='Save failed!\n\n%s\n%s' % (e, problem_file_line(e))
)
goto_top_menu()

View File

@ -56,11 +56,7 @@ async def test_secure_element():
assert not get_is_bricked() # bricked already
# test right chips installed
if version.has_fatram:
assert version.has_608 # expect 608
else:
assert not version.has_608 # expect 508a
assert version.hw_label == 'mk2'
assert version.has_608 # expect 608
if ckcc.is_simulator(): return
@ -115,9 +111,6 @@ async def test_sd_active():
async def test_usb_light():
# Mk4's new USB activity light (right by connector)
if version.mk_num < 4: return
from machine import Pin
p = Pin('USB_ACTIVE', Pin.OUT)
@ -139,8 +132,6 @@ async def test_nfc():
await NFCHandler.selftest()
async def test_psram():
if not version.has_psram: return
from glob import PSRAM
from ustruct import pack
import ngu
@ -168,54 +159,6 @@ async def test_psram():
assert chk == rnd, "RB bad @ 0x%x" % pos
dis.progress_bar_show((PSRAM.length + pos) / test_len)
async def test_sflash():
if version.has_psram: return
dis.clear()
dis.text(None, 18, 'Serial Flash')
dis.show()
from sflash import SF
from ustruct import pack
import ngu
msize = 1024*1024
SF.chip_erase()
for phase in [0, 1]:
steps = 7*4
for i in range(steps):
dis.progress_bar(i/steps)
dis.show()
await sleep_ms(250)
if not SF.is_busy(): break
assert not SF.is_busy() # "didn't finish"
# leave chip blank
if phase == 1: break
buf = bytearray(32)
for addr in range(0, msize, 1024):
SF.read(addr, buf)
assert set(buf) == {255} # "not blank"
rnd = ngu.hash.sha256s(pack('I', addr))
SF.write(addr, rnd)
SF.wait_done()
SF.read(addr, buf)
assert buf == rnd # "write failed"
dis.progress_bar_show(addr/msize)
# check no aliasing, also right size part
for addr in range(0, msize, 1024):
expect = ngu.hash.sha256s(pack('I', addr))
SF.read(addr, buf)
assert buf == expect # "readback failed"
dis.progress_bar_show(addr/msize)
async def test_oled():
# all on/off tests
@ -306,7 +249,6 @@ async def start_selftest():
await test_oled()
await test_psram()
await test_nfc()
await test_sflash()
await test_microsd()
await test_numpad()
await test_secure_element()

View File

@ -16,7 +16,6 @@ ser_*, deser_*: functions that handle serialization/deserialization
"""
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
import ustruct as struct
import ngu
from opcodes import *
@ -30,13 +29,15 @@ hash160 = ngu.hash.hash160
def bytes_to_hex_str(s):
return str(b2a_hex(s), 'ascii')
SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2)
SIGHASH_SINGLE = const(3)
SIGHASH_DEFAULT = const(0) # in taproot meaning same as SIGHASH_ALL (over whole TX)
SIGHASH_ALL = const(1)
SIGHASH_NONE = const(2)
SIGHASH_SINGLE = const(3)
SIGHASH_ANYONECANPAY = const(0x80)
# list containing all flags that we support signing for
ALL_SIGHASH_FLAGS = [
SIGHASH_DEFAULT,
SIGHASH_ALL,
SIGHASH_NONE,
SIGHASH_SINGLE,
@ -56,14 +57,20 @@ def ser_compact_size(l):
else:
return struct.pack("<BQ", 255, l)
def deser_compact_size(f):
def deser_compact_size(f, ret_num_bytes=False):
nit = struct.unpack("<B", f.read(1))[0]
num_bytes = 1
if nit == 253:
nit = struct.unpack("<H", f.read(2))[0]
num_bytes += 2
elif nit == 254:
nit = struct.unpack("<I", f.read(4))[0]
num_bytes += 4
elif nit == 255:
nit = struct.unpack("<Q", f.read(8))[0]
num_bytes += 8
if ret_num_bytes:
return nit, num_bytes
return nit
def deser_string(f):
@ -367,6 +374,11 @@ class CTxOut(object):
# aka. P2WPKH
return 'p2pkh', self.scriptPubKey[2:2+20], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 81 and self.scriptPubKey[1] == 32:
# aka. P2TR
return 'p2tr', self.scriptPubKey[2:2+32], True
if len(self.scriptPubKey) == 34 and \
self.scriptPubKey[0] == 0 and self.scriptPubKey[1] == 32:
# aka. P2WSH
@ -384,7 +396,7 @@ class CTxOut(object):
# If this is reached, we do not understand the output well
# enough to allow the user to authorize the spend, so fail hard.
raise ValueError('scriptPubKey template fail: ' + b2a_hex(self.scriptPubKey))
raise ValueError('scriptPubKey template fail: ' + b2a_hex(self.scriptPubKey).decode())
def is_p2sh(self):
return len(self.scriptPubKey) == 23 and self.scriptPubKey[0] == 0xa9 \
@ -557,6 +569,7 @@ class CTransaction(object):
self.hash = b2a_hex(bytes(tmp[i] for i in range(len(tmp)-1, -1, -1)))
def is_valid(self):
COIN = 100000000
self.calc_sha256()
for tout in self.vout:
if tout.nValue < 0 or tout.nValue > 21000000 * COIN:

View File

@ -1,42 +1,25 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# sffile.py - file-like objects stored in SPI Flash (Mk1-3) or PSRAM (Mk4+)
# sffile.py - file-like objects stored in PSRAM (Mk4+) (used to be SPI Flash)
#
# - implements stream IO protoccol
# - does erasing for you
# - random read, sequential write
# - only a few of these are possible
# - the offset is the file name
# - (<Mk3) last 64k of memory reserved for settings
#
from uasyncio import sleep_ms
from uio import BytesIO
from uhashlib import sha256
from version import has_psram
if not has_psram:
# Use SPI flash chip
from sflash import SF
# Use PSRAM chip
from glob import PSRAM
blksize = 4
# this code works on large "blocks" defined by the chip as 64k
blksize = 65536
def PADOUT(n):
# rounds up
return (n + blksize - 1) & ~(blksize-1)
else:
# Use PSRAM chip
from glob import PSRAM
blksize = 4
PADOUT = lambda n: n
def ALIGN4(n):
return n & ~0x3
def ALIGN4(n):
return n & ~0x3
class SFFile:
def __init__(self, start, length=0, max_size=None, message=None, pre_erased=False):
if not pre_erased:
assert start % blksize == 0 # 'misaligned'
def __init__(self, start, length=0, max_size=None, message=None):
# Operate in PSRAM and pretend to be a filesystem's file
assert start % blksize == 0 # 'misaligned'
self.start = start
self.pos = 0
self.length = length # byte-wise length
@ -45,14 +28,13 @@ class SFFile:
if max_size != None:
# Write
self.max_size = PADOUT(max_size) if not pre_erased else max_size
self.max_size = max_size
self.readonly = False
self.checksum = sha256()
if has_psram:
# up to 3 bytes that haven't been written-out yet
self.runt = bytearray()
self._pos = 0
# up to 3 bytes that haven't been written-out yet
self.runt = bytearray()
self.wr_pos = 0
else:
# Read
self.readonly = True
@ -87,21 +69,10 @@ class SFFile:
async def erase(self):
# must be used by caller before writing any bytes
# - now just checks, used to be a slow erase cycle
assert not self.readonly
assert self.length == 0 # 'already wrote?'
if has_psram: return
for i in range(0, self.max_size, blksize):
SF.block_erase(self.start + i)
if i and self.message:
from glob import dis
dis.progress_bar_show(i/self.max_size)
# expect block erase to take up to 2 seconds
while SF.is_busy():
await sleep_ms(50)
assert self.length == 0 # 'already wrote?'
return
def __enter__(self):
if self.message:
@ -118,23 +89,17 @@ class SFFile:
return False
def wait_writable(self):
if has_psram: return
while SF.is_busy():
pass
def close(self):
# PSRAM might leave a little behind
if self.runt:
# write final runt, might be up to 3 bytes (padding w/ zeros)
assert len(self.runt) <= 3 # , 'rl=%d'%len(self.runt)
assert self._pos + len(self.runt) == self.pos
assert self.wr_pos + len(self.runt) == self.pos
self.runt.extend(bytes(4-len(self.runt)))
PSRAM.write(self.start + self._pos, self.runt)
PSRAM.write(self.start + self.wr_pos, self.runt)
self.runt = None
self._pos = self.pos
self.wr_pos = self.pos
def write(self, b):
# immediate write, no buffering
@ -144,58 +109,24 @@ class SFFile:
left = len(b)
if has_psram:
# Mk4: memory-mapped, but can only do word-aligned writes
self.checksum.update(b)
# PSRAM is memory-mapped, but can only do word-aligned writes!
self.checksum.update(b)
self.runt.extend(b)
here = ALIGN4(len(self.runt))
if here:
PSRAM.write(self.start + self._pos, self.runt[0:here])
self._pos += here
self.runt = self.runt[here:]
self.runt.extend(b)
here = ALIGN4(len(self.runt))
if here:
PSRAM.write(self.start + self.wr_pos, self.runt[0:here])
self.wr_pos += here
self.runt = self.runt[here:]
self.pos += left
self.length = self.pos
self.pos += left
self.length = self.pos
if self.message:
from glob import dis
dis.progress_sofar(self.pos, self.length)
if self.message:
from glob import dis
dis.progress_sofar(self.pos, self.length)
return left
else:
# must perform page-aligned (256) writes, but can start
# anywhere in the page, and can write just one byte
sofar = 0
while left:
if (self.pos + sofar) % 256 != 0:
# start is unaligned, do a partial write to align
assert sofar == 0 #, (sofar, (self.pos+sofar)) # can only happen on first page
runt = min(left, 256 - (self.pos % 256))
here = memoryview(b)[0:runt]
assert len(here) == runt
else:
# write full pages, or final runt
here = memoryview(b)[sofar:sofar+256]
assert 1 <= len(here) <= 256
self.wait_writable()
SF.write(self.start + self.pos + sofar, here)
left -= len(here)
sofar += len(here)
self.checksum.update(here)
assert left >= 0
assert sofar == len(b)
self.pos += sofar
self.length = self.pos
return sofar
return left
def read(self, ll=None):
if ll == 0:
@ -210,10 +141,13 @@ class SFFile:
return b''
rv = bytearray(ll)
if has_psram:
PSRAM.read(self.start + self.pos, rv)
else:
SF.read(self.start + self.pos, rv)
if self.runt and self.pos + ll > self.wr_pos:
# put the runt data into place, because we are about to read it
t = bytearray(self.runt)
t.extend(bytes(4-len(t)))
PSRAM.write(self.start + self.wr_pos, t)
PSRAM.read(self.start + self.pos, rv)
self.pos += ll
@ -227,10 +161,7 @@ class SFFile:
if actual <= 0:
return 0
if has_psram:
PSRAM.read(self.start + self.pos, b)
else:
SF.read(self.start + self.pos, b)
PSRAM.read(self.start + self.pos, b)
self.pos += actual
@ -252,9 +183,6 @@ class SizerFile(SFFile):
def __exit__(self, exc_type, exc_val, exc_tb):
return False
def wait_writable(self):
return
def write(self, b):
# immediate write, no buffering
assert self.pos == self.length # "can only append"

View File

@ -1,137 +0,0 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# sflash.py - SPI Flash on rev D and up boards. Simple serial SPI flash on SPI2 port.
#
# see also ../external/micropython/drivers/memory/spiflash.c
# but not using that, because:
# - not exposed as python objects
# - it wants to waste 4k on a buffer
#
# Layout for project:
# - 384k PSBT incoming (MAX_TXN_LEN)
# - 384k PSBT outgoing (MAX_TXN_LEN)
# - 128k nvram settings (32 slots of 4k each)
#
# During firmware updates, entire flash, starting at zero may be used.
#
import machine
from version import mk_num
CMD_WRSR = const(0x01)
CMD_WRITE = const(0x02)
CMD_READ = const(0x03)
CMD_FAST_READ = const(0x0b)
CMD_RDSR = const(0x05)
CMD_WREN = const(0x06)
CMD_RDCR = const(0x35)
CMD_RD_DEVID = const(0x9f)
CMD_SEC_ERASE = const(0x20) # 4k unit, 40-200ms
CMD_BLK_ERASE = const(0xd8) # 64k, 0.4 - 2s
CMD_CHIP_ERASE = const(0xc7) # 1MB, 3.5 - 6s
CMD_C4READ = const(0xeb)
class SPIFlash:
# must write with this page size granulatity
PAGE_SIZE = 256
# must erase with one of these size granulatity!
SECTOR_SIZE = 4096
BLOCK_SIZE = 65536
def __init__(self):
from machine import Pin
# chip can do 80Mhz, but very limited prescaler-only baudrate generation, so
# sysclk/2 or /4 will happen depending on Mk3 vs. 4
# - Mk4: 120Mhz => 60Mhz result (div 2)
# - Mk3: 80Mhz => 40Mhz result (div 2)
self.spi = machine.SPI(2, baudrate=80_000_000)
self.cs = Pin('SF_CS', Pin.OUT)
def cmd(self, cmd, addr=None, complete=True, pad=False):
if addr is not None:
buf = bytes([cmd, (addr>>16) & 0xff, (addr >> 8) & 0xff, addr & 0xff])
else:
buf = bytes([cmd])
if pad:
buf = buf + b'\0'
self.cs.low()
self.spi.write(buf)
if complete:
self.cs.high()
def read(self, address, buf, cmd=CMD_FAST_READ):
# random read (fast mode, because why wouldn't we?!)
self.cmd(cmd, address, complete=False, pad=True)
self.spi.readinto(buf)
self.cs.high()
def write(self, address, buf):
# 'page program', must already be erased
assert 1 <= len(buf) <= 256 # "max 256"
assert address & ~0xff == (address+len(buf)-1) & ~0xff # "page boundary"
self.cmd(CMD_WREN)
self.cmd(CMD_WRITE, address, complete=False)
self.spi.write(buf)
self.cs.high()
def read_reg(self, cmd, length=3):
# read register
rv = bytearray(length)
self.cmd(cmd, 0, complete=False)
self.spi.readinto(rv)
self.cs.high()
return rv
def is_busy(self):
# return status of WIP = Write In Progress bit
r = self.read_reg(CMD_RDSR, 1)
return bool(r[0] & 0x01)
def wait_done(self):
# wait until write done; could be fancier
while 1:
if not self.is_busy():
return
def chip_erase(self):
# can take up to 6 seconds, so poll is_busy()
self.cmd(CMD_WREN)
self.cmd(CMD_CHIP_ERASE)
def sector_erase(self, address):
# erase 4k. 40-200ms delay; poll is_busy()
assert address % 4096 == 0 # "not sector start"
self.cmd(CMD_WREN)
self.cmd(CMD_SEC_ERASE, address)
def block_erase(self, address):
# erase 64k at once
assert address % 65536 == 0 # "not block start"
self.cmd(CMD_WREN)
self.cmd(CMD_BLK_ERASE, address)
def wipe_most(self):
# erase everything except settings: takes 5 seconds at least
from glob import dis
assert mk_num <= 3 # obsolete in mk4
from nvstore import SLOTS
end = SLOTS[0]
for addr in range(0, end, self.BLOCK_SIZE):
self.block_erase(addr)
dis.progress_bar_show(addr/end)
while self.is_busy():
pass
# singleton
SF = SPIFlash()
# EOF

View File

@ -1,46 +0,0 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# sram2.py - Jam some larger, long-lived objects into the SRAM2 area, which isn't used enough.
#
# Cautions/Notes:
# - mpy heap does not include SRAM2, so doing manual memory alloc here.
# - top 8k reserved for bootloader, which will wipe it on each entry
# - top page of that is specially marked to cause reset if any attempt to change
# - 2k at bottom reserved for code in `flashbdev.c` to use as cache data for flash writing
# - keep this file in sync with simulated version
# - none of the above is true anymore on mk4
#
import uctypes, ckcc
from version import mk_num
if mk_num < 4:
# see stm32/COLDCARD/layout.ld where this is effectively defined
SRAM2_START = const(0x10000800)
SRAM2_LENGTH = const(0x5800)
_start = SRAM2_START
def _alloc(ln):
global _start
rv = uctypes.bytearray_at(_start, ln)
_start += ln
return rv
else:
# Mk4 has tons of memory, so just use it
def _alloc(ln):
return bytearray(ln)
nvstore_buf = _alloc(4096-32)
display_buf = _alloc(1024)
display2_buf = _alloc(1024)
usb_buf = _alloc(2048+12) # 2060 @ 0x10001be0
tmp_buf = _alloc(1024)
psbt_tmp256 = _alloc(256)
if mk_num < 4:
assert _start <= 0x10006000
# observed: about 22k on Mk2
ckcc.stack_limit(SRAM2_LENGTH - (_start - SRAM2_START))
# EOF

View File

@ -37,8 +37,7 @@ class SSD1306(framebuf.FrameBuffer):
#self.buffer = bytearray(self.pages * self.width)
from sram2 import display_buf
self.buffer = display_buf
self.buffer = bytearray(1024)
assert len(self.buffer) == self.pages * self.width
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)

View File

@ -12,8 +12,12 @@
#
import ngu, uctypes, gc, bip39, utime
from uhashlib import sha256
from pincodes import AE_SECRET_LEN
from utils import swab32, call_later_ms
from utils import swab32, call_later_ms, B2A
class ZeroSecretException(ValueError):
# raised when there is no secret or secret is zero
pass
def blank_object(item):
# Use/abuse uctypes to blank objects under python. Will likely
@ -49,7 +53,7 @@ class SecretStash:
@staticmethod
def encode(seed_phrase=None, master_secret=None, xprv=None):
nv = bytearray(AE_SECRET_LEN)
nv = bytearray(72) # AE_SECRET_LEN
if seed_phrase:
# typical: packed version of memonic phrase
@ -115,7 +119,7 @@ class SecretStash:
elif marker == 0x00:
# probably all zeros, which we don't normally store, and represents "no secret"
raise ValueError('actually zero secret')
raise ZeroSecretException
else:
# variable-length master secret for BIP-32
vlen = secret[0]
@ -127,8 +131,34 @@ class SecretStash:
return 'master', ms, hd
@staticmethod
def storage_serialize(secret):
# make it a JSON-compatible field
return B2A(bytes(secret).rstrip(b"\x00"))
@staticmethod
def summary(marker):
# decode enough to explain what we have in a text form
# - give us the first byte of the stored, encoded secret
if marker == 0x01:
# xprv => BIP-32 private key values
return 'xprv'
if marker & 0x80:
# seed phrase
ll = ((marker & 0x3) + 2) * 8
return '%d words' % len_to_numwords(ll)
if marker == 0x00:
# probably all zeros, which we don't normally store, and represents "no secret"
return 'zeros'
# variable-length master secret for BIP-32
return '%d bytes' % marker
# optional global value: user-supplied passphrase to salt BIP-39 seed process
bip39_passphrase = ''
# just a boolean flag from version 5.2.0
bip39_passphrase = False
CACHE_CHECK_RATE = const(10*1000) # 10 seconds
CACHE_MAX_LIFE = const(60*1000) # one minute
@ -136,16 +166,14 @@ CACHE_MAX_LIFE = const(60*1000) # one minute
class SensitiveValues:
# be a context manager, and holder of secrets in-memory
# class-level cache, key is bip39 pass
_cache = {}
# class-level cache
_cache_secret = None
_cache_used = None
def __init__(self, secret=None, bypass_pw=False):
def __init__(self, secret=None, bip39pw='', bypass_tmp=False):
self.spots = []
# backup during volatile bip39 encryption: do not use passphrase
self._bip39pw = '' if bypass_pw else str(bip39_passphrase)
self._bip39pw = bip39pw
if secret is not None:
# sometimes we already know the secret
@ -159,27 +187,23 @@ class SensitiveValues:
from pincodes import pa
if pa.is_secret_blank():
raise ValueError('no secrets yet')
raise ZeroSecretException
self.deltamode = pa.is_deltamode()
if self._bip39pw in self._cache:
# cache hit
if self._cache_secret and not bypass_tmp:
# they are using new BIP39 passphrase but we already have raw secret
self.secret = bytearray(self._cache_secret)
self.mode, r, n = self._cache[self._bip39pw]
self.raw = bytearray(r)
self.node = n.copy()
self.__class__._cache_used = utime.ticks_ms()
else:
if self._cache_secret:
# they are using new BIP39 passphrase but we already have raw secret
self.secret = bytearray(self._cache_secret)
else:
# slow: read from secure element(s)
self.secret = pa.fetch()
# slow: read from secure element(s)
self.secret = pa.fetch(bypass_tmp=bypass_tmp)
# slow: do bip39 key stretching (typically)
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
# slow: do bip39 key stretching (typically)
self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw)
if not bypass_tmp:
# DO NOT save to cache if we are bypassing tmp
# we mostly just need it for some specific
# operation after which we go back to tmp
self.save_to_cache()
self.spots.append(self.secret)
@ -197,12 +221,6 @@ class SensitiveValues:
# - will be called after 2 minutes of idle keypad
blank_object(cls._cache_secret)
cls._cache_secret = None
for _,raw,node in cls._cache.values():
blank_object(raw)
blank_object(node)
cls._cache.clear()
cls._cache_used = None
def save_to_cache(self):
@ -212,7 +230,6 @@ class SensitiveValues:
else:
assert SensitiveValues._cache_secret == self.secret
SensitiveValues._cache[self._bip39pw] = ( self.mode, bytearray(self.raw), self.node.copy() )
SensitiveValues._cache_used = utime.ticks_ms()
call_later_ms(CACHE_CHECK_RATE, self.cache_check)
@ -290,14 +307,8 @@ class SensitiveValues:
xfp = swab32(self.node.my_fp())
xpub = self.chain.serialize_public(self.node)
if self._bip39pw:
settings.put_volatile('xfp', xfp)
settings.put_volatile('xpub', xpub)
else:
settings.overrides.clear()
settings.put('xfp', xfp)
settings.put('xpub', xpub)
settings.put('xfp', xfp)
settings.put('xpub', xpub)
settings.put('chain', self.chain.ctype)
# calc num words in seed, or zero
@ -368,4 +379,13 @@ class SensitiveValues:
self.register(pk)
return pk
def encoded_secret(self):
# we do not support master as secret - only extended keys and mnemonics
if self.mode == "xprv":
nv = SecretStash.encode(xprv=self.node)
else:
assert self.mode == "words"
nv = SecretStash.encode(seed_phrase=self.raw)
return nv
# EOF

View File

@ -7,8 +7,7 @@
# - replaces old "duress wallet" and "brickme" features
# - changes require knowledge of real PIN code (it is checked)
#
import version, uctypes, errno, ngu, sys, ckcc, stash, bip39
from ubinascii import hexlify as b2a_hex
import uctypes, errno, ngu, sys, stash, bip39
from menu import MenuSystem, MenuItem
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_enter_number, the_ux, ux_aborted
from stash import SecretStash
@ -437,6 +436,8 @@ class TrickPinMenu(MenuSystem):
async def done_picking(self, item, parents):
# done picking/drilling down tree.
# - shows point-form summary and gets confirmation
from glob import dis
wants_wipe = (self.WillWipeMenu in parents)
self.WillWipeMenu = None # memory free
@ -477,10 +478,11 @@ class TrickPinMenu(MenuSystem):
if ch != 'y': return
# save it
dis.fullscreen("Saving...")
try:
bpin = self.proposed_pin.encode()
b, slot = tp.update_slot(bpin, new=True, tc_flags=flags,
tc_arg=tc_arg, secret=new_secret)
tp.update_slot(bpin, new=True, tc_flags=flags,
tc_arg=tc_arg, secret=new_secret)
await ux_dramatic_pause("Saved.", 1)
except BaseException as exc:
sys.print_exception(exc)
@ -512,7 +514,7 @@ class TrickPinMenu(MenuSystem):
have.remove(existing_pin)
if (new_pin == self.current_pin) or (new_pin in have):
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin);
await ux_show_story("That PIN (%s) is already in use. All PIN codes must be unique." % new_pin)
return
# check if we "forgot" this pin, and read it back if we did.
@ -555,7 +557,6 @@ class TrickPinMenu(MenuSystem):
StoryMenuItem('BIP-85 Wallet #2', b85, arg=dbase+2, flags=TC_WORD_WALLET),
StoryMenuItem('BIP-85 Wallet #3', b85, arg=dbase+3, flags=TC_WORD_WALLET),
StoryMenuItem('Legacy Wallet', "Uses duress wallet created on Mk3 Coldcard, using a fixed derivation.\n\nRecommended only for existing UTXO compatibility.", flags=TC_XPRV_WALLET),
StoryMenuItem('Blank Coldcard', "Look and act like a freshly wiped Coldcard", flags=TC_BLANK_WALLET),
]
self.WillWipeMenu = MenuSystem([
# xxxxxxxxxxxxxxxx
@ -586,7 +587,7 @@ class TrickPinMenu(MenuSystem):
StoryMenuItem('Wipe Seed', "Wipe the seed and maybe do more. See next menu.",
menu=self.WillWipeMenu),
StoryMenuItem('Duress Wallet', "Goes directly to a specific duress wallet. No side effects.", menu=DuressOptions),
StoryMenuItem('Login Countdown', "Pretends a login countdown timer (%s) is in effect but wipes seed first. Resets system at end of countdown or bricks it." % lgto_map[def_to].strip(),
StoryMenuItem('Login Countdown', "Pretends a login countdown timer (%s) is in effect. Can wipe seed or brick system or do nothing." % lgto_map[def_to].strip(),
menu=countdown_menu),
StoryMenuItem('Look Blank', "Look and act like a freshly- wiped Coldcard but don't affect actual seed.", flags=TC_BLANK_WALLET),
StoryMenuItem('Just Reboot', "Reboot when this PIN is entered. Doesn't do anything else.", flags=TC_REBOOT),
@ -634,7 +635,6 @@ setting) the Coldcard will always brick after 13 failed PIN attempts.''')
arg=num, flags=TC_WIPE|TC_FAKE_OUT),
StoryMenuItem('Brick Self', "Become a brick instantly and forever.", flags=TC_BRICK),
StoryMenuItem('Last Chance', "Wipe seed, then give one more try and then brick if wrong PIN.", arg=num, flags=TC_WIPE|TC_BRICK),
StoryMenuItem('Look Blank', "Look and act like a freshly- wiped Coldcard but don't affect actual seed.", arg=num, flags=TC_BLANK_WALLET),
StoryMenuItem('Just Reboot', "Reboot when this happens. Doesn't do anything else.", arg=num, flags=TC_REBOOT),
])
@ -750,25 +750,24 @@ normal operation.''')
# TC_BLANK_WALLET here would be nice, but no support working w/ fake empty secret
# emulate stash.py encoding
name = 'Duress #%d' % (arg % 10)
if flags & TC_XPRV_WALLET:
encoded = b'\x01' + slot.xdata[0:64]
name = 'Mk3 Duress'
elif flags & TC_WORD_WALLET and (arg // 1000 == 1):
encoded = b'\x82' + slot.xdata[0:32]
elif flags & TC_WORD_WALLET and (arg // 1000 == 2):
encoded = b'\x80' + slot.xdata[0:16]
else:
raise ValueError('f=0x%x a=%d' % (flags, arg))
raise ValueError #('f=0x%x a=%d' % (flags, arg))
from glob import dis
from seed import set_ephemeral_seed
from actions import goto_top_menu
# switch over to new secret!
dis.fullscreen("Applying...")
pa.tmp_secret(encoded)
tp.reload()
await ux_show_story("New master key in effect until next power down.")
from actions import goto_top_menu
await set_ephemeral_seed(encoded, meta=name)
goto_top_menu()
async def countdown_details(self, m, l, item):
@ -801,7 +800,7 @@ normal operation.''')
va = lgto_va[1:]
def set_it(idx, text):
new_val = va[idx+1]
new_val = va[idx]
# save it
try:
b, slot = tp.update_slot(pin.encode(), tc_flags=flags, tc_arg=new_val)
@ -905,10 +904,14 @@ class StoryMenuItem(MenuItem):
super().__init__(label, **kws)
async def activate(self, menu, idx):
from glob import dis
ch = await ux_show_story(self.story)
if ch == 'x':
return
dis.fullscreen('Wait...')
if getattr(self, 'next_menu', None):
# drill down more
return await super().activate(menu, idx)

View File

@ -10,7 +10,7 @@ from public_constants import STXN_FLAGS_MASK
from ustruct import pack, unpack_from
from ckcc import watchpoint, is_simulator
from utils import problem_file_line, call_later_ms
from version import has_fatram, is_devmode, has_psram, MAX_TXN_LEN, MAX_UPLOAD_LEN
from version import is_devmode, MAX_TXN_LEN, MAX_UPLOAD_LEN
from exceptions import FramingError, CCBusyError, HSMDenied, HSMCMDDisabled
# Unofficial, unpermissioned... numbers
@ -53,7 +53,7 @@ HSM_WHITELIST = frozenset({
'blkc', 'hsts', # report status values
'stok', 'smok', # completion check: sign txn or msg
'xpub', 'msck', # quick status checks
'p2sh', 'show', # limited by HSM policy
'p2sh', 'show', 'msas', # limited by HSM policy
'user', # auth HSM user, other user cmds not allowed
'gslr', # read storage locker; hsm mode only, limited usage
})
@ -90,7 +90,7 @@ def enable_usb():
else:
# subclass, protocol, max packet length, polling interval, report descriptor
hid_info = (0x0, 0x0, 64, 5, hid_descp)
classes = 'VCP+HID' if not has_psram else 'VCP+MSC+HID'
classes = 'VCP+MSC+HID'
pyb.usb_mode(classes, vid=COINKITE_VID, pid=CKCC_PID, hid=hid_info)
global handler
@ -129,8 +129,7 @@ class USBHandler:
# handle simulator
self.blockable = getattr(self.dev, 'pipe', self.dev)
from sram2 import usb_buf
self.msg = usb_buf
self.msg = bytearray(2048+12)
assert len(self.msg) == MAX_MSG_LEN
self.encrypted_req = False
@ -478,7 +477,7 @@ class USBHandler:
file_len, file_sha = unpack_from('<I32s', args)
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 100 < file_len <= (20*200), "badlen"
assert 100 < file_len <= (32*200), "badlen"
# Start an UX interaction, return immediately here
from auth import maybe_enroll_xpub
@ -486,6 +485,82 @@ 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, miniscript=True)
return None
if cmd == "msls":
# list all registered miniscript wallet names
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
wallets = [w.name for w in MiniScriptWallet.iter_wallets()]
import ujson
return b'asci' + ujson.dumps(wallets)
if cmd == "msdl":
# delete miniscript wallet by its name (unique id)
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
assert len(args) < 40, "len args"
for w in MiniScriptWallet.iter_wallets():
if w.name == str(args, 'ascii'):
break
else:
return b'err_Miniscript wallet not found'
from auth import maybe_delete_miniscript
maybe_delete_miniscript(w)
return None
if cmd == "msgt":
# takes name and returns descriptor + name json
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
assert len(args) < 40, "len args"
for w in MiniScriptWallet.iter_wallets():
if w.name == str(args, 'ascii'):
import ujson
return b'asci' + ujson.dumps({"name": w.name, "desc": w.to_string()})
return b'err_Miniscript wallet not found'
if cmd == "msas":
# get miniscript address based on int/ext index
assert self.encrypted_req, 'must encrypt'
if hsm_active and not hsm_active.approve_address_share(miniscript=True):
raise HSMDenied
from miniscript import MiniScriptWallet
change, idx, = unpack_from('<II', args)
assert change in (0,1), "change not bool"
assert 0 <= idx < (2 ** 31), "child idx"
name = args[8:]
msc = None
for w in MiniScriptWallet.iter_wallets():
if w.name == str(name, 'ascii'):
msc = w
break
else:
return b'err_Miniscript wallet not found'
from auth import start_show_miniscript_address
return b'asci' + start_show_miniscript_address(msc, change, idx)
if cmd == 'msck':
# Quick check to test if we have a wallet already installed.
from multisig import MultisigWallet
@ -547,10 +622,11 @@ class USBHandler:
if cmd == 'pass':
# bip39 passphrase provided, maybe use it if authorized
assert self.encrypted_req, 'must encrypt'
from flow import bip39_passphrase_active
from auth import start_bip39_passphrase
from glob import settings
assert settings.get('words', True), 'no seed'
assert bip39_passphrase_active(), 'no seed'
assert len(args) < 400, 'too long'
pw = str(args, 'utf8')
assert len(pw) < 100, 'too long'
@ -571,66 +647,64 @@ class USBHandler:
if cmd == 'bagi':
return self.handle_bag_number(args)
if has_fatram:
# HSM and user-related features only supported on larger-memory Mk3
# HSM and user-related features only supported on larger-memory Mk3
if cmd == 'hsms':
# HSM mode "start" -- requires user approval
if args:
file_len, file_sha = unpack_from('<I32s', args)
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 2 <= file_len <= (200*1000), "badlen"
else:
file_len = 0
if cmd == 'hsms':
# HSM mode "start" -- requires user approval
if args:
file_len, file_sha = unpack_from('<I32s', args)
if file_sha != self.file_checksum.digest():
return b'err_Checksum'
assert 2 <= file_len <= (200*1000), "badlen"
else:
file_len = 0
# Start an UX interaction but return (mostly) immediately here
from hsm_ux import start_hsm_approval
await start_hsm_approval(sf_len=file_len, usb_mode=True)
# Start an UX interaction but return (mostly) immediately here
from hsm_ux import start_hsm_approval
await start_hsm_approval(sf_len=file_len, usb_mode=True)
return None
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':
# get the value held in the Storage Locker
assert hsm_active, 'need hsm'
return b'biny' + hsm_active.fetch_storage_locker()
# User Mgmt
if cmd == 'nwur': # new user
from users import Users
auth_mode, ul, sl = unpack_from('<BBB', args)
username = bytes(args[3:3+ul]).decode('ascii')
secret = bytes(args[3+ul:3+ul+sl])
return b'asci' + Users.create(username, auth_mode, secret).encode('ascii')
if cmd == 'rmur': # delete user
from users import Users
ul, = unpack_from('<B', args)
username = bytes(args[1:1+ul]).decode('ascii')
return Users.delete(username)
if cmd == 'user': # auth user (HSM mode)
from users import Users
totp_time, ul, tl = unpack_from('<IBB', args)
username = bytes(args[6:6+ul]).decode('ascii')
token = bytes(args[6+ul:6+ul+tl])
if hsm_active:
# just queues these details, can't be checked until PSBT on-hand
hsm_active.usb_auth_user(username, token, totp_time)
return None
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':
# get the value held in the Storage Locker
assert hsm_active, 'need hsm'
return b'biny' + hsm_active.fetch_storage_locker()
# User Mgmt
if cmd == 'nwur': # new user
from users import Users
auth_mode, ul, sl = unpack_from('<BBB', args)
username = bytes(args[3:3+ul]).decode('ascii')
secret = bytes(args[3+ul:3+ul+sl])
return b'asci' + Users.create(username, auth_mode, secret).encode('ascii')
if cmd == 'rmur': # delete user
from users import Users
ul, = unpack_from('<B', args)
username = bytes(args[1:1+ul]).decode('ascii')
return Users.delete(username)
if cmd == 'user': # auth user (HSM mode)
from users import Users
totp_time, ul, tl = unpack_from('<IBB', args)
username = bytes(args[6:6+ul]).decode('ascii')
token = bytes(args[6+ul:6+ul+tl])
if hsm_active:
# just queues these details, can't be checked until PSBT on-hand
hsm_active.usb_auth_user(username, token, totp_time)
return None
else:
# dryrun/testing purposes: validate only, doesn't unlock nothing
return b'asci' + Users.auth_okay(username, token, totp_time).encode('ascii')
else:
# dryrun/testing purposes: validate only, doesn't unlock nothing
return b'asci' + Users.auth_okay(username, token, totp_time).encode('ascii')
#print("USB garbage: %s +[%d]" % (cmd, len(args)))
@ -721,23 +795,16 @@ class USBHandler:
buf = memoryview(resp)[4:]
pos = (MAX_TXN_LEN * file_number) + offset
if has_psram:
from glob import PSRAM
PSRAM.read(pos, buf)
else:
from sflash import SF
SF.read(pos, buf)
from glob import PSRAM
PSRAM.read(pos, buf)
self.file_checksum.update(buf)
return resp
async def handle_upload(self, offset, total_size, data):
if has_psram:
from glob import PSRAM
else:
from sflash import SF
from glob import PSRAM
from glob import dis, hsm_active
from utils import check_firmware_hdr
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE, FW_HEADER_MAGIC
@ -760,15 +827,6 @@ class USBHandler:
if pos % 4096 == 0:
dis.fullscreen("Receiving...", offset/total_size)
if not has_psram:
# erase here
SF.sector_erase(pos)
# expect 10-22 ms delay here
await sleep_ms(12)
while SF.is_busy():
await sleep_ms(2)
# write up to 256 bytes
here = data[pos-offset:pos-offset+256]
self.file_checksum.update(here)
@ -802,15 +860,8 @@ class USBHandler:
# pretend we wrote it, so ckcc-protocol or whatever gives normal feedback
return offset
# write to SPI Flash / PSRAM
if has_psram:
PSRAM.write(pos, here)
else:
SF.write(pos, here)
# full page write: 0.6 to 3ms
while SF.is_busy():
await sleep_ms(1)
# write to PSRAM
PSRAM.write(pos, here)
if offset+len(data) >= total_size and not hsm_active:
# probably done

View File

@ -1,8 +1,5 @@
# items imported here may be useful to EVAL and EXEC commands, which test cases depend on.
import uio, sys, version, nvstore, glob, callgate
try:
from sflash import SF
except: pass
try:
import sim_display
except: pass

View File

@ -2,11 +2,14 @@
#
# utils.py - Misc utils. My favourite kind of source file.
#
import gc, sys, ustruct, ngu, chains, ure
import gc, sys, ustruct, chains, ure, uos, uio, time, aes256ctr
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64
from uhashlib import sha256
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR, MAX_PATH_DEPTH
from public_constants import AF_P2WSH, AF_P2WSH_P2SH
B2A = lambda x: str(b2a_hex(x), 'ascii')
@ -84,7 +87,6 @@ def pop_count(i):
def get_filesize(fn):
# like os.path.getsize()
import uos
try:
return uos.stat(fn)[6]
except OSError:
@ -177,8 +179,6 @@ def str2xfp(txt):
def problem_file_line(exc):
# return a string of just the filename.py and line number where
# an exception occured. Best used on AssertionError.
import uio, sys, ure
tmp = uio.StringIO()
sys.print_exception(exc, tmp)
lines = tmp.getvalue().split('\n')[-3:]
@ -208,8 +208,6 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# - assume 'm' prefix, so '34' becomes 'm/34', etc
# - do not assume /// is m/0/0/0
# - if allow_star, then final position can be * or *' (wildcard)
import ure
from public_constants import MAX_PATH_DEPTH
try:
s = str(bin_path, 'ascii').lower()
except UnicodeError:
@ -240,7 +238,7 @@ def cleanup_deriv_path(bin_path, allow_star=False):
# - star or star' can be last only (checked by regex above)
assert p == '*' or p == "*'", "bad wildcard"
continue
if p[-1] == "'":
if p[-1] in "'h":
p = p[0:-1]
try:
ip = int(p, 10)
@ -266,7 +264,7 @@ def str_to_keypath(xfp, path):
if i == 'm': continue
if not i: continue # trailing or duplicated slashes
if i[-1] == "'":
if i[-1] in "'h":
here = int(i[:-1]) | 0x80000000
else:
here = int(i)
@ -296,6 +294,13 @@ 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()
@ -387,17 +392,11 @@ def clean_shutdown(style=0):
settings.save_if_dirty()
try:
from glob import dis
from glob import dis, NFC
dis.fullscreen("Cleanup...")
if not version.has_psram:
from sflash import SF
SF.wipe_most()
else:
from glob import NFC
if NFC:
uasyncio.run(NFC.wipe(True))
if NFC:
uasyncio.run(NFC.wipe(True))
except: pass
@ -436,9 +435,7 @@ def word_wrap(ln, w):
def parse_addr_fmt_str(addr_fmt):
# accepts strings and also integers if already parsed
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC]:
if addr_fmt in [AF_P2WPKH_P2SH, AF_P2WPKH, AF_CLASSIC, AF_P2TR]:
return addr_fmt
addr_fmt = addr_fmt.lower()
@ -448,9 +445,10 @@ 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("Invalid address format: '%s'\n\n"
"Choose from p2pkh, p2wpkh, p2sh-p2wpkh." % addr_fmt)
raise ValueError("Unsupported address format: '%s'" % addr_fmt)
def parse_extended_key(ln, private=False):
@ -479,16 +477,16 @@ def parse_extended_key(ln, private=False):
return node, chain, addr_fmt
def import_prompt_builder(title):
def import_prompt_builder(title, no_nfc=False):
from glob import NFC, VD
prompt, escape = None, None
if NFC or VD:
if (NFC and (not no_nfc)) or VD:
prompt = "Press (1) to import %s from SD Card" % title
escape = "1"
if VD is not None:
prompt += ", press (2) to import from Virtual Disk"
escape += "2"
if NFC is not None:
if NFC is not None and not no_nfc:
prompt += ", press (3) to import via NFC"
escape += "3"
prompt += "."
@ -526,7 +524,7 @@ def chunk_writer(fd, body):
def decrypt_tapsigner_backup(backup_key, data):
try:
backup_key = a2b_hex(backup_key)
decrypt = ngu.aes.CTR(backup_key, bytes(16)) # IV 0
decrypt = aes256ctr.new(backup_key, bytes(16)) # IV 0
decrypted = decrypt.cipher(data).decode().strip()
# format of TAPSIGNER backup is known in advance
# extended private key is expected at the beginning of the first line
@ -536,4 +534,144 @@ def decrypt_tapsigner_backup(backup_key, data):
return decrypted.split("\n")
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"
}[addr_fmt]
def pad_raw_secret(raw_sec_str):
# Chip can hold 72-bytes as a secret
# every secret has 0th byte as marker
# then secret and padded to zero to AE_SECRET_LEN
from pincodes import AE_SECRET_LEN
raw = bytearray(AE_SECRET_LEN)
if len(raw_sec_str) % 2:
raw_sec_str += '0'
x = a2b_hex(raw_sec_str)
raw[0:len(x)] = x
return raw
def seconds2human_readable(s):
days = s // (3600 * 24)
hours = s % (3600 * 24) // 3600
minutes = (s % 3600) // 60
seconds = (s % 3600) % 60
msg = []
if days:
msg.append("%dd" % days)
if hours:
msg.append("%dh" % hours)
if minutes:
msg.append("%dm" % minutes)
if seconds:
msg.append("%ds" % seconds)
return " ".join(msg)
def datetime_from_timestamp(ts):
gm_t = time.gmtime(0)
if gm_t[0] == 1970:
# unix
epoch_sub = 0
elif gm_t[0] == 2000:
# stm32
epoch_sub = 946684800
else:
assert False
return time.gmtime(ts - epoch_sub)
def datetime_to_str(dt, fmt="%d-%02d-%02d %02d:%02d:%02d"):
y, mo, d, h, mi, s = dt[:6]
dts = fmt % (y, mo, d, h, mi, s)
return dts + " UTC"
def check_xpub(xfp, xpub, deriv, expect_chain, my_xfp, disable_checks=False):
# Shared code: consider an xpub for inclusion into a wallet
# return T if it's our own key and parsed details in form (xfp, deriv, xpub)
# - deriv can be None, and in very limited cases can recover derivation path
# - could enforce all same depth, and/or all depth >= 1, but
# seems like more restrictive than needed, so "m" is allowed
import stash
from public_constants import AF_P2SH
try:
# Note: addr fmt detected here via SLIP-132 isn't useful
node, chain, _ = parse_extended_key(xpub)
except:
raise AssertionError('unable to parse xpub')
try:
assert node.privkey() == None # 'no privkeys plz'
except ValueError:
pass
if expect_chain == "XRT":
# HACK but there is no difference extended_keys - just bech32 hrp
assert chain.ctype == "XTN"
else:
assert chain.ctype == expect_chain, 'wrong chain'
depth = node.depth()
if depth == 1:
if not xfp:
# allow a shortcut: zero/omit xfp => use observed parent value
xfp = swab32(node.parent_fp())
else:
# generally cannot check fingerprint values, but if we can, do so.
if not disable_checks:
assert swab32(node.parent_fp()) == xfp, 'xfp depth=1 wrong'
assert xfp, 'need fingerprint' # happens if bare xpub given
# In most cases, we cannot verify the derivation path because it's hardened
# and we know none of the private keys involved.
if depth == 1:
# but derivation is implied at depth==1
kn, is_hard = node.child_number()
if is_hard: kn |= 0x80000000
guess = keypath_to_str([kn], skip=0)
if deriv:
if not disable_checks:
assert guess == deriv, '%s != %s' % (guess, deriv)
else:
deriv = guess # reachable? doubt it
assert deriv, 'empty deriv' # or force to be 'm'?
assert deriv[0] == 'm'
# path length of derivation given needs to match xpub's depth
if not disable_checks:
p_len = deriv.count('/')
assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % (
p_len, depth, xfp2str(xfp))
if xfp == my_xfp:
# its 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
with stash.SensitiveValues() as sv:
chk_node = sv.derive_path(deriv)
assert node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
# serialize xpub w/ BIP-32 standard now.
# - this has effect of stripping SLIP-132 confusion away
return xfp == my_xfp, (xfp, deriv, chain.serialize_public(node, AF_P2SH))
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
# - 16 chars screen width
# - but 2 lost at left (menu arrow, corner arrow)
# - want to show not truncated on right side
return addr[0:6] + '' + addr[-6:]
# EOF

View File

@ -53,6 +53,12 @@ class UserInteraction:
old = self.stack.pop()
del old
def parent_of(self, child_ux):
for n, x in enumerate(self.stack):
if x == child_ux and n:
return self.stack[n-1]
return None
# Singleton. User interacts with this "menu" stack.
the_ux = UserInteraction()
@ -167,7 +173,8 @@ class PressRelease:
# (using FontSmall)
CH_PER_W = const(17)
async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_escape=False):
async def ux_show_story(msg, title=None, escape=None, sensitive=False,
strict_escape=False, scrollbar=True):
# show a big long string, and wait for XY to continue
# - returns character used to get out (X or Y)
# - can accept other chars to 'escape' as well.
@ -233,7 +240,10 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
y += 13
dis.scroll_bar(top / len(lines))
if scrollbar:
# help in cases when last char in a row hidden by scroll bar
dis.scroll_bar(top / len(lines))
dis.show()
# wait to do something
@ -358,9 +368,9 @@ async def show_qr_codes(addrs, is_alnum, start_n):
o = QRDisplaySingle(addrs, is_alnum, start_n, sidebar=None)
await o.interact_bare()
async def show_qr_code(data, is_alnum):
async def show_qr_code(data, is_alnum, msg=None):
from qrs import QRDisplaySingle
o = QRDisplaySingle([data], is_alnum)
o = QRDisplaySingle([data], is_alnum, sidebar=msg)
await o.interact_bare()
async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):

View File

@ -21,12 +21,12 @@ def decode_firmware_header(hdr):
def get_fw_header():
# located in our own flash
from sigheader import FLASH_HEADER_BASE, FLASH_HEADER_BASE_MK4, FW_HEADER_SIZE
from sigheader import FLASH_HEADER_BASE_MK4, FW_HEADER_SIZE
import uctypes
global mk_num
return uctypes.bytes_at(FLASH_HEADER_BASE_MK4 if mk_num == 4 else FLASH_HEADER_BASE,
return uctypes.bytes_at(FLASH_HEADER_BASE_MK4,
FW_HEADER_SIZE)
def get_mpy_version():
@ -58,36 +58,8 @@ def nfc_presence_check():
def get_is_devmode():
# what firmware signing key did we boot with? are we in dev mode?
if mk_num == 4:
# mk4: are we built differently?
import ckcc
return ckcc.is_debug_build()
from sigheader import RAM_HEADER_BASE, FWH_PK_NUM_OFFSET
import stm
# Important? Use the RAM version of this, not flash version!
kn = stm.mem32[RAM_HEADER_BASE + FWH_PK_NUM_OFFSET]
# For now, all keys are "production" except number zero, which will be made public
# - some other keys may be de-authorized and so on in the future
is_devmode = (kn == 0)
return is_devmode
def is_fresh_version():
# Did we just boot into a new firmware for the first time?
# - mk4+ does not use this approach, light will be solid green during upgrade
if mk_num >= 4: return False
from sigheader import RAM_BOOT_FLAGS, RBF_FRESH_VERSION
import stm
flags = stm.mem32[RAM_BOOT_FLAGS]
return bool(flags & RBF_FRESH_VERSION)
import ckcc
return ckcc.is_debug_build()
def serial_number():
@ -102,49 +74,25 @@ def serial_number():
def probe_system():
# run-once code to determine what hardware we are running on
global hw_label, has_608, has_fatram, is_factory_mode, is_devmode, has_psram
global has_se2, mk_num, has_nfc
global hw_label, has_608, is_factory_mode
global mk_num, has_nfc, is_devmode, is_edge
global MAX_UPLOAD_LEN, MAX_TXN_LEN
from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE
import ckcc, callgate, stm
from machine import Pin
import ckcc, callgate
# NOTE: mk1 not supported anymore.
# PA10 is pulled-down in Mark2, open in previous revs
#mark2 = (Pin('MARK2', Pin.IN, pull=Pin.PULL_UP).value() == 0)
hw_label = 'mk2'
has_fatram = False
has_psram = False
hw_label = 'mk4'
has_608 = True
has_se2 = False
has_nfc = False # hardware present; they might not be using it
mk_num = 2
has_nfc = nfc_presence_check() # hardware present; they might not be using it
mk_num = 4
cpuid = ckcc.get_cpu_id()
if cpuid == 0x461: # STM32L496RG6
hw_label = 'mk3'
has_fatram = True
mk_num = 3
elif cpuid == 0x470: # STM32L4S5VI
hw_label = 'mk4'
has_fatram = True
has_psram = True
has_se2 = True
mk_num = 4
has_nfc = nfc_presence_check()
else:
# mark 2
has_608 = callgate.has_608()
assert cpuid == 0x470 # STM32L4S5VI
# Boot loader needs to tell us stuff about how we were booted, sometimes:
# - did we just install a new version, for example (obsolete in mk4)
# - are we running in "factory mode" with flash un-secured?
if mk_num < 4:
is_factory_mode = bool(stm.mem32[RAM_BOOT_FLAGS] & RBF_FACTORY_MODE)
else:
is_factory_mode = callgate.get_factory_mode()
is_factory_mode = callgate.get_factory_mode()
bn = callgate.get_bag_number()
if bn:
@ -154,11 +102,13 @@ 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')
# increase size limits for mk4
if has_psram:
from public_constants import MAX_TXN_LEN_MK4, MAX_UPLOAD_LEN_MK4
MAX_UPLOAD_LEN = MAX_UPLOAD_LEN_MK4
MAX_TXN_LEN = MAX_TXN_LEN_MK4
from public_constants import MAX_TXN_LEN_MK4, MAX_UPLOAD_LEN_MK4
MAX_UPLOAD_LEN = MAX_UPLOAD_LEN_MK4
MAX_TXN_LEN = MAX_TXN_LEN_MK4
probe_system()

125
shared/wallet_base.py Normal file
View File

@ -0,0 +1,125 @@
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
import chains
from glob import settings
class WalletOutOfSpace(RuntimeError):
pass
class BaseWallet:
key_name = None
def __init__(self, chain_type=None):
self.storage_idx = -1
self.chain_type = chain_type or 'BTC'
@property
def chain(self):
return chains.get_chain(self.chain_type)
@classmethod
def none_setup_yet(cls, other_chain=False):
return '(none setup yet)' + ("*" if other_chain else "")
@classmethod
def is_correct_chain(cls, o, curr_chain):
if o[1] is None:
# mainnet
ch = "BTC"
else:
ch = o[1]
if ch == curr_chain.ctype:
return True
return False
@classmethod
def exists(cls):
# are there any wallets defined?
exists = False
exists_other_chain = False
c = chains.current_key_chain()
for o in settings.get(cls.key_name, []):
if cls.is_correct_chain(o, c):
exists = True
else:
exists_other_chain = True
return exists, exists_other_chain
@classmethod
def get_all(cls):
# return them all, as a generator
return cls.iter_wallets()
@classmethod
def iter_wallets(cls):
# - this is only place we should be searching this list, please!!
lst = settings.get(cls.key_name, [])
c = chains.current_key_chain()
for idx, rec in enumerate(lst):
if cls.is_correct_chain(rec, c):
yield cls.deserialize(rec, idx)
def serialize(self):
raise NotImplemented
@classmethod
def deserialize(cls, c, idx=-1):
raise NotImplemented
@classmethod
def get_by_idx(cls, nth):
# instance from index number (used in menu)
lst = settings.get(cls.key_name, [])
try:
obj = lst[nth]
except IndexError:
return None
return cls.deserialize(obj, nth)
def commit(self):
# data to save
# - important that this fails immediately when nvram overflows
obj = self.serialize()
v = settings.get(self.key_name, [])
orig = v.copy()
if not v or self.storage_idx == -1:
# create
self.storage_idx = len(v)
v.append(obj)
else:
# update in place
v[self.storage_idx] = obj
settings.set(self.key_name, v)
# save now, rather than in background, so we can recover
# from out-of-space situation
try:
settings.save()
except:
# back out change; no longer sure of NVRAM state
try:
settings.set(self.key_name, orig)
settings.save()
except: pass # give up on recovery
raise WalletOutOfSpace
def delete(self):
# remove saved entry
# - important: not expecting more than one instance of this class in memory
assert self.storage_idx >= 0
lst = settings.get(self.key_name, [])
try:
del lst[self.storage_idx]
settings.set(self.key_name, lst)
settings.save()
except IndexError: pass
self.storage_idx = -1

View File

@ -7,10 +7,11 @@
#
import stash, ngu, bip39, random
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause
from seed import word_quiz, WordNestMenu, set_seed_value
from seed import word_quiz, WordNestMenu, set_seed_value, set_ephemeral_seed
from glob import settings
from actions import goto_top_menu
def xor(*args):
# bit-wise xor between all args
vlen = len(args[0])
@ -132,11 +133,13 @@ class XORWordNestMenu(WordNestMenu):
XORWordNestMenu.pop_all()
num_parts = len(import_xor_parts)
seed = xor(*(bip39.a2b_words(w) for w in import_xor_parts))
enc_parts = [bip39.a2b_words(w) for w in import_xor_parts]
seed = xor(*enc_parts)
chk_word = bip39.b2a_words(seed).split(' ')[-1]
msg = "You've entered %d parts so far.\n\n" % num_parts
if num_parts >= 2:
chk_word = bip39.b2a_words(seed).split(' ')[-1]
msg += "If you stop now, the %dth word of the XOR-combined seed phrase\nwill be:\n\n" % self.target_words
msg += "%d: %s\n\n" % (self.target_words, chk_word)
@ -169,8 +172,22 @@ class XORWordNestMenu(WordNestMenu):
# update menu contents now that wallet defined
goto_top_menu(first_time=True)
else:
pa.tmp_secret(enc)
await ux_show_story("New master key in effect until next power down.")
# set as ephemeral seed, maybe save it too
# below is super costly as we need to bip32 generate master secret from entropy bytes
# only need XFPs for UI
# xfps = [
# xfp2str(swab32(
# stash.SecretStash.decode(stash.SecretStash.encode(seed_phrase=i))[2].my_fp()
# ))
# for i in enc_parts
# ]
await set_ephemeral_seed(
enc,
meta='SeedXOR(%d parts, check: "%s")' % (
num_parts, chk_word
)
)
goto_top_menu()
return None

View File

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

View File

@ -1,13 +1,13 @@
// (c) Copyright 2020-2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
// (c) Copyright 2020-2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
//
// AUTO-generated.
//
// built: 2023-02-03
// version: 5.1.0
// built: 2023-05-12
// version: 6.0.0X
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x56432820UL;
return 0x56ac3000UL;
}

View File

@ -1,13 +1,13 @@
// (c) Copyright 2020-2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
// (c) Copyright 2020-2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
//
// AUTO-generated.
//
// built: 2023-04-07
// version: 5.1.2
// built: 2024-01-18
// version: 6.2.2X
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x56872820UL;
return 0x58323040UL;
}

View File

@ -14,12 +14,10 @@
$(MAKE) DEBUG_BUILD=1 -f MK4-Makefile $(MAKECMDGOALS)
clean clobber rc1:
clean clobber:
$(MAKE) -f MK4-Makefile $(MAKECMDGOALS)
$(MAKE) -f MK3-Makefile $(MAKECMDGOALS)
release repro:
release repro rc1:
$(MAKE) -f MK4-Makefile $(MAKECMDGOALS)
#NOTYET#$(MAKE) -f MK3-Makefile $(MAKECMDGOALS)
# EOF

View File

@ -24,12 +24,13 @@ 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
# anyway, let's omit ... worst case, use the date instead
h, m, _ = [int(x) for x in version.split('b')[0].split('.')]
ver = version.replace("X", "")
h, m, _ = [int(x) for x in ver.split('b')[0].split('.')]
value |= (h << 11) | (m << 5)
with open(out_fname, 'wt') as fd:
fd.write('''\
// (c) Copyright 2020-2022 by Coinkite Inc. This file is covered by license found in COPYING-CC.
// (c) Copyright 2020-2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
//
// AUTO-generated.
//

View File

@ -335,7 +335,7 @@ psram_do_upgrade(const uint8_t *start, uint32_t size)
if(dest % FLASH_ERASE_SIZE == 0) {
// page erase as we go
rv = flash_page_erase(dest);
#if 1
#if 0
if(rv) {
puts2("erase rv=");
puthex2(rv);
@ -347,7 +347,7 @@ psram_do_upgrade(const uint8_t *start, uint32_t size)
memcpy(&tmp, start+pos, 8);
rv = flash_burn(dest, tmp);
#if 1
#if 0
if(rv) {
puts2("burn rv=");
puthex2(rv);

View File

@ -102,7 +102,8 @@ checksum_flash(uint8_t fw_digest[32], uint8_t world_digest[32], uint32_t fw_leng
const uint8_t *base = (const uint8_t *)BL_FLASH_BASE;
checksum_more(&ctx, &total_len, base, ((uint8_t *)MCU_KEYS)-base);
// Probably-blank area after firmware, and filesystem area
// Probably-blank area after firmware, and filesystem area.
// Important: firmware images (fw_length) must be aligned with flash erase unit size (4k).
const uint8_t *fs = start + fw_length;
const uint8_t *last = base + MAIN_FLASH_SIZE;
checksum_more(&ctx, &total_len, fs, last-fs);

View File

@ -28,7 +28,6 @@ if ! touch repro-build.sh ; then
git clone /work/src/.git firmware
cd firmware/external
git submodule update --init
cd ../stm32
rsync --ignore-missing-args -av /work/src/releases/20*.dfu ../releases
fi

View File

@ -72,6 +72,11 @@ relink:
dev: dev.dfu
ckcc upgrade dev.dfu
# Requires special bootorm w/ DFU still enabled
.PHONY: up-dfu
up-dfu: dev.dfu
$(PYTHON_DO_DFU) -u dev.dfu
$(BOARD)/file_time.c: make_filetime.py version.mk
./make_filetime.py $(BOARD)/file_time.c $(VERSION_STRING)
cp $(BOARD)/file_time.c .
@ -215,6 +220,7 @@ size:
setup:
cd $(MPY_TOP) ; git submodule update --init lib/stm32lib
cd ../external/libngu; make min-one-time
cd ../external/libngu/libs/bech32; git apply ../../bech32.patch || true
cd $(MPY_TOP)/mpy-cross ; make
-ln -s $(PORT_TOP) l-port
-ln -s $(MPY_TOP) l-mpy

View File

@ -1,4 +1,4 @@
# Our version for this release.
VERSION_STRING = 5.1.2
VERSION_STRING = 6.2.2X

View File

@ -2,7 +2,7 @@
#
# needs local bitcoind in PATH
import os, time, uuid, socket, shutil, pytest, tempfile, subprocess, signal
import os, time, uuid, socket, shutil, pytest, tempfile, subprocess, signal, base64
from authproxy import AuthServiceProxy, JSONRPCException
@ -30,6 +30,7 @@ class Bitcoind:
self.bitcoind_proc = None
self.userpass = None
self.supply_wallet = None
self.has_bdb = True
def start(self):
@ -47,6 +48,9 @@ class Bitcoind:
self.bitcoind_proc = subprocess.Popen(
[
self.bitcoind_path,
# needed for newest master
# TODO legacy wallet will be deprecated in 26
"-deprecatedrpc=create_bdb",
"-regtest",
f"-datadir={self.datadir}",
"-noprinttoconsole",
@ -85,7 +89,13 @@ class Bitcoind:
assert self.rpc.getblockchaininfo()['chain'] == 'regtest'
assert self.rpc.getnetworkinfo()['version'] >= 220000, "we require >= 22.0 of Core"
# not descriptors so that we can do dumpwallet
self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=False)
try:
self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=False)
except JSONRPCException as e:
assert "BDB wallet creation is deprecated" in str(e)
self.has_bdb = False
self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=True)
# Make sure there are blocks and coins available
self.supply_wallet.generatetoaddress(101, self.supply_wallet.getnewaddress())
@ -144,17 +154,24 @@ def match_key(bitcoind, set_master_key, reset_seed_words):
# bummer: dumpmasterprivkey RPC call was removed!
#prv = bitcoind.dumpmasterprivkey()
from tempfile import mktemp
fn = mktemp()
bitcoind.supply_wallet.dumpwallet(fn)
prv = None
# bummer: dumpwallet RPC call was removed does not work with descriptor wallets
try:
from tempfile import mktemp
fn = mktemp()
bitcoind.supply_wallet.dumpwallet(fn)
prv = None
for ln in open(fn, 'rt').readlines():
if 'extended private masterkey' in ln:
assert not prv
prv = ln.split(": ", 1)[1].strip()
for ln in open(fn, 'rt').readlines():
if 'extended private masterkey' in ln:
assert not prv
prv = ln.split(": ", 1)[1].strip()
os.unlink(fn)
os.unlink(fn)
except JSONRPCException as e:
print(str(e))
assert "Only legacy wallets are supported by this command" in str(e)
prv_descs = bitcoind.supply_wallet.listdescriptors(True) # True --> show private
prv = prv_descs["descriptors"][0]["desc"].replace("pkh(", "").split("/")[0]
assert prv.startswith('tprv')
@ -163,12 +180,33 @@ def match_key(bitcoind, set_master_key, reset_seed_words):
yield xfp
@pytest.fixture
def finalize_v2_v0_convert(bitcoind):
def doit(psbt_obj):
# compat wrapper - can be removed after below released
# https://github.com/bitcoin/bitcoin/pull/21283 PSBTv2
# convert v2 -> v0 if bitcoind does not support PSBTv2
# to be able to finalize
from authproxy import JSONRPCException
try:
resp = bitcoind.supply_wallet.finalizepsbt(psbt_obj.as_b64_str())
except JSONRPCException as e:
assert "Unsupported version number" in e.error["message"]
# this version of bitcoind does not support PSBTv2
# convert to v0 - needed for finalize
resp = bitcoind.supply_wallet.finalizepsbt(
base64.b64encode(psbt_obj.to_v0()).decode()
)
return resp
return doit
@pytest.fixture
def bitcoind_wallet(bitcoind):
# Use bitcoind to create a temporary wallet file
w_name = 'ckcc-test-wallet-%s' % uuid.uuid4()
conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True,
passphrase=None, avoid_reuse=False, descriptors=False)
passphrase=None, avoid_reuse=False, descriptors=not bitcoind.has_bdb)
yield conn

View File

@ -1,12 +1,16 @@
# (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
import pytest, time, sys, random, re, ndef, os, glob, hashlib, json, bech32
from subprocess import check_output
from ckcc.protocol import CCProtocolPacker
from helpers import B2A, U2SAT
from helpers import B2A, U2SAT, prandom, taptweak
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_sim_watch
from api import bitcoind_wallet, bitcoind_d_wallet, bitcoind_d_wallet_w_sk, bitcoind_d_sim_sign
from api import bitcoind_d_sim_watch, finalize_v2_v0_convert
from binascii import b2a_hex, a2b_hex
from pycoin.contrib.segwit_addr import encode as sw_encode
from pycoin.encoding import a2b_hashed_base58, hash160
from constants import *
# lock down randomness
@ -33,6 +37,10 @@ def pytest_addoption(parser):
parser.addoption("--ms-danger", action="store_true",
default=False, help="Operate with multisig checks off")
parser.addoption("--psbt2", action="store_true",
default=False, help="fake_txn produces PSBTv2")
# to make bitcoind produce psbt v2 one currently needs https://github.com/achow101/bitcoin/tree/psbt2
# or wait until https://github.com/bitcoin/bitcoin/pull/21283 merged and released
@pytest.fixture(scope='session')
def dev(request):
@ -194,7 +202,7 @@ def enter_pin(enter_number, need_keypress, cap_screen):
def master_xpub(dev):
if hasattr(dev.dev, 'pipe'):
# this works better against simulator in HSM mode, where the xpub cmd may be disabled
return simulator_fixed_xpub
return simulator_fixed_tpub
r = dev.send_recv(CCProtocolPacker.get_xpub('m'), timeout=None, encrypt=1)
@ -247,7 +255,7 @@ def addr_vs_path(master_xpub):
from pycoin.key.BIP32Node 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, Encoding
from bech32 import bech32_decode, convertbits, Encoding, decode
from pycoin.encoding import a2b_hashed_base58, hash160
from pycoin.key.BIP32Node import PublicPrivateMismatchError
from hashlib import sha256
@ -261,12 +269,17 @@ def addr_vs_path(master_xpub):
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
except PublicPrivateMismatchError:
mk = BIP32Node.from_wallet_key(simulator_fixed_xprv)
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
if not testnet:
mk._netcode = "BTC"
sk = mk.subkey_for_path(path[2:])
if addr_fmt in {None, AF_CLASSIC}:
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}:
# easy
assert sk.address() == given_addr
@ -337,7 +350,7 @@ def cap_menu(sim_exec):
@pytest.fixture(scope='module')
def is_ftux_screen(sim_exec):
"are we presenting a view from ftux.py"
"are we presenting a view from ftux.py??"
def doit():
rv = sim_exec('from ux import the_ux; RV.write(repr('
'type(the_ux.top_of_stack())))')
@ -354,15 +367,7 @@ def expect_ftux(cap_menu, cap_story, need_keypress, is_ftux_screen):
_, story = cap_story()
if not story:
break
# XXX test more here
if 'Enable NFC' in story:
need_keypress('x')
elif 'Enable USB' in story:
need_keypress('y')
elif 'Disable USB' in story:
need_keypress('x')
else:
raise ValueError(story)
need_keypress('y')
m = cap_menu()
assert m[0] == 'Ready To Sign'
@ -448,7 +453,9 @@ def qr_quality_check():
fnt = ImageFont.load_default()
dr = ImageDraw.Draw(rv)
mw = int((w*scale) / dr.textsize('M', fnt)[0])
left, top, right, bottom = dr.textbbox((0, 0), text='M', font=fnt)
size = (right - left, bottom - top)
mw = int((w*scale) / size[0])
for test_name, img in QR_HISTORY:
if '[' in test_name:
@ -533,9 +540,8 @@ def get_secrets(sim_execfile):
assert 'Error' not in resp
for ln in resp.split('\n'):
ln = ln.strip()
if '#' in ln:
ln = ln[0:ln.index('#')]
if not ln: continue
if ln[0] == '#': continue
assert ' = ' in ln
n, v = ln.split(' = ', 1)
@ -544,6 +550,12 @@ 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 goto_home(cap_menu, need_keypress, pick_menu_item):
@ -563,6 +575,9 @@ def goto_home(cap_menu, need_keypress, pick_menu_item):
if m[0] in { 'New Seed Words', 'Ready To Sign'}:
break
if len(m) > 1 and (m[1] == "Ready To Sign") and (m[0][0] == "["):
# ephemeral has XFP as first menu item
break
else:
raise pytest.fail("trapped in a menu")
@ -666,6 +681,24 @@ def open_microsd(simulator, microsd_path):
return doit
@pytest.fixture(scope='module')
def settings_path(simulator):
# open a file from the simulated microsd
def doit(fn):
# could use: ckcc.get_sim_root_dirs() here
return '../unix/work/settings/' + fn
return doit
@pytest.fixture
def settings_slots(settings_path):
def doit():
return [fn
for fn in os.listdir(settings_path(""))
if fn.endswith(".aes")]
return doit
@pytest.fixture(scope="function")
def set_master_key(sim_exec, sim_execfile, simulator, reset_seed_words):
# load simulator w/ a specific bip32 master key
@ -691,7 +724,7 @@ def set_master_key(sim_exec, sim_execfile, simulator, reset_seed_words):
reset_seed_words()
@pytest.fixture(scope="function")
def set_xfp(sim_exec, sim_execfile, simulator, reset_seed_words):
def set_xfp(sim_exec):
# set the XFP, without really knowing the private keys
# - won't be able to sign, but should accept PSBT for signing
@ -701,11 +734,12 @@ def set_xfp(sim_exec, sim_execfile, simulator, reset_seed_words):
import struct
need_xfp, = struct.unpack("<I", a2b_hex(xfp))
sim_exec('from main import settings; settings.put_volatile("xfp", 0x%x);' % need_xfp)
sim_exec('from main import settings; settings.set("xfp", 0x%x);' % need_xfp)
yield doit
sim_exec('from main import settings; settings.overrides.clear();')
sim_exec('from main import settings; settings.set("xfp", 0x%x);' % simulator_fixed_xfp)
@pytest.fixture(scope="function")
def set_encoded_secret(sim_exec, sim_execfile, simulator, reset_seed_words):
@ -760,8 +794,8 @@ def set_seed_words(sim_exec, sim_execfile, simulator, reset_seed_words):
# load simulator w/ a specific bip32 master key
def doit(words):
sim_exec('import main; main.WORDS = %r; ' % words.split())
cmd = 'import main; main.WORDS = %r;' % words.split()
sim_exec(cmd)
rv = sim_execfile('devtest/set_seed.py')
if rv: pytest.fail(rv)
@ -783,8 +817,8 @@ def reset_seed_words(sim_exec, sim_execfile, simulator):
def doit():
words = simulator_fixed_words
sim_exec('import main; main.WORDS = %r; ' % words.split())
cmd = 'import main; main.WORDS = %r;' % words.split()
sim_exec(cmd)
rv = sim_execfile('devtest/set_seed.py')
if rv: pytest.fail(rv)
@ -819,6 +853,17 @@ def settings_get(sim_exec):
return doit
@pytest.fixture()
def master_settings_get(sim_exec):
def doit(key):
cmd = f"RV.write(repr(settings.master_get('{key}')))"
resp = sim_exec(cmd)
assert 'Traceback' not in resp, resp
return eval(resp)
return doit
@pytest.fixture()
def settings_remove(sim_exec):
@ -1215,7 +1260,7 @@ def end_sign(dev, need_keypress):
else:
done = None
while done == None:
time.sleep(0.00)
time.sleep(0.050)
done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None)
assert len(done) == 2
@ -1375,6 +1420,9 @@ def nfc_read(request, only_mk4):
def nfc_write(request, only_mk4):
# 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')
need_keypress = request.getfixturevalue('need_keypress')
rv = sim_exec('list(glob.NFC.big_write(%r))' % ccfile)
@ -1441,7 +1489,7 @@ def nfc_block4rf(sim_eval):
for i in range(timeout*4):
rv = sim_eval('glob.NFC.rf_on')
if rv: break
sleep(0.250)
time.sleep(.25)
else:
raise pytest.fail("NFC timeout")
@ -1547,37 +1595,38 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_
load_export_and_verify_signature):
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):
fpattern=None, skip_query=False):
key_map = {
"sd": sd_key or "1",
"vdisk": vdisk_key or "2",
"nfc": nfc_key or "3",
}
time.sleep(0.2)
title, story = cap_story()
if way == "sd":
if f"({key_map['sd']}) to save {label} file to SD Card" in story:
need_keypress(key_map['sd'])
if not skip_query:
time.sleep(0.2)
title, story = cap_story()
if way == "sd":
if f"({key_map['sd']}) to save {label} file to SD Card" in story:
need_keypress(key_map['sd'])
elif way == "nfc":
if f"({key_map['nfc']}) to share via NFC" not in story:
pytest.skip("NFC disabled")
else:
need_keypress(key_map['nfc'])
time.sleep(0.2)
if is_json:
nfc_export = nfc_read_json()
elif way == "nfc":
if f"({key_map['nfc']}) to share via NFC" not in story:
pytest.skip("NFC disabled")
else:
nfc_export = nfc_read_text()
time.sleep(0.3)
need_keypress("x") # exit NFC animation
return nfc_export
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_json:
nfc_export = nfc_read_json()
else:
nfc_export = nfc_read_text()
time.sleep(0.3)
need_keypress("x") # exit NFC animation
return nfc_export
else:
need_keypress(key_map['vdisk'])
# 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()
@ -1650,11 +1699,150 @@ def tapsigner_encrypted_backup(microsd_path, virtdisk_path):
return fname, backup_key_hex, node
return doit
@pytest.fixture
def choose_by_word_length(need_keypress):
# for use in seed XOR menu system
def doit(num_words):
if num_words == 12:
need_keypress('1')
elif num_words == 18:
need_keypress("2")
else:
need_keypress("y")
return doit
# useful fixtures related to multisig
from test_multisig import (import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn,
make_ms_address, clear_ms, make_myself_wallet)
from test_bip39pw import set_bip39_pw, clear_bip39_pw
# workaround: need these fixtures to be global so I can call test from a test
from test_se2 import clear_all_tricks, new_trick_pin, new_pin_confirmed, goto_trick_menu, se2_gate
@pytest.fixture
def validate_address():
# Check whether an address is covered by the given subkey
def doit(addr, sk):
if addr[0] in '1mn':
assert addr == sk.address(False)
elif addr[0:4] in { 'bc1q', 'tb1q' } or addr[0:6] == "bcrt1q":
h20 = sk.hash160()
if addr[:4] == "bcrt":
hrp = addr[:4]
else:
hrp = addr[:2]
assert addr == sw_encode(hrp, 0, h20)
elif addr[0:4] in {'bc1p', 'tb1p'} or addr[0:6] == "bcrt1p":
if addr[:4] == "bcrt":
hrp = addr[:4]
else:
hrp = addr[:2]
tweaked_xonly = taptweak(sk.sec()[1:])
decoded = bech32.decode(hrp, addr)
assert tweaked_xonly == bytes(decoded[1])
elif addr[0] in '23':
h20 = hash160(b'\x00\x14' + sk.hash160())
assert h20 == a2b_hashed_base58(addr)[1:]
else:
raise ValueError(addr)
return doit
@pytest.fixture
def verify_backup_file(goto_home, pick_menu_item, cap_story, need_keypress):
def doit(fn):
# Check on-device verify UX works.
goto_home()
pick_menu_item('Advanced/Tools')
pick_menu_item('Backup')
pick_menu_item('Verify Backup')
time.sleep(0.1)
title, body = cap_story()
assert "Select file" in body
need_keypress('y')
time.sleep(0.1)
pick_menu_item(os.path.basename(fn))
time.sleep(0.1)
title, body = cap_story()
assert "Backup file CRC checks out okay" in body
return doit
@pytest.fixture
def check_and_decrypt_backup(microsd_path):
def doit(fn, passphrase):
# List contents using unix tools
pn = microsd_path(fn)
out = check_output(['7z', 'l', pn], encoding='utf8')
xfname, = re.findall('[a-z0-9]{4,30}.txt', out)
print(f"Filename inside 7z: {xfname}")
assert xfname in out
assert 'Method = 7zAES' in out
xfn_path = microsd_path(xfname)
if os.path.exists(xfn_path):
os.remove(xfn_path)
# does decryption; at least for CRC purposes
args = ['7z', 'e', '-p' + ' '.join(passphrase), pn, xfname, '-o' + microsd_path("")]
out = check_output(args, encoding='utf8')
assert "Extracting archive" in out, out
assert "Everything is Ok" in out, out
with open(xfn_path, "r") as f:
res = f.read()
return res
return doit
@pytest.fixture
def restore_backup_cs(unit_test, pick_menu_item, cap_story, cap_menu,
need_keypress, word_menu_entry, get_setting):
# restore backup with clear seed as first step
def doit(fn, passphrase, avail_settings=None):
unit_test('devtest/clear_seed.py')
m = cap_menu()
assert m[0] == 'New Seed Words'
pick_menu_item('Import Existing')
pick_menu_item('Restore Backup')
# skip
title, body = cap_story()
if ('files to pick from' in body) or ("only one file to pick from" in body):
need_keypress('y')
time.sleep(.01)
pick_menu_item(fn)
time.sleep(.1)
word_menu_entry(passphrase)
title, body = cap_story()
assert title == 'Success!'
assert 'has been successfully restored' in body
if avail_settings:
for key in avail_settings:
assert get_setting(key)
# avoid simulator reboot; restore normal state
unit_test('devtest/abort_ux.py')
return doit
# useful fixtures
from test_backup import backup_system
from test_bip39pw import set_bip39_pw
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_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
from test_miniscript import offer_minsc_import
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed
from test_seed_xor import restore_seed_xor
from test_ux import enter_complex, pass_word_quiz, word_menu_entry
from txn import fake_txn
# EOF

View File

@ -4,9 +4,12 @@
SIM_PATH = '/tmp/ckcc-simulator.sock'
# Simulator normally powers up with this 'wallet'
simulator_fixed_xprv = "tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4"
simulator_fixed_tprv = "tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4"
simulator_fixed_tpub = "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh"
simulator_fixed_xpub = "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh"
# same wallet but mainnet BTC
simulator_fixed_xprv = "xprv9s21ZrQH143K3i4kfV4tE2qAvhys9WDCpHJXKz2biqWkZwLKma1dzWaqin8CxCKPF3tX2fVRD9tBggJtxvdAxTpKfz8zRUoJZa3S7MtMgwy"
simulator_fixed_xpub = "xpub661MyMwAqRbcGC9DmWbtbAmuUjpMYxw4BWE88NSDHB3jSjfUK7KtYJuKa52GbowD3DVLkgsxH9QwPnTx5mjdHykYFEncnmAsNsCTbWzBhA7"
simulator_fixed_words = "wife shiver author away frog air rough vanish fantasy frozen noodle athlete pioneer citizen symptom firm much faith extend rare axis garment kiwi clarify"
@ -14,7 +17,7 @@ simulator_fixed_xfp = 0x4369050f
simulator_serial_number = 'F1F1F1F1F1F1'
from ckcc_protocol.constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from ckcc_protocol.constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR
unmap_addr_fmt = {
'p2sh': AF_P2SH,
@ -28,6 +31,7 @@ msg_sign_unmap_addr_fmt = {
'p2wpkh': AF_P2WPKH,
'p2sh-p2wpkh': AF_P2WPKH_P2SH,
'p2wpkh-p2sh': AF_P2WPKH_P2SH,
'p2tr': AF_P2TR,
}
addr_fmt_names = {
@ -37,14 +41,15 @@ addr_fmt_names = {
AF_P2WSH: 'p2wsh',
AF_P2WPKH_P2SH: 'p2wpkh-p2sh',
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']
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2tr']
# single-signer
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh']
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh', 'p2tr']
# multi signer
ADDR_STYLES_MS = ['p2sh', 'p2wsh', 'p2wsh-p2sh']

View File

@ -0,0 +1 @@
{"pubkey": "038e96756bb520bc3fece6c663c61db10cd8c971dfdbf757f1602fa7eed3f83689"}

View File

BIN
testing/data/pwsave.tmp Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a075701133f173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc3475000000

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757011342173bb3d36c074afb716fec6307a069a2e450b995f3c82785945ab8df0e24260dcd703b0cbf34de399184a9481ac2b3586db6601f026a77f7e4938481bc34751701aa000000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926315c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e1f80023202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6926115c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac06f7d62059e9497a1a4a267569d9876da60101aff38e3529b9b939ce7f91ae970115f2e490af7cc45c4f78511f36057ce5c5a5c56325a29fb44dfc203f356e123202cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2acc00000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b6924214022cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094089756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb0000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b094289756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd43cb01010000

View File

@ -0,0 +1 @@
70736274ff01005e02000000019bd48765230bf9a72e662001f972556e54f0c6f97feb56bcb5600d817f6995260100000000ffffffff0148e6052a01000000225120030da4fce4f7db28c2cb2951631e003713856597fe963882cb500e68112cca63000000000001012b00f2052a01000000225120c2247efbfd92ac47f6f40b8d42d169175a19fa9fa10e4a25d7f35eb4dd85b69241142cb13ac68248de806aa6a3659cf3c03eb6821d09c8114a4e868febde865bb6d2cd970e15f53fc0c82f950fd560ffa919b76172be017368a89913af074f400b093f89756aa3739ccc689ec0fcf3a360be32cc0b59b16e93a1e8bb4605726b2ca7a3ff706c4176649632b2cc68e1f912b8a578e3719ce7710885c7a966f49bcd430000

View File

@ -0,0 +1 @@
70736274ff010071020000000127744ababf3027fe0d6cf23a96eee2efb188ef52301954585883e69b6624b2420000000000ffffffff02787c01000000000016001483a7e34bd99ff03a4962ef8a1a101bb295461ece606b042a010000001600147ac369df1b20e033d6116623957b0ac49f3c52e8000000000001012b00f2052a010000002251205a2c2cf5b52cf31f83ad2e8da63ff03183ecd8f609c7510ae8a48e03910a0757221602fe349064c98d6e2a853fa3c9b12bd8b304a19c195c60efa7ee2393046d3fa2321900772b2da75600008001000080000000800100000000000000000000

468
testing/descriptor.py Normal file
View File

@ -0,0 +1,468 @@
# (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 struct
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2TR
MULTI_FMT_TO_SCRIPT = {
AF_P2SH: "sh(%s)",
AF_P2WSH_P2SH: "sh(wsh(%s))",
AF_P2WSH: "wsh(%s)",
AF_P2TR: "tr(%s)",
None: "wsh(%s)",
# hack for tests
"p2sh": "sh(%s)",
"p2sh-p2wsh": "sh(wsh(%s))",
"p2wsh-p2sh": "sh(wsh(%s))",
"p2wsh": "wsh(%s)",
"p2tr": "tr(%s)"
}
SINGLE_FMT_TO_SCRIPT = {
AF_P2WPKH: "wpkh(%s)",
AF_CLASSIC: "pkh(%s)",
AF_P2WPKH_P2SH: "sh(wpkh(%s))",
AF_P2TR: "tr(%s)",
None: "wpkh(%s)",
"p2pkh": "pkh(%s)",
"p2wpkh": "wpkh(%s)",
"p2sh-p2wpkh": "sh(wpkh(%s))",
"p2wpkh-p2sh": "sh(wpkh(%s))",
"p2tr": "tr(%s)",
}
PROVABLY_UNSPENDABLE = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
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 WrongCheckSumError(Exception):
pass
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 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,...))"
elif addr_fmt == AF_P2TR:
# provably unspendable BIP-0341
descriptor_template = "tr(" + PROVABLY_UNSPENDABLE + ",sortedmulti_a(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
class Descriptor:
__slots__ = (
"keys",
"addr_fmt",
)
def __init__(self, keys, addr_fmt):
self.keys = keys
self.addr_fmt = addr_fmt
@staticmethod
def checksum_check(desc_w_checksum: str, 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
@staticmethod
def parse_key_orig_info(key: str):
# 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:]
assert "/" in key_orig_info, "Malformed key derivation info"
return key_orig_info, key
@staticmethod
def parse_key_derivation_info(key: str):
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")
def checksum(self):
return descriptor_checksum(self._serialize())
def serialize_keys(self, internal=False, int_ext=False, keys=None):
to_do = keys if keys is not None else self.keys
result = []
for xfp, deriv, xpub in to_do:
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:
key_str = key_str + "/" + "/".join(["1", "*"] if internal else ["0", "*"])
result.append(key_str.replace("'", "h"))
return result
def _serialize(self, internal=False, int_ext=False) -> str:
"""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)
def serialize(self, internal=False, int_ext=False) -> str:
"""Serialize with checksum"""
return append_checksum(self._serialize(internal=internal, int_ext=int_ext))
@classmethod
def parse(cls, desc_w_checksum: str) -> "Descriptor":
# 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(")")
# native segwit
elif desc.startswith("wpkh("):
addr_fmt = AF_P2WPKH
tmp_desc = desc.replace("wpkh(", "")
tmp_desc = tmp_desc.rstrip(")")
# wrapped segwit
elif desc.startswith("sh(wpkh("):
addr_fmt = AF_P2WPKH_P2SH
tmp_desc = desc.replace("sh(wpkh(", "")
tmp_desc = tmp_desc.rstrip("))")
# wrapped segwit
elif desc.startswith("tr("):
addr_fmt = AF_P2TR
tmp_desc = desc.replace("tr(", "")
tmp_desc = tmp_desc.rstrip(")")
else:
raise ValueError("Unsupported descriptor. Supported: pkh(, wpkh(, sh(wpkh(.")
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")
xpub = cls.parse_key_derivation_info(key)
xfp = str2xfp(koi[:8])
origin_deriv = "m" + koi[8:]
return cls(keys=[(xfp, origin_deriv, xpub)], addr_fmt=addr_fmt)
@classmethod
def is_descriptor(cls, desc_str):
"""Quick method to guess whether this is a descriptor"""
try:
temp = parse_desc_str(desc_str)
except:
return False
for prefix in ("pk(", "pkh(", "wpkh(", "tr(", "addr(", "raw(", "rawtr(", "combo(",
"sh(", "wsh(", "multi(", "sortedmulti(", "multi_a(", "sortedmulti_a("):
if temp.startswith(prefix):
return True
return False
def bitcoin_core_serialize(self, external_label=None):
# this will become legacy one day
# instead use <0;1> descriptor format
res = []
for internal in [False, True]:
desc_obj = {
"desc": self.serialize(internal=internal),
"active": True,
"timestamp": "now",
"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",
"internal_key",
"keys",
"addr_fmt",
)
def __init__(self, M, N, keys, addr_fmt, internal_key=None):
self.M = M
self.N = N
self.internal_key = internal_key
super().__init__(keys, addr_fmt)
@classmethod
def parse(cls, desc_w_checksum: str) -> "MultisigDescriptor":
internal_key = None # taproot
# 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("sh(sortedmulti("):
addr_fmt = AF_P2SH
tmp_desc = desc.replace("sh(sortedmulti(", "")
tmp_desc = tmp_desc.rstrip("))")
# native segwit
elif desc.startswith("wsh(sortedmulti("):
addr_fmt = AF_P2WSH
tmp_desc = desc.replace("wsh(sortedmulti(", "")
tmp_desc = tmp_desc.rstrip("))")
# wrapped segwit
elif desc.startswith("sh(wsh(sortedmulti("):
addr_fmt = AF_P2WSH_P2SH
tmp_desc = desc.replace("sh(wsh(sortedmulti(", "")
tmp_desc = tmp_desc.rstrip(")))")
elif desc.startswith("tr("):
addr_fmt = AF_P2TR
tmp_desc = desc.replace("tr(", "")
tmp_desc = tmp_desc.rstrip(")")
internal_key, tmp_desc = tmp_desc.split(",", 1)
assert tmp_desc.startswith("sortedmulti_a("), "Only one sortedmulti_a allowed"
tmp_desc = tmp_desc.replace("sortedmulti_a(", "")
tmp_desc = tmp_desc.rstrip(")")
try:
koi, key = cls.parse_key_orig_info(internal_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:]
internal_key = (xfp, origin_deriv, xpub)
except ValueError:
# https://github.com/BlockstreamResearch/secp256k1-zkp/blob/11af7015de624b010424273be3d91f117f172c82/src/modules/rangeproof/main_impl.h#L16
# H = lift_x(0x0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0)
# if internal_key == PROVABLY_UNSPENDABLE:
# # unspendable H as defined in BIP-0341
# pass
# else:
# assert "r=" in internal_key
# _, r = internal_key.split("=")
# if r == "@":
# # pick a fresh integer r in the range 0...n-1 uniformly at random and use H + rG
# kp = ngu.secp256k1.keypair()
# else:
# # H + rG where r is provided from user
# r = a2b_hex(r)
# assert len(r) == 32, "r != 32"
# kp = ngu.secp256k1.keypair(r)
#
# H = a2b_hex(PROVABLY_UNSPENDABLE)
# H_xo = ngu.secp256k1.xonly_pubkey(H)
# internal_key = H_xo.tweak_add(kp.xonly_pubkey().to_bytes())
# internal_key = b2a_hex(internal_key.to_bytes()).decode()
pass
else:
raise ValueError("Unsupported descriptor. Supported: sh(, sh(wsh(, wsh(. All have to be sortedmulti.")
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, internal_key=internal_key)
def _serialize(self, internal=False, int_ext=False) -> str:
"""Serialize without checksum"""
desc_base = MULTI_FMT_TO_SCRIPT[self.addr_fmt]
if self.addr_fmt == AF_P2TR:
if isinstance(self.internal_key, str):
desc_base = desc_base % (self.internal_key + ",sortedmulti_a(%s)")
else:
ik_ser = self.serialize_keys(keys=[self.internal_key])[0]
desc_base = desc_base % (ik_ser + ",sortedmulti_a(%s)")
else:
desc_base = desc_base % "sortedmulti(%s)"
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"""
inner_ident = 1
res = "# Coldcard descriptor export\n"
res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n"
if self.addr_fmt == AF_P2SH:
res += "# bare multisig - p2sh\n"
res += "sh(sortedmulti(\n%s\n))"
# native segwit
elif self.addr_fmt == AF_P2WSH:
res += "# native segwit - p2wsh\n"
res += "wsh(sortedmulti(\n%s\n))"
# wrapped segwit
elif self.addr_fmt == AF_P2WSH_P2SH:
res += "# wrapped segwit - p2sh-p2wsh\n"
res += "sh(wsh(sortedmulti(\n%s\n)))"
elif self.addr_fmt == AF_P2TR:
inner_ident = 2
res += "# taproot multisig - p2tr\n"
res += "tr(\n"
if isinstance(self.internal_key, str):
res += "\t" + "# internal key (provably unspendable)\n"
res += "\t" + self.internal_key + ",\n"
res += "\t" + "sortedmulti_a(\n%s\n))"
else:
ik_ser = self.serialize_keys(keys=[self.internal_key])[0]
res += "\t" + "# internal key\n"
res += "\t" + ik_ser + ",\n"
res += "\t" + "sortedmulti_a(\n%s\n))"
else:
raise ValueError("Malformed descriptor")
assert len(self.keys) == self.N
inner = ("\t" * inner_ident) + "# %d of %d (%s)\n" % (
self.M, self.N,
"requires all participants to sign" if self.M == self.N else "threshold")
inner += ("\t" * inner_ident) + 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" * inner_ident) + key_str
else:
inner += ("\t" * inner_ident) + key_str + ",\n"
checksum = self.serialize().split("#")[1]
return (res % inner) + "#" + checksum
# EOF

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