Abort signing when proposed multisig import fails
What changed, and why it matters
This firmware update changes how Passport handles Bitcoin transactions that come with a new multisig wallet configuration. Previously, if importing that proposed multisig config failed, the device would warn the user but still allow the transaction to be signed. Now it aborts signing instead. It also changes the default behavior so the device always asks before using a temporary multisig config rather than silently skipping the import. The commit does not describe this as a security fix, but the change removes a risky path where a transaction could be signed under an unapproved or failed wallet setup.
Treat as a security-hardening change worth reviewing in context. Verify that aborting signing on failed multisig import does not introduce denial-of-service for legitimate transactions, and that MUSIG_TEMP_DEFAULT change is reflected in user-facing documentation. Consider whether a CVE or security advisory is warranted if prior behavior could have led to signing under attacker-controlled multisig parameters.
Security signals we found
Behavioral change aborts transaction signing when multisig wallet import fails
Default policy changed from silent skip to explicit user ask for temporary multisig configs
Unit test added to enforce abort-on-failed-import behavior
Original code allowed signing despite failed/unapproved multisig config
Evidence from the diff
In sign_psbt_common_flow.py, check_multisig_import() no longer shows an ErrorPage with the message ‘The transaction can still be signed, but this multisig config will not be saved.’ when ImportMultisigWalletFlow returns false. Instead it calls set_result(None) and returns, aborting the signing flow. public_constants.py changes MUSIG_TEMP_DEFAULT from MUSIG_SKIP to MUSIG_ASK, meaning temporary multisig configs now require explicit user approval by default. A unit test is added to verify that failed multisig import aborts signing, successful import proceeds, and no approval needed proceeds.
Changed components
ports/stm32/boards/Passport/modules/flows/sign_psbt_common_flow.pyports/stm32/boards/Passport/modules/public_constants.pyports/stm32/boards/Passport/modules/tests/test_unit.pyports/stm32/boards/Passport/modules/tests/unit/psbt_multisig_approval.pyInspect captured patch +89 / −6
### ports/stm32/boards/Passport/modules/flows/sign_psbt_common_flow.py
@@ -42,17 +42,14 @@ async def validate_psbt(self):
async def check_multisig_import(self):
from flows import ImportMultisigWalletFlow
- from pages import ErrorPage
# Based on the import mode and whether this already exists, the validation step
# will have set this flag.
if self.psbt.multisig_import_needs_approval:
result = await ImportMultisigWalletFlow(self.psbt.active_multisig).run()
if not result:
- text = 'The transaction can still be signed, but this multisig config will not be saved.'
- result2 = await ErrorPage(text=text, left_micron=microns.Back).show()
- if not result2:
- return
+ self.set_result(None)
+ return
self.goto(self.show_transaction_details)
### ports/stm32/boards/Passport/modules/public_constants.py
@@ -113,7 +113,7 @@
MUSIG_ASK = const(1)
MUSIG_SKIP = const(2)
MUSIG_DEFAULT = MUSIG_ASK
-MUSIG_TEMP_DEFAULT = MUSIG_SKIP
+MUSIG_TEMP_DEFAULT = MUSIG_ASK
# Default Directories
DIR_BACKUPS = 'backups'
### ports/stm32/boards/Passport/modules/tests/test_unit.py
@@ -20,6 +20,10 @@ def test_ext_settings(test):
assert test('ext_settings.py') == b'OK'
+def test_psbt_multisig_approval(test):
+ assert test('psbt_multisig_approval.py') == b'OK'
+
+
def test_ui(test):
assert test('ui.py') == b'OK'
### ports/stm32/boards/Passport/modules/tests/unit/psbt_multisig_approval.py
@@ -0,0 +1,82 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# Test approval of multisig wallets proposed by PSBTs.
+
+import flows
+import uasyncio as asyncio
+from flows import SignPsbtCommonFlow
+from public_constants import MUSIG_ASK, MUSIG_TEMP_DEFAULT
+
+
+class FakeImportMultisigWalletFlow:
+ result = None
+ calls = 0
+
+ def __init__(self, wallet):
+ assert wallet == 'proposed-wallet'
+ FakeImportMultisigWalletFlow.calls += 1
+
+ async def run(self):
+ return self.result
+
+
+class FakePsbt:
+ def __init__(self, needs_approval):
+ self.multisig_import_needs_approval = needs_approval
+ self.active_multisig = 'proposed-wallet'
+
+
+class FakeSignFlow:
+ def __init__(self, needs_approval):
+ self.psbt = FakePsbt(needs_approval)
+ self.show_transaction_details = 'transaction-details'
+ self.completed = False
+ self.result = 'unset'
+ self.next_state = None
+
+ def set_result(self, result):
+ self.completed = True
+ self.result = result
+
+ def goto(self, state):
+ self.next_state = state
+
+
+async def run_tests():
+ original_import_flow = flows.ImportMultisigWalletFlow
+
+ try:
+ flows.ImportMultisigWalletFlow = FakeImportMultisigWalletFlow
+
+ FakeImportMultisigWalletFlow.result = False
+ FakeImportMultisigWalletFlow.calls = 0
+ flow = FakeSignFlow(needs_approval=True)
+ await SignPsbtCommonFlow.check_multisig_import(flow)
+ assert FakeImportMultisigWalletFlow.calls == 1
+ assert flow.completed
+ assert flow.result is None
+ assert flow.next_state is None
+
+ FakeImportMultisigWalletFlow.result = True
+ FakeImportMultisigWalletFlow.calls = 0
+ flow = FakeSignFlow(needs_approval=True)
+ await SignPsbtCommonFlow.check_multisig_import(flow)
+ assert FakeImportMultisigWalletFlow.calls == 1
+ assert not flow.completed
+ assert flow.next_state == flow.show_transaction_details
+
+ FakeImportMultisigWalletFlow.calls = 0
+ flow = FakeSignFlow(needs_approval=False)
+ await SignPsbtCommonFlow.check_multisig_import(flow)
+ assert FakeImportMultisigWalletFlow.calls == 0
+ assert not flow.completed
+ assert flow.next_state == flow.show_transaction_details
+
+ assert MUSIG_TEMP_DEFAULT == MUSIG_ASK
+ return_value.write(b'OK')
+ finally:
+ flows.ImportMultisigWalletFlow = original_import_flow
+
+
+asyncio.run(run_tests())Why this scored 59/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.