Reject foreign BIP322 reserve inputs
What changed, and why it matters
This update fixes a bug in the COLDCARD hardware wallet's handling of BIP-322 Proof of Reserves (PoR) transactions. Previously, the wallet could be presented with a PoR PSBT containing extra 'foreign' inputs that it did not control, and it would not clearly reject the transaction. This could allow a malicious or malformed PoR request to include unrelated inputs, potentially misleading the user or weakening the proof. The fix now rejects any BIP-322 PoR PSBT that contains inputs not belonging to the wallet.
Users relying on BIP-322 Proof of Reserves should upgrade to a release containing this commit. Review any custom tooling that builds PoR PSBTs to ensure it does not include foreign inputs, as such PSBTs will now be rejected. Security researchers may want to audit whether prior firmware versions could be coerced into signing or displaying misleading PoR messages when foreign inputs were present.
Security signals we found
BIP-322 Proof of Reserves input validation bugfix
Rejection of foreign/uncontrolled inputs in PoR PSBTs
Change in test expectations from warning/limited-signing to FatalPSBTIssue
Explicit changelog entry: 'Bugfix: Reject foreign inputs from BIP-322 Proof of Reserves.'
Evidence from the diff
In shared/psbt.py, the consider_inputs method now tracks whether a BIP-322 Proof of Reserves PSBT contains any input beyond the first (i > 0) that has no keys owned by the device (inp.num_our_keys == 0). If such a ‘foreign’ input is found, a FatalPSBTIssue is raised with the message ‘Foreign inputs not allowed in BIP-322 Proof of Reserves’. The test file test_bip322.py was updated to expect this failure instead of the previous behavior, which allowed limited signing with a warning. The change is explicitly listed as a bugfix in the release changelog.
Changed components
shared/psbt.pyBIP-322 Proof of Reserves signing flowtesting/test_bip322.pyInspect captured patch +11 / −9
### releases/Next-ChangeLog.md
@@ -4,6 +4,7 @@ This lists the new changes that have not yet been published in a normal release.
# Shared Improvements - Both Mk and Q
+- Bugfix: Reject foreign inputs from BIP-322 Proof of Reserves.
- Bugfix: Restore the ability to view the device-generated seed before adding user
entropy, which was available in the previous dice-roll workflow but was inadvertently
removed in 5.6.1/1.5.1Q. The new **View TRNG Words** menu item displays the full
### shared/psbt.py
@@ -1849,6 +1849,7 @@ def consider_inputs(self, cosign_xfp=None):
total_in = 0
from_wif_store = []
prevouts = set()
+ foreign_por = False
for i, txi in self.input_iter():
# check for duplicate inputs
@@ -1884,6 +1885,9 @@ def consider_inputs(self, cosign_xfp=None):
# - also finds appropriate multisig wallet to be used
inp.determine_my_signing_key(i, utxo, self.my_xfp, self, cosign_xfp)
+ if self.por322 and i and not inp.num_our_keys:
+ foreign_por = True
+
if inp.required_key and not inp.is_segwit and not inp.utxo:
raise FatalPSBTIssue('Legacy input #%d requires non-witness UTXO' % i)
@@ -1919,6 +1923,9 @@ def consider_inputs(self, cosign_xfp=None):
# XXX scan witness data provided, and consider those ins signed if not multisig?
+ if foreign_por:
+ raise FatalPSBTIssue("Foreign inputs not allowed in BIP-322 Proof of Reserves")
+
if not foreign and not unverified_witness_utxo:
# no foreign inputs, we can calculate the total input value
self.total_value_in = total_in
### testing/test_bip322.py
@@ -320,8 +320,7 @@ def test_bip322_Xth_input_witness_utxo(ins, bip322_txn, start_sign, cap_story, e
[["p2wpkh", None, None], ["p2pkh", None, 10000000], ["p2pkh", None, 10000000]],
[["p2sh-p2wpkh", None, None], ["p2sh-p2wpkh", None, 10000000], ["p2sh-p2wpkh", None, 10000000]],
])
-def test_bip322_incomplete_psbt_bip32_paths(ins, bip322_txn, start_sign, cap_story,
- verify_msg_bip322_por):
+def test_bip322_incomplete_psbt_bip32_paths(ins, bip322_txn, start_sign, cap_story):
def hack(psbt_in):
without_paths = 0 if len(psbt_in.inputs) == 1 else 1
@@ -332,16 +331,11 @@ def hack(psbt_in):
psbt, _ = bip322_txn(ins, psbt_hacker=hack)
start_sign(psbt)
title, story = cap_story()
+ assert title == "Failure"
if len(ins) == 1:
- assert title == "Failure"
assert 'PSBT does not contain any key path information.' in story
else:
- verify_msg_bip322_por("POR")
- time.sleep(.1)
- title, story = cap_story()
- assert "warning" in story
- assert "Limited Signing" in story
- assert "because we do not know the key: 1" in story
+ assert "Foreign inputs not allowed in BIP-322 Proof of Reserves" in story
def test_bip322_por_input0_bip32_paths_required(bip322_txn, start_sign, cap_story):Why this scored 62/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.