What changed, and why it matters
This firmware update tightens checks on Bitcoin transaction amounts inside PSBT files. Before the patch, input amounts were only checked with a simple 'greater than zero' assertion, and the running total of input value was not capped. A malicious or malformed PSBT could use negative, zero, or absurdly large input amounts to make the wallet miscalculate the transaction fee or bypass sanity checks. The patch rejects any input amount outside the valid Bitcoin range and stops the total from exceeding Bitcoin's maximum money limit, matching protections that already existed for outputs.
Treat this as a security-hardening fix and include it in the next firmware release. Users should upgrade when available. Wallet software that builds PSBTs for Passport should ensure it does not create inputs with zero, negative, or over-MAX_MONEY amounts.
Security signals we found
Addition of input amount range validation against MAX_MONEY
Addition of cumulative input total overflow validation
Replacement of assert with explicit FatalPSBTIssue exception for invalid input amounts
Expanded unit tests covering negative, zero, and overflow input/output amounts
Existing output validation extended with index reporting
Evidence from the diff
The commit modifies psbt.py in the Passport firmware. In output_iter, the total output overflow check now reports the offending output index. In consider_inputs, the previous ‘assert utxo.nValue > 0’ is replaced by an explicit range check ‘0 < utxo.nValue <= MAX_MONEY’ that raises FatalPSBTIssue, and a new running-total check ‘total_in > MAX_MONEY’ is added. Unit tests are expanded from output-only to cover both input and output consensus bounds, including zero, negative, per-input over MAX_MONEY, and cumulative over MAX_MONEY cases.
Changed components
ports/stm32/boards/Passport/modules/psbt.pyports/stm32/boards/Passport/modules/tests/test_unit.pyports/stm32/boards/Passport/modules/tests/unit/psbt_amounts.pyports/stm32/boards/Passport/modules/tests/unit/psbt_output_values.pyInspect captured patch +140 / −59
### ports/stm32/boards/Passport/modules/psbt.py
@@ -1009,7 +1009,7 @@ def output_iter(self):
total_out += tx_out.nValue
if total_out > MAX_MONEY:
- raise FatalPSBTIssue('Total output amount exceeds maximum')
+ raise FatalPSBTIssue('Total output amount exceeds maximum at output #%d' % idx)
cont = fd.tell()
yield idx, tx_out
@@ -1414,8 +1414,12 @@ def consider_inputs(self):
# pull out just the CTXOut object (expensive)
utxo = inp.get_utxo(txi.prevout.n)
- assert utxo.nValue > 0
+ if not 0 < utxo.nValue <= MAX_MONEY:
+ raise FatalPSBTIssue('Invalid amount for input #%d' % i)
+
total_in += utxo.nValue
+ if total_in > MAX_MONEY:
+ raise FatalPSBTIssue('Total input amount exceeds maximum at input #%d' % i)
# Look at what kind of input this will be, and therefore what
# type of signing will be required, and which key we need.
### ports/stm32/boards/Passport/modules/tests/test_unit.py
@@ -28,5 +28,5 @@ def test_foundation(test):
assert test('foundation.py') == b'OK'
-def test_psbt_output_values(test):
- assert test('psbt_output_values.py') == b'OK'
+def test_psbt_amounts(test):
+ assert test('psbt_amounts.py') == b'OK'
### ports/stm32/boards/Passport/modules/tests/unit/psbt_amounts.py
@@ -0,0 +1,132 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# Test consensus bounds for input and output amounts in unsigned PSBT transactions.
+
+from uio import BytesIO
+
+from exceptions import FatalPSBTIssue
+from psbt import psbtObject
+from public_constants import MAX_MONEY
+from serializations import CTxOut
+
+
+P2WPKH_SCRIPT = b'\x00\x14' + (b'\x11' * 20)
+
+
+class FakePSBT:
+ def __init__(self, values):
+ self.fd = BytesIO(b''.join(CTxOut(value, P2WPKH_SCRIPT).serialize() for value in values))
+ self.vout_start = 0
+ self.num_outputs = len(values)
+ self.total_value_out = None
+
+
+class FakePrevout:
+ n = 0
+
+
+class FakeTxIn:
+ prevout = FakePrevout()
+
+
+class FakeInput:
+ def __init__(self, value):
+ self.value = value
+ self.fully_signed = False
+ self.num_our_keys = 1
+ self.required_key = b'key'
+ self.is_segwit = False
+
+ def has_utxo(self):
+ return True
+
+ def get_utxo(self, _idx):
+ return CTxOut(self.value, P2WPKH_SCRIPT)
+
+ def determine_my_signing_key(self, _idx, _utxo, _xfp, _psbt):
+ pass
+
+
+class FakeInputPSBT:
+ def __init__(self, values):
+ self.inputs = [FakeInput(value) for value in values]
+ self.my_xfp = 0
+ self.total_value_in = None
+ self.fee_is_verified = True
+ self.presigned_inputs = set()
+ self.num_inputs = len(values)
+ self.warnings = []
+
+ def input_iter(self):
+ for idx in range(self.num_inputs):
+ yield idx, FakeTxIn()
+
+
+def read_outputs(values):
+ psbt = FakePSBT(values)
+ parsed = [tx_out.nValue for _, tx_out in psbtObject.output_iter(psbt)]
+ return psbt, parsed
+
+
+def read_inputs(values):
+ psbt = FakeInputPSBT(values)
+ psbtObject.consider_inputs(psbt)
+ return psbt
+
+
+def assert_invalid_outputs(values, invalid_idx):
+ psbt = FakePSBT(values)
+ try:
+ list(psbtObject.output_iter(psbt))
+ except FatalPSBTIssue as exc:
+ assert '#%d' % invalid_idx in str(exc)
+ assert psbt.total_value_out is None
+ return
+ raise AssertionError('Expected invalid output values to be rejected')
+
+
+def assert_invalid_inputs(values, invalid_idx):
+ psbt = FakeInputPSBT(values)
+ try:
+ psbtObject.consider_inputs(psbt)
+ except FatalPSBTIssue as exc:
+ assert '#%d' % invalid_idx in str(exc)
+ assert psbt.total_value_in is None
+ return
+ raise AssertionError('Expected invalid input values to be rejected')
+
+
+zero, parsed = read_outputs([0])
+assert parsed == [0]
+assert zero.total_value_out == 0
+
+maximum, parsed = read_outputs([MAX_MONEY])
+assert parsed == [MAX_MONEY]
+assert maximum.total_value_out == MAX_MONEY
+
+split_maximum, parsed = read_outputs([MAX_MONEY - 1, 1])
+assert parsed == [MAX_MONEY - 1, 1]
+assert split_maximum.total_value_out == MAX_MONEY
+
+assert_invalid_outputs([-1], 0)
+assert_invalid_outputs([MAX_MONEY + 1], 0)
+assert_invalid_outputs([1, -(1 << 63)], 1)
+assert_invalid_outputs([MAX_MONEY, 1], 1)
+
+one_input = read_inputs([1])
+assert one_input.total_value_in == 1
+
+maximum_input = read_inputs([MAX_MONEY])
+assert maximum_input.total_value_in == MAX_MONEY
+
+split_maximum_inputs = read_inputs([MAX_MONEY - 1, 1])
+assert split_maximum_inputs.total_value_in == MAX_MONEY
+
+assert_invalid_inputs([0], 0)
+assert_invalid_inputs([-1], 0)
+assert_invalid_inputs([MAX_MONEY + 1], 0)
+assert_invalid_inputs([1, 1 << 62], 1)
+assert_invalid_inputs([MAX_MONEY, 1], 1)
+
+return_value.write(b'OK')
### ports/stm32/boards/Passport/modules/tests/unit/psbt_output_values.py
@@ -1,55 +0,0 @@
-# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# Test consensus bounds for output amounts in unsigned PSBT transactions.
-
-from uio import BytesIO
-
-from exceptions import FatalPSBTIssue
-from psbt import psbtObject
-from public_constants import MAX_MONEY
-from serializations import CTxOut
-
-
-P2WPKH_SCRIPT = b'\x00\x14' + (b'\x11' * 20)
-
-
-class FakePSBT:
- def __init__(self, values):
- self.fd = BytesIO(b''.join(CTxOut(value, P2WPKH_SCRIPT).serialize() for value in values))
- self.vout_start = 0
- self.num_outputs = len(values)
- self.total_value_out = None
-
-
-def read_outputs(values):
- psbt = FakePSBT(values)
- parsed = [tx_out.nValue for _, tx_out in psbtObject.output_iter(psbt)]
- return psbt, parsed
-
-
-def assert_invalid(values):
- try:
- read_outputs(values)
- except FatalPSBTIssue:
- return
- raise AssertionError('Expected invalid output values to be rejected')
-
-
-zero, parsed = read_outputs([0])
-assert parsed == [0]
-assert zero.total_value_out == 0
-
-maximum, parsed = read_outputs([MAX_MONEY])
-assert parsed == [MAX_MONEY]
-assert maximum.total_value_out == MAX_MONEY
-
-split_maximum, parsed = read_outputs([MAX_MONEY - 1, 1])
-assert parsed == [MAX_MONEY - 1, 1]
-assert split_maximum.total_value_out == MAX_MONEY
-
-assert_invalid([-1])
-assert_invalid([MAX_MONEY + 1])
-assert_invalid([MAX_MONEY, 1])
-
-return_value.write(b'OK')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.