What changed, and why it matters
This commit only adds new unit tests to the Ledger Bitcoin app. It does not change any production code, so it cannot introduce a security vulnerability or fix one directly. The tests exercise edge cases and adversarial inputs for functions that handle Merkle-tree preimages and BIP32 derivation extraction, which helps confirm existing defensive checks reject malformed data.
No security action required. Treat as normal QA/test-coverage improvement. If reviewing for security, use these tests as a baseline and consider whether the mocked adversarial inputs accurately reflect real host-side behavior.
Security signals we found
No production code modified
Tests target defensive checks in Merkle preimage and BIP32 derivation parsing
Adversarial test cases simulate malformed client-command responses
Tests assert negative error codes for out-of-range lengths and truncated data
Evidence from the diff
The commit is a pure test-only change (+1431 lines across six unit-test files, zero production-file changes). It adds adversarial and edge-case tests for call_get_preimage, call_stream_preimage, call_get_merkle_preimage, call_get_merkle_leaf_hash, call_get_merkle_leaf_index, and extract_bip32_derivation. Test cases tamper with mock client-command responses to verify that out-of-range lengths, truncated responses, invalid continuation sizes, and overflow values are rejected with negative error codes. No source code under test is modified.
Changed components
unit-tests/test_extract_bip32_derivation.cunit-tests/test_get_merkle_leaf_hash.cunit-tests/test_get_merkle_leaf_index.cunit-tests/test_get_merkle_preimage.cunit-tests/test_get_preimage.cunit-tests/test_stream_preimage.cInspect captured patch +1431 / −0
diff --git a/unit-tests/test_extract_bip32_derivation.c b/unit-tests/test_extract_bip32_derivation.c
index 8a45d1c..4c3cbad 100644
--- a/unit-tests/test_extract_bip32_derivation.c
+++ b/unit-tests/test_extract_bip32_derivation.c
@@ -29,6 +29,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "psbt_parse.h"
+#include "client_commands.h"
#include "handler/sign_psbt/extract_bip32_derivation.h"
#include "common/psbt.h"
@@ -307,6 +308,265 @@ static void test_taproot_output_tap_bip32_derivation(void **state) {
assert_int_equal(out[5], 2);
}
+/* ===========================================================================
+ * Edge-case tests: directly register a single value with a 1-element tree.
+ * For a 1-element tree, the Merkle root equals the leaf hash, so
+ * call_stream_merkle_leaf_element returns the registered value directly.
+ * =========================================================================== */
+
+static void register_single_value(mock_dispatcher_t *mock,
+ const uint8_t *value,
+ size_t len,
+ uint8_t root_out[32]) {
+ const uint8_t *elems[] = {value};
+ size_t lens[] = {len};
+ mock_dispatcher_add_list(mock, elems, lens, 1);
+ memcpy(root_out, mock->trees[mock->n_trees - 1].root, 32);
+}
+
+/**
+ * Non-taproot value longer than max_out_data_length (= 4 * (1 + MAX_BIP32_PATH_STEPS)).
+ * Exercises the early-reject branch in the data callback.
+ */
+static void test_extract_nontap_too_long(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* max_out_data_length = 4*(1+10) = 44 → 45 bytes is too long */
+ uint8_t value[45];
+ for (size_t i = 0; i < sizeof(value); i++) value[i] = (uint8_t) i;
+
+ uint8_t root[32];
+ register_single_value(&mock, value, sizeof(value), root);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_BIP32_DERIVATION, root, 1, 0, out);
+
+ assert_int_equal(result, -1);
+}
+
+/**
+ * Taproot value of 1 byte where n_hashes >= 1 — total_data_length is too short
+ * for the announced number of hashes.
+ */
+static void test_extract_tap_too_short(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t value[1] = {0x01}; /* n_hashes = 1, but no room for the hash */
+
+ uint8_t root[32];
+ register_single_value(&mock, value, sizeof(value), root);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_TAP_BIP32_DERIVATION, root, 1, 0, out);
+
+ assert_int_equal(result, -1);
+}
+
+/**
+ * Taproot value where, after removing the leaf hashes, the remaining bytes
+ * exceed max_out_data_length.
+ */
+static void test_extract_tap_out_too_long(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* n_hashes=0, 50 bytes of fingerprint+path → out_data_length = 49 > 44 */
+ uint8_t value[50];
+ memset(value, 0, sizeof(value));
+
+ uint8_t root[32];
+ register_single_value(&mock, value, sizeof(value), root);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_TAP_BIP32_DERIVATION, root, 1, 0, out);
+
+ assert_int_equal(result, -1);
+}
+
+/**
+ * Taproot value with n_hashes=7 and a 28-byte derivation, total 253 bytes.
+ * The 254-byte preimage spans two chunks (251 + 3); the second chunk is
+ * smaller than out_data_length (=28), exercising the "carry-over" path in
+ * the data callback (memmove + read into tail).
+ */
+static void test_extract_tap_multi_chunk(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t value[1 + 32 * 7 + 28];
+ memset(value, 0, sizeof(value));
+ value[0] = 7; /* n_hashes */
+ /* fingerprint (BE) at offset 225 */
+ value[1 + 32 * 7 + 0] = 0xAA;
+ value[1 + 32 * 7 + 1] = 0xBB;
+ value[1 + 32 * 7 + 2] = 0xCC;
+ value[1 + 32 * 7 + 3] = 0xDD;
+ /* 6 path steps (4 bytes each, LE) at offsets 229.. */
+ for (int s = 0; s < 6; s++) {
+ for (int b = 0; b < 4; b++) {
+ value[1 + 32 * 7 + 4 + 4 * s + b] = (uint8_t) (s * 4 + b + 1);
+ }
+ }
+
+ uint8_t root[32];
+ register_single_value(&mock, value, sizeof(value), root);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_TAP_BIP32_DERIVATION, root, 1, 0, out);
+
+ /* 28 bytes / 4 = 7 → n_steps = 6 (returned value = 7 - 1) */
+ assert_int_equal(result, 6);
+ assert_int_equal(out[0], 0xAABBCCDDu); /* fingerprint, BE */
+}
+
+/**
+ * Empty value (0 bytes) — the data callback is invoked with data->size == 0,
+ * exercising the empty-chunk early-return branch. out_data_length is never
+ * set, so extract_bip32_derivation reports an error.
+ */
+static void test_extract_empty_value(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t value[1] = {0x00}; /* unused — register_single_value needs a non-NULL ptr */
+
+ uint8_t root[32];
+ register_single_value(&mock, value, 0, root);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_BIP32_DERIVATION, root, 1, 0, out);
+
+ assert_int_equal(result, -1);
+}
+
+/* ===========================================================================
+ * Adversarial tests: tampering with the preimage length to trigger checks
+ * that cannot be reached via well-formed values.
+ * =========================================================================== */
+
+/**
+ * Adversarial: tamper the GET_PREIMAGE response to claim a preimage length
+ * just past INT_MAX. The len callback in extract_bip32_derivation then sees
+ * data_length > INT_MAX and rejects.
+ */
+static int tamper_huge_preimage_len(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE && *response_len >= 2) {
+ /* Rewrite the varint header to claim preimage_len = 0x80000001
+ * (> INT_MAX, still < UINT32_MAX). Then a 0-byte partial_data_len. */
+ /* Original layout: <varint preimage_len> <partial_data_len:1> <data...>
+ * Replace with: <0xFE><u32 LE = 0x80000001><partial_data_len=0>
+ * Note: this is interpreted by call_stream_preimage as preimage_len=0x80000001,
+ * partial_data_len=0 → caught by "preimage_len<1 OR partial_data_len==0" check
+ * (= -3). But we want to bypass that: set partial_data_len=1 instead. */
+ response_buf[0] = 0xFE; /* 5-byte varint */
+ response_buf[1] = 0x01;
+ response_buf[2] = 0x00;
+ response_buf[3] = 0x00;
+ response_buf[4] = 0x80; /* preimage_len = 0x80000001 */
+ response_buf[5] = 0x01; /* partial_data_len = 1 */
+ response_buf[6] = 0x00; /* one data byte (would-be 0x00 prefix) */
+ *response_len = 7;
+ }
+ return 0;
+}
+
+static void test_extract_huge_preimage_len(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t value[5] = {0, 0, 0, 0, 0};
+ uint8_t root[32];
+ register_single_value(&mock, value, sizeof(value), root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_huge_preimage_len, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_BIP32_DERIVATION, root, 1, 0, out);
+
+ assert_int_equal(result, -1);
+}
+
+/**
+ * Adversarial: tamper to declare preimage_len much larger than what's actually
+ * sent, while keeping a valid first byte = 130 (claimed n_hashes). Total
+ * declared length satisfies the early "too short" check but n_hashes > 128
+ * triggers the rejection.
+ */
+static int tamper_long_len_n_hashes(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE && *response_len >= 2) {
+ /* Claim preimage_len = 5000 (= 0x1388), partial_data_len = 2,
+ * partial data = [0x00, 130]. */
+ response_buf[0] = 0xFD; /* 3-byte varint */
+ response_buf[1] = 0x88;
+ response_buf[2] = 0x13; /* 5000 LE */
+ response_buf[3] = 2; /* partial_data_len */
+ response_buf[4] = 0x00; /* prefix */
+ response_buf[5] = 130; /* n_hashes */
+ *response_len = 6;
+ }
+ return 0;
+}
+
+static void test_extract_tap_too_many_hashes(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t value[2] = {130, 0};
+ uint8_t root[32];
+ register_single_value(&mock, value, sizeof(value), root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_long_len_n_hashes, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ uint32_t out[1 + MAX_BIP32_PATH_STEPS];
+ int result = extract_bip32_derivation(dc, PSBT_IN_TAP_BIP32_DERIVATION, root, 1, 0, out);
+
+ assert_int_equal(result, -1);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -315,6 +575,13 @@ int main(void) {
cmocka_unit_test(test_wpkh_output_bip32_derivation),
cmocka_unit_test(test_taproot_input_tap_bip32_derivation),
cmocka_unit_test(test_taproot_output_tap_bip32_derivation),
+ cmocka_unit_test(test_extract_nontap_too_long),
+ cmocka_unit_test(test_extract_tap_too_short),
+ cmocka_unit_test(test_extract_tap_out_too_long),
+ cmocka_unit_test(test_extract_tap_multi_chunk),
+ cmocka_unit_test(test_extract_empty_value),
+ cmocka_unit_test(test_extract_huge_preimage_len),
+ cmocka_unit_test(test_extract_tap_too_many_hashes),
};
return cmocka_run_group_tests(tests, NULL, NULL);
diff --git a/unit-tests/test_get_merkle_leaf_hash.c b/unit-tests/test_get_merkle_leaf_hash.c
index 6597ef1..098a091 100644
--- a/unit-tests/test_get_merkle_leaf_hash.c
+++ b/unit-tests/test_get_merkle_leaf_hash.c
@@ -564,6 +564,207 @@ static void test_get_leaf_hash_bad_proof_element_size(void **state) {
assert_true(result < 0);
}
+/**
+ * Adversarial: claim n_proof_elements <= proof_size (no early reject), but
+ * truncate the response so buffer_can_read(32 * n_proof_elements) fails.
+ */
+static int tamper_truncate_proof_elements(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MERKLE_LEAF_PROOF && *response_len >= 34) {
+ /* keep header + 1 hash, but n_proof_elements still claims 2 → can't read 64 */
+ *response_len = 34 + 32;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_truncated_proof_elements(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0x00, 0x01};
+ uint8_t e1[] = {0x10, 0x11};
+ uint8_t e2[] = {0x20, 0x21};
+ uint8_t e3[] = {0x30, 0x31};
+
+ const uint8_t *elems[] = {e0, e1, e2, e3};
+ size_t lens[] = {sizeof(e0), sizeof(e1), sizeof(e2), sizeof(e3)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 4, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_truncate_proof_elements, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 4, 0, out);
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: cause process_interruption to fail on the GET_MORE_ELEMENTS
+ * continuation (second call_count).
+ */
+static int tamper_fail_more(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) response_len;
+ (void) cmd;
+ (void) user_data;
+
+ if (call_count >= 1) {
+ return -1;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_more_comm_failure(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* 128 elements so the proof has 7 hashes; first response fits 6, so 1
+ * goes via GET_MORE_ELEMENTS. */
+ uint8_t data[128][2];
+ const uint8_t *elems[128];
+ size_t lens[128];
+ for (size_t i = 0; i < 128; i++) {
+ data[i][0] = (uint8_t) (i >> 8);
+ data[i][1] = (uint8_t) (i & 0xFF);
+ elems[i] = data[i];
+ lens[i] = 2;
+ }
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 128, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_fail_more, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 128, 0, out);
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: truncate the GET_MORE_ELEMENTS response so buffer_can_read fails
+ * in the continuation.
+ */
+static int tamper_truncate_more(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ /* keep n_proof_elements and elements_len, drop the hash payload */
+ *response_len = 2;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_truncated_more(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t data[128][2];
+ const uint8_t *elems[128];
+ size_t lens[128];
+ for (size_t i = 0; i < 128; i++) {
+ data[i][0] = (uint8_t) (i >> 8);
+ data[i][1] = (uint8_t) (i & 0xFF);
+ elems[i] = data[i];
+ lens[i] = 2;
+ }
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 128, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_truncate_more, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 128, 0, out);
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, claim n_proof_elements such that
+ * cur_step + n_proof_elements > proof_size, while keeping elements_len = 32 and
+ * extending the buffer so buffer_can_read still passes.
+ */
+static int tamper_more_proof_overflow(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ uint8_t orig_n = response_buf[0];
+ response_buf[0] = orig_n + 1;
+ /* Append a 32-byte hash so buffer_can_read((orig_n+1)*32) passes. */
+ for (int i = 0; i < 32; i++) {
+ response_buf[*response_len + i] = 0x00;
+ }
+ *response_len += 32;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_more_proof_overflow(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t data[128][2];
+ const uint8_t *elems[128];
+ size_t lens[128];
+ for (size_t i = 0; i < 128; i++) {
+ data[i][0] = (uint8_t) (i >> 8);
+ data[i][1] = (uint8_t) (i & 0xFF);
+ elems[i] = data[i];
+ lens[i] = 2;
+ }
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 128, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_proof_overflow, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 128, 0, out);
+
+ assert_true(result < 0);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -581,6 +782,10 @@ int main(void) {
cmocka_unit_test(test_get_leaf_hash_proof_elements_overflow),
cmocka_unit_test(test_get_leaf_hash_zero_proof_size),
cmocka_unit_test(test_get_leaf_hash_bad_proof_element_size),
+ cmocka_unit_test(test_get_leaf_hash_truncated_proof_elements),
+ cmocka_unit_test(test_get_leaf_hash_more_comm_failure),
+ cmocka_unit_test(test_get_leaf_hash_truncated_more),
+ cmocka_unit_test(test_get_leaf_hash_more_proof_overflow),
};
return cmocka_run_group_tests(tests, NULL, NULL);
diff --git a/unit-tests/test_get_merkle_leaf_index.c b/unit-tests/test_get_merkle_leaf_index.c
index 068dfdb..4f723e4 100644
--- a/unit-tests/test_get_merkle_leaf_index.c
+++ b/unit-tests/test_get_merkle_leaf_index.c
@@ -408,6 +408,138 @@ static void test_get_leaf_index_oob_index(void **state) {
assert_true(result < 0);
}
+/**
+ * Adversarial: process_interruption fails on the initial
+ * CCMD_GET_MERKLE_LEAF_INDEX call — must return -3.
+ */
+static int tamper_fail_first(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) response_len;
+ (void) cmd;
+ (void) user_data;
+ (void) call_count;
+ return -1;
+}
+
+static void test_get_leaf_index_initial_comm_failure(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ const uint8_t elem[] = {0xCA, 0xFE};
+ const uint8_t *elems[] = {elem};
+ size_t lens[] = {sizeof(elem)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 1, root);
+
+ uint8_t leaf_hash[32];
+ compute_leaf_hash(elem, sizeof(elem), leaf_hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_fail_first, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_index(dc, 1, root, leaf_hash);
+
+ assert_int_equal(result, -3);
+}
+
+/**
+ * Adversarial: client returns `found` byte that is neither 0 nor 1 — must
+ * return -2.
+ */
+static int tamper_invalid_found(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MERKLE_LEAF_INDEX && *response_len >= 2) {
+ response_buf[0] = 7; /* invalid */
+ response_buf[1] = 0;
+ }
+ return 0;
+}
+
+static void test_get_leaf_index_invalid_found(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ const uint8_t elem[] = {0xCA, 0xFE};
+ const uint8_t *elems[] = {elem};
+ size_t lens[] = {sizeof(elem)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 1, root);
+
+ uint8_t leaf_hash[32];
+ compute_leaf_hash(elem, sizeof(elem), leaf_hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_invalid_found, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_index(dc, 1, root, leaf_hash);
+
+ assert_int_equal(result, -2);
+}
+
+/**
+ * Adversarial: after a valid CCMD_GET_MERKLE_LEAF_INDEX reply, the follow-up
+ * CCMD_GET_MERKLE_LEAF_PROOF call (issued by call_get_merkle_leaf_hash) fails.
+ * call_get_merkle_leaf_index must surface this as -4.
+ */
+static int tamper_fail_second_call(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) response_len;
+ (void) cmd;
+ (void) user_data;
+
+ if (call_count >= 1) {
+ return -1;
+ }
+ return 0;
+}
+
+static void test_get_leaf_index_verify_comm_failure(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ const uint8_t elem[] = {0xCA, 0xFE};
+ const uint8_t *elems[] = {elem};
+ size_t lens[] = {sizeof(elem)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 1, root);
+
+ uint8_t leaf_hash[32];
+ compute_leaf_hash(elem, sizeof(elem), leaf_hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_fail_second_call, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_index(dc, 1, root, leaf_hash);
+
+ assert_int_equal(result, -4);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -422,6 +554,9 @@ int main(void) {
cmocka_unit_test(test_get_leaf_index_eight_elements),
cmocka_unit_test(test_get_leaf_index_wrong_index),
cmocka_unit_test(test_get_leaf_index_oob_index),
+ cmocka_unit_test(test_get_leaf_index_initial_comm_failure),
+ cmocka_unit_test(test_get_leaf_index_invalid_found),
+ cmocka_unit_test(test_get_leaf_index_verify_comm_failure),
};
return cmocka_run_group_tests(tests, NULL, NULL);
diff --git a/unit-tests/test_get_merkle_preimage.c b/unit-tests/test_get_merkle_preimage.c
index 8d3b538..97e64de 100644
--- a/unit-tests/test_get_merkle_preimage.c
+++ b/unit-tests/test_get_merkle_preimage.c
@@ -377,6 +377,302 @@ static void test_get_merkle_preimage_corrupted_continuation(void **state) {
assert_true(result < 0);
}
+/**
+ * Adversarial: truncated response — buffer reads fail. Must return -2.
+ */
+static int tamper_truncate(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE) {
+ *response_len = 0;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_truncated(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[10];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_truncate, NULL);
+
+ uint8_t out[64];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -2);
+}
+
+/**
+ * Adversarial: preimage_len = 0 or partial_data_len = 0 — must return -3.
+ */
+static int tamper_zero_len(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE) {
+ response_buf[0] = 0x00;
+ response_buf[1] = 0x00;
+ *response_len = 2;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_zero_len(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[5] = {1, 2, 3, 4, 5};
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_zero_len, NULL);
+
+ uint8_t out[64];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -3);
+}
+
+/**
+ * Adversarial: partial_data_len > preimage_len with padded buffer.
+ * Must return -5.
+ */
+static int tamper_partial_len_over(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE && *response_len >= 2) {
+ uint8_t preimage_len = response_buf[0];
+ response_buf[1] = preimage_len + 1;
+ response_buf[*response_len] = 0xCC;
+ (*response_len)++;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_partial_len_over(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[5] = {1, 2, 3, 4, 5};
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_partial_len_over, NULL);
+
+ uint8_t out[64];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -5);
+}
+
+/**
+ * Adversarial: second-call communication failure during GET_MORE_ELEMENTS.
+ * Must return -6.
+ */
+static int tamper_fail_second(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) response_len;
+ (void) cmd;
+ (void) user_data;
+
+ if (call_count >= 1) {
+ return -1;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_comm_failure(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[300];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_fail_second, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -6);
+}
+
+/**
+ * Adversarial: truncated GET_MORE_ELEMENTS response — must return -7.
+ */
+static int tamper_truncate_more(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS) {
+ *response_len = 0;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_truncated_more(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[300];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_truncate_more, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -7);
+}
+
+/**
+ * Adversarial: GET_MORE_ELEMENTS with elements_len != 1 — must return -8.
+ */
+static int tamper_more_bad_size(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ response_buf[0] = 0;
+ response_buf[1] = 4;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_bad_element_size(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[300];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_bad_size, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -8);
+}
+
+/**
+ * Adversarial: GET_MORE_ELEMENTS with n_bytes > bytes_remaining (padded buffer).
+ * Must return -9.
+ */
+static int tamper_more_bytes(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ uint8_t orig_n = response_buf[0];
+ response_buf[0] = orig_n + 1;
+ response_buf[*response_len] = 0xAB;
+ (*response_len)++;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_more_bytes(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* element 253 → preimage 254 → spill 3 bytes */
+ uint8_t element[253];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) (i * 7);
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_bytes, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -9);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -391,6 +687,13 @@ int main(void) {
cmocka_unit_test(test_get_merkle_preimage_exact_buffer),
cmocka_unit_test(test_get_merkle_preimage_corrupted_data),
cmocka_unit_test(test_get_merkle_preimage_corrupted_continuation),
+ cmocka_unit_test(test_get_merkle_preimage_truncated),
+ cmocka_unit_test(test_get_merkle_preimage_zero_len),
+ cmocka_unit_test(test_get_merkle_preimage_partial_len_over),
+ cmocka_unit_test(test_get_merkle_preimage_comm_failure),
+ cmocka_unit_test(test_get_merkle_preimage_truncated_more),
+ cmocka_unit_test(test_get_merkle_preimage_bad_element_size),
+ cmocka_unit_test(test_get_merkle_preimage_more_bytes),
};
return cmocka_run_group_tests(tests, NULL, NULL);
diff --git a/unit-tests/test_get_preimage.c b/unit-tests/test_get_preimage.c
index 8125c0d..de3e1fe 100644
--- a/unit-tests/test_get_preimage.c
+++ b/unit-tests/test_get_preimage.c
@@ -521,6 +521,200 @@ static void test_get_preimage_corrupted_continuation(void **state) {
assert_true(result < 0);
}
+/**
+ * Adversarial: client returns a preimage_len > UINT32_MAX (in the 9-byte varint
+ * encoding). call_get_preimage should reject it with -11.
+ */
+static int tamper_preimage_len_too_big(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE) {
+ /* Rewrite the response with a 9-byte varint encoding 2^32 (overflow). */
+ response_buf[0] = 0xFF; /* 9-byte varint marker */
+ /* Little-endian uint64 = 0x0000000100000000 (= 2^32) */
+ response_buf[1] = 0x00;
+ response_buf[2] = 0x00;
+ response_buf[3] = 0x00;
+ response_buf[4] = 0x00;
+ response_buf[5] = 0x01;
+ response_buf[6] = 0x00;
+ response_buf[7] = 0x00;
+ response_buf[8] = 0x00;
+ response_buf[9] = 0x00; /* partial_data_len */
+ *response_len = 10;
+ }
+ return 0;
+}
+
+static void test_get_preimage_overflow_len(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[10] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_preimage_len_too_big, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ /* preimage_len_u64 > UINT32_MAX → return -11 */
+ assert_int_equal(result, -11);
+}
+
+/**
+ * Adversarial: client claims partial_data_len > preimage_len, while also
+ * supplying enough trailing bytes to satisfy buffer_can_read. Must hit the
+ * `partial_data_len > preimage_len` check (return -4), not -2.
+ */
+static int tamper_partial_len_strictly_over(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE && *response_len >= 2) {
+ uint8_t preimage_len = response_buf[0];
+ /* Claim one more byte than actually exists in the preimage. */
+ response_buf[1] = preimage_len + 1;
+ /* Append a single dummy byte so buffer_can_read(partial_data_len) passes. */
+ response_buf[*response_len] = 0xCC;
+ (*response_len)++;
+ }
+ return 0;
+}
+
+static void test_get_preimage_partial_len_strictly_over(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[5] = {0x10, 0x20, 0x30, 0x40, 0x50};
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_partial_len_strictly_over, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -4);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, client returns elements_len != 1 while
+ * setting n_bytes = 0 so buffer_can_read(0) trivially passes — exercises the
+ * `elements_len != 1` rejection (return -7).
+ */
+static int tamper_more_elements_bad_size_strict(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ response_buf[0] = 0; /* n_bytes = 0 → buffer_can_read(0) is true */
+ response_buf[1] = 2; /* elements_len = 2 (invalid) */
+ }
+ return 0;
+}
+
+static void test_get_preimage_bad_element_size_strict(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[300];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) (i & 0xFF);
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_elements_bad_size_strict, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -7);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, client claims n_bytes > bytes_remaining
+ * but pads the buffer so buffer_can_read still passes — exercises the
+ * `n_bytes > bytes_remaining` rejection (return -8).
+ */
+static int tamper_more_bytes_with_padding(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ uint8_t orig_n = response_buf[0];
+ response_buf[0] = orig_n + 1;
+ /* Pad one extra byte so buffer_can_read((orig_n+1)*1) still passes. */
+ response_buf[*response_len] = 0xAB;
+ (*response_len)++;
+ }
+ return 0;
+}
+
+static void test_get_preimage_more_bytes_strict(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[254];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) (i * 7);
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_bytes_with_padding, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -8);
+}
+
/**
* Adversarial: communication failure mid-transfer (process_interruption fails
* on the second call).
@@ -584,6 +778,10 @@ int main(void) {
cmocka_unit_test(test_get_preimage_bad_element_size),
cmocka_unit_test(test_get_preimage_more_bytes_than_remaining),
cmocka_unit_test(test_get_preimage_corrupted_continuation),
+ cmocka_unit_test(test_get_preimage_overflow_len),
+ cmocka_unit_test(test_get_preimage_partial_len_strictly_over),
+ cmocka_unit_test(test_get_preimage_bad_element_size_strict),
+ cmocka_unit_test(test_get_preimage_more_bytes_strict),
cmocka_unit_test(test_get_preimage_communication_failure),
};
diff --git a/unit-tests/test_stream_preimage.c b/unit-tests/test_stream_preimage.c
index 8f22c5b..8a429d3 100644
--- a/unit-tests/test_stream_preimage.c
+++ b/unit-tests/test_stream_preimage.c
@@ -387,6 +387,322 @@ static void test_stream_preimage_corrupted_continuation(void **state) {
assert_true(result < 0);
}
+/**
+ * Adversarial: response is truncated so buffer_read_varint already fails — must
+ * return -2.
+ */
+static int tamper_truncate_response(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE) {
+ *response_len = 0;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_truncated_response(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[20];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_truncate_response, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -2);
+}
+
+/**
+ * Adversarial: client returns preimage_len > UINT32_MAX (9-byte varint).
+ * call_stream_preimage must reject with -10.
+ */
+static int tamper_overflow_len(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE) {
+ response_buf[0] = 0xFF; /* 9-byte varint */
+ response_buf[1] = 0x00;
+ response_buf[2] = 0x00;
+ response_buf[3] = 0x00;
+ response_buf[4] = 0x00;
+ response_buf[5] = 0x01; /* 2^32 */
+ response_buf[6] = 0x00;
+ response_buf[7] = 0x00;
+ response_buf[8] = 0x00;
+ response_buf[9] = 0x00; /* partial_data_len */
+ *response_len = 10;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_overflow_len(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[5] = {1, 2, 3, 4, 5};
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_overflow_len, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -10);
+}
+
+/**
+ * Adversarial: client returns preimage_len = 0 — must return -3.
+ */
+static int tamper_zero_preimage_len(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE) {
+ response_buf[0] = 0x00; /* preimage_len = 0 */
+ response_buf[1] = 0x00; /* partial_data_len = 0 */
+ *response_len = 2;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_zero_len(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[5] = {1, 2, 3, 4, 5};
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_zero_preimage_len, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -3);
+}
+
+/**
+ * Adversarial: client claims partial_data_len > preimage_len while padding the
+ * buffer so buffer_can_read(partial_data_len) passes — must return -4.
+ */
+static int tamper_partial_len_over(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_PREIMAGE && *response_len >= 2) {
+ uint8_t preimage_len = response_buf[0];
+ response_buf[1] = preimage_len + 1;
+ response_buf[*response_len] = 0xCC;
+ (*response_len)++;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_partial_len_over(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[5] = {1, 2, 3, 4, 5};
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_partial_len_over, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -4);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, response is truncated so buffer_read_u8
+ * fails on n_bytes — must return -6.
+ */
+static int tamper_truncate_more_elements(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) response_buf;
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS) {
+ *response_len = 0;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_truncated_more_elements(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[300];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_truncate_more_elements, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -6);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, set n_bytes=0 (so buffer_can_read(0)
+ * trivially passes) and elements_len != 1 — must return -7.
+ */
+static int tamper_more_elements_bad_size(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ response_buf[0] = 0;
+ response_buf[1] = 4; /* elements_len != 1 */
+ }
+ return 0;
+}
+
+static void test_stream_preimage_bad_element_size(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[300];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) i;
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_elements_bad_size, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -7);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, claim n_bytes > bytes_remaining with
+ * the buffer extended so buffer_can_read still passes — must return -8.
+ */
+static int tamper_more_bytes_with_padding(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+ (void) call_count;
+
+ if (cmd == CCMD_GET_MORE_ELEMENTS && *response_len >= 2) {
+ uint8_t orig_n = response_buf[0];
+ response_buf[0] = orig_n + 1;
+ response_buf[*response_len] = 0xAB;
+ (*response_len)++;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_more_bytes_than_remaining(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[253]; /* spill = 3 bytes */
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) (i * 7);
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_more_bytes_with_padding, NULL);
+
+ stream_accumulator_t acc;
+ memset(&acc, 0, sizeof(acc));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_stream_preimage(dc, hash, acc_len_callback, acc_data_callback, &acc);
+
+ assert_int_equal(result, -8);
+}
+
/**
* Adversarial: communication failure mid-stream (second interruption fails).
*/
@@ -445,6 +761,13 @@ int main(void) {
cmocka_unit_test(test_stream_preimage_one_byte_overflow),
cmocka_unit_test(test_stream_preimage_corrupted_data),
cmocka_unit_test(test_stream_preimage_corrupted_continuation),
+ cmocka_unit_test(test_stream_preimage_truncated_response),
+ cmocka_unit_test(test_stream_preimage_overflow_len),
+ cmocka_unit_test(test_stream_preimage_zero_len),
+ cmocka_unit_test(test_stream_preimage_partial_len_over),
+ cmocka_unit_test(test_stream_preimage_truncated_more_elements),
+ cmocka_unit_test(test_stream_preimage_bad_element_size),
+ cmocka_unit_test(test_stream_preimage_more_bytes_than_remaining),
cmocka_unit_test(test_stream_preimage_communication_failure),
};
Why this scored 12/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.