What changed, and why it matters
This commit makes a small user-interface change in the COLDCARD firmware for BIP-322 proof-of-reserves signing. When the user chooses to type in the human-readable message manually, pressing Cancel on the empty text-entry screen now returns to the signing flow instead of aborting the entire operation. The change is accompanied by new tests that verify the device rejects empty or wrong messages with clear failure screens. There is no direct evidence in the commit that this fixes an exploitable security vulnerability; it appears to be a usability improvement to prevent accidental cancellation.
Treat as a minor hardening/usability fix. Reviewers should verify that `confirm_exit=False` does not introduce any unintended state in the BIP-322 signing flow and that the failure path always returns to a safe menu. No urgent action is indicated by the diff alone.
Security signals we found
BIP-322 proof-of-reserves signing requires user-supplied message verification against a hash in the PSBT
Empty or mismatched messages are rejected with a failure screen
Change only affects the Cancel behavior of the manual text-input prompt
No cryptographic, parsing, or authorization logic is modified
Evidence from the diff
In shared/auth.py, the manual BIP-322 message import path changes ux_input_text("") to ux_input_text("", confirm_exit=False). The confirm_exit parameter presumably controls whether the input screen asks ‘are you sure you want to cancel?’ when the user presses Cancel. Setting it to false means Cancel immediately returns an empty string, which the existing code then treats as a missing message and shows a ‘Failure’ screen. The new test test_bip322_msg_import_fail confirms that empty input and wrong input both produce failure screens rather than signing. The docs update explains that the user must supply the exact human-readable message whose hash matches the PSBT’s message_hash.
Changed components
shared/auth.py: BIP-322 message import UXdocs/proof-of-reserves-bip-322.md: user-facing documentationtesting/test_bip322.py: regression tests for empty/wrong message inputInspect captured patch +53 / −1
diff --git a/docs/proof-of-reserves-bip-322.md b/docs/proof-of-reserves-bip-322.md
index ca9dd3f..9017423 100644
--- a/docs/proof-of-reserves-bip-322.md
+++ b/docs/proof-of-reserves-bip-322.md
@@ -25,6 +25,10 @@ COLDCARD accepts specially crafted PSBT to sign BIP-322 Proof of Reserves
### POR Signing UX
+After Coldcard recognizes BIP-322 POR PSBT it asks user to import human-readable message that was used to build
+`to_spend` scriptSig. This message must hash exactly to message_hash, otherwise signing is not offered.
+Read more [here](https://gist.github.com/orangesurf/0c1d0a31d3ebe7e48335a34d56788d4c)
+
```text
Proof of Reserves
diff --git a/shared/auth.py b/shared/auth.py
index 5bf4639..2d6991b 100644
--- a/shared/auth.py
+++ b/shared/auth.py
@@ -302,7 +302,7 @@ class ApproveTransaction(UserAuthorizedAction):
if ch == KEY_CANCEL:
return
elif ch == "0":
- msg = await ux_input_text("")
+ msg = await ux_input_text("", confirm_exit=False)
elif ch == KEY_NFC:
msg = await NFC.read_bip322_msg()
elif ch == KEY_QR:
diff --git a/testing/test_bip322.py b/testing/test_bip322.py
index b4b3043..1940975 100644
--- a/testing/test_bip322.py
+++ b/testing/test_bip322.py
@@ -523,4 +523,52 @@ def test_bip322_msg_import(msg, ins, way, bip322_txn, start_sign, end_sign, cap_
assert title == "OK TO SIGN?"
assert "Proof of Reserves" in story
+
+def test_bip322_msg_import_fail(bip322_txn, start_sign, end_sign, cap_story, need_keypress,
+ press_select, OK, press_cancel, cap_menu, microsd_path, enter_complex):
+
+ msg = b"it's me!"
+ psbt, msg_challenge = bip322_txn([["p2wpkh", None, None]], msg=msg)
+ start_sign(psbt, finalize=True)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "BIP-322 MSG"
+
+ need_keypress("1") # SD
+ time.sleep(.1)
+ title, story = cap_story()
+ assert f"Press {OK} to approve message" in story
+ press_cancel() # refuse
+ time.sleep(.1)
+ assert "Ready To Sign" in cap_menu()
+
+ start_sign(psbt, finalize=True)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "BIP-322 MSG"
+
+ need_keypress("0") # manual input
+ # leave empty
+ press_cancel()
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Failure"
+ assert "need msg" in story
+ assert "Msg verification failed" in story
+ press_cancel()
+
+ start_sign(psbt, finalize=True)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "BIP-322 MSG"
+
+ need_keypress("0") # manual input
+ enter_complex("AAA", apply=False, b39pass=False) # msg wrong
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Failure"
+ assert "Msg verification failed" in story
+ assert "hash verification failed" in story
+ press_cancel()
+
# EOF
\ No newline at end of file
Why this scored 21/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.