Add various tests with client tampering
What changed, and why it matters
This commit only adds new unit tests that simulate a malicious or buggy companion app sending bad data to the Ledger device. It does not change the actual security code being tested. The tests check that the device-side functions correctly reject corrupted Merkle proofs, wrong leaf indices, bad preimage lengths, and interrupted communications. Because no production code is modified, the commit itself does not introduce or fix a vulnerability; it improves test coverage for existing defensive behavior.
No immediate action required. Review the new tests to confirm they accurately model the protocol threats and that the tested functions are indeed used in production code paths. Consider whether additional tampering scenarios (e.g., truncated responses, swapped Merkle proof sides, replayed responses) should also be covered.
Security signals we found
Adds adversarial unit tests simulating a malicious client
Tests rejection of corrupted Merkle proof sibling hashes and leaf hashes
Tests rejection of malformed proof metadata (proof_size, n_proof_elements, element_len)
Tests rejection of corrupted preimage data and continuation chunks
Tests rejection of out-of-bounds/wrong leaf index claims
Tests rejection of invalid preimage length fields and more-bytes-than-remaining
Tests handling of simulated communication failures during multi-message transfers
Evidence from the diff
The diff is limited to six unit-test files under unit-tests/. It introduces adversarial/tampering test cases using a mock dispatcher tamper hook to corrupt responses for CCMD_GET_MERKLE_LEAF_PROOF, CCMD_GET_PREIMAGE, CCMD_GET_MORE_ELEMENTS, and CCMD_GET_MERKLE_LEAF_INDEX. Each new test asserts that the corresponding library function (call_get_merkle_leaf_element, call_get_merkle_leaf_hash, call_get_merkle_leaf_index, call_get_merkle_preimage, call_get_preimage, call_stream_preimage) returns a negative error code. A single non-test code change increases a local buffer in test_get_merkle_preimage.c from 257 to 513 bytes to accommodate larger test preimages. No firmware/library source files are modified.
Changed components
unit-tests/test_get_merkle_leaf_element.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 +999 / −1
diff --git a/unit-tests/test_get_merkle_leaf_element.c b/unit-tests/test_get_merkle_leaf_element.c
index c268079..4f00176 100644
--- a/unit-tests/test_get_merkle_leaf_element.c
+++ b/unit-tests/test_get_merkle_leaf_element.c
@@ -27,6 +27,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "sha-256.h"
+#include "client_commands.h"
#include "handler/lib/get_merkle_leaf_element.h"
/* ---------- Helpers ---------- */
@@ -283,6 +284,99 @@ static void test_get_leaf_element_eight_elements(void **state) {
}
}
+/* ==========================================================================
+ * Adversarial tests: malicious client behavior
+ * ========================================================================== */
+
+/**
+ * Adversarial: client corrupts the leaf hash in the proof response so it
+ * doesn't match the actual leaf. call_get_merkle_leaf_hash will fail
+ * (root mismatch), causing call_get_merkle_leaf_element to fail.
+ */
+static int tamper_corrupt_leaf_in_proof(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_PROOF && *response_len >= 32) {
+ response_buf[5] ^= 0x01;
+ }
+ return 0;
+}
+
+static void test_get_leaf_element_corrupted_proof(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0x01, 0x02, 0x03};
+ uint8_t e1[] = {0x04, 0x05, 0x06};
+ uint8_t e2[] = {0x07, 0x08, 0x09};
+ uint8_t e3[] = {0x0A, 0x0B, 0x0C};
+
+ 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_corrupt_leaf_in_proof, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_element(dc, root, 4, 0, out, sizeof(out));
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client corrupts the preimage data returned after the proof
+ * verified. The leaf hash was correct (proof passed), but the preimage
+ * returned doesn't hash to it.
+ */
+static int tamper_corrupt_preimage_after_proof(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+
+ /* Only tamper the second interruption (first is the proof, second is preimage) */
+ if (cmd == CCMD_GET_PREIMAGE && call_count == 1 && *response_len > 3) {
+ response_buf[3] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_leaf_element_corrupted_preimage(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0xDE, 0xAD, 0xBE, 0xEF};
+ const uint8_t *elems[] = {e0};
+ size_t lens[] = {sizeof(e0)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 1, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_corrupt_preimage_after_proof, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_element(dc, root, 1, 0, out, sizeof(out));
+
+ /* Preimage hash mismatch → must fail */
+ assert_true(result < 0);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -295,6 +389,8 @@ int main(void) {
cmocka_unit_test(test_get_leaf_element_wrong_root),
cmocka_unit_test(test_get_leaf_element_index_out_of_bounds),
cmocka_unit_test(test_get_leaf_element_eight_elements),
+ cmocka_unit_test(test_get_leaf_element_corrupted_proof),
+ cmocka_unit_test(test_get_leaf_element_corrupted_preimage),
};
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 074ef9c..b2854b3 100644
--- a/unit-tests/test_get_merkle_leaf_hash.c
+++ b/unit-tests/test_get_merkle_leaf_hash.c
@@ -26,6 +26,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "sha-256.h"
+#include "client_commands.h"
#include "handler/lib/get_merkle_leaf_hash.h"
/* ---------- Helpers ---------- */
@@ -330,6 +331,239 @@ static void test_get_leaf_hash_two_elements(void **state) {
}
}
+/* ==========================================================================
+ * Adversarial tests: malicious client behavior
+ * ========================================================================== */
+
+/**
+ * Adversarial: client returns a corrupted sibling hash in the Merkle proof.
+ * The proof won't verify against the root.
+ */
+static int tamper_corrupt_proof_hash(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_PROOF && *response_len > 35) {
+ /* Response: <leaf_hash:32> <proof_size:1> <n_proof_elements:1> <proof_hashes...>
+ * Corrupt byte in first proof hash (offset 34) */
+ response_buf[35] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_corrupted_proof(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_corrupt_proof_hash, 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: client returns a corrupted leaf hash. The proof (built for the
+ * real leaf) won't verify when starting from the wrong leaf hash.
+ */
+static int tamper_corrupt_leaf_hash(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_PROOF && *response_len >= 32) {
+ response_buf[0] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_corrupted_leaf(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0xAA, 0xBB};
+ uint8_t e1[] = {0xCC, 0xDD};
+
+ const uint8_t *elems[] = {e0, e1};
+ size_t lens[] = {sizeof(e0), sizeof(e1)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 2, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_corrupt_leaf_hash, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 2, 0, out);
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client claims n_proof_elements > proof_size.
+ */
+static int tamper_proof_elements_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_MERKLE_LEAF_PROOF && *response_len >= 34) {
+ uint8_t proof_size = response_buf[32];
+ response_buf[33] = proof_size + 5;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_proof_elements_overflow(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0xAA};
+ uint8_t e1[] = {0xBB};
+
+ const uint8_t *elems[] = {e0, e1};
+ size_t lens[] = {sizeof(e0), sizeof(e1)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 2, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_proof_elements_overflow, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 2, 0, out);
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client claims proof_size=0 for a multi-element tree.
+ * The leaf hash alone won't match the root.
+ */
+static int tamper_zero_proof_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_MERKLE_LEAF_PROOF && *response_len >= 34) {
+ response_buf[32] = 0;
+ response_buf[33] = 0;
+ *response_len = 34;
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_zero_proof_size(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0xAA};
+ uint8_t e1[] = {0xBB};
+ uint8_t e2[] = {0xCC};
+
+ const uint8_t *elems[] = {e0, e1, e2};
+ size_t lens[] = {1, 1, 1};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 3, root);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_zero_proof_size, NULL);
+
+ uint8_t out[32];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_hash(dc, root, 3, 0, out);
+
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: during proof continuation (GET_MORE_ELEMENTS), client sends
+ * elements_len != 32.
+ */
+static int tamper_bad_proof_element_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[1] = 16; /* should be 32 */
+ }
+ return 0;
+}
+
+static void test_get_leaf_hash_bad_proof_element_size(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* Need a tree large enough that proof spills: 128 elements → depth 7,
+ * first response fits 6 proof hashes → 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_bad_proof_element_size, 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) {
@@ -342,6 +576,11 @@ int main(void) {
cmocka_unit_test(test_get_leaf_hash_one_byte_element),
cmocka_unit_test(test_get_leaf_hash_wrong_root),
cmocka_unit_test(test_get_leaf_hash_two_elements),
+ cmocka_unit_test(test_get_leaf_hash_corrupted_proof),
+ cmocka_unit_test(test_get_leaf_hash_corrupted_leaf),
+ 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),
};
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 6e96c37..068dfdb 100644
--- a/unit-tests/test_get_merkle_leaf_index.c
+++ b/unit-tests/test_get_merkle_leaf_index.c
@@ -27,6 +27,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "sha-256.h"
+#include "client_commands.h"
#include "handler/lib/get_merkle_leaf_index.h"
/* ---------- Helpers ---------- */
@@ -306,6 +307,107 @@ static void test_get_leaf_index_eight_elements(void **state) {
}
}
+/* ==========================================================================
+ * Adversarial tests: malicious client behavior
+ * ========================================================================== */
+
+/**
+ * Adversarial: client claims a leaf is at a wrong index. The device fetches
+ * the leaf hash at the claimed index and compares — must detect mismatch.
+ */
+static int tamper_wrong_index(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+
+ if (cmd == CCMD_GET_MERKLE_LEAF_INDEX && call_count == 0) {
+ if (*response_len >= 2) {
+ response_buf[0] = 1; /* found */
+ /* Swap: 0↔1 */
+ response_buf[1] = (response_buf[1] == 0) ? 1 : 0;
+ }
+ }
+ return 0;
+}
+
+static void test_get_leaf_index_wrong_index(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0x01, 0x02, 0x03};
+ uint8_t e1[] = {0x04, 0x05, 0x06};
+
+ const uint8_t *elems[] = {e0, e1};
+ size_t lens[] = {sizeof(e0), sizeof(e1)};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 2, root);
+
+ /* Query for e1 (index 1), tamper will claim index 0 */
+ uint8_t leaf_hash[32];
+ compute_leaf_hash(e1, sizeof(e1), leaf_hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_wrong_index, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_index(dc, 2, root, leaf_hash);
+
+ /* Verification fetch at wrong index → hash mismatch → must fail */
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client claims found=1 but returns an out-of-bounds index.
+ */
+static int tamper_oob_index(uint8_t *response_buf,
+ size_t *response_len,
+ uint8_t cmd,
+ int call_count,
+ void *user_data) {
+ (void) user_data;
+
+ if (cmd == CCMD_GET_MERKLE_LEAF_INDEX && call_count == 0) {
+ if (*response_len >= 2) {
+ response_buf[0] = 1; /* found */
+ response_buf[1] = 2; /* out of bounds */
+ }
+ }
+ return 0;
+}
+
+static void test_get_leaf_index_oob_index(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t e0[] = {0xAA};
+ uint8_t e1[] = {0xBB};
+
+ const uint8_t *elems[] = {e0, e1};
+ size_t lens[] = {1, 1};
+
+ uint8_t root[32];
+ build_tree(&mock, elems, lens, 2, root);
+
+ uint8_t leaf_hash[32];
+ compute_leaf_hash(e0, sizeof(e0), leaf_hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_oob_index, NULL);
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_leaf_index(dc, 2, root, leaf_hash);
+
+ /* index >= size → must fail */
+ assert_true(result < 0);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -318,6 +420,8 @@ int main(void) {
cmocka_unit_test(test_get_leaf_index_wrong_root),
cmocka_unit_test(test_get_leaf_index_two_elements),
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),
};
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 074c603..8d3b538 100644
--- a/unit-tests/test_get_merkle_preimage.c
+++ b/unit-tests/test_get_merkle_preimage.c
@@ -27,6 +27,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "sha-256.h"
+#include "client_commands.h"
#include "handler/lib/get_merkle_preimage.h"
/* ---------- Helpers ---------- */
@@ -44,7 +45,7 @@ static void add_merkle_preimage(mock_dispatcher_t *mock,
const uint8_t *element,
size_t element_len,
uint8_t hash_out[32]) {
- uint8_t prefixed[257];
+ uint8_t prefixed[513];
prefixed[0] = 0x00;
memcpy(prefixed + 1, element, element_len);
@@ -286,6 +287,96 @@ static void test_get_merkle_preimage_exact_buffer(void **state) {
assert_memory_equal(out, element, sizeof(element));
}
+/* ==========================================================================
+ * Adversarial tests: malicious client behavior
+ * ========================================================================== */
+
+/**
+ * Adversarial: client returns valid structure but corrupted data bytes,
+ * causing a SHA-256 hash mismatch on the leaf preimage.
+ */
+static int tamper_corrupt_data(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 > 3) {
+ response_buf[3] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_corrupted_data(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[50];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) (i & 0xFF);
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_corrupt_data, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_merkle_preimage(dc, hash, out, sizeof(out));
+
+ /* Must detect hash mismatch */
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client corrupts the continuation data for a large preimage.
+ */
+static int tamper_corrupt_continuation(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[2] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_merkle_preimage_corrupted_continuation(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* Element large enough to require continuation (preimage = 0x00 || element) */
+ uint8_t element[300];
+ 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_corrupt_continuation, 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));
+
+ /* Must detect the corruption (hash mismatch or protocol error) */
+ assert_true(result < 0);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -298,6 +389,8 @@ int main(void) {
cmocka_unit_test(test_get_merkle_preimage_exact_fit),
cmocka_unit_test(test_get_merkle_preimage_one_byte_overflow),
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),
};
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 ec8962f..8125c0d 100644
--- a/unit-tests/test_get_preimage.c
+++ b/unit-tests/test_get_preimage.c
@@ -27,6 +27,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "sha-256.h"
+#include "client_commands.h"
#include "handler/lib/get_preimage.h"
/* ---------- Helpers ---------- */
@@ -248,6 +249,324 @@ static void test_get_preimage_one_byte_overflow(void **state) {
assert_memory_equal(out, preimage, sizeof(preimage));
}
+/* ==========================================================================
+ * Adversarial tests: malicious client behavior
+ * ========================================================================== */
+
+/**
+ * Adversarial: client returns valid structure but corrupted data bytes,
+ * causing a SHA-256 hash mismatch.
+ */
+static int tamper_corrupt_preimage_data(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 > 3) {
+ /* Flip a byte in the data portion (after 1-byte varint + partial_data_len) */
+ response_buf[3] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_preimage_corrupted_data(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[50];
+ 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_corrupt_preimage_data, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ /* Must detect the hash mismatch */
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client claims partial_data_len > preimage_len.
+ */
+static int tamper_partial_len_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_PREIMAGE && *response_len >= 2) {
+ uint8_t preimage_len = response_buf[0];
+ if (preimage_len < 200) {
+ response_buf[1] = preimage_len + 10;
+ }
+ }
+ return 0;
+}
+
+static void test_get_preimage_partial_len_overflow(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[20];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) i;
+ }
+
+ 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_overflow, NULL);
+
+ uint8_t out[256];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ /* Detected via buffer_can_read or partial_data_len > preimage_len */
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client returns preimage_len = 0 (invalid: at minimum the prefix
+ * byte should be present).
+ */
+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;
+ response_buf[1] = 0x00;
+ *response_len = 2;
+ }
+ return 0;
+}
+
+static void test_get_preimage_zero_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_zero_preimage_len, 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_true(result < 0);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, client returns elements_len != 1.
+ */
+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[1] = 4; /* el_len = 4 instead of 1 */
+ }
+ return 0;
+}
+
+static void test_get_preimage_bad_element_size(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, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ /* Detected via buffer_can_read or elements_len != 1 */
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: during GET_MORE_ELEMENTS, client sends more bytes than remaining.
+ */
+static int tamper_more_bytes_than_remaining(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] = 250; /* claim 250 elements */
+ }
+ return 0;
+}
+
+static void test_get_preimage_more_bytes_than_remaining(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* 254 bytes: varint=3 bytes, max_payload = 255-3-1 = 251, spill = 3 bytes */
+ 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_than_remaining, NULL);
+
+ uint8_t out[512];
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ /* n_bytes > bytes_remaining → -8, or buffer_can_read fails → -6 */
+ assert_true(result < 0);
+}
+
+/**
+ * Adversarial: client corrupts continuation data (GET_MORE_ELEMENTS payload),
+ * causing hash mismatch at the end.
+ */
+static int tamper_corrupt_continuation(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[2] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_get_preimage_corrupted_continuation(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_corrupt_continuation, 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_true(result < 0);
+}
+
+/**
+ * Adversarial: communication failure mid-transfer (process_interruption fails
+ * on the second call).
+ */
+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_preimage_communication_failure(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 * 3);
+ }
+
+ 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_fail_second_call, 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_true(result < 0);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -259,6 +578,13 @@ int main(void) {
cmocka_unit_test(test_get_preimage_one_byte),
cmocka_unit_test(test_get_preimage_exact_fit),
cmocka_unit_test(test_get_preimage_one_byte_overflow),
+ cmocka_unit_test(test_get_preimage_corrupted_data),
+ cmocka_unit_test(test_get_preimage_partial_len_overflow),
+ cmocka_unit_test(test_get_preimage_zero_len),
+ 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_communication_failure),
};
return cmocka_run_group_tests(tests, NULL, NULL);
diff --git a/unit-tests/test_stream_preimage.c b/unit-tests/test_stream_preimage.c
index 7dc9600..8f22c5b 100644
--- a/unit-tests/test_stream_preimage.c
+++ b/unit-tests/test_stream_preimage.c
@@ -28,6 +28,7 @@ unsigned int pic(unsigned int linked_address) {
#include "cx_hash_mock.h"
#include "sha-256.h"
+#include "client_commands.h"
#include "handler/lib/stream_preimage.h"
/* ---------- Helpers ---------- */
@@ -295,6 +296,142 @@ static void test_stream_preimage_one_byte_overflow(void **state) {
assert_memory_equal(acc.buf, element, sizeof(element));
}
+/* ==========================================================================
+ * Adversarial tests: malicious client behavior
+ * ========================================================================== */
+
+/**
+ * Adversarial: client returns corrupted data in the initial chunk.
+ * Hash verification must detect the mismatch.
+ */
+static int tamper_corrupt_stream_data(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 > 3) {
+ response_buf[3] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_corrupted_data(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t element[50];
+ for (size_t i = 0; i < sizeof(element); i++) {
+ element[i] = (uint8_t) (i & 0xFF);
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_corrupt_stream_data, 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_true(result < 0);
+}
+
+/**
+ * Adversarial: client corrupts continuation data during streaming.
+ */
+static int tamper_corrupt_stream_continuation(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[2] ^= 0xFF;
+ }
+ return 0;
+}
+
+static void test_stream_preimage_corrupted_continuation(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 * 7);
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_corrupt_stream_continuation, 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_true(result < 0);
+}
+
+/**
+ * Adversarial: communication failure mid-stream (second interruption fails).
+ */
+static int tamper_fail_on_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_stream_preimage_communication_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 * 3);
+ }
+
+ uint8_t hash[32];
+ add_merkle_preimage(&mock, element, sizeof(element), hash);
+
+ mock_dispatcher_set_tamper_hook(&mock, tamper_fail_on_second, 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_true(result < 0);
+}
+
/* ---------- Main ---------- */
int main(void) {
@@ -306,6 +443,9 @@ int main(void) {
cmocka_unit_test(test_stream_preimage_null_len_callback),
cmocka_unit_test(test_stream_preimage_exact_fit),
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_communication_failure),
};
return cmocka_run_group_tests(tests, NULL, NULL);
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.