sign_utils: use checked arithmetic for liquid assets
What changed, and why it matters
This commit fixes arithmetic handling in Blockstream Jade's signing code for Liquid (Elements-based) transactions. Previously, the wallet added up asset amounts using plain 64-bit integer math and, for change outputs, relied on an underflow trick (0 - value) to subtract amounts. That could overflow or underflow if a malicious host provided crafted values, potentially letting a transaction pass validation when it should not. The patch introduces checked add/subtract helpers, tracks change separately, and aborts signing if the totals do not add up safely.
Treat this as a security fix and include it in the next firmware release. Review whether any other host-provided amount accumulations elsewhere in the codebase use unchecked arithmetic and apply the same checked helpers. No explicit CVE or advisory is supplied, so downstream consumers should monitor Blockstream's release notes for attribution or a CVE assignment.
Security signals we found
Integer overflow/underflow protection added to host-provided amount aggregation
Elimination of 0 - value unsigned subtraction idiom for change outputs
Addition of checked_add_uint64 / checked_sub_uint64 primitives
Validation now fails closed on arithmetic failure (returns false and logs)
Comment explicitly notes amounts are host-provided and arithmetic is checked
Evidence from the diff
The change refactors asset_summary_update() in main/process/sign_utils.c. It adds checked_add_uint64() and checked_sub_uint64() helpers in main/utils/util.h and uses them when accumulating validated input values and change values. A new asset_summary_update_change() path records change outputs in a dedicated change_value field instead of subtracting via 0 - outinfo->value. asset_summary_validate() now subtracts change_value from validated_value with an underflow check before comparing to the declared value. This removes reliance on unsigned wrap-around and prevents overflow/underflow in host-provided amount aggregation during Liquid transaction signing.
Changed components
main/process/sign_utils.cmain/process/sign_utils.hmain/utils/util.hLiquid/Elements asset transaction signing flowInspect captured patch +62 / −8
### main/process/sign_utils.c
@@ -258,8 +258,11 @@ bool params_additional_info(jade_process_t* process, CborValue* params, const st
return true;
}
-bool asset_summary_update(asset_summary_t* sums, const size_t num_sums, const uint8_t* asset_id,
- const size_t asset_id_len, const uint64_t value)
+// Update the first summary record found with matching asset_id, adding the passed
+// sats amount to either the verified total or the verified change total.
+// NOTE: All arithmetic is checked as the amounts being totalled here are all host-provided
+static bool asset_summary_update_impl(asset_summary_t* sums, const size_t num_sums, const uint8_t* asset_id,
+ const size_t asset_id_len, const uint64_t value, const bool is_change)
{
if (!sums) {
return true; // Nothing to do
@@ -270,20 +273,44 @@ bool asset_summary_update(asset_summary_t* sums, const size_t num_sums, const ui
// Add passed sats amount to the first record found with matching asset_id
for (size_t i = 0; i < num_sums; ++i) {
if (!memcmp(asset_id, sums[i].asset_id, sizeof(sums[i].asset_id))) {
- sums[i].validated_value += value;
+ uint64_t* const total = is_change ? &sums[i].change_value : &sums[i].validated_value;
+ if (!checked_add_uint64(*total, value, total)) {
+ JADE_LOGW("Asset summary overflow");
+ return false;
+ }
return true;
}
}
return false;
}
+bool asset_summary_update(asset_summary_t* sums, const size_t num_sums, const uint8_t* asset_id,
+ const size_t asset_id_len, const uint64_t value)
+{
+ const bool is_change = false;
+ return asset_summary_update_impl(sums, num_sums, asset_id, asset_id_len, value, is_change);
+}
+
+bool asset_summary_update_change(asset_summary_t* sums, const size_t num_sums, const uint8_t* asset_id,
+ const size_t asset_id_len, const uint64_t value)
+{
+ const bool is_change = true;
+ return asset_summary_update_impl(sums, num_sums, asset_id, asset_id_len, value, is_change);
+}
+
bool asset_summary_validate(asset_summary_t* sums, const size_t num_sums)
{
JADE_ASSERT(!sums == !num_sums);
// Check every asset record (if any) has been fully validated
for (size_t i = 0; i < num_sums; ++i) {
- if (sums[i].value != sums[i].validated_value) {
+ // Deduct any change recorded against this summary, then check the remainder
+ // matches the declared total.
+ // Change is accumulated separately so validation does not depend on whether inputs
+ // and outputs were processed in a particular order.
+ uint64_t remainder = 0;
+ if (!checked_sub_uint64(sums[i].validated_value, sums[i].change_value, &remainder)
+ || remainder != sums[i].value) {
char* asset_id_hex = NULL;
wally_hex_from_bytes(sums[i].asset_id, sizeof(sums[i].asset_id), &asset_id_hex);
JADE_LOGW("Failed to validate input/output summary for %s", asset_id_hex);
@@ -654,9 +681,8 @@ bool validate_elements_outputs(const network_t network_id, const struct wally_tx
bool is_valid;
if (outinfo->flags & OUTPUT_FLAG_CHANGE) {
- // NOTE: change outputs are subtracted from the relevant 'input summary'.
- is_valid = asset_summary_update(
- in_sums, num_in_sums, outinfo->asset_id, sizeof(outinfo->asset_id), 0 - outinfo->value);
+ is_valid = asset_summary_update_change(
+ in_sums, num_in_sums, outinfo->asset_id, sizeof(outinfo->asset_id), outinfo->value);
} else {
is_valid = asset_summary_update(
out_sums, num_out_sums, outinfo->asset_id, sizeof(outinfo->asset_id), outinfo->value);
### main/process/sign_utils.h
@@ -18,6 +18,7 @@ typedef struct _asset_summary {
uint8_t asset_id[32];
uint64_t value;
uint64_t validated_value;
+ uint64_t change_value;
} asset_summary_t;
// Maximum number of inputs/outputs accepted
@@ -43,7 +44,10 @@ WARN_UNUSED_RESULT bool params_commitment_data(
WARN_UNUSED_RESULT bool asset_summary_update(
asset_summary_t* sums, size_t num_sums, const uint8_t* asset_id, size_t asset_id_len, uint64_t value);
-bool asset_summary_validate(asset_summary_t* sums, size_t num_sums);
+WARN_UNUSED_RESULT bool asset_summary_update_change(
+ asset_summary_t* sums, size_t num_sums, const uint8_t* asset_id, size_t asset_id_len, uint64_t value);
+
+WARN_UNUSED_RESULT bool asset_summary_validate(asset_summary_t* sums, size_t num_sums);
WARN_UNUSED_RESULT bool update_elements_outputs(
const struct wally_tx* tx, commitment_t* commitments, output_info_t* outinfo, const char** errmsg);
### main/utils/util.h
@@ -150,4 +150,28 @@ size_t base32_to_bin(const char* b32_str, size_t b32_str_len, uint8_t* bin, size
WARN_UNUSED_RESULT bool bin_to_base32(
const uint8_t* bin, size_t bin_len, char* b32_str, size_t b32_str_len, bool use_padding);
+// Add two uint64 values, returning false on overflow (leaving '*result' untouched)
+WARN_UNUSED_RESULT static inline bool checked_add_uint64(const uint64_t lhs, const uint64_t rhs, uint64_t* result)
+{
+ JADE_ASSERT(result);
+
+ if (lhs > UINT64_MAX - rhs) {
+ return false;
+ }
+ *result = lhs + rhs;
+ return true;
+}
+
+// Subtract two uint64 values, returning false on underflow (leaving '*result' untouched)
+WARN_UNUSED_RESULT static inline bool checked_sub_uint64(const uint64_t lhs, const uint64_t rhs, uint64_t* result)
+{
+ JADE_ASSERT(result);
+
+ if (rhs > lhs) {
+ return false;
+ }
+ *result = lhs - rhs;
+ return true;
+}
+
#endif /* UTIL_H_ */Why this scored 69/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.