testing: add BIP-322 POR signed with WIF Store test
What changed, and why it matters
This commit only adds new automated tests for an existing feature: signing BIP-322 Proof of Reserves messages using private keys stored via WIF (Wallet Import Format) on a COLDCARD. It does not change the firmware's actual signing code, user interface, or security behavior. The changes are confined to test files and test helpers.
No security action required. This is a test-only change. Routine review/merge is appropriate.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies three files under the testing/ directory. testing/bip322.py is updated so the bip322_txn helper can accept an externally-supplied public key instead of always deriving one from a BIP32 master key, enabling test cases that use WIF-imported keys. testing/test_bip322.py adds a new parametrized test test_wif_store_sign_bip322_por that imports WIF keys, builds a BIP-322 PSBT, and verifies the COLDCARD simulator produces the expected signing flow and warnings. testing/test_wif.py removes an unused privkeys list. No firmware source code is changed.
Changed components
testing/bip322.pytesting/test_bip322.pytesting/test_wif.pyInspect captured patch +72 / −12
diff --git a/testing/bip322.py b/testing/bip322.py
index 3152a73..32ed216 100644
--- a/testing/bip322.py
+++ b/testing/bip322.py
@@ -7,7 +7,7 @@ from ckcc_protocol.protocol import MAX_TXN_LEN
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput
from io import BytesIO
from helpers import hash160, taptweak, str_to_path
-from bip32 import BIP32Node
+from bip32 import BIP32Node, PublicKey
from constants import simulator_fixed_tprv, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH
from ctransaction import CTransaction, COutPoint, CTxIn, CTxOut, uint256_from_str
@@ -59,6 +59,7 @@ def bip322_txn(dev, pytestconfig, create_msg_file):
sp = f"0/{i}"
af = addr_fmt
ia = input_amount
+ pubkey = None # public key
try:
if inp[0] is not None:
af = inp[0]
@@ -66,12 +67,20 @@ def bip322_txn(dev, pytestconfig, create_msg_file):
sp = inp[1]
if inp[2] is not None:
ia = inp[2]
+ if inp[3] is not None:
+ pubkey = inp[3]
except:
pass
- int_path = str_to_path(sp)
- subkey = mk.subkey_for_path(sp)
- sec = subkey.sec()
+ if pubkey:
+ int_path = [0]
+ sec = pubkey
+ else:
+ int_path = str_to_path(sp)
+ sec = mk.subkey_for_path(sp).sec()
+
+ subkey = PublicKey.parse(sec)
+
assert len(sec) == 33, "expect compressed"
if af == "p2tr":
@@ -82,7 +91,7 @@ def bip322_txn(dev, pytestconfig, create_msg_file):
elif af in ("p2wpkh", "p2sh-p2wpkh", "p2wpkh-p2sh"):
psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
- scr = bytes([0x00, 0x14]) + subkey.hash160()
+ scr = bytes([0x00, 0x14]) + subkey.h160()
if af != "p2wpkh":
# use classic p2wpkh (from above) as redeem script
@@ -90,8 +99,8 @@ def bip322_txn(dev, pytestconfig, create_msg_file):
scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87])
elif af == "p2pkh":
- psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack('<II', 0, i)
- scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes([0x88, 0xac])
+ psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path)
+ scr = bytes([0x76, 0xa9, 0x14]) + subkey.h160() + bytes([0x88, 0xac])
else:
raise ValueError("unknown addr_fmt %s" % af)
diff --git a/testing/test_bip322.py b/testing/test_bip322.py
index f561267..e364913 100644
--- a/testing/test_bip322.py
+++ b/testing/test_bip322.py
@@ -2,11 +2,11 @@
#
# BIP-322 Message Signing and Proof of Reserves
#
-import pytest, time
+import pytest, time, os
from io import BytesIO
from decimal import Decimal
from constants import SIGHASH_MAP, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH
-from bip322 import bip322_txn, bip322_ms_txn, bip322_msg_hash
+from bip322 import bip322_txn, bip322_ms_txn, bip322_msg_hash, BIP32Node
from ctransaction import CTransaction, CTxIn, COutPoint
from helpers import str_to_path
from charcodes import KEY_QR, KEY_NFC
@@ -574,4 +574,58 @@ def test_bip322_msg_import_fail(bip322_txn, start_sign, end_sign, cap_story, nee
assert "hash verification failed" in story
press_cancel()
+
+@pytest.mark.parametrize("num_ins", [1, 12])
+@pytest.mark.parametrize("addr_fmt", ["p2pkh", "p2wpkh", "p2sh-p2wpkh"])
+def test_wif_store_sign_bip322_por(num_ins, addr_fmt, bip322_txn, goto_home, pick_menu_item,
+ need_keypress, start_sign, end_sign, cap_menu, cap_story,
+ press_cancel, settings_remove, press_select, import_wif_to_store):
+
+ settings_remove("wifs")
+
+ node = BIP32Node.from_master_secret(os.urandom(32))
+
+ ins = []
+ wifs = []
+ for i in range(num_ins):
+ n = node.subkey_for_path("0/%d" % i)
+ wifs.append(n.node.private_key.wif(testnet=True))
+ if i == 0:
+ amt = None
+ elif i // 2 == 0:
+ amt = 10000000
+ else:
+ amt = 900000000
+
+ ins.append([addr_fmt, None, amt , n.node.private_key.K.sec()])
+
+ msg = b"Coinkite"
+ psbt, msg_challenge = bip322_txn(ins, msg=b"Coinkite")
+
+ import_wif_to_store(wifs)
+
+ menu = cap_menu()
+ assert menu[0] == "Import WIF"
+
+ start_sign(psbt, finalize=True)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert "import message" in story
+ # msg file was auto-gened on SD card
+ need_keypress("1")
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Message:"
+ assert msg.decode() in story
+ press_select()
+ time.sleep(.1)
+ title, story = cap_story()
+ assert "Proof of Reserves" in story
+ assert "warning" in story
+ if num_ins == 1:
+ assert "WIF store: 0" in story
+ else:
+ assert f"WIF store: {', '.join([str(i) for i in range(num_ins)])}" in story
+ end_sign(finalize=True)
+
# EOF
diff --git a/testing/test_wif.py b/testing/test_wif.py
index c268801..cf399c0 100644
--- a/testing/test_wif.py
+++ b/testing/test_wif.py
@@ -613,11 +613,8 @@ def test_wif_store_signing(num_ins, addr_fmt, fake_txn, goto_home, pick_menu_ite
psbt = fake_txn(num_ins, 1, segwit_in=sw, wrapped=wrap, master_xpub=node.hwif())
wifs = []
- privkeys = []
for i in range(num_ins):
n = node.subkey_for_path("0/%d" % i)
- sk = n.node.private_key
- privkeys.append(sk)
wifs.append(n.node.private_key.wif(testnet=True))
import_wif_to_store(wifs)
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.