Merge pull request #533 from LedgerHQ/psbt_refactor
What changed, and why it matters
This commit is a hardening and refactor of the code that reads PSBT (Partially Signed Bitcoin Transaction) fields from a host computer into a Ledger hardware wallet. The core security fix is that the device now clearly distinguishes between a field that is genuinely missing in a PSBT map and a field that is present but malformed or failed to verify. Previously, both cases could return the same error code, which could trick the wallet into silently using a default value (for example, default nSequence or default locktime) when the host had actually committed to something else. The commit also adds a guard that refuses to look up values by key unless the map's keys have already been proven to be sorted and unique, preventing a malicious host from supplying two different values for the same key.
Treat this commit as a security-hardening patch and include it in any release. Review all downstream callers of the new `psbt_fields.h` API to ensure `PSBT_FIELD_ABSENT` is only used for intentional defaults and `PSBT_FIELD_ERROR` always aborts signing. Run the new unit tests (`test_psbt_fields`, updated `test_get_merkleized_map_value`, `test_get_merkle_leaf_index`) in CI. Consider whether any other optional PSBT fields elsewhere in the app still use a simple negative-error check that could silently default.
Security signals we found
Refactor of security-critical PSBT parsing code with explicit hardening goal
Introduction of sorted-key precondition and assertion before by-key map reads
Separation of 'field absent' vs 'field malformed/proof failed' return codes
Zeroing of output buffers on failed/absent reads to prevent use of uninitialized data
Fixing of default-value substitution for optional PSBT fields (nSequence, fallback locktime)
Addition of unit tests specifically targeting regression scenarios where errors were misclassified as absence
Evidence from the diff
The PR refactors the PSBT accessor API in the Ledger Bitcoin app. Key changes: (1) merkleized_map_commitment_t gains a _keys_are_sorted flag that is set only after the keys Merkle tree is validated as lexicographically sorted. (2) New call_check_merkleized_map_sorted validates standalone maps (e.g., the global map from the APDU) and marks them safe for by-key reads. (3) The by-key readers (call_get_merkleized_map_value, _hash, and stream variant) now LEDGER_ASSERT that _keys_are_sorted is true, return distinct MAP_VALUE_ABSENT vs MAP_VALUE_ERROR statuses, and normalize underlying errors so a transport/proof failure cannot be mistaken for a missing key. (4) A new psbt_fields.c/h layer provides typed, length-checked PSBT field accessors returning PSBT_FIELD_ABSENT/PRESENT/ERROR, and zeroes output buffers on non-present outcomes. (5) Call sites in init_global_state.c, preprocess_inputs.c, preprocess_outputs.c, sign_input.c, transaction_display.c, and txhashes.c are migrated to the new API, notably fixing nSequence and fallback locktime defaulting logic so defaults are applied only on genuine absence. Unit tests are added/updated to enforce the new status separation.
Changed components
src/common/merkle.hsrc/handler/lib/check_merkle_tree_sorted.hsrc/handler/lib/get_merkle_leaf_index.c/hsrc/handler/lib/get_merkleized_map.csrc/handler/lib/get_merkleized_map_value.c/hsrc/handler/lib/get_merkleized_map_value_hash.c/hsrc/handler/lib/map_value_status.hsrc/handler/lib/stream_merkleized_map_value.c/hsrc/handler/sign_psbt/amount_from_psbt.csrc/handler/sign_psbt/init_global_state.csrc/handler/sign_psbt/preprocess_inputs.csrc/handler/sign_psbt/preprocess_outputs.csrc/handler/sign_psbt/psbt_fields.c/hsrc/handler/sign_psbt/sign_input.csrc/handler/sign_psbt/transaction_display.csrc/handler/sign_psbt/txhashes.cunit-tests/CMakeLists.txtunit-tests/libs/mock_dispatcher.cunit-tests/test_get_merkle_leaf_index.cunit-tests/test_get_merkleized_map_value.cunit-tests/test_psbt_fields.cInspect captured patch +1218 / −401
### src/common/merkle.h
@@ -1,6 +1,7 @@
#pragma once
+#include <stdbool.h>
#include <stdint.h>
// TODO: RFC6962 defines the empty list hash as sha256(b''); while we're using 0 here. Should we
@@ -86,4 +87,11 @@ typedef struct {
uint64_t size;
uint8_t keys_root[32];
uint8_t values_root[32];
+
+ // PRIVATE - managed only by the merkleized-map API (call_get_merkleized_map* /
+ // call_check_merkleized_map_sorted). Set to true once the keys tree has been verified to be
+ // lexicographically sorted (and therefore the keys are unique), which is the precondition for
+ // reading a value by key. The by-key readers assert this is set.
+ // Callers must not read or set it directly.
+ bool _keys_are_sorted;
} merkleized_map_commitment_t;
### src/handler/lib/check_merkle_tree_sorted.h
@@ -44,4 +44,26 @@ static inline int call_check_merkle_tree_sorted(dispatcher_context_t *dispatcher
size,
NULL,
NULL);
-}
\ No newline at end of file
+}
+
+/**
+ * Validates a merkleized map commitment whose fields were populated directly (rather than obtained
+ * from call_get_merkleized_map): checks that its keys tree is lexicographically sorted, and on
+ * success marks the commitment as validated so that its values can be read by key.
+ *
+ * This is the counterpart of call_get_merkleized_map for maps that are not fetched from an outer
+ * Merkle tree of maps (e.g. the PSBT global map, whose commitment comes straight from the APDU).
+ *
+ * Returns 0 on success, or a negative number on failure.
+ */
+static inline int call_check_merkleized_map_sorted(dispatcher_context_t *dispatcher_context,
+ merkleized_map_commitment_t *map) {
+ // The map is not yet validated; explicitly mark it as such
+ map->_keys_are_sorted = false;
+
+ int ret = call_check_merkle_tree_sorted(dispatcher_context, map->keys_root, (size_t) map->size);
+ if (ret >= 0) {
+ map->_keys_are_sorted = true;
+ }
+ return ret;
+}
### 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.c
@@ -21,6 +21,9 @@ int call_get_merkleized_map_with_callback(dispatcher_context_t *dispatcher_conte
uint8_t raw_output[9 + 2 * 32]; // maximum size of serialized result (9 bytes for the varint,
// and the 2 Merkle roots)
+ // The map is not yet validated; explicitly mark it as such
+ out_ptr->_keys_are_sorted = false;
+
int el_len = call_get_merkle_leaf_element(dispatcher_context,
root,
size,
@@ -38,10 +41,15 @@ int call_get_merkleized_map_with_callback(dispatcher_context_t *dispatcher_conte
return -1;
}
- return call_check_merkle_tree_sorted_with_callback(dispatcher_context,
- callback_state,
- out_ptr->keys_root,
- out_ptr->size,
- callback,
- out_ptr);
+ int ret = call_check_merkle_tree_sorted_with_callback(dispatcher_context,
+ callback_state,
+ out_ptr->keys_root,
+ out_ptr->size,
+ callback,
+ out_ptr);
+ if (ret >= 0) {
+ // keys were verified to be lexicographically sorted: the map is now safe for by-key reads
+ out_ptr->_keys_are_sorted = true;
+ }
+ return ret;
}
### src/handler/lib/get_merkleized_map_value.c
@@ -2,6 +2,9 @@
#include "get_merkleized_map_value.h"
+/* SDK headers */
+#include "ledger_assert.h"
+
/* Local headers */
#include "get_merkle_leaf_element.h"
#include "get_merkle_leaf_index.h"
@@ -14,21 +17,31 @@ int call_get_merkleized_map_value(dispatcher_context_t *dispatcher_context,
size_t out_len) {
// LOG_PROCESSOR(__FILE__, __LINE__, __func__);
+ // Reading a value by key is only sound once the map's keys have been verified sorted (hence
+ // unique); otherwise a malicious client could equivocate. This must hold by construction.
+ LEDGER_ASSERT(map->_keys_are_sorted, "map keys not validated as sorted");
+
uint8_t key_merkle_hash[32];
merkle_compute_element_hash(key, key_len, key_merkle_hash);
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
@@ -1,10 +1,8 @@
#pragma once
-/* SDK headers */
-#include "read.h"
-
/* Local headers */
#include "dispatcher.h"
+#include "map_value_status.h"
#include "merkle.h"
/**
@@ -13,36 +11,24 @@
* 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
+ * `call_get_merkleized_map[_with_callback]` (which validates before returning) or by
+ * `call_check_merkleized_map_sorted`. This function asserts that precondition (LEDGER_ASSERT on
+ * `map->_keys_are_sorted`); it does NOT re-check the ordering itself.
*
- * NOTE: this does _not_ check that the keys are lexicographically sorted; the sanity check needs to
- * be done before.
+ * NOTE for callbacks fired during validation (via call_get_merkleized_map_with_callback): at that
+ * point the map is not yet validated, so values must be read by index (on `values_root`) and never
+ * by key through this function or its siblings.
*/
int call_get_merkleized_map_value(dispatcher_context_t *dispatcher_context,
const merkleized_map_commitment_t *map,
const uint8_t *key,
size_t key_len,
uint8_t *out,
size_t out_len);
-
-/**
- * Convenience shortcut to read a little-endian unsigned 32-bit int.
- * TODO: more docs
- */
-static inline int call_get_merkleized_map_value_u32_le(dispatcher_context_t *dispatcher_context,
- const merkleized_map_commitment_t *map,
- const uint8_t *key,
- size_t key_len,
- uint32_t *out) {
- uint8_t result_raw[4];
-
- int res = call_get_merkleized_map_value(dispatcher_context, map, key, key_len, result_raw, 4);
- if (res != 4) {
- return -1;
- }
-
- *out = read_u32_le(result_raw, 0);
-
- return 4;
-}
### src/handler/lib/get_merkleized_map_value_hash.c
@@ -2,6 +2,9 @@
#include "get_merkleized_map_value_hash.h"
+/* SDK headers */
+#include "ledger_assert.h"
+
/* Local headers */
#include "get_merkle_leaf_hash.h"
#include "get_merkle_leaf_index.h"
@@ -13,15 +16,26 @@ int call_get_merkleized_map_value_hash(dispatcher_context_t *dispatcher_context,
uint8_t out[static 32]) {
// LOG_PROCESSOR(__FILE__, __LINE__, __func__);
+ // Reading a value by key is only sound once the map's keys have been verified sorted (hence
+ // unique); otherwise a malicious client could equivocate. This must hold by construction.
+ LEDGER_ASSERT(map->_keys_are_sorted, "map keys not validated as sorted");
+
uint8_t key_merkle_hash[32];
merkle_compute_element_hash(key, key_len, key_merkle_hash);
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,11 +11,12 @@
* 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.
*
- * NOTE: this does _not_ check that the keys are lexicographically sorted; the sanity check needs to
- * be done before.
+ * 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`).
*/
int call_get_merkleized_map_value_hash(dispatcher_context_t *dispatcher_context,
const merkleized_map_commitment_t *map,
### src/handler/lib/map_value_status.h
@@ -0,0 +1,19 @@
+#pragma once
+
+/**
+ * Outcome of the by-key merkleized map readers (call_get_merkleized_map_value[_hash],
+ * call_stream_merkleized_map_value): non-negative is success (byte count, or 0 if not meaningful),
+ * negative is one of the statuses below.
+ *
+ * Callers applying a default for an optional field MUST branch on MAP_VALUE_ABSENT, never on
+ * `res < 0`, which would silently apply the default on errors too.
+ *
+ * SECURITY: MAP_VALUE_ABSENT is only the client asserting `found = 0`; absence is never proved, so
+ * a malicious client can suppress any key. Where soundness matters, use the presence flags computed
+ * while validating the map against the committed `keys_root` (see input_keys_callback in
+ * sign_psbt/preprocess_inputs.c).
+ */
+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
@@ -1,5 +1,8 @@
#include "stream_merkleized_map_value.h"
+/* SDK headers */
+#include "ledger_assert.h"
+
/* Local headers */
#include "get_merkle_leaf_index.h"
#include "stream_merkle_leaf_element.h"
@@ -13,22 +16,32 @@ int call_stream_merkleized_map_value(dispatcher_context_t *dispatcher_context,
void *callback_state) {
LOG_PROCESSOR(__FILE__, __LINE__, __func__);
+ // Reading a value by key is only sound once the map's keys have been verified sorted (hence
+ // unique); otherwise a malicious client could equivocate. This must hold by construction.
+ LEDGER_ASSERT(map->_keys_are_sorted, "map keys not validated as sorted");
+
uint8_t key_merkle_hash[32];
merkle_compute_element_hash(key, key_len, key_merkle_hash);
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,17 +2,20 @@
/* 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.
*
- * NOTE: this does _not_ check that the keys are lexicographically sorted; the sanity check needs to
- * be done before.
+ * 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`).
*/
int call_stream_merkleized_map_value(dispatcher_context_t *dispatcher_context,
const merkleized_map_commitment_t *map,
### src/handler/sign_psbt/amount_from_psbt.c
@@ -20,8 +20,12 @@
/* Local headers */
#include "get_merkleized_map_value.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "psbt_parse_rawtx.h"
+/* SDK headers */
+#include "read.h"
+
/*
Convenience function to get the amount and scriptpubkey from the non-witness-utxo of a certain
input in a PSBTv2.
@@ -42,11 +46,7 @@ int __attribute__((noinline)) get_amount_scriptpubkey_from_psbt_nonwitness(
// Read the prevout index
uint32_t prevout_n;
- if (4 != call_get_merkleized_map_value_u32_le(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- &prevout_n)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, input_map, &prevout_n)) {
return -1;
}
### src/handler/sign_psbt/init_global_state.c
@@ -37,11 +37,11 @@
#include "error_codes.h"
#include "get_merkle_leaf_element.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "get_preimage.h"
#include "musig.h"
#include "policy.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sign_psbt_cache.h"
#include "sw.h"
#include "wallet.h"
@@ -123,46 +123,32 @@ static bool __attribute__((noinline)) parse_sign_psbt_apdu(dispatcher_context_t
*/
static bool __attribute__((noinline)) process_global_map(dispatcher_context_t *dc,
sign_psbt_state_t *st) {
- // Check integrity of the global map
- if (call_check_merkle_tree_sorted(dc, st->global_map.keys_root, (size_t) st->global_map.size) <
- 0) {
+ // Check integrity of the global map (this also marks it as validated, so that its values may
+ // be read by key below).
+ if (call_check_merkleized_map_sorted(dc, &st->global_map) < 0) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- uint8_t raw_result[9]; // max size for a varint
- int result_len;
-
// Read tx version
- result_len = call_get_merkleized_map_value(dc,
- &st->global_map,
- (uint8_t[]) {PSBT_GLOBAL_TX_VERSION},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len != 4) {
+ if (PSBT_FIELD_PRESENT != psbt_get_global_tx_version(dc, &st->global_map, &st->tx_version)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- st->tx_version = read_u32_le(raw_result, 0);
// Read fallback locktime.
// Unlike BIP-0370 recommendation, we use the fallback locktime as-is, ignoring each input's
// preferred height/block locktime. If that's relevant, the client must set the fallback
// locktime to the appropriate value before calling sign_psbt.
- result_len = call_get_merkleized_map_value(dc,
- &st->global_map,
- (uint8_t[]) {PSBT_GLOBAL_FALLBACK_LOCKTIME},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len < 0) {
- st->locktime = 0;
- } else if (result_len != 4) {
- SEND_SW(dc, SW_INCORRECT_DATA);
- return false;
- } else {
- st->locktime = read_u32_le(raw_result, 0);
+ switch (psbt_get_global_fallback_locktime(dc, &st->global_map, &st->locktime)) {
+ case PSBT_FIELD_ABSENT:
+ st->locktime = 0;
+ break;
+ case PSBT_FIELD_PRESENT:
+ break;
+ default: // PSBT_FIELD_ERROR: present but malformed
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return false;
}
return true;
### src/handler/sign_psbt/preprocess_inputs.c
@@ -33,11 +33,11 @@
#include "dispatcher.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "init_global_state.h"
#include "policy.h"
#include "process_in_outs.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sighash.h"
#include "sign_psbt_cache.h"
#include "sw.h"
@@ -168,12 +168,8 @@ bool __attribute__((noinline)) preprocess_inputs(
// check if the prevout_hash of the transaction matches the computed one from the
// non-witness utxo
- if (0 > call_get_merkleized_map_value(dc,
- &input.in_out.map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- prevout_hash,
- sizeof(prevout_hash))) {
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_input_prevout_txid(dc, &input.in_out.map, prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
@@ -301,11 +297,8 @@ bool __attribute__((noinline)) preprocess_inputs(
}
// get the sighash_type
- if (4 != call_get_merkleized_map_value_u32_le(dc,
- &input.in_out.map,
- (uint8_t[]) {PSBT_IN_SIGHASH_TYPE},
- 1,
- &input.sighash_type)) {
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_input_sighash_type(dc, &input.in_out.map, &input.sighash_type)) {
PRINTF("Malformed PSBT_IN_SIGHASH_TYPE for input %d\n", cur_input_index);
SEND_SW(dc, SW_INCORRECT_DATA);
### src/handler/sign_psbt/preprocess_outputs.c
@@ -30,9 +30,9 @@
#include "dispatcher.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "process_in_outs.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sign_psbt_cache.h"
#include "sw.h"
@@ -131,21 +131,12 @@ bool __attribute__((noinline)) preprocess_outputs(
return false;
}
- // Read output amount
- uint8_t raw_result[8];
-
// Read the output's amount
- int result_len = call_get_merkleized_map_value(dc,
- &output.in_out.map,
- (uint8_t[]) {PSBT_OUT_AMOUNT},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len != 8) {
+ uint64_t value;
+ if (PSBT_FIELD_PRESENT != psbt_get_output_amount(dc, &output.in_out.map, &value)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- uint64_t value = read_u64_le(raw_result, 0);
if (value > BITCOIN_TOTAL_SUPPLY) {
// sanity check to avoid overflows in amounts
@@ -158,20 +149,15 @@ bool __attribute__((noinline)) preprocess_outputs(
st->outputs.total_amount += value;
// Read the output's scriptPubKey
- result_len = call_get_merkleized_map_value(dc,
- &output.in_out.map,
- (uint8_t[]) {PSBT_OUT_SCRIPT},
- 1,
- output.in_out.scriptPubKey,
- sizeof(output.in_out.scriptPubKey));
-
- if (result_len < 0 || result_len > (int) sizeof(output.in_out.scriptPubKey)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_output_script(dc,
+ &output.in_out.map,
+ output.in_out.scriptPubKey,
+ sizeof(output.in_out.scriptPubKey),
+ &output.in_out.scriptPubKey_len)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- output.in_out.scriptPubKey_len = result_len;
-
int is_internal = is_in_out_internal(dc, st, sign_psbt_cache, &output.in_out, false);
if (is_internal < 0) {
### src/handler/sign_psbt/psbt_fields.c
@@ -0,0 +1,207 @@
+/*****************************************************************************
+ * Ledger App Bitcoin.
+ * (c) 2026 Ledger SAS.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *****************************************************************************/
+
+#include <string.h>
+
+#include "psbt_fields.h"
+
+/* SDK headers */
+#include "read.h"
+
+/* Local headers */
+#include "constants.h"
+#include "get_merkleized_map_value.h"
+#include "psbt.h"
+
+/* -------------------------------------------------------------------------- */
+/* Reading helpers */
+/* -------------------------------------------------------------------------- */
+
+/*
+ * Every PSBT key handled here is a single key-type byte, and every field has a length policy (an
+ * exact size, or a maximum). These helpers apply that policy on top of the raw map reader and
+ * translate its outcome into a psbt_field_status_t, so the accessors below only have to name the
+ * key type and the expected shape.
+ *
+ * The length policy is a PSBT-level concern, which is why these live here rather than alongside
+ * call_get_merkleized_map_value: the map layer has no opinion on how long a value ought to be.
+ */
+
+/**
+ * Reads a value of variable length (up to `out_cap` bytes) into `out`, writing its length to
+ * `*out_len`. A value longer than `out_cap` is reported as PSBT_FIELD_ERROR.
+ *
+ * On any non-PRESENT outcome `out` is left zeroed and `*out_len` is 0.
+ */
+static psbt_field_status_t read_var(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len) {
+ int res = call_get_merkleized_map_value(dc, map, &key_type, 1, out, out_cap);
+ if (res < 0) {
+ // on absent or error, zero out the output buffer and length,
+ // preventing the caller from possibly using uninitialized data
+ explicit_bzero(out, out_cap);
+ *out_len = 0;
+ return res == MAP_VALUE_ABSENT ? PSBT_FIELD_ABSENT : PSBT_FIELD_ERROR;
+ }
+ *out_len = (size_t) res;
+ return PSBT_FIELD_PRESENT;
+}
+
+/**
+ * Reads a value that must be exactly `len` bytes into `out`. A present value of any other length
+ * is malformed, hence PSBT_FIELD_ERROR.
+ *
+ * On any non-PRESENT outcome `out` is left zeroed.
+ */
+static psbt_field_status_t read_fixed(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint8_t *out,
+ size_t len) {
+ size_t read_len;
+ psbt_field_status_t status = read_var(dc, map, key_type, out, len, &read_len);
+ if (status != PSBT_FIELD_PRESENT) {
+ return status;
+ }
+ if (read_len != len) {
+ // on error, zero out the output buffer, preventing the caller from
+ // possibly using uninitialized data
+ explicit_bzero(out, len);
+ return PSBT_FIELD_ERROR;
+ }
+ return PSBT_FIELD_PRESENT;
+}
+
+/** Reads a value that must be exactly 4 bytes, decoded as a little-endian unsigned 32-bit int. */
+static psbt_field_status_t read_u32_le_field(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint32_t *out) {
+ uint8_t raw[4];
+ psbt_field_status_t status = read_fixed(dc, map, key_type, raw, sizeof(raw));
+ if (status == PSBT_FIELD_PRESENT) {
+ *out = read_u32_le(raw, 0);
+ }
+ return status;
+}
+
+/** Reads a value that must be exactly 8 bytes, decoded as a little-endian unsigned 64-bit int. */
+static psbt_field_status_t read_u64_le_field(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint64_t *out) {
+ uint8_t raw[8];
+ psbt_field_status_t status = read_fixed(dc, map, key_type, raw, sizeof(raw));
+ if (status == PSBT_FIELD_PRESENT) {
+ *out = read_u64_le(raw, 0);
+ }
+ return status;
+}
+
+/* -------------------------------------------------------------------------- */
+/* Global map */
+/* -------------------------------------------------------------------------- */
+
+psbt_field_status_t psbt_get_global_tx_version(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, global_map, PSBT_GLOBAL_TX_VERSION, out);
+}
+
+psbt_field_status_t psbt_get_global_fallback_locktime(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, global_map, PSBT_GLOBAL_FALLBACK_LOCKTIME, out);
+}
+
+/* -------------------------------------------------------------------------- */
+/* Input map */
+/* -------------------------------------------------------------------------- */
+
+psbt_field_status_t psbt_get_input_prevout_txid(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t out[static 32]) {
+ return read_fixed(dc, input_map, PSBT_IN_PREVIOUS_TXID, out, 32);
+}
+
+psbt_field_status_t psbt_get_input_prevout_index(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, input_map, PSBT_IN_OUTPUT_INDEX, out);
+}
+
+psbt_field_status_t psbt_get_input_sequence(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, input_map, PSBT_IN_SEQUENCE, out);
+}
+
+psbt_field_status_t psbt_get_input_sighash_type(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, input_map, PSBT_IN_SIGHASH_TYPE, out);
+}
+
+psbt_field_status_t psbt_get_input_redeem_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len) {
+ return read_var(dc, input_map, PSBT_IN_REDEEM_SCRIPT, out, out_cap, out_len);
+}
+
+psbt_field_status_t psbt_get_input_witness_utxo_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint64_t *amount) {
+ // The witness UTXO is encoded as [8-byte amount][varint scriptPubKey len][scriptPubKey]. Here
+ // we only need the amount, but the whole value must fit in the buffer for the read to succeed.
+ uint8_t raw[8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN];
+ size_t len;
+ psbt_field_status_t status =
+ read_var(dc, input_map, PSBT_IN_WITNESS_UTXO, raw, sizeof(raw), &len);
+ if (status != PSBT_FIELD_PRESENT) {
+ return status;
+ }
+ if (len < 8) {
+ // present, but too short to even contain the amount
+ return PSBT_FIELD_ERROR;
+ }
+ *amount = read_u64_le(raw, 0);
+ return PSBT_FIELD_PRESENT;
+}
+
+/* -------------------------------------------------------------------------- */
+/* Output map */
+/* -------------------------------------------------------------------------- */
+
+psbt_field_status_t psbt_get_output_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint64_t *out) {
+ return read_u64_le_field(dc, output_map, PSBT_OUT_AMOUNT, out);
+}
+
+psbt_field_status_t psbt_get_output_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len) {
+ return read_var(dc, output_map, PSBT_OUT_SCRIPT, out, out_cap, out_len);
+}
### src/handler/sign_psbt/psbt_fields.h
@@ -0,0 +1,144 @@
+/*****************************************************************************
+ * Ledger App Bitcoin.
+ * (c) 2026 Ledger SAS.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *****************************************************************************/
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Local headers */
+#include "dispatcher.h"
+#include "merkle.h"
+
+/**
+ * Per-field accessors for the PSBT maps read while signing.
+ *
+ * Each function wraps the low-level merkleized-map value reader and owns the *structural*
+ * validation of one PSBT field: the key type, the expected length, whether the field is optional,
+ * and how to decode it into a typed value.
+ */
+
+/**
+ * Outcome of reading one PSBT field. Every accessor below returns this, so that a caller can
+ * always tell a field that is simply not there from one that could not be read.
+ *
+ * Callers applying a default for an optional field MUST branch on PSBT_FIELD_ABSENT specifically
+ * and treat PSBT_FIELD_ERROR as fatal. Testing for "not present" would silently substitute the
+ * default after a Merkle proof failure, making the device sign over a value the client never
+ * committed to.
+ *
+ * SECURITY: PSBT_FIELD_ABSENT rests on the client's word. See the note in
+ * handler/lib/map_value_status.h — the device requests no proof of absence, so a suppressed key
+ * cannot be told apart from an honest omission. Where that matters, use the presence flags derived
+ * from key enumeration (has_witnessUtxo and friends), which are committed to by keys_root.
+ */
+typedef enum {
+ PSBT_FIELD_ERROR = -1, // present but malformed (wrong length), or a fetch/proof failure
+ PSBT_FIELD_ABSENT = 0, // the key is not present in the map (per the client)
+ PSBT_FIELD_PRESENT = 1, // present and well-formed; the out-parameter has been written
+} psbt_field_status_t;
+
+/* -------------------------------------------------------------------------- */
+/* Global map */
+/* -------------------------------------------------------------------------- */
+
+/** PSBT_GLOBAL_TX_VERSION: 4-byte little-endian version. Mandatory: ABSENT is a malformed PSBT. */
+psbt_field_status_t psbt_get_global_tx_version(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out);
+
+/**
+ * PSBT_GLOBAL_FALLBACK_LOCKTIME: optional 4-byte little-endian locktime.
+ * On ABSENT the caller must use locktime 0 (BIP-0370); ERROR must abort.
+ */
+psbt_field_status_t psbt_get_global_fallback_locktime(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out);
+
+/* -------------------------------------------------------------------------- */
+/* Input map */
+/* -------------------------------------------------------------------------- */
+
+/** PSBT_IN_PREVIOUS_TXID: 32-byte prevout txid. Mandatory. */
+psbt_field_status_t psbt_get_input_prevout_txid(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t out[static 32]);
+
+/** PSBT_IN_OUTPUT_INDEX: 4-byte little-endian prevout index. Mandatory. */
+psbt_field_status_t psbt_get_input_prevout_index(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out);
+
+/**
+ * PSBT_IN_SEQUENCE: optional 4-byte little-endian nSequence.
+ * On ABSENT the caller must use the 0xFFFFFFFF default (BIP-0370); ERROR must abort. Note this is
+ * deliberately not folded into the accessor: a default returned from an error path would be signed
+ * over without the client ever having committed to it.
+ */
+psbt_field_status_t psbt_get_input_sequence(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out);
+
+/**
+ * PSBT_IN_SIGHASH_TYPE: 4-byte little-endian sighash type. Callers that only read it once
+ * has_sighash_type is set treat ABSENT as a malformed PSBT.
+ */
+psbt_field_status_t psbt_get_input_sighash_type(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out);
+
+/**
+ * PSBT_IN_REDEEM_SCRIPT: variable-length redeem script. On PSBT_FIELD_PRESENT, copies the value
+ * into `out` and writes its length to `*out_len`. A script longer than `out_cap` yields
+ * PSBT_FIELD_ERROR.
+ */
+psbt_field_status_t psbt_get_input_redeem_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len);
+
+/**
+ * PSBT_IN_WITNESS_UTXO amount: reads the witness UTXO and returns its 8-byte little-endian amount.
+ * Only requires the value to be at least 8 bytes and at most 8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN;
+ * the full structural validation of the witness UTXO is done elsewhere (see
+ * get_amount_scriptpubkey_from_psbt_witness).
+ */
+psbt_field_status_t psbt_get_input_witness_utxo_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint64_t *amount);
+
+/* -------------------------------------------------------------------------- */
+/* Output map */
+/* -------------------------------------------------------------------------- */
+
+/** PSBT_OUT_AMOUNT: 8-byte little-endian amount. Mandatory. */
+psbt_field_status_t psbt_get_output_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint64_t *out);
+
+/**
+ * PSBT_OUT_SCRIPT: variable-length scriptPubKey. On PSBT_FIELD_PRESENT, copies the value into
+ * `out` and writes its length to `*out_len`. A script longer than `out_cap` yields
+ * PSBT_FIELD_ERROR.
+ */
+psbt_field_status_t psbt_get_output_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len);
### src/handler/sign_psbt/sign_input.c
@@ -34,12 +34,12 @@
#include "dispatcher.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "init_global_state.h"
#include "musig_signing.h"
#include "policy.h"
#include "preprocess_inputs.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sign_psbt_cache.h"
#include "sw.h"
#include "txhashes.h"
@@ -231,11 +231,8 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_
// changes depending on the type of spend; therefore, we set it later.
if (input->has_sighash_type) {
// Get sighash type
- if (4 != call_get_merkleized_map_value_u32_le(dc,
- &input->in_out.map,
- (uint8_t[]) {PSBT_IN_SIGHASH_TYPE},
- 1,
- &input->sighash_type)) {
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_input_sighash_type(dc, &input->in_out.map, &input->sighash_type)) {
PRINTF("Malformed PSBT_IN_SIGHASH_TYPE for input %d\n", cur_input_index);
SEND_SW(dc, SW_INCORRECT_DATA);
@@ -324,14 +321,12 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_
// wrapped segwit transactions that we support
uint8_t redeemScript[MAX_PREVOUT_SCRIPTPUBKEY_LEN];
- int redeemScript_length =
- call_get_merkleized_map_value(dc,
- &input->in_out.map,
- (uint8_t[]) {PSBT_IN_REDEEM_SCRIPT},
- 1,
- redeemScript,
- sizeof(redeemScript));
- if (redeemScript_length < 0) {
+ size_t redeemScript_length;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_redeem_script(dc,
+ &input->in_out.map,
+ redeemScript,
+ sizeof(redeemScript),
+ &redeemScript_length)) {
PRINTF("Error fetching redeem script\n");
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
### src/handler/sign_psbt/transaction_display.c
@@ -29,9 +29,9 @@
#include "dispatcher.h"
#include "display.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "menu.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "script.h"
#include "sighash.h"
#include "sw.h"
@@ -91,38 +91,22 @@ static bool get_output_script_and_amount(
return false;
}
- // Read output amount
- uint8_t raw_result[8];
-
// Read the output's amount
- int result_len = call_get_merkleized_map_value(dc,
- &map,
- (uint8_t[]) {PSBT_OUT_AMOUNT},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len != 8) {
+ if (PSBT_FIELD_PRESENT != psbt_get_output_amount(dc, &map, out_amount)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- uint64_t value = read_u64_le(raw_result, 0);
- *out_amount = value;
// Read the output's scriptPubKey
- result_len = call_get_merkleized_map_value(dc,
- &map,
- (uint8_t[]) {PSBT_OUT_SCRIPT},
- 1,
- out_scriptPubKey,
- MAX_OUTPUT_SCRIPTPUBKEY_LEN);
-
- if (result_len < 0 || result_len > MAX_OUTPUT_SCRIPTPUBKEY_LEN) {
+ if (PSBT_FIELD_PRESENT != psbt_get_output_script(dc,
+ &map,
+ out_scriptPubKey,
+ MAX_OUTPUT_SCRIPTPUBKEY_LEN,
+ out_scriptPubKey_len)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- *out_scriptPubKey_len = result_len;
-
return true;
}
### src/handler/sign_psbt/txhashes.c
@@ -17,12 +17,15 @@
#include "txhashes.h"
+/* SDK headers */
+#include "write.h"
+
/* Local headers */
#include "amount_from_psbt.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "stream_merkleized_map_value.h"
/* BIP0341 tags for computing the tagged hashes when computing he sighash */
@@ -75,6 +78,32 @@ static int update_hashes_with_map_value(dispatcher_context_t *dispatcher_context
&cb_state);
}
+/**
+ * Reads an input's nSequence, substituting the BIP-0370 default of 0xFFFFFFFF when the field is
+ * genuinely absent from the map.
+ *
+ * The default is applied only on PSBT_FIELD_ABSENT: a field that is present but unreadable (wrong
+ * length, or a failed Merkle proof) is an error, because silently defaulting there would hash an
+ * nSequence the client never committed to.
+ *
+ * Returns false after sending an error status word.
+ */
+static bool get_nsequence_or_default(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint32_t *out) {
+ switch (psbt_get_input_sequence(dc, map, out)) {
+ case PSBT_FIELD_PRESENT:
+ return true;
+ case PSBT_FIELD_ABSENT:
+ *out = 0xFFFFFFFF;
+ return true;
+ default:
+ PRINTF("Malformed PSBT_IN_SEQUENCE\n");
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return false;
+ }
+}
+
// Updates the hash_context with the output of given index
// returns -1 on error. 0 on success.
static int hash_output_n(dispatcher_context_t *dc,
@@ -94,28 +123,21 @@ static int hash_output_n(dispatcher_context_t *dc,
}
// get output's amount
- uint8_t amount_raw[8];
- if (8 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_OUT_AMOUNT},
- 1,
- amount_raw,
- 8)) {
+ uint64_t amount;
+ if (PSBT_FIELD_PRESENT != psbt_get_output_amount(dc, &ith_map, &amount)) {
return -1;
}
+ uint8_t amount_raw[8];
+ write_u64_le(amount_raw, 0, amount);
crypto_hash_update(hash_context, amount_raw, 8);
// get output's scriptPubKey
uint8_t out_script[MAX_OUTPUT_SCRIPTPUBKEY_LEN];
- int out_script_len = call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_OUT_SCRIPT},
- 1,
- out_script,
- sizeof(out_script));
- if (out_script_len < 0) {
+ size_t out_script_len;
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_output_script(dc, &ith_map, out_script, sizeof(out_script), &out_script_len)) {
return -1;
}
@@ -160,42 +182,30 @@ bool __attribute__((noinline)) compute_tx_hashes(dispatcher_context_t *dc,
// get prevout hash and output index for the i-th input
uint8_t ith_prevout_hash[32];
- if (32 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- ith_prevout_hash,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, &ith_map, ith_prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sha_prevouts_context.header, ith_prevout_hash, 32);
- uint8_t ith_prevout_n_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- ith_prevout_n_raw,
- 4)) {
+ uint32_t ith_prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, &ith_map, &ith_prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ uint8_t ith_prevout_n_raw[4];
+ write_u32_le(ith_prevout_n_raw, 0, ith_prevout_n);
crypto_hash_update(&sha_prevouts_context.header, ith_prevout_n_raw, 4);
- uint8_t ith_nSequence_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- ith_nSequence_raw,
- 4)) {
- // if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(ith_nSequence_raw, 0xFF, 4);
+ uint32_t ith_nSequence;
+ if (!get_nsequence_or_default(dc, &ith_map, &ith_nSequence)) {
+ return false;
}
+ uint8_t ith_nSequence_raw[4];
+ write_u32_le(ith_nSequence_raw, 0, ith_nSequence);
crypto_hash_update(&sha_sequences_context.header, ith_nSequence_raw, 4);
}
@@ -309,29 +319,21 @@ bool __attribute__((noinline)) compute_sighash_legacy(dispatcher_context_t *dc,
// get prevout hash and output index for the i-th input
uint8_t ith_prevout_hash[32];
- if (32 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- ith_prevout_hash,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, &ith_map, ith_prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sighash_context.header, ith_prevout_hash, 32);
- uint8_t ith_prevout_n_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- ith_prevout_n_raw,
- 4)) {
+ uint32_t ith_prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, &ith_map, &ith_prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ uint8_t ith_prevout_n_raw[4];
+ write_u32_le(ith_prevout_n_raw, 0, ith_prevout_n);
crypto_hash_update(&sighash_context.header, ith_prevout_n_raw, 4);
if (i != input_index) {
@@ -368,17 +370,13 @@ bool __attribute__((noinline)) compute_sighash_legacy(dispatcher_context_t *dc,
}
}
- uint8_t ith_nSequence_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- ith_nSequence_raw,
- 4)) {
- // if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(ith_nSequence_raw, 0xFF, 4);
+ uint32_t ith_nSequence;
+ if (!get_nsequence_or_default(dc, &ith_map, &ith_nSequence)) {
+ return false;
}
+ uint8_t ith_nSequence_raw[4];
+ write_u32_le(ith_nSequence_raw, 0, ith_nSequence);
crypto_hash_update(&sighash_context.header, ith_nSequence_raw, 4);
}
@@ -450,29 +448,21 @@ bool __attribute__((noinline)) compute_sighash_segwitv0(
// get prevout hash and output index for the current input
uint8_t prevout_hash[32];
- if (32 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- prevout_hash,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, input_map, prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sighash_context.header, prevout_hash, 32);
- uint8_t prevout_n_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- prevout_n_raw,
- 4)) {
+ uint32_t prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, input_map, &prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ uint8_t prevout_n_raw[4];
+ write_u32_le(prevout_n_raw, 0, prevout_n);
crypto_hash_update(&sighash_context.header, prevout_n_raw, 4);
}
@@ -521,37 +511,27 @@ bool __attribute__((noinline)) compute_sighash_segwitv0(
}
{
- // input value, taken from the WITNESS_UTXO field
- uint8_t witness_utxo[8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN];
-
- int witness_utxo_len = call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_WITNESS_UTXO},
- 1,
- witness_utxo,
- sizeof(witness_utxo));
- if (witness_utxo_len < 8) {
+ // input value (8-byte amount), taken from the WITNESS_UTXO field
+ uint64_t amount;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_witness_utxo_amount(dc, input_map, &amount)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- crypto_hash_update(&sighash_context.header,
- witness_utxo,
- 8); // only the first 8 bytes (amount)
+ uint8_t amount_raw[8];
+ write_u64_le(amount_raw, 0, amount);
+ crypto_hash_update(&sighash_context.header, amount_raw, 8);
}
// nSequence
{
- uint8_t nSequence_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- nSequence_raw,
- 4)) {
- // if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(nSequence_raw, 0xFF, 4);
+ uint32_t nSequence;
+ if (!get_nsequence_or_default(dc, input_map, &nSequence)) {
+ return false;
}
+
+ uint8_t nSequence_raw[4];
+ write_u32_le(nSequence_raw, 0, nSequence);
crypto_hash_update(&sighash_context.header, nSequence_raw, 4);
}
@@ -610,7 +590,8 @@ bool __attribute__((noinline)) compute_sighash_segwitv1(
// the first 0x00 byte is not part of SigMsg
crypto_hash_update_u8(&sighash_context.header, 0x00);
- uint8_t tmp[MAX(32, 8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN)];
+ // re-used multiple times below; the largest size we need is a 32-byte hash
+ uint8_t tmp[32];
// hash type
crypto_hash_update_u8(&sighash_context.header, sighash_byte);
@@ -643,56 +624,41 @@ bool __attribute__((noinline)) compute_sighash_segwitv1(
if ((sighash_byte & 0x80) == SIGHASH_ANYONECANPAY) {
// outpoint (hash)
- if (32 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- tmp,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, input_map, tmp)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sighash_context.header, tmp, 32);
// outpoint (output index)
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- tmp,
- 4)) {
+ uint32_t prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, input_map, &prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ write_u32_le(tmp, 0, prevout_n);
crypto_hash_update(&sighash_context.header, tmp, 4);
- if (8 > call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_WITNESS_UTXO},
- 1,
- tmp,
- 8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN)) {
+ uint64_t amount;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_witness_utxo_amount(dc, input_map, &amount)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
// amount
+ write_u64_le(tmp, 0, amount);
crypto_hash_update(&sighash_context.header, tmp, 8);
// scriptPubKey
crypto_hash_update_varint(&sighash_context.header, scriptPubKey_len);
crypto_hash_update(&sighash_context.header, scriptPubKey, scriptPubKey_len);
// nSequence
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- tmp,
- 4)) {
- // if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(tmp, 0xFF, 4);
+ uint32_t nSequence;
+ if (!get_nsequence_or_default(dc, input_map, &nSequence)) {
+ return false;
}
+ write_u32_le(tmp, 0, nSequence);
crypto_hash_update(&sighash_context.header, tmp, 4);
} else {
// input_index
### src/swap/handle_get_printable_amount.c
@@ -1,5 +1,7 @@
#ifdef HAVE_SWAP
+#include <string.h>
+
/* SDK headers */
#include "read.h"
#include "swap_lib_calls.h"
### unit-tests/CMakeLists.txt
@@ -263,6 +263,7 @@ if(SPECULOS AND SPECULOS_SRC)
../src/handler/lib/stream_merkleized_map_value.c
../src/handler/lib/stream_preimage.c
../src/handler/sign_psbt/extract_bip32_derivation.c
+ ../src/handler/sign_psbt/psbt_fields.c
../src/handler/sign_psbt/sign_psbt_cache.c
../src/musig/musig.c
$ENV{BOLOS_SDK}/lib_standard_app/crypto_helpers.c
@@ -307,13 +308,14 @@ if(SPECULOS AND SPECULOS_SRC)
add_unit_test(test_stream_merkleized_map_value LIBS test_merkle_common)
add_unit_test(test_get_merkleized_map LIBS test_merkle_common)
add_unit_test(test_get_merkleized_map_value LIBS test_merkle_common)
+ add_unit_test(test_psbt_fields LIBS test_merkle_common)
foreach(t
test_get_preimage test_get_merkle_preimage test_extract_bip32_derivation
test_check_merkle_tree_sorted test_get_merkle_leaf_element
test_get_merkle_leaf_hash test_get_merkle_leaf_index test_stream_preimage
test_stream_merkle_leaf_element test_stream_merkleized_map_value
- test_get_merkleized_map test_get_merkleized_map_value)
+ test_get_merkleized_map test_get_merkleized_map_value test_psbt_fields)
app_apply_real_sdk_config(${t})
endforeach()
### unit-tests/libs/mock_dispatcher.c
@@ -553,6 +553,9 @@ void mock_dispatcher_add_map(mock_dispatcher_t *mock,
out_commitment->size = (uint64_t) n;
memcpy(out_commitment->keys_root, mock->trees[keys_tree_idx].root, 32);
memcpy(out_commitment->values_root, mock->trees[values_tree_idx].root, 32);
+ /* The mock builds the keys tree already sorted, so the commitment satisfies the invariant that
+ * the by-key value readers assert on (matching what call_get_merkleized_map guarantees). */
+ out_commitment->_keys_are_sorted = true;
}
/* ---- Helper: register a psbt_map_t with the mock ---- */
### 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 ---------- */
### unit-tests/test_get_merkleized_map_value.c
@@ -1,7 +1,5 @@
/**
- * Unit tests for call_get_merkleized_map_value (and the
- * call_get_merkleized_map_value_u32_le convenience wrapper) using the
- * mock dispatcher.
+ * Unit tests for call_get_merkleized_map_value using the mock dispatcher.
*
* call_get_merkleized_map_value looks up a key in a merkleized key-value
* map (by finding its index via Merkle leaf index), then fetches the
@@ -125,8 +123,8 @@ static void test_map_value_unsorted_input(void **state) {
}
/**
- * Error: the key is not present in the map. call_get_merkle_leaf_index
- * returns a negative value, which must be propagated as -1.
+ * The key is not present in the map, which must be reported as MAP_VALUE_ABSENT rather than as a
+ * generic failure.
*/
static void test_map_value_key_not_found(void **state) {
mock_dispatcher_t *mock = *state;
@@ -152,13 +150,14 @@ static void test_map_value_key_not_found(void **state) {
sizeof(missing_key),
out,
sizeof(out));
- assert_int_equal(result, -1);
+ /* A genuinely missing key must be reported as absent, distinctly from a failed lookup, so
+ * that callers may safely apply a default for an optional field. */
+ assert_int_equal(result, MAP_VALUE_ABSENT);
}
/**
- * Error: output buffer too small to hold the value. The underlying
- * call_get_merkle_leaf_element returns a negative value when the
- * preimage does not fit, which must be propagated.
+ * Error: output buffer too small to hold the value. The underlying flow reports that the preimage
+ * does not fit, which must reach the caller as MAP_VALUE_ERROR.
*/
static void test_map_value_out_buffer_too_small(void **state) {
mock_dispatcher_t *mock = *state;
@@ -178,7 +177,10 @@ static void test_map_value_out_buffer_too_small(void **state) {
uint8_t out[2];
dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
int result = call_get_merkleized_map_value(dc, &commitment, key, sizeof(key), out, sizeof(out));
- assert_true(result < 0);
+ /* Present but too long for the buffer is an ERROR, NOT ABSENT. Before the statuses were
+ * separated both surfaced as -1, so a caller substituting a default for an optional field
+ * would have signed over a value the client never committed to. */
+ assert_int_equal(result, MAP_VALUE_ERROR);
}
/**
@@ -207,93 +209,6 @@ static void test_map_value_empty_value(void **state) {
assert_int_equal(out[0], 0xCD);
}
-/**
- * Happy path for call_get_merkleized_map_value_u32_le: a 4-byte
- * little-endian value is decoded into a uint32_t.
- */
-static void test_map_value_u32_le(void **state) {
- mock_dispatcher_t *mock = *state;
-
- const uint8_t key[] = {0x10};
- /* 0xDEADBEEF in little-endian */
- const uint8_t value[] = {0xEF, 0xBE, 0xAD, 0xDE};
-
- const uint8_t *keys[] = {key};
- const size_t key_lens[] = {sizeof(key)};
- const uint8_t *values[] = {value};
- const size_t value_lens[] = {sizeof(value)};
-
- merkleized_map_commitment_t commitment;
- mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, &commitment);
-
- uint32_t got = 0;
- dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
- int result = call_get_merkleized_map_value_u32_le(dc, &commitment, key, sizeof(key), &got);
-
- assert_int_equal(result, 4);
- assert_int_equal(got, 0xDEADBEEFu);
-}
-
-/**
- * Error path for call_get_merkleized_map_value_u32_le: the value at the
- * key has a length different from 4. The wrapper must return -1 without
- * writing to the output.
- */
-static void test_map_value_u32_le_wrong_length(void **state) {
- mock_dispatcher_t *mock = *state;
-
- const uint8_t key[] = {0x10};
- /* Only 3 bytes — not a valid 32-bit value. */
- const uint8_t value[] = {0x01, 0x02, 0x03};
-
- const uint8_t *keys[] = {key};
- const size_t key_lens[] = {sizeof(key)};
- const uint8_t *values[] = {value};
- const size_t value_lens[] = {sizeof(value)};
-
- merkleized_map_commitment_t commitment;
- mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, &commitment);
-
- uint32_t got = 0xCAFEBABEu;
- dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
- int result = call_get_merkleized_map_value_u32_le(dc, &commitment, key, sizeof(key), &got);
-
- assert_int_equal(result, -1);
- /* The wrapper bails out before assigning to *out. */
- assert_int_equal(got, 0xCAFEBABEu);
-}
-
-/**
- * Error path for call_get_merkleized_map_value_u32_le: the key is not
- * present in the map.
- */
-static void test_map_value_u32_le_key_not_found(void **state) {
- mock_dispatcher_t *mock = *state;
-
- const uint8_t key[] = {0x10};
- const uint8_t value[] = {0x01, 0x02, 0x03, 0x04};
-
- const uint8_t *keys[] = {key};
- const size_t key_lens[] = {sizeof(key)};
- const uint8_t *values[] = {value};
- const size_t value_lens[] = {sizeof(value)};
-
- merkleized_map_commitment_t commitment;
- mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, &commitment);
-
- const uint8_t missing_key[] = {0xFF};
-
- uint32_t got = 0;
- dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
- int result = call_get_merkleized_map_value_u32_le(dc,
- &commitment,
- missing_key,
- sizeof(missing_key),
- &got);
-
- assert_int_equal(result, -1);
-}
-
/**
* Adversarial: client corrupts the leaf hash in the merkle proof for the
* VALUES tree. After the key index is resolved correctly, the value leaf
@@ -354,9 +269,6 @@ int main(void) {
T(test_map_value_key_not_found),
T(test_map_value_out_buffer_too_small),
T(test_map_value_empty_value),
- T(test_map_value_u32_le),
- T(test_map_value_u32_le_wrong_length),
- T(test_map_value_u32_le_key_not_found),
T(test_map_value_corrupted_value_proof),
};
#undef T
### unit-tests/test_psbt_fields.c
@@ -0,0 +1,521 @@
+/**
+ * Unit tests for the per-field PSBT accessors in handler/sign_psbt/psbt_fields.c.
+ *
+ * The property under test throughout is that the three outcomes stay separated: a field that is
+ * not in the map (PSBT_FIELD_ABSENT), a field that is there and well-formed (PSBT_FIELD_PRESENT),
+ * and a field that is there but cannot be used (PSBT_FIELD_ERROR).
+ *
+ * This matters because the callers of the optional fields substitute a default value on ABSENT.
+ * If a malformed or unfetchable field were reported as absent, the device would hash and sign that
+ * default even though the client committed to something else.
+ */
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <cmocka.h>
+
+#include "mock_dispatcher.h"
+
+#include "common/psbt.h"
+#include "handler/sign_psbt/psbt_fields.h"
+
+/* ---------- Helpers ---------- */
+
+/** Asserts that a rejected read left nothing of the caller's sentinel, nor of the client's data. */
+static void assert_cleared(const uint8_t *buf, size_t len) {
+ for (size_t i = 0; i < len; i++) {
+ assert_int_equal(buf[i], 0);
+ }
+}
+
+/**
+ * Registers a map holding a single (key_type, value) pair and returns its commitment.
+ */
+static void map_with_one_field(mock_dispatcher_t *mock,
+ uint8_t key_type,
+ const uint8_t *value,
+ size_t value_len,
+ merkleized_map_commitment_t *out) {
+ const uint8_t key[] = {key_type};
+ const uint8_t *keys[] = {key};
+ const size_t key_lens[] = {sizeof(key)};
+ const uint8_t *values[] = {value};
+ const size_t value_lens[] = {value_len};
+
+ mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, out);
+}
+
+/**
+ * Registers a map holding a single unrelated field, so that lookups of anything else are absent.
+ */
+static void map_without_field(mock_dispatcher_t *mock, merkleized_map_commitment_t *out) {
+ /* An arbitrary key type that none of the accessors under test reads. */
+ const uint8_t filler[] = {0xAB};
+ map_with_one_field(mock, 0xFD, filler, sizeof(filler), out);
+}
+
+/* ---------- PSBT_IN_SEQUENCE (optional) ---------- */
+
+static void test_sequence_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ /* 0x12345678 little-endian */
+ const uint8_t value[] = {0x78, 0x56, 0x34, 0x12};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SEQUENCE, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 0x12345678u);
+}
+
+static void test_sequence_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ /* Only this outcome may lead the caller to substitute the 0xFFFFFFFF default. */
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/**
+ * A present-but-malformed nSequence must NOT be reported as absent. This is the case that used to
+ * silently yield the 0xFFFFFFFF default.
+ */
+static void test_sequence_wrong_length_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t too_short[] = {0x01, 0x02, 0x03};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SEQUENCE, too_short, sizeof(too_short), &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+static void test_sequence_too_long_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t too_long[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SEQUENCE, too_long, sizeof(too_long), &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/* ---------- PSBT_GLOBAL_FALLBACK_LOCKTIME (optional) ---------- */
+
+static void test_fallback_locktime_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0xD2, 0x04, 0x00, 0x00}; /* 1234 */
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_GLOBAL_FALLBACK_LOCKTIME, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 1234);
+}
+
+static void test_fallback_locktime_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ /* Only this outcome may lead the caller to substitute locktime 0. */
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+static void test_fallback_locktime_wrong_length_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t wrong_length[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock,
+ PSBT_GLOBAL_FALLBACK_LOCKTIME,
+ wrong_length,
+ sizeof(wrong_length),
+ &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/**
+ * Regression: the accessor used to read into a 9-byte buffer and map every negative result to
+ * PSBT_FIELD_ABSENT. A value longer than that buffer therefore came back as "absent", and the
+ * caller substituted locktime 0 for a field the client had actually committed to. It must be an
+ * error.
+ */
+static void test_fallback_locktime_over_buffer_is_error_not_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t very_long[12];
+ memset(very_long, 0x77, sizeof(very_long));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_GLOBAL_FALLBACK_LOCKTIME, very_long, sizeof(very_long), &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/* ---------- Mandatory fields ---------- */
+
+static void test_prevout_txid_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t txid[32];
+ memset(txid, 0x5A, sizeof(txid));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_PREVIOUS_TXID, txid, sizeof(txid), &map);
+
+ uint8_t got[32];
+ memset(got, 0, sizeof(got));
+ psbt_field_status_t status =
+ psbt_get_input_prevout_txid(mock_dispatcher_get_dc(mock), &map, got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_memory_equal(got, txid, sizeof(txid));
+}
+
+static void test_prevout_txid_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint8_t got[32];
+ psbt_field_status_t status =
+ psbt_get_input_prevout_txid(mock_dispatcher_get_dc(mock), &map, got);
+
+ /* Fatal for the caller either way, but still reported distinctly from a malformed value. */
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+}
+
+/**
+ * A short txid must be rejected rather than accepted with an uninitialized tail. The 31 bytes the
+ * client did send are proved, so read_var reports success and only read_fixed can clear them: they
+ * must not be left in the caller's buffer alongside an uninitialized 32nd byte.
+ */
+static void test_prevout_txid_short_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t txid[31];
+ memset(txid, 0x5A, sizeof(txid));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_PREVIOUS_TXID, txid, sizeof(txid), &map);
+
+ uint8_t got[32];
+ memset(got, 0xEE, sizeof(got));
+ psbt_field_status_t status =
+ psbt_get_input_prevout_txid(mock_dispatcher_get_dc(mock), &map, got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_cleared(got, sizeof(got));
+}
+
+static void test_output_amount_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ /* 0x0102030405060708 little-endian */
+ const uint8_t value[] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_AMOUNT, value, sizeof(value), &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status = psbt_get_output_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_true(got == 0x0102030405060708ull);
+}
+
+static void test_output_amount_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status = psbt_get_output_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+}
+
+static void test_output_amount_wrong_length_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x01, 0x02, 0x03, 0x04};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_AMOUNT, value, sizeof(value), &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status = psbt_get_output_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+}
+
+/* ---------- Key-type pinning for the remaining fixed-width fields ---------- */
+
+/*
+ * These accessors are one-line delegations to the shared readers, whose behaviour is already
+ * exercised above. What is specific to each of them - and what nothing else would catch - is that
+ * it names the right PSBT key type: reading the wrong one would report the field as absent, and
+ * the caller would silently carry on with a default.
+ */
+
+static void test_global_tx_version_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x02, 0x00, 0x00, 0x00};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_GLOBAL_TX_VERSION, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_global_tx_version(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 2);
+}
+
+static void test_prevout_index_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x03, 0x00, 0x00, 0x00};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_OUTPUT_INDEX, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_input_prevout_index(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 3);
+}
+
+static void test_sighash_type_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x01, 0x00, 0x00, 0x00};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SIGHASH_TYPE, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_input_sighash_type(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 1);
+}
+
+/* ---------- Variable-length fields ---------- */
+
+static void test_output_script_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t script[] = {0x76, 0xA9, 0x14, 0x01, 0x02, 0x03};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_SCRIPT, script, sizeof(script), &map);
+
+ uint8_t got[64];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_output_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got_len, sizeof(script));
+ assert_memory_equal(got, script, sizeof(script));
+}
+
+static void test_output_script_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint8_t got[64];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_output_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+}
+
+/**
+ * A script longer than the caller's buffer is an error, not an absent field. Neither the buffer nor
+ * the length may be left holding anything a caller could mistake for a value.
+ */
+static void test_output_script_too_long_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t script[40];
+ memset(script, 0x51, sizeof(script));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_SCRIPT, script, sizeof(script), &map);
+
+ uint8_t got[16];
+ memset(got, 0xEE, sizeof(got));
+ size_t got_len = 123;
+ psbt_field_status_t status =
+ psbt_get_output_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got_len, 0);
+ assert_cleared(got, sizeof(got));
+}
+
+static void test_redeem_script_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t script[] = {0xA9, 0x14, 0xDE, 0xAD, 0xBE, 0xEF, 0x87};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_REDEEM_SCRIPT, script, sizeof(script), &map);
+
+ uint8_t got[64];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_input_redeem_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got_len, sizeof(script));
+ assert_memory_equal(got, script, sizeof(script));
+}
+
+/* ---------- Witness UTXO amount ---------- */
+
+/**
+ * The witness UTXO is a serialized txout: [8-byte amount][varint len][scriptPubKey]. The accessor
+ * reads the whole value but returns only the amount.
+ */
+static void test_witness_utxo_amount_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ /* amount 0x0000000005F5E100 (100000000) little-endian, then a 22-byte P2WPKH scriptPubKey. */
+ uint8_t txout[8 + 1 + 22];
+ memset(txout, 0, sizeof(txout));
+ txout[0] = 0x00;
+ txout[1] = 0xE1;
+ txout[2] = 0xF5;
+ txout[3] = 0x05;
+ txout[8] = 22;
+ memset(txout + 9, 0x33, 22);
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_WITNESS_UTXO, txout, sizeof(txout), &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_input_witness_utxo_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 100000000u);
+}
+
+static void test_witness_utxo_amount_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint64_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_input_witness_utxo_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/**
+ * A witness UTXO too short to even contain the 8-byte amount is malformed, not absent: defaulting
+ * on it would mean signing over an amount the client never committed to.
+ */
+static void test_witness_utxo_amount_too_short_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t truncated[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_WITNESS_UTXO, truncated, sizeof(truncated), &map);
+
+ uint64_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_input_witness_utxo_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/* ---------- Main ---------- */
+
+int main(void) {
+#define T(fn) cmocka_unit_test_setup_teardown(fn, mock_dispatcher_setup, mock_dispatcher_teardown)
+ const struct CMUnitTest tests[] = {
+ T(test_sequence_present),
+ T(test_sequence_absent),
+ T(test_sequence_wrong_length_is_error),
+ T(test_sequence_too_long_is_error),
+ T(test_fallback_locktime_present),
+ T(test_fallback_locktime_absent),
+ T(test_fallback_locktime_wrong_length_is_error),
+ T(test_fallback_locktime_over_buffer_is_error_not_absent),
+ T(test_prevout_txid_present),
+ T(test_prevout_txid_absent),
+ T(test_prevout_txid_short_is_error),
+ T(test_output_amount_present),
+ T(test_output_amount_absent),
+ T(test_output_amount_wrong_length_is_error),
+ T(test_global_tx_version_present),
+ T(test_prevout_index_present),
+ T(test_sighash_type_present),
+ T(test_output_script_present),
+ T(test_output_script_absent),
+ T(test_output_script_too_long_is_error),
+ T(test_redeem_script_present),
+ T(test_witness_utxo_amount_present),
+ T(test_witness_utxo_amount_absent),
+ T(test_witness_utxo_amount_too_short_is_error),
+ };
+#undef T
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}Why this scored 80/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.