Report a missing map key distinctly from a failed lookup
What changed, and why it matters
This commit fixes a design bug in how the Ledger Bitcoin app asks a connected computer for data stored in a cryptographic map (used for PSBT transaction fields). Previously, 'key genuinely missing' and 'lookup failed due to a bad proof or communication error' were reported the same way. That meant the app could silently apply a default value when it should have rejected a faulty response. The patch separates the two cases so future callers can tell them apart, though existing callers still use the old '< 0' check and are not changed here. The commit also documents that 'missing key' is only what the host claims, not a cryptographic proof.
Review all current and future callers of the three map-value functions to ensure they branch explicitly on MAP_VALUE_ABSENT when applying defaults for optional PSBT fields, rather than using res < 0. Consider backporting the status-code contract to any release branches that may later add callers. Continue strengthening unit tests for proof-of-absence vs. failure cases.
Security signals we found
Ambiguous error handling between missing optional key and lookup failure
Merkle proof failure could be swallowed as 'key not found'
Client assertion of absence is explicitly not cryptographically verified
New status contract added to prevent default-value fallback on errors
Unit tests strengthened to distinguish not-found from error conditions
Evidence from the diff
The commit introduces explicit status codes for Merkleized map value lookups. call_get_merkle_leaf_index now returns MERKLE_LEAF_NOT_FOUND (-1) when the host reports the leaf is absent, and MERKLE_LEAF_ERROR (-2) for malformed responses, proof mismatches, transport errors, or out-of-range data. The three map-value helpers (call_get_merkleized_map_value, call_get_merkleized_map_value_hash, call_stream_merkleized_map_value) now translate those into MAP_VALUE_ABSENT (-1) and MAP_VALUE_ERROR (-2) via the new map_value_status.h contract. This prevents transport/proof failures from being misclassified as absent optional keys. Existing callers still test res < 0, so no runtime behavior changes yet; the patch is preparatory and adds documentation that MAP_VALUE_ABSENT is a client assertion, not a proof of absence.
Changed components
src/handler/lib/get_merkle_leaf_index.csrc/handler/lib/get_merkle_leaf_index.hsrc/handler/lib/get_merkleized_map_value.csrc/handler/lib/get_merkleized_map_value.hsrc/handler/lib/get_merkleized_map_value_hash.csrc/handler/lib/get_merkleized_map_value_hash.hsrc/handler/lib/map_value_status.hsrc/handler/lib/stream_merkleized_map_value.csrc/handler/lib/stream_merkleized_map_value.hunit-tests/test_get_merkle_leaf_index.cInspect captured patch +122 / −38
### src/handler/lib/get_merkle_leaf_index.c
@@ -1,6 +1,8 @@
#include <string.h>
#include <limits.h>
+#include "get_merkle_leaf_index.h"
+
/* Local headers */
#include "client_commands.h"
#include "get_merkle_leaf_hash.h"
@@ -21,7 +23,8 @@ int call_get_merkle_leaf_index(dispatcher_context_t *dispatcher_context,
SET_RESPONSE(dispatcher_context, request, sizeof(request), SW_INTERRUPTED_EXECUTION);
}
if (dispatcher_context->process_interruption(dispatcher_context) < 0) {
- return -3;
+ PRINTF("Interrupted execution failed.\n");
+ return MERKLE_LEAF_ERROR;
}
uint8_t found;
@@ -30,27 +33,32 @@ int call_get_merkle_leaf_index(dispatcher_context_t *dispatcher_context,
if (!buffer_read_u8(&dispatcher_context->read_buffer, &found) ||
!buffer_read_varint(&dispatcher_context->read_buffer, &index) || index > INT_MAX ||
index >= (uint64_t) size) {
- return -1;
+ PRINTF("Malformed response, or index out of range.\n");
+ return MERKLE_LEAF_ERROR;
}
if (found != 0 && found != 1) {
- return -2;
+ PRINTF("Invalid value for the 'found' flag.\n");
+ return MERKLE_LEAF_ERROR;
}
if (!found) {
- return -3;
+ // The client claims the leaf is not in the tree; this is not verified.
+ return MERKLE_LEAF_NOT_FOUND;
}
// Ask the host for the leaf hash with that index
uint8_t returned_merkle_leaf_hash[32];
int res =
call_get_merkle_leaf_hash(dispatcher_context, root, size, index, returned_merkle_leaf_hash);
if (res < 0) {
- return -4;
+ PRINTF("Failed to retrieve the leaf hash at the returned index.\n");
+ return MERKLE_LEAF_ERROR;
}
if (memcmp(leaf_hash, returned_merkle_leaf_hash, 32) != 0) {
- return -5;
+ PRINTF("Leaf hash at the returned index does not match.\n");
+ return MERKLE_LEAF_ERROR;
}
return index;
### src/handler/lib/get_merkle_leaf_index.h
@@ -3,13 +3,28 @@
/* Local headers */
#include "dispatcher.h"
+/**
+ * The client asserts that no leaf with the given hash is in the tree.
+ *
+ * NOTE: this is not verified. The device requests no proof of absence, so this reports what the
+ * client claims, not what the committed tree contains. See map_value_status.h for the implications.
+ */
+#define MERKLE_LEAF_NOT_FOUND (-1)
+
+/**
+ * The lookup failed: malformed client response, Merkle proof mismatch, or transport error.
+ * Nothing may be concluded about whether the leaf is in the tree.
+ */
+#define MERKLE_LEAF_ERROR (-2)
+
/**
* Retrieves the index of the leaf whose hash is `leaf_hash` in the Merkle tree identified by
* `root` and `size`.
*
- * Returns the leaf index on success, or a negative value on failure. This function validates the
- * index returned by the host by retrieving the leaf hash at that index and checking that it matches
- * `leaf_hash`.
+ * Returns the leaf index on success, MERKLE_LEAF_NOT_FOUND if the client reports the leaf is not
+ * in the tree, or MERKLE_LEAF_ERROR on failure. When the client reports the leaf as found, this
+ * function validates the returned index by retrieving the leaf hash at that index and checking
+ * that it matches `leaf_hash`.
*/
int call_get_merkle_leaf_index(dispatcher_context_t *dispatcher_context,
size_t size,
### src/handler/lib/get_merkleized_map_value.c
@@ -27,15 +27,21 @@ int call_get_merkleized_map_value(dispatcher_context_t *dispatcher_context,
int index =
call_get_merkle_leaf_index(dispatcher_context, map->size, map->keys_root, key_merkle_hash);
+ if (index == MERKLE_LEAF_NOT_FOUND) {
+ return MAP_VALUE_ABSENT;
+ }
if (index < 0) {
- PRINTF("Key not found, or incorrect data.\n");
- return -1;
+ PRINTF("Failed to look up the key.\n");
+ return MAP_VALUE_ERROR;
}
- return call_get_merkle_leaf_element(dispatcher_context,
- map->values_root,
- map->size,
- index,
- out,
- out_len);
+ int res = call_get_merkle_leaf_element(dispatcher_context,
+ map->values_root,
+ map->size,
+ index,
+ out,
+ out_len);
+ // Normalize: the failure codes of the underlying flows overlap with MAP_VALUE_ABSENT, and
+ // leaking them would make a transport error look like a missing key.
+ return res < 0 ? MAP_VALUE_ERROR : res;
}
\ No newline at end of file
### src/handler/lib/get_merkleized_map_value.h
@@ -5,6 +5,7 @@
/* Local headers */
#include "dispatcher.h"
+#include "map_value_status.h"
#include "merkle.h"
/**
@@ -13,8 +14,10 @@
* Merkle proof matches. The value is then stored in the `out` pointer, which must be large enough
* to contain the preimage.
*
- * Returns a negative number if the response is too long to fit into the output buffer, or if the
- * key is not found, or if any of the proofs failed. Returns the length of the preimage on success.
+ * Returns the length of the preimage on success, MAP_VALUE_ABSENT if the key is not in the map, or
+ * MAP_VALUE_ERROR if any of the proofs failed, the response was malformed, or the value is too
+ * long to fit into the output buffer. See map_value_status.h; in particular, callers must branch
+ * on MAP_VALUE_ABSENT explicitly rather than on `res < 0` when a missing key is not an error.
*
* PRECONDITION: the map's keys must have already been verified to be lexicographically sorted (and
* therefore unique); this is what makes a by-key lookup unambiguous. A map is validated either by
### src/handler/lib/get_merkleized_map_value_hash.c
@@ -25,10 +25,17 @@ int call_get_merkleized_map_value_hash(dispatcher_context_t *dispatcher_context,
int index =
call_get_merkle_leaf_index(dispatcher_context, map->size, map->keys_root, key_merkle_hash);
+ if (index == MERKLE_LEAF_NOT_FOUND) {
+ return MAP_VALUE_ABSENT;
+ }
if (index < 0) {
- PRINTF("Key not found, or incorrect data.\n");
- return -1;
+ PRINTF("Failed to look up the key.\n");
+ return MAP_VALUE_ERROR;
}
- return call_get_merkle_leaf_hash(dispatcher_context, map->values_root, map->size, index, out);
+ int res =
+ call_get_merkle_leaf_hash(dispatcher_context, map->values_root, map->size, index, out);
+ // Normalize: the failure codes of the underlying flows overlap with MAP_VALUE_ABSENT, and
+ // leaking them would make a transport error look like a missing key.
+ return res < 0 ? MAP_VALUE_ERROR : res;
}
### src/handler/lib/get_merkleized_map_value_hash.h
@@ -2,6 +2,7 @@
/* Local headers */
#include "dispatcher.h"
+#include "map_value_status.h"
#include "merkle.h"
/**
@@ -10,8 +11,9 @@
* pointer. As the value is a Merkle tree preimage, it is always the hash of a string starting with
* a 0x00 byte.
*
- * Returns a negative number if the key is not found, or any of the proofs failed. Returns 0 on
- * success.
+ * Returns 0 on success, MAP_VALUE_ABSENT if the key is not in the map, or MAP_VALUE_ERROR if any
+ * of the proofs failed. See map_value_status.h; in particular, callers must branch on
+ * MAP_VALUE_ABSENT explicitly rather than on `res < 0` when a missing key is not an error.
*
* PRECONDITION: the map's keys must have already been verified to be lexicographically sorted;
* this function asserts it (LEDGER_ASSERT on `map->_keys_are_sorted`).
### src/handler/lib/map_value_status.h
@@ -0,0 +1,29 @@
+#pragma once
+
+/**
+ * Outcome of reading a value by key out of a merkleized map.
+ *
+ * The by-key readers (call_get_merkleized_map_value, call_get_merkleized_map_value_hash,
+ * call_stream_merkleized_map_value) all share this contract:
+ *
+ * - a non-negative return means success; where the operation has a natural length (the number of
+ * bytes read) it is returned, otherwise 0;
+ * - MAP_VALUE_ABSENT means that the client responded that the key is not in the map;
+ * - MAP_VALUE_ERROR means the read failed and no conclusion may be drawn about the key.
+ *
+ * Callers that apply a default value for an optional field MUST branch on MAP_VALUE_ABSENT
+ * specifically, and never on `res < 0`: doing the latter would apply the default on failures,
+ * swallowing an error condition.
+ *
+ * SECURITY — MAP_VALUE_ABSENT is a CLIENT ASSERTION, NOT A PROOF.
+ * It reflects the client answering `found = 0` to CCMD_GET_MERKLE_LEAF_INDEX. The device requests
+ * no proof of absence, so it cannot distinguish an honest omission from a key the client chose to
+ * suppress. Do not treat it as evidence that the committed map lacks the key.
+ * Where soundness is required, derive presence from the key enumeration performed while validating
+ * the map (see input_keys_callback in sign_psbt/preprocess_inputs.c): those flags are computed by
+ * walking the keys tree against the committed `keys_root`, so the client cannot lie about them.
+ */
+typedef enum {
+ MAP_VALUE_ABSENT = -1, // the key is not in the map (client assertion - see above)
+ MAP_VALUE_ERROR = -2, // proof failure, protocol violation, oversized value, transport error
+} map_value_status_t;
### src/handler/lib/stream_merkleized_map_value.c
@@ -26,16 +26,22 @@ int call_stream_merkleized_map_value(dispatcher_context_t *dispatcher_context,
int index =
call_get_merkle_leaf_index(dispatcher_context, map->size, map->keys_root, key_merkle_hash);
+ if (index == MERKLE_LEAF_NOT_FOUND) {
+ return MAP_VALUE_ABSENT;
+ }
if (index < 0) {
- PRINTF("Key not found, or incorrect data.\n");
- return -1;
+ PRINTF("Failed to look up the key.\n");
+ return MAP_VALUE_ERROR;
}
- return call_stream_merkle_leaf_element(dispatcher_context,
- map->values_root,
- map->size,
- index,
- len_callback,
- callback,
- callback_state);
+ int res = call_stream_merkle_leaf_element(dispatcher_context,
+ map->values_root,
+ map->size,
+ index,
+ len_callback,
+ callback,
+ callback_state);
+ // Normalize: the failure codes of the underlying flows overlap with MAP_VALUE_ABSENT, and
+ // leaking them would make a transport error look like a missing key.
+ return res < 0 ? MAP_VALUE_ERROR : res;
}
### src/handler/lib/stream_merkleized_map_value.h
@@ -2,14 +2,17 @@
/* Local headers */
#include "dispatcher.h"
+#include "map_value_status.h"
#include "merkle.h"
/**
* Given a commitment to a merkleized key-value map, this flow find out the index of the
* corresponding element, then it fetches it and it streams it back via the callback. If
* len_callback is not NONE, it is called before the other callback with the length of the element.
*
- * Returns a negative number on failure, or the preimage length on success.
+ * Returns the preimage length on success, MAP_VALUE_ABSENT if the key is not in the map, or
+ * MAP_VALUE_ERROR on failure. See map_value_status.h; in particular, callers must branch on
+ * MAP_VALUE_ABSENT explicitly rather than on `res < 0` when a missing key is not an error.
*
* PRECONDITION: the map's keys must have already been verified to be lexicographically sorted;
* this function asserts it (LEDGER_ASSERT on `map->_keys_are_sorted`).
### unit-tests/test_get_merkle_leaf_index.c
@@ -178,7 +178,9 @@ static void test_get_leaf_index_unknown_hash(void **state) {
dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
int result = call_get_merkle_leaf_index(dc, 1, root, fake_hash);
- assert_true(result < 0);
+ /* A key that is genuinely not in the tree must be reported as such, and must be
+ * distinguishable from a failed lookup. */
+ assert_int_equal(result, MERKLE_LEAF_NOT_FOUND);
}
/**
@@ -394,7 +396,8 @@ static void test_get_leaf_index_initial_comm_failure(void **state) {
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);
+ /* A communication failure must NOT be reported as a missing key. */
+ assert_int_equal(result, MERKLE_LEAF_ERROR);
}
/**
@@ -434,7 +437,8 @@ static void test_get_leaf_index_invalid_found(void **state) {
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);
+ /* An out-of-range 'found' flag is a protocol violation, not a missing key. */
+ assert_int_equal(result, MERKLE_LEAF_ERROR);
}
/**
@@ -476,7 +480,8 @@ static void test_get_leaf_index_verify_comm_failure(void **state) {
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);
+ /* A failure while verifying the returned index must NOT be reported as a missing key. */
+ assert_int_equal(result, MERKLE_LEAF_ERROR);
}
/* ---------- Main ---------- */Why this scored 50/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.