wallet: fix unhandled wrong asset in wallet summaries
What changed, and why it matters
This commit fixes a bug in the Blockstream Jade hardware wallet where a function that tracks assets during transaction signing could fail silently. Previously, if an unexpected or wrong asset was encountered while building a summary of transaction inputs and outputs, the wallet ignored the failure and continued signing. The patch now checks the function's return value and aborts signing with an error. This could have allowed a malicious or malformed transaction to bypass the wallet's balance checks, potentially misleading the user about what assets were actually being moved.
Treat this as a security-relevant fix. Review whether the previous behavior could have been exploited to misreport balances or approve invalid transactions, and consider whether additional hardening (e.g., asserting on unexpected assets, stricter pre-validation) is warranted. Users should upgrade to a firmware version containing this commit.
Security signals we found
Unchecked return value from asset_summary_update() in signing path
Potential bypass of asset/balance validation during transaction signing
Wrong or unknown asset could be silently dropped from wallet summaries
Patch adds explicit failure handling and aborts signing on validation error
Evidence from the diff
The patch modifies three files in the transaction/PSBT signing path: sign_psbt.c, sign_tx.c, and sign_utils.c. The function asset_summary_update() returns a bool indicating whether the asset was found and the amount successfully added to the running summary. Before this commit, callers in all three locations ignored that return value. The patch adds explicit checks: if asset_summary_update() returns false, signing is aborted with CBOR_RPC_BAD_PARAMETERS and the message ‘Failed to validate input/output summary information’. This prevents silent corruption or omission of asset summaries, which are used to validate that inputs cover outputs and to compute net balances shown to the user.
Changed components
main/process/sign_psbt.cmain/process/sign_tx.cmain/process/sign_utils.casset_summary_update()Transaction/PSBT signing flowLiquid/Elements asset balance validationInspect captured patch +18 / −5
diff --git a/main/process/sign_psbt.c b/main/process/sign_psbt.c
index 65c8c47..5b6770b 100644
--- a/main/process/sign_psbt.c
+++ b/main/process/sign_psbt.c
@@ -825,7 +825,11 @@ int sign_psbt(jade_process_t* process, CborValue* params, const network_t networ
// TODO: additional_info should store asset_ids in binary order,
// so we shouldn't have to reverse_in_place() here
reverse_in_place(asset_id, sizeof(asset_id));
- asset_summary_update(in_sums, num_in_sums, asset_id, sizeof(asset_id), input->amount);
+ if (!asset_summary_update(in_sums, num_in_sums, asset_id, sizeof(asset_id), input->amount)) {
+ *errmsg = "Failed to validate input/output summary information";
+ retval = CBOR_RPC_BAD_PARAMETERS;
+ goto cleanup;
+ }
}
uint32_t sig_type;
diff --git a/main/process/sign_tx.c b/main/process/sign_tx.c
index 81aa234..7167509 100644
--- a/main/process/sign_tx.c
+++ b/main/process/sign_tx.c
@@ -619,7 +619,11 @@ static void sign_tx_impl(jade_process_t* process, const bool for_liquid)
if (params_commitment_data(¶ms, &c, NULL, &errmsg)) {
JADE_ASSERT(!errmsg);
// Valid input commitments: update the summary
- asset_summary_update(in_sums, num_in_sums, c.asset_id, sizeof(c.asset_id), c.value);
+ if (!asset_summary_update(in_sums, num_in_sums, c.asset_id, sizeof(c.asset_id), c.value)) {
+ errmsg = "Failed to validate input/output summary information";
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
+ goto cleanup;
+ }
} else if (errmsg) {
// Invalid input commitments (rather than simply not present)
jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
diff --git a/main/process/sign_utils.c b/main/process/sign_utils.c
index d3004c0..c74eeca 100644
--- a/main/process/sign_utils.c
+++ b/main/process/sign_utils.c
@@ -628,14 +628,19 @@ bool validate_elements_outputs(const network_t network_id, const struct wally_tx
if (outinfo->flags & OUTPUT_FLAG_IS_OURS) {
JADE_ASSERT(outinfo->flags & OUTPUT_FLAG_HAS_UNBLINDED);
+ bool is_valid;
if (outinfo->flags & OUTPUT_FLAG_CHANGE) {
// NOTE: change outputs are subtracted from the relevant 'input summary'.
- asset_summary_update(
- in_sums, num_in_sums, outinfo->asset_id, sizeof(outinfo->asset_id), (0 - outinfo->value));
+ is_valid = asset_summary_update(
+ in_sums, num_in_sums, outinfo->asset_id, sizeof(outinfo->asset_id), 0 - outinfo->value);
} else {
- asset_summary_update(
+ is_valid = asset_summary_update(
out_sums, num_out_sums, outinfo->asset_id, sizeof(outinfo->asset_id), outinfo->value);
}
+ if (!is_valid) {
+ *errmsg = "Failed to validate input/output summary information";
+ return false;
+ }
}
}
return true;
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.