sign_tx: split out updating output info from validating it
What changed, and why it matters
This commit is a code cleanup in the transaction signing logic for Blockstream Jade hardware wallets. It splits one function into two: one that gathers output information and another that validates it. There is no direct evidence in the commit that this fixes a security vulnerability; it appears to be a refactoring to make the code clearer and to allow future reuse in PSBT signing.
No immediate action required. Treat as routine refactoring. If this commit is part of a larger security fix, review adjacent commits in the same release branch for related hardening changes.
Security signals we found
Refactoring of validation logic with no change to underlying checks
Separation of data mutation from validation (defensive coding improvement)
No new bounds checks, cryptographic operations, or permission changes introduced
Evidence from the diff
The change refactors validate_elements_outputs() in main/process/sign_utils.c. Previously, this function both populated output_info from transaction outputs/commitments and validated the resulting data. The commit introduces a new function update_elements_outputs() that handles only the population of output info, while validate_elements_outputs() now takes a const output_info_t* and performs only validation. The call site in sign_tx.c now invokes both functions sequentially. Error handling is also changed so that validate_elements_outputs() returns an error string pointer rather than directly calling jade_process_reject_message(). The commit message frames this as removing confusion and enabling code sharing with PSBT signing.
Changed components
main/process/sign_tx.cmain/process/sign_utils.cmain/process/sign_utils.hInspect captured patch +66 / −65
diff --git a/main/process/sign_tx.c b/main/process/sign_tx.c
index b7865d1..88f290d 100644
--- a/main/process/sign_tx.c
+++ b/main/process/sign_tx.c
@@ -475,11 +475,16 @@ static void sign_tx_impl(jade_process_t* process, const bool for_liquid)
goto cleanup;
}
- // Liquid: Validate commitment, outputs and additional_info
- if (for_liquid
- && !validate_elements_outputs(
- process, network_id, tx, txtype, commitments, output_info, in_sums, num_in_sums, out_sums, num_out_sums)) {
- goto cleanup;
+ // Liquid: Gather the (unblinded) output info for user confirmation,
+ // then validate output and additional_info values
+ if (for_liquid) {
+ const char* errmsg = NULL;
+ if (!update_elements_outputs(tx, commitments, output_info, &errmsg)
+ || !validate_elements_outputs(
+ network_id, tx, txtype, output_info, in_sums, num_in_sums, out_sums, num_out_sums, &errmsg)) {
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
+ goto cleanup;
+ }
}
const char* cancelmsg = NULL;
diff --git a/main/process/sign_utils.c b/main/process/sign_utils.c
index 210f8ef..5f0074c 100644
--- a/main/process/sign_utils.c
+++ b/main/process/sign_utils.c
@@ -543,88 +543,86 @@ cleanup:
return true;
}
-static bool add_output_info(
- commitment_t* commitments, const struct wally_tx_output* txoutput, output_info_t* outinfo, const char** errmsg)
+// Populate a (blinding_key, asset, value) output_info array for each tx output
+bool update_elements_outputs(
+ const struct wally_tx* tx, commitment_t* commitments, output_info_t* output_info, const char** errmsg)
{
+ JADE_ASSERT(tx);
JADE_ASSERT(commitments);
- JADE_ASSERT(txoutput);
- JADE_ASSERT(outinfo);
+ JADE_ASSERT(output_info);
JADE_INIT_OUT_PPTR(errmsg);
- JADE_STATIC_ASSERT(sizeof(outinfo->asset_id) == sizeof(commitments->asset_id));
- JADE_STATIC_ASSERT(sizeof(outinfo->blinding_key) == sizeof(commitments->blinding_key));
- JADE_ASSERT(!(outinfo->flags & (OUTPUT_FLAG_CONFIDENTIAL | OUTPUT_FLAG_HAS_UNBLINDED)));
- if (commitments->content != COMMITMENTS_NONE) {
- // Output to be confidential/blinded, use the commitments data
- outinfo->flags |= (OUTPUT_FLAG_CONFIDENTIAL | OUTPUT_FLAG_HAS_UNBLINDED);
+ for (size_t i = 0; i < tx->num_outputs; ++i) {
+ output_info_t* const outinfo = output_info + i;
+ const struct wally_tx_output* const txoutput = tx->outputs + i;
+ const commitment_t* commitment = commitments + i;
- // Fetch the asset_id, value, and optional blinding_key into the info struct
- memcpy(outinfo->asset_id, commitments->asset_id, sizeof(commitments->asset_id));
- outinfo->value = commitments->value;
+ JADE_STATIC_ASSERT(sizeof(outinfo->asset_id) == sizeof(commitment->asset_id));
+ JADE_STATIC_ASSERT(sizeof(outinfo->blinding_key) == sizeof(commitment->blinding_key));
- if (commitments->content & COMMITMENTS_BLINDING_KEY) {
- memcpy(outinfo->blinding_key, commitments->blinding_key, sizeof(commitments->blinding_key));
- outinfo->flags |= OUTPUT_FLAG_HAS_BLINDING_KEY;
- }
- } else if (txoutput->asset[0] != WALLY_TX_ASSET_CT_EXPLICIT_PREFIX
- || txoutput->value[0] != WALLY_TX_ASSET_CT_EXPLICIT_PREFIX) {
- // No blinding info for blinded output - may not be an issue if we're
- // not interested in this output. Just set flags appropriately.
- outinfo->flags |= OUTPUT_FLAG_CONFIDENTIAL;
+ JADE_ASSERT(!(outinfo->flags & (OUTPUT_FLAG_CONFIDENTIAL | OUTPUT_FLAG_HAS_UNBLINDED)));
+ if (commitment->content != COMMITMENTS_NONE) {
+ // Output to be confidential/blinded, use the commitment data
+ outinfo->flags |= (OUTPUT_FLAG_CONFIDENTIAL | OUTPUT_FLAG_HAS_UNBLINDED);
- // NOTE: This is not valid if this output has been validated as belonging to this wallet
- if (outinfo->flags & OUTPUT_FLAG_VALIDATED) {
- *errmsg = "Missing blinding information for wallet output";
- return false;
- }
- } else {
- // unconfidential, take directly from the tx
- outinfo->flags |= OUTPUT_FLAG_HAS_UNBLINDED;
+ // Fetch the asset_id, value, and optional blinding_key into the info struct
+ memcpy(outinfo->asset_id, commitment->asset_id, sizeof(commitment->asset_id));
+ outinfo->value = commitment->value;
- // Copy the asset ID without the leading unconfidential tag byte
- // NOTE: we reverse the asset-id bytes to the 'display' order
- reverse(outinfo->asset_id, txoutput->asset + 1, sizeof(outinfo->asset_id));
+ if (commitment->content & COMMITMENTS_BLINDING_KEY) {
+ memcpy(outinfo->blinding_key, commitment->blinding_key, sizeof(commitment->blinding_key));
+ outinfo->flags |= OUTPUT_FLAG_HAS_BLINDING_KEY;
+ }
+ } else if (txoutput->asset[0] != WALLY_TX_ASSET_CT_EXPLICIT_PREFIX
+ || txoutput->value[0] != WALLY_TX_ASSET_CT_EXPLICIT_PREFIX) {
+ // No blinding info for blinded output - may not be an issue if we're
+ // not interested in this output. Just set flags appropriately.
+ outinfo->flags |= OUTPUT_FLAG_CONFIDENTIAL;
+
+ // NOTE: This is not valid if this output has been validated as belonging to this wallet
+ if (outinfo->flags & OUTPUT_FLAG_VALIDATED) {
+ *errmsg = "Missing blinding information for wallet output";
+ return false;
+ }
+ } else {
+ // unconfidential, take directly from the tx
+ outinfo->flags |= OUTPUT_FLAG_HAS_UNBLINDED;
- JADE_WALLY_VERIFY(
- wally_tx_confidential_value_to_satoshi(txoutput->value, txoutput->value_len, &outinfo->value));
- }
+ // Copy the asset ID without the leading unconfidential tag byte
+ // NOTE: we reverse the asset-id bytes to the 'display' order
+ reverse(outinfo->asset_id, txoutput->asset + 1, sizeof(outinfo->asset_id));
+ JADE_WALLY_VERIFY(
+ wally_tx_confidential_value_to_satoshi(txoutput->value, txoutput->value_len, &outinfo->value));
+ }
+ }
return true;
}
-bool validate_elements_outputs(jade_process_t* process, const network_t network_id, const struct wally_tx* tx,
- const TxType_t txtype, commitment_t* commitments, output_info_t* output_info, asset_summary_t* in_sums,
- const size_t num_in_sums, asset_summary_t* out_sums, const size_t num_out_sums)
+bool validate_elements_outputs(const network_t network_id, const struct wally_tx* tx, const TxType_t txtype,
+ const output_info_t* const output_info, asset_summary_t* in_sums, const size_t num_in_sums,
+ asset_summary_t* out_sums, const size_t num_out_sums, const char** errmsg)
{
JADE_ASSERT(tx);
- JADE_ASSERT(commitments);
JADE_ASSERT(output_info);
-
- const char* errmsg = NULL;
+ JADE_INIT_OUT_PPTR(errmsg);
uint8_t policy_asset[ASSET_TAG_LEN];
network_to_policy_asset(network_id, policy_asset, sizeof(policy_asset));
- // Check the trusted commitments: expect one element in the array for each output.
- // Can be null for unblinded outputs as we will skip them.
- // Populate an `output_index` -> (blinding_key, asset, value) map
-
// NOTE: some advanced tx types permit some outputs to be blind (ie blinded, without unblinding info/proofs)
// By default/in the basic 'send payment' case all outputs must have unconfidential/unblinded.
const bool allow_blind_outputs = txtype == TXTYPE_SWAP; // swaps allow 'other wallets' blind outputs
for (size_t i = 0; i < tx->num_outputs; ++i) {
// Gather the (unblinded) output info for user confirmation
- output_info_t* outinfo = output_info + i;
- if (!add_output_info(&commitments[i], &tx->outputs[i], outinfo, &errmsg)) {
- goto done;
- }
+ const output_info_t* const outinfo = output_info + i;
// If are not allowing blinded outputs, check each confidential output has unblinding info
if (!allow_blind_outputs && outinfo->flags & OUTPUT_FLAG_CONFIDENTIAL) {
if (!(outinfo->flags & OUTPUT_FLAG_HAS_UNBLINDED) || !(outinfo->flags & OUTPUT_FLAG_HAS_BLINDING_KEY)) {
- errmsg = "Missing trusted commitment data for blinded output";
- goto done;
+ *errmsg = "Missing trusted commitment data for blinded output";
+ return false;
}
}
@@ -643,11 +641,6 @@ bool validate_elements_outputs(jade_process_t* process, const network_t network_
}
}
}
-done:
- if (errmsg) {
- jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
- return false;
- }
return true;
}
diff --git a/main/process/sign_utils.h b/main/process/sign_utils.h
index f578924..83a9eb7 100644
--- a/main/process/sign_utils.h
+++ b/main/process/sign_utils.h
@@ -39,9 +39,12 @@ bool asset_summary_update(
bool asset_summary_validate(asset_summary_t* sums, size_t num_sums);
-bool validate_elements_outputs(jade_process_t* process, network_t network_id, const struct wally_tx* tx,
- TxType_t txtype, commitment_t* commitments, output_info_t* output_info, asset_summary_t* in_sums,
- size_t num_in_sums, asset_summary_t* out_sums, size_t num_out_sums);
+bool update_elements_outputs(
+ const struct wally_tx* tx, commitment_t* commitments, output_info_t* outinfo, const char** errmsg);
+
+bool validate_elements_outputs(network_t network_id, const struct wally_tx* tx, TxType_t txtype,
+ const output_info_t* const output_info, asset_summary_t* in_sums, size_t num_in_sums, asset_summary_t* out_sums,
+ size_t num_out_sums, const char** errmsg);
// Whether or not the sighash flags for a given tx/signature type is supported
bool sighash_is_supported(TxType_t txtype, uint32_t sig_type, uint32_t sighash, bool for_liquid, bool is_partial);
Why this scored 11/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.