sign_tx: Strengthen liquid commitment checks against the tx being signed
What changed, and why it matters
This commit hardens how Blockstream Jade validates confidential (blinded) transaction data on Liquid. It moves and strengthens checks so that any asset/value commitment pair supplied by the caller must match the transaction actually being signed, and it removes redundant commitment fields from an internal data structure. The change is defensive: it reduces the chance that a malicious or buggy host could trick the hardware wallet into signing a transaction with mismatched blinding data.
Review the refactored validation logic for completeness, especially the SPIRAM-gated explicit proof verification and the handling of partial commitment fields. Run the updated test_jade.py bad-commitment test vectors to confirm all expected rejections still occur. Consider whether any caller still relies on the removed verify_commitment_consistent() symbol.
Security signals we found
Strengthens validation of asset/value commitment pairs against the transaction being signed
Makes input commitments mandatory and output commitments optional-but-matched
Centralizes commitment validation in get_commitment_data() to reduce inconsistent validation paths
Removes redundant commitment fields from commitment_t to prevent use of unvalidated data later
Improves error messages to clarify 'trusted commitment' mismatches
Evidence from the diff
The patch refactors commitment handling in Jade’s sign_tx flow. It introduces an ext_commitment_t struct to hold computed asset_generator/value_commitment separately from commitment_t, removes COMMITMENTS_INCLUDES_COMMITMENTS, and moves validation into get_commitment_data(). That function now accepts an optional wally_tx_output pointer; when provided (output path), supplied commitments are checked against the tx output, and when absent (input path), commitments are mandatory. Reconstruction checks for asset generator and value commitment, plus explicit proof verification on SPIRAM devices, are now performed at fetch time. verify_commitment_consistent() is removed and its logic inlined. Error messages are unified to reference ‘trusted commitment’. Tests are updated to match new messages.
Changed components
main/process/get_commitments.cmain/process/process_utils.hmain/process/sign_tx.cmain/process/sign_utils.cmain/process/sign_utils.htest_jade.pyInspect captured patch +209 / −227
diff --git a/main/process/get_commitments.c b/main/process/get_commitments.c
index d30a6f6..60f3b41 100644
--- a/main/process/get_commitments.c
+++ b/main/process/get_commitments.c
@@ -15,21 +15,19 @@ static void reply_commitments(const void* ctx, CborEncoder* container)
{
JADE_ASSERT(ctx);
- const commitment_t* commitments = (const commitment_t*)ctx;
- JADE_ASSERT(commitments->content == (COMMITMENTS_ABF | COMMITMENTS_VBF | COMMITMENTS_INCLUDES_COMMITMENTS));
+ const ext_commitment_t* ec = (const ext_commitment_t*)ctx;
+ const commitment_t* c = &ec->c;
CborEncoder map_encoder; // result data
CborError cberr = cbor_encoder_create_map(container, &map_encoder, 6);
JADE_ASSERT(cberr == CborNoError);
- add_bytes_to_map(&map_encoder, "abf", commitments->abf, sizeof(commitments->abf));
- add_bytes_to_map(&map_encoder, "vbf", commitments->vbf, sizeof(commitments->vbf));
- add_bytes_to_map(
- &map_encoder, "asset_generator", commitments->asset_generator, sizeof(commitments->asset_generator));
- add_bytes_to_map(
- &map_encoder, "value_commitment", commitments->value_commitment, sizeof(commitments->value_commitment));
- add_bytes_to_map(&map_encoder, "asset_id", commitments->asset_id, sizeof(commitments->asset_id));
- add_uint_to_map(&map_encoder, "value", commitments->value);
+ add_bytes_to_map(&map_encoder, "abf", c->abf, sizeof(c->abf));
+ add_bytes_to_map(&map_encoder, "vbf", c->vbf, sizeof(c->vbf));
+ add_bytes_to_map(&map_encoder, "asset_generator", ec->asset_generator, sizeof(ec->asset_generator));
+ add_bytes_to_map(&map_encoder, "value_commitment", ec->value_commitment, sizeof(ec->value_commitment));
+ add_bytes_to_map(&map_encoder, "asset_id", c->asset_id, sizeof(c->asset_id));
+ add_uint_to_map(&map_encoder, "value", c->value);
cberr = cbor_encoder_close_container(container, &map_encoder);
JADE_ASSERT(cberr == CborNoError);
@@ -46,14 +44,15 @@ void get_commitments_process(void* process_ptr)
GET_MSG_PARAMS(process);
const char* errmsg = NULL;
- commitment_t commitments = { .content = COMMITMENTS_NONE };
+ ext_commitment_t ec;
- if (!rpc_get_n_bytes("asset_id", ¶ms, sizeof(commitments.asset_id), commitments.asset_id)) {
- jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Failed to extract asset_id from parameters");
+ if (!rpc_get_n_bytes("asset_id", ¶ms, sizeof(ec.c.asset_id), ec.c.asset_id)) {
+ jade_process_reject_message(
+ process, CBOR_RPC_BAD_PARAMETERS, "Failed to extract asset_id from parameters");
goto cleanup;
}
- bool ret = rpc_get_uint64_t("value", ¶ms, &commitments.value);
+ bool ret = rpc_get_uint64_t("value", ¶ms, &ec.c.value);
if (!ret) {
jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Failed to extract value from parameters");
goto cleanup;
@@ -70,8 +69,8 @@ void get_commitments_process(void* process_ptr)
// optional vbf provided to balance the blinded amounts
size_t vbf_len = 0;
- rpc_get_bytes("vbf", sizeof(commitments.vbf), ¶ms, commitments.vbf, &vbf_len);
- if (vbf_len && vbf_len != sizeof(commitments.vbf)) {
+ rpc_get_bytes("vbf", sizeof(ec.c.vbf), ¶ms, ec.c.vbf, &vbf_len);
+ if (vbf_len && vbf_len != sizeof(ec.c.vbf)) {
jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Failed to extract vbf from parameters");
goto cleanup;
}
@@ -85,50 +84,48 @@ void get_commitments_process(void* process_ptr)
if (!vbf_len) {
// Compute both abf and vbf
- JADE_STATIC_ASSERT(sizeof(commitments.abf) + sizeof(commitments.vbf) == WALLY_ABF_VBF_LEN);
- uint8_t tmp_abf_vbf[sizeof(commitments.abf) + sizeof(commitments.vbf)];
+ JADE_STATIC_ASSERT(sizeof(ec.c.abf) + sizeof(ec.c.vbf) == WALLY_ABF_VBF_LEN);
+ uint8_t tmp_abf_vbf[sizeof(ec.c.abf) + sizeof(ec.c.vbf)];
if (!wallet_get_blinding_factor(master_blinding_key, sizeof(master_blinding_key), hash_prevouts,
hash_prevouts_len, output_index, BF_ASSET_VALUE, tmp_abf_vbf, sizeof(tmp_abf_vbf))) {
jade_process_reject_message(
process, CBOR_RPC_BAD_PARAMETERS, "Failed to compute abf/vbf from the parameters");
goto cleanup;
}
- memcpy(commitments.abf, tmp_abf_vbf, sizeof(commitments.abf));
- memcpy(commitments.vbf, tmp_abf_vbf + sizeof(commitments.abf), sizeof(commitments.vbf));
+ memcpy(ec.c.abf, tmp_abf_vbf, sizeof(ec.c.abf));
+ memcpy(ec.c.vbf, tmp_abf_vbf + sizeof(ec.c.abf), sizeof(ec.c.vbf));
} else {
// Compute abf only
if (!wallet_get_blinding_factor(master_blinding_key, sizeof(master_blinding_key), hash_prevouts,
- hash_prevouts_len, output_index, BF_ASSET, commitments.abf, sizeof(commitments.abf))) {
- jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Failed to compute abf from the parameters");
+ hash_prevouts_len, output_index, BF_ASSET, ec.c.abf, sizeof(ec.c.abf))) {
+ jade_process_reject_message(
+ process, CBOR_RPC_BAD_PARAMETERS, "Failed to compute abf from the parameters");
goto cleanup;
}
}
// flip the asset_id for computing asset-generator
- uint8_t reversed_asset_id[sizeof(commitments.asset_id)];
- reverse(reversed_asset_id, commitments.asset_id, sizeof(commitments.asset_id));
+ uint8_t reversed_asset_id[sizeof(ec.c.asset_id)];
+ reverse(reversed_asset_id, ec.c.asset_id, sizeof(ec.c.asset_id));
- if (wally_asset_generator_from_bytes(reversed_asset_id, sizeof(reversed_asset_id), commitments.abf,
- sizeof(commitments.abf), commitments.asset_generator, sizeof(commitments.asset_generator))
+ if (wally_asset_generator_from_bytes(reversed_asset_id, sizeof(reversed_asset_id), ec.c.abf, sizeof(ec.c.abf),
+ ec.asset_generator, sizeof(ec.asset_generator))
!= WALLY_OK) {
jade_process_reject_message(
process, CBOR_RPC_BAD_PARAMETERS, "Failed to build asset generator from the parameters");
goto cleanup;
}
- if (wally_asset_value_commitment(commitments.value, commitments.vbf, sizeof(commitments.vbf),
- commitments.asset_generator, sizeof(commitments.asset_generator), commitments.value_commitment,
- sizeof(commitments.value_commitment))
+ if (wally_asset_value_commitment(ec.c.value, ec.c.vbf, sizeof(ec.c.vbf), ec.asset_generator,
+ sizeof(ec.asset_generator), ec.value_commitment, sizeof(ec.value_commitment))
!= WALLY_OK) {
jade_process_reject_message(
process, CBOR_RPC_BAD_PARAMETERS, "Failed to build value commitment from the parameters");
goto cleanup;
}
- commitments.content = COMMITMENTS_ABF | COMMITMENTS_VBF | COMMITMENTS_INCLUDES_COMMITMENTS;
-
uint8_t buf[320];
- jade_process_reply_to_message_result(process->ctx, buf, sizeof(buf), &commitments, reply_commitments);
+ jade_process_reply_to_message_result(process->ctx, buf, sizeof(buf), &ec, reply_commitments);
JADE_LOGI("Success");
diff --git a/main/process/process_utils.h b/main/process/process_utils.h
index 6ad4ee0..48dc711 100644
--- a/main/process/process_utils.h
+++ b/main/process/process_utils.h
@@ -13,22 +13,27 @@
#define COMMITMENTS_BLINDING_KEY 0x4
#define COMMITMENTS_ASSET_BLIND_PROOF 0x8
#define COMMITMENTS_VALUE_BLIND_PROOF 0x10
-#define COMMITMENTS_INCLUDES_COMMITMENTS 0x20
+// Holds asset/value blinding data
typedef struct {
uint8_t asset_blind_proof[ASSET_EXPLICIT_SURJECTIONPROOF_LEN];
uint8_t value_blind_proof[ASSET_EXPLICIT_RANGEPROOF_MAX_LEN];
- uint8_t asset_generator[ASSET_GENERATOR_LEN];
- uint8_t value_commitment[ASSET_COMMITMENT_LEN];
uint8_t asset_id[ASSET_TAG_LEN];
uint8_t abf[BLINDING_FACTOR_LEN];
uint8_t vbf[BLINDING_FACTOR_LEN];
uint8_t blinding_key[EC_PUBLIC_KEY_LEN];
uint64_t value;
- size_t value_blind_proof_len;
+ uint8_t value_blind_proof_len;
uint8_t content;
} commitment_t;
+// Holds asset/value blinding data plus the resulting blinded commitments
+typedef struct {
+ commitment_t c;
+ uint8_t asset_generator[ASSET_GENERATOR_LEN];
+ uint8_t value_commitment[ASSET_COMMITMENT_LEN];
+} ext_commitment_t;
+
#define MAX_REQUEST_URLS 2
#define CLIENT_REQUEST_TYPE_HTTP "http_request"
diff --git a/main/process/sign_tx.c b/main/process/sign_tx.c
index 2cb87eb..d0c969d 100644
--- a/main/process/sign_tx.c
+++ b/main/process/sign_tx.c
@@ -607,14 +607,15 @@ static void sign_tx_impl(jade_process_t* process, const bool for_liquid)
}
// Verify any blinding info for this input - note can only use blinded inputs
- commitment_t commitment;
- if (get_commitment_data(¶ms, &commitment)) {
- if (!verify_commitment_consistent(&commitment, &errmsg)) {
- jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
- goto cleanup;
- }
- asset_summary_update(
- in_sums, num_in_sums, commitment.asset_id, sizeof(commitment.asset_id), commitment.value);
+ commitment_t c;
+ if (get_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);
+ } else if (errmsg) {
+ // Invalid input commitments (rather than simply not present)
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
+ goto cleanup;
}
}
if (for_liquid && input_data->sig_type != WALLY_SIGTYPE_PRE_SW) {
diff --git a/main/process/sign_utils.c b/main/process/sign_utils.c
index 1485a74..9cd18ac 100644
--- a/main/process/sign_utils.c
+++ b/main/process/sign_utils.c
@@ -275,10 +275,17 @@ bool asset_summary_validate(asset_summary_t* sums, const size_t num_sums)
return true;
}
-bool get_commitment_data(CborValue* item, commitment_t* commitment)
+#ifdef CONFIG_SPIRAM
+static bool verify_explicit_proofs(void* ctx);
+#endif
+
+bool get_commitment_data(
+ CborValue* item, commitment_t* commitment, const struct wally_tx_output* const txout, const char** errmsg)
{
JADE_ASSERT(item);
JADE_ASSERT(commitment);
+ // txout is optional
+ JADE_INIT_OUT_PPTR(errmsg);
commitment->content = COMMITMENTS_NONE;
@@ -293,6 +300,7 @@ bool get_commitment_data(CborValue* item, commitment_t* commitment)
}
if (!(commitment->content & (COMMITMENTS_ABF | COMMITMENTS_ASSET_BLIND_PROOF))) {
+ // No commitment data present
return false;
}
@@ -305,19 +313,14 @@ bool get_commitment_data(CborValue* item, commitment_t* commitment)
rpc_get_bytes(
"value_blind_proof", sizeof(commitment->value_blind_proof), item, commitment->value_blind_proof, &written);
if (written && written <= sizeof(commitment->value_blind_proof)) {
- commitment->value_blind_proof_len = written;
+ commitment->value_blind_proof_len = (uint8_t)written; // Sufficient
commitment->content |= COMMITMENTS_VALUE_BLIND_PROOF;
}
- if (!(commitment->content & (COMMITMENTS_VBF | COMMITMENTS_VALUE_BLIND_PROOF))) {
- return false;
- }
-
- if (!rpc_get_n_bytes("asset_id", item, sizeof(commitment->asset_id), commitment->asset_id)) {
- return false;
- }
-
- if (!rpc_get_uint64_t("value", item, &commitment->value)) {
+ if (!(commitment->content & (COMMITMENTS_VBF | COMMITMENTS_VALUE_BLIND_PROOF))
+ || !rpc_get_n_bytes("asset_id", item, sizeof(commitment->asset_id), commitment->asset_id)
+ || !rpc_get_uint64_t("value", item, &commitment->value)) {
+ *errmsg = "Invalid or missing trusted commitment data";
return false;
}
@@ -326,25 +329,98 @@ bool get_commitment_data(CborValue* item, commitment_t* commitment)
commitment->content |= COMMITMENTS_BLINDING_KEY;
}
- // Actual commitments are optional - but must be both commitments or neither.
- // If both are passed these will be copied into the tx and signed.
- // If not passed, the above blinding factors/proofs must match what is already present in the transaction output.
- // Must be both or neither - error if only one commitment passed.
- if (rpc_has_field_data("asset_generator", item) || rpc_has_field_data("value_commitment", item)) {
- if (!rpc_get_n_bytes(
- "asset_generator", item, sizeof(commitment->asset_generator), commitment->asset_generator)) {
+ // For tx output commitments:
+ // - Actual commitments are optional - but must be both commitments or neither.
+ // - If passed, these must match values in the tx output.
+ // - If not passed, the values from the tx output are used instead.
+ // For tx input commitments (i.e. 'txout' parameter is NULL):
+ // - Actual commitments are mandatory
+ //
+ // The above blinding factors/proofs are then verified against the commitments.
+ ext_commitment_t ec;
+ const bool have_asset_generator
+ = rpc_get_n_bytes("asset_generator", item, sizeof(ec.asset_generator), ec.asset_generator);
+ const bool have_value_commitment
+ = rpc_get_n_bytes("value_commitment", item, sizeof(ec.value_commitment), ec.value_commitment);
+ const bool are_commitments_consistent = have_asset_generator == have_value_commitment;
+
+ if (!are_commitments_consistent || (!txout && !have_asset_generator)) {
+ // Either inconsistently provided, or not provided for a tx input
+ *errmsg = "Invalid or missing trusted commitment data";
+ return false;
+ }
+ if (txout) {
+ if (txout->asset_len != sizeof(ec.asset_generator)
+ || (have_asset_generator && memcmp(txout->asset, ec.asset_generator, sizeof(ec.asset_generator)))) {
+ *errmsg = "Failed to verify trusted commitment data with tx";
+ return false;
+ }
+ if (txout->value_len != sizeof(ec.value_commitment)) {
+ *errmsg = "Failed to verify trusted commitment data with tx";
return false;
}
+ if (have_asset_generator) {
+ // Ensure the commitments match the output values
+ if (memcmp(txout->asset, ec.asset_generator, sizeof(ec.asset_generator))
+ || memcmp(txout->value, ec.value_commitment, sizeof(ec.value_commitment))) {
+ *errmsg = "Failed to verify trusted commitment data with tx";
+ return false;
+ }
+ } else {
+ // Copy the commitments from the tx output for validation
+ memcpy(ec.asset_generator, txout->asset, sizeof(ec.asset_generator));
+ memcpy(ec.value_commitment, txout->value, sizeof(ec.value_commitment));
+ }
+ }
- if (!rpc_get_n_bytes(
- "value_commitment", item, sizeof(commitment->value_commitment), commitment->value_commitment)) {
+ // 1. Asset generator
+ // If passed the abf, check the blinded asset commitment can be reconstructed
+ // (ie. from the given reversed asset_id and abf)
+ if (commitment->content & COMMITMENTS_ABF) {
+ uint8_t reversed_asset_id[sizeof(commitment->asset_id)];
+ reverse(reversed_asset_id, commitment->asset_id, sizeof(commitment->asset_id));
+
+ uint8_t cmp[sizeof(ec.asset_generator)];
+ if (wally_asset_generator_from_bytes(reversed_asset_id, sizeof(reversed_asset_id), commitment->abf,
+ sizeof(commitment->abf), cmp, sizeof(cmp))
+ != WALLY_OK
+ || sodium_memcmp(ec.asset_generator, cmp, sizeof(cmp)) != 0) {
+ *errmsg = "Failed to verify trusted commitment data with tx";
return false;
}
+ }
- // Set flag to show struct is fully populated/initialised, including commitments to sign.
- commitment->content |= COMMITMENTS_INCLUDES_COMMITMENTS;
+ // 2. Value commitment
+ // If passed the vbf, check the blinded value commitment can be reconstructed
+ // (ie. from the given value, asset_generator and vbf)
+ if (commitment->content & COMMITMENTS_VBF) {
+ uint8_t cmp[sizeof(ec.value_commitment)];
+ if (wally_asset_value_commitment(commitment->value, commitment->vbf, sizeof(commitment->vbf),
+ ec.asset_generator, sizeof(ec.asset_generator), cmp, sizeof(cmp))
+ != WALLY_OK
+ || sodium_memcmp(ec.value_commitment, cmp, sizeof(cmp)) != 0) {
+ *errmsg = "Failed to verify trusted commitment data with tx";
+ return false;
+ }
}
+ // Verify any blinded proofs
+ // NOTE: only a device with SPIRAM has sufficient memory to be able to do this verification.
+ if (commitment->content & (COMMITMENTS_ASSET_BLIND_PROOF | COMMITMENTS_VALUE_BLIND_PROOF)) {
+#ifdef CONFIG_SPIRAM
+ // Because the libsecp calls 'secp256k1_surjectionproof_verify()' and 'secp256k1_rangeproof_verify()'
+ // requires more stack space than is available to the main task, we run that function in a temporary task.
+ const size_t stack_size = 54 * 1024; // 54kb seems sufficient
+ memcpy(&ec.c, commitment, sizeof(*commitment));
+ if (!run_in_temporary_task(stack_size, verify_explicit_proofs, (void*)&ec)) {
+ *errmsg = "Failed to verify explicit asset/value commitment proofs";
+ return false;
+ }
+#else
+ *errmsg = "Devices without external SPIRAM are unable to verify explicit proofs";
+ return false;
+#endif // CONFIG_SPIRAM
+ }
return true;
}
@@ -366,9 +442,9 @@ bool params_trusted_commitments(
// Expect one commitment element in the array for each output.
// (Can be null/zero's for unblinded outputs.)
- size_t num_array_items = 0;
- CborError cberr = cbor_value_get_array_length(&result, &num_array_items);
- if (cberr != CborNoError || !num_array_items || num_array_items != tx->num_outputs) {
+ size_t num_outputs = 0;
+ CborError cberr = cbor_value_get_array_length(&result, &num_outputs);
+ if (cberr != CborNoError || !num_outputs || num_outputs != tx->num_outputs) {
errmsg = "Unexpected number of trusted commitments for transaction";
goto cleanup;
}
@@ -380,10 +456,10 @@ bool params_trusted_commitments(
goto cleanup;
}
- commitment_t* const commitments = JADE_CALLOC(num_array_items, sizeof(commitment_t));
+ commitment_t* const commitments = JADE_CALLOC(num_outputs, sizeof(commitment_t));
jade_process_free_on_exit(process, commitments);
- for (size_t i = 0; i < num_array_items; ++i) {
+ for (size_t i = 0; i < tx->num_outputs; ++i) {
JADE_ASSERT(!cbor_value_at_end(&arrayItem));
commitments[i].content = COMMITMENTS_NONE;
@@ -394,7 +470,7 @@ bool params_trusted_commitments(
}
if (!cbor_value_is_map(&arrayItem)) {
- errmsg = "Invalid trusted commitments for transaction";
+ errmsg = "Invalid or missing trusted commitment data";
goto cleanup;
}
@@ -405,9 +481,9 @@ bool params_trusted_commitments(
continue;
}
- // Populate commitments data
- if (!get_commitment_data(&arrayItem, &commitments[i])) {
- errmsg = "Invalid trusted commitments for transaction";
+ // Populate commitments data for the tx output if present
+ get_commitment_data(&arrayItem, &commitments[i], &tx->outputs[i], &errmsg);
+ if (errmsg) {
goto cleanup;
}
@@ -419,7 +495,7 @@ bool params_trusted_commitments(
if (cberr == CborNoError) {
*data = commitments;
} else {
- errmsg = "Invalid trusted commitments for transaction";
+ errmsg = "Invalid or missing trusted commitment data";
}
cleanup:
@@ -439,29 +515,27 @@ static bool verify_explicit_proofs(void* ctx)
{
JADE_ASSERT(ctx);
- const commitment_t* commitments = (const commitment_t*)ctx;
- JADE_ASSERT(commitments->content & (COMMITMENTS_ASSET_BLIND_PROOF | COMMITMENTS_VALUE_BLIND_PROOF));
- JADE_ASSERT(commitments->content & COMMITMENTS_INCLUDES_COMMITMENTS);
+ const ext_commitment_t* ec = (const ext_commitment_t*)ctx;
+ const commitment_t* c = &ec->c;
+ JADE_ASSERT(c->content & (COMMITMENTS_ASSET_BLIND_PROOF | COMMITMENTS_VALUE_BLIND_PROOF));
- if (commitments->content & COMMITMENTS_ASSET_BLIND_PROOF) {
- uint8_t reversed_asset_id[sizeof(commitments->asset_id)];
- reverse(reversed_asset_id, commitments->asset_id, sizeof(commitments->asset_id));
+ if (c->content & COMMITMENTS_ASSET_BLIND_PROOF) {
+ uint8_t reversed_asset_id[sizeof(c->asset_id)];
+ reverse(reversed_asset_id, c->asset_id, sizeof(c->asset_id));
// NOTE: Appears to require ~52kb of stack space
- if (wally_explicit_surjectionproof_verify(commitments->asset_blind_proof,
- sizeof(commitments->asset_blind_proof), reversed_asset_id, sizeof(reversed_asset_id),
- commitments->asset_generator, sizeof(commitments->asset_generator))
+ if (wally_explicit_surjectionproof_verify(c->asset_blind_proof, sizeof(c->asset_blind_proof), reversed_asset_id,
+ sizeof(reversed_asset_id), ec->asset_generator, sizeof(ec->asset_generator))
!= WALLY_OK) {
// Failed to verify explicit asset proof
return false;
}
}
- if (commitments->content & COMMITMENTS_VALUE_BLIND_PROOF) {
+ if (c->content & COMMITMENTS_VALUE_BLIND_PROOF) {
// NOTE: Appears to require ~40kb of stack space
- if (wally_explicit_rangeproof_verify(commitments->value_blind_proof, commitments->value_blind_proof_len,
- commitments->value, commitments->value_commitment, sizeof(commitments->value_commitment),
- commitments->asset_generator, sizeof(commitments->asset_generator))
+ if (wally_explicit_rangeproof_verify(c->value_blind_proof, c->value_blind_proof_len, c->value,
+ ec->value_commitment, sizeof(ec->value_commitment), ec->asset_generator, sizeof(ec->asset_generator))
!= WALLY_OK) {
// Failed to verify explicit value proof
return false;
@@ -472,73 +546,6 @@ static bool verify_explicit_proofs(void* ctx)
}
#endif // CONFIG_SPIRAM
-bool verify_commitment_consistent(const commitment_t* commitments, const char** errmsg)
-{
- JADE_ASSERT(commitments);
- JADE_INIT_OUT_PPTR(errmsg);
-
- if (!(commitments->content & COMMITMENTS_INCLUDES_COMMITMENTS)) {
- *errmsg = "Failed to extract final commitment values from commitments data";
- return false;
- }
-
- if (!(commitments->content & (COMMITMENTS_ABF | COMMITMENTS_ASSET_BLIND_PROOF))
- || !(commitments->content & (COMMITMENTS_VBF | COMMITMENTS_VALUE_BLIND_PROOF))) {
- *errmsg = "Failed to extract blinding factors or proofs from commitments data";
- return false;
- }
-
- // 1. Asset generator
- // If passed the abf, check the blinded asset commitment can be reconstructed
- // (ie. from the given reversed asset_id and abf)
- if (commitments->content & COMMITMENTS_ABF) {
- uint8_t reversed_asset_id[sizeof(commitments->asset_id)];
- reverse(reversed_asset_id, commitments->asset_id, sizeof(commitments->asset_id));
-
- uint8_t generator_tmp[sizeof(commitments->asset_generator)];
- if (wally_asset_generator_from_bytes(reversed_asset_id, sizeof(reversed_asset_id), commitments->abf,
- sizeof(commitments->abf), generator_tmp, sizeof(generator_tmp))
- != WALLY_OK
- || sodium_memcmp(commitments->asset_generator, generator_tmp, sizeof(generator_tmp)) != 0) {
- *errmsg = "Failed to verify blinded asset generator from commitments data";
- return false;
- }
- }
-
- // 2. Value commitment
- // If passed the vbf, check the blinded value commitment can be reconstructed
- // (ie. from the given value, asset_generator and vbf)
- if (commitments->content & COMMITMENTS_VBF) {
- uint8_t commitment_tmp[sizeof(commitments->value_commitment)];
- if (wally_asset_value_commitment(commitments->value, commitments->vbf, sizeof(commitments->vbf),
- commitments->asset_generator, sizeof(commitments->asset_generator), commitment_tmp,
- sizeof(commitment_tmp))
- != WALLY_OK
- || sodium_memcmp(commitments->value_commitment, commitment_tmp, sizeof(commitment_tmp)) != 0) {
- *errmsg = "Failed to verify blinded value commitment from commitments data";
- return false;
- }
- }
-
- // Verify any blinded proofs
- // NOTE: only a device with SPIRAM has sufficient memory to be able to do this verification.
- if (commitments->content & (COMMITMENTS_ASSET_BLIND_PROOF | COMMITMENTS_VALUE_BLIND_PROOF)) {
-#ifdef CONFIG_SPIRAM
- // Because the libsecp calls 'secp256k1_surjectionproof_verify()' and 'secp256k1_rangeproof_verify()'
- // requires more stack space than is available to the main task, we run that function in a temporary task.
- const size_t stack_size = 54 * 1024; // 54kb seems sufficient
- if (!run_in_temporary_task(stack_size, verify_explicit_proofs, (void*)commitments)) {
- *errmsg = "Failed to verify explicit asset/value commitment proofs";
- return false;
- }
-#else
- *errmsg = "Devices without external SPIRAM are unable to verify explicit proofs";
- return false;
-#endif // CONFIG_SPIRAM
- }
- return true;
-}
-
static bool add_output_info(
commitment_t* commitments, const struct wally_tx_output* txoutput, output_info_t* outinfo, const char** errmsg)
{
@@ -546,47 +553,19 @@ static bool add_output_info(
JADE_ASSERT(txoutput);
JADE_ASSERT(outinfo);
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);
- // 1. Sanity checks
- if (txoutput->asset_len != sizeof(commitments->asset_generator)) {
- *errmsg = "Invalid asset generator in tx output";
- return false;
- }
- if (txoutput->value_len != sizeof(commitments->value_commitment)) {
- *errmsg = "Invalid value commitment in tx output";
- return false;
- }
-
- // 2. If passed explicit commitments copy them into the transaction output ready for signing
- // If not, copy the values from the tx into the commitment structure.
- // ie. so in any case commitment struct is complete, and reflects what is in the tx output
- if (commitments->content & COMMITMENTS_INCLUDES_COMMITMENTS) {
- memcpy(txoutput->asset, commitments->asset_generator, sizeof(commitments->asset_generator));
- memcpy(txoutput->value, commitments->value_commitment, sizeof(commitments->value_commitment));
- } else {
- memcpy(commitments->asset_generator, txoutput->asset, sizeof(commitments->asset_generator));
- memcpy(commitments->value_commitment, txoutput->value, sizeof(commitments->value_commitment));
- commitments->content |= COMMITMENTS_INCLUDES_COMMITMENTS;
- }
-
- // 3. Check the asset generator and value commitment can be reconstructed
- if (!verify_commitment_consistent(commitments, errmsg)) {
- // errmsg populated by call if failure
- return false;
- }
-
- // 4. Fetch the asset_id, value, and optional blinding_key into the info struct
- JADE_STATIC_ASSERT(sizeof(outinfo->asset_id) == sizeof(commitments->asset_id));
+ // 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;
if (commitments->content & COMMITMENTS_BLINDING_KEY) {
- JADE_STATIC_ASSERT(sizeof(outinfo->blinding_key) == sizeof(commitments->blinding_key));
memcpy(outinfo->blinding_key, commitments->blinding_key, sizeof(commitments->blinding_key));
outinfo->flags |= OUTPUT_FLAG_HAS_BLINDING_KEY;
}
@@ -647,7 +626,7 @@ bool validate_elements_outputs(jade_process_t* process, const network_t network_
// 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 commitments data for blinded output";
+ errmsg = "Missing trusted commitment data for blinded output";
goto done;
}
}
diff --git a/main/process/sign_utils.h b/main/process/sign_utils.h
index 48177ee..cc4f8f5 100644
--- a/main/process/sign_utils.h
+++ b/main/process/sign_utils.h
@@ -28,8 +28,8 @@ bool params_trusted_commitments(
TxType_t params_additional_info(jade_process_t* process, CborValue* params, const struct wally_tx* tx, TxType_t* txtype,
bool* is_partial, asset_summary_t** in_sums, size_t* num_in_sums, asset_summary_t** out_sums, size_t* num_out_sums);
-bool get_commitment_data(CborValue* item, commitment_t* commitment);
-bool verify_commitment_consistent(const commitment_t* commitments, const char** errmsg);
+bool get_commitment_data(
+ CborValue* item, commitment_t* commitment, const struct wally_tx_output* const txout, const char** errmsg);
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);
diff --git a/test_jade.py b/test_jade.py
index 34c083b..f913299 100644
--- a/test_jade.py
+++ b/test_jade.py
@@ -2007,7 +2007,7 @@ dab03ecc4ae0b5e77c4fc0e5cf6c95a0100000000000f4240000000000000')
(('badsignliq14', 'sign_liquid_tx', # Empty commitments for blinded output
{'network': 'localtest-liquid', 'txn': GOODTX,
'num_inputs': 1, 'trusted_commitments': [{}, {}]}),
- 'Missing commitments data for blinded output'),
+ 'Missing trusted commitment data for blinded output'),
(('badsignliq15', 'sign_liquid_tx', # invalid network
{'network': 'made-up', 'txn': GOODTX, 'num_inputs': 1,
'trusted_commitments': [{}, {}],
@@ -2092,44 +2092,44 @@ dab03ecc4ae0b5e77c4fc0e5cf6c95a0100000000000f4240000000000000')
# Some bad commitment data is detected immediately... esp if it is
# missing or not syntactically valid, unparseable etc.
bad_commitments = [ # Field missing - note commitments are optional so not an error to omit
- (_commitsMinus('asset_id'), 'trusted commitments'),
- (_commitsMinus('value'), 'trusted commitments'),
- (_commitsMinus('abf'), 'trusted commitments'),
- (_commitsMinus('vbf'), 'trusted commitments'),
- (_commitsMinus('blinding_key'), 'Missing commitments data'),
+ (_commitsMinus('asset_id'), 'trusted commitment'),
+ (_commitsMinus('value'), 'trusted commitment'),
+ (_commitsMinus('abf'), 'trusted commitment'),
+ (_commitsMinus('vbf'), 'trusted commitment'),
+ (_commitsMinus('blinding_key'), 'Missing trusted commitment'),
# Field bad type/length etc.
- (_commitsUpdate('asset_id', 'notbin'), 'trusted commitments'),
- (_commitsUpdate('asset_id', h2b('123abc')), 'trusted commitments'),
- (_commitsUpdate('asset_id', b''), 'trusted commitments'),
- (_commitsUpdate('value', 'notint'), 'trusted commitments'),
- (_commitsUpdate('abf', 'notbin'), 'trusted commitments'),
- (_commitsUpdate('abf', h2b('123abc')), 'trusted commitments'),
- (_commitsUpdate('abf', b''), 'trusted commitments'),
- (_commitsUpdate('vbf', 'notbin'), 'trusted commitments'),
- (_commitsUpdate('vbf', h2b('123abc')), 'trusted commitments'),
- (_commitsUpdate('vbf', b''), 'trusted commitments'),
- (_commitsUpdate('asset_generator', 'notbin'), 'trusted commitments'),
- (_commitsUpdate('asset_generator', '123abc'), 'trusted commitments'),
- (_commitsUpdate('value_commitment', 'notbin'), 'trusted commitments'),
- (_commitsUpdate('value_commitment', '123abc'), 'trusted commitments'),
- (_commitsUpdate('blinding_key', 'notbin'), 'Missing commitments data'),
- (_commitsUpdate('blinding_key', '123abc'), 'Missing commitments data'),
+ (_commitsUpdate('asset_id', 'notbin'), 'trusted commitment'),
+ (_commitsUpdate('asset_id', h2b('123abc')), 'trusted commitment'),
+ (_commitsUpdate('asset_id', b''), 'trusted commitment'),
+ (_commitsUpdate('value', 'notint'), 'trusted commitment'),
+ (_commitsUpdate('abf', 'notbin'), 'trusted commitment'),
+ (_commitsUpdate('abf', h2b('123abc')), 'trusted commitment'),
+ (_commitsUpdate('abf', b''), 'trusted commitment'),
+ (_commitsUpdate('vbf', 'notbin'), 'trusted commitment'),
+ (_commitsUpdate('vbf', h2b('123abc')), 'trusted commitment'),
+ (_commitsUpdate('vbf', b''), 'trusted commitment'),
+ (_commitsUpdate('asset_generator', 'notbin'), 'trusted commitment'),
+ (_commitsUpdate('asset_generator', '123abc'), 'trusted commitment'),
+ (_commitsUpdate('value_commitment', 'notbin'), 'trusted commitment'),
+ (_commitsUpdate('value_commitment', '123abc'), 'trusted commitment'),
+ (_commitsUpdate('blinding_key', 'notbin'), 'Missing trusted commitment'),
+ (_commitsUpdate('blinding_key', '123abc'), 'Missing trusted commitment'),
# Field bad value
- (_commitsUpdate('asset_id', BADVAL32), 'verify blinded asset generator'),
- (_commitsUpdate('abf', BADVAL32), 'verify blinded asset generator'),
- (_commitsUpdate('vbf', BADVAL32), 'verify blinded value commitment'),
- (_commitsUpdate('asset_generator', BADVAL33), 'blinded asset generator'),
- (_commitsUpdate('value_commitment', BADVAL33), 'blinded value commitment'),
+ (_commitsUpdate('asset_id', BADVAL32), 'verify trusted commitment data'),
+ (_commitsUpdate('abf', BADVAL32), 'verify trusted commitment'),
+ (_commitsUpdate('vbf', BADVAL32), 'verify trusted commitment'),
+ (_commitsUpdate('asset_generator', BADVAL33), 'verify trusted commitment'),
+ (_commitsUpdate('value_commitment', BADVAL33), 'verify trusted commitment'),
# Asset blind proof in place of abf
- (_commitsAssetBlindProof(''), 'trusted commitments'),
- (_commitsAssetBlindProof('notbin'), 'trusted commitments'),
- (_commitsAssetBlindProof('123abc'), 'trusted commitments'),
- (_commitsAssetBlindProof(b''), 'trusted commitments'),
+ (_commitsAssetBlindProof(''), 'trusted commitment'),
+ (_commitsAssetBlindProof('notbin'), 'trusted commitment'),
+ (_commitsAssetBlindProof('123abc'), 'trusted commitment'),
+ (_commitsAssetBlindProof(b''), 'trusted commitment'),
# Value blind proof in place of vbf
- (_commitsValueBlindProof(''), 'trusted commitments'),
- (_commitsValueBlindProof('notbin'), 'trusted commitments'),
- (_commitsValueBlindProof('123abc'), 'trusted commitments'),
- (_commitsValueBlindProof(b''), 'trusted commitments')]
+ (_commitsValueBlindProof(''), 'trusted commitment'),
+ (_commitsValueBlindProof('notbin'), 'trusted commitment'),
+ (_commitsValueBlindProof('123abc'), 'trusted commitment'),
+ (_commitsValueBlindProof(b''), 'trusted commitment')]
if has_psram:
# Invalid/incorrect explicit proofs
bad_commitments.append((_commitsAssetBlindProof(BAD_ASSET_PROOF),
@@ -2649,7 +2649,7 @@ def _check_tx_signatures(jadeapi, testcase, rslt):
signer_commitment, signature = actual
else:
# Standard EC signature should be low-s and low-r
- assert actual == expected, f'{actual.hex()} != {expected.hex()}'
+ assert actual == expected, f'{actual.hex()} != {expected.hex()} {testcase["filename"]}'
# NOTE: low-s is implied/assumed here, so no need to remove one from max-len
assert len(actual) <= wally.EC_SIGNATURE_DER_MAX_LOW_R_LEN + 1 # sighash byte, low-s
@@ -3377,7 +3377,7 @@ def test_generic_multisig_ss_signer(jadeapi):
assert False, 'Accessing other wallet multisig should fail'
except JadeError as e:
assert e.code == JadeError.BAD_PARAMETERS
- assert e.message == 'Cannot de-serialise multisig wallet data'
+ assert e.message == 'Cannot de-serialise multisig wallet data', e.message
# If we register the same multisig description to this wallet, it should produce
# the same addresses as it did previously (for the other signatory)
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.