From c03b3f188ab0c7f416eaf922474a8075e7ab3775 Mon Sep 17 00:00:00 2001 From: Johnson Ajibi Date: Sun, 27 Sep 2026 10:26:45 +0100 Subject: [PATCH 1/2] lms/lmots: apply the same output-buffer zeroization consistently Commit 9b3051fb10 ("Built-in lms driver: always zeroize output-buffer in create_merkle_leaf_value"), part of the June 2025 LMS fix batch, added a defensive memset() of the output buffer on entry to create_merkle_leaf_value(), guarding against a stale buffer (holding data from a previous successful verification) being consumed if a caller ever failed to check a return value -- the exact shape of CVE-2025-49600. Four other functions in lms.c/lmots.c have the identical shape (a caller-supplied output buffer filled by a fallible PSA hash chain) and did not get the same treatment: - create_merkle_internal_value() (lms.c) - create_digit_array_with_checksum() (lmots.c) - hash_digit_array() (lmots.c) - public_key_from_hashed_digit_array() (lmots.c) None of these has a reachable bug today -- every call site checks its return value -- so this is purely about making the defence-in-depth measure consistent rather than fixing a live issue. But the asymmetry is the wrong way round: the extra barrier currently sits only at the one site that was hardened after the fact, rather than at every site an equivalent bug could next appear. Sizes zeroized match what each function actually writes: - create_digit_array_with_checksum(): N_HASH_LEN + CHECKSUM_LEN (the hash output plus the 2-byte checksum appended after it) - hash_digit_array(): P_SIG_DIGIT_COUNT * N_HASH_LEN (one hash per digit) - the other two: their respective MBEDTLS_LM(OT)S_*_LEN, matching create_merkle_leaf_value()'s existing convention of sizing from the parameter set rather than sizeof(). Also straightened out public_key_from_hashed_digit_array()'s control flow first: its exit: label sat inside the body of the final if (status != PSA_SUCCESS) block, so the success path fell through to return without calling psa_hash_abort() at all (safe today, since psa_hash_abort() on an already-finished operation is a no-op per the PSA Crypto API, but inconsistent with every other function in this file, which calls it unconditionally). Replaced it with a plain goto exit; and a fall-through label, matching its siblings. Verified: gcc -Wall -Wextra -fsyntax-only and a full compile of both files (this driver's actual build needs the full PSA crypto stack, which I could not get this checkout's CMake test target to configure independently of this change -- see the separate SHA-256 PR for that unrelated issue) produce zero warnings. Fixes #10952. Signed-off-by: Johnson Ajibi --- library/lmots.c | 16 ++++++++++++++-- library/lms.c | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/library/lmots.c b/library/lmots.c index 404aa80da6..3925f780e0 100644 --- a/library/lmots.c +++ b/library/lmots.c @@ -127,6 +127,9 @@ static int create_digit_array_with_checksum(const mbedtls_lmots_parameters_t *pa size_t output_hash_len; unsigned short checksum; + /* Always zeroize the output buffer because it may contain data from the previous invocation */ + memset(out, 0, MBEDTLS_LMOTS_N_HASH_LEN(params->type) + CHECKSUM_LEN); + status = psa_hash_setup(&op, PSA_ALG_SHA_256); if (status != PSA_SUCCESS) { goto exit; @@ -222,6 +225,11 @@ static int hash_digit_array(const mbedtls_lmots_parameters_t *params, size_t output_hash_len; unsigned char tmp_hash[MBEDTLS_LMOTS_N_HASH_LEN_MAX]; + /* Always zeroize the output buffer because it may contain data from the previous invocation */ + memset(output, 0, + (size_t) MBEDTLS_LMOTS_P_SIG_DIGIT_COUNT(params->type) * + MBEDTLS_LMOTS_N_HASH_LEN(params->type)); + for (i_digit_idx = 0; i_digit_idx < MBEDTLS_LMOTS_P_SIG_DIGIT_COUNT(params->type); i_digit_idx++) { @@ -318,6 +326,9 @@ static int public_key_from_hashed_digit_array(const mbedtls_lmots_parameters_t * psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED; size_t output_hash_len; + /* Always zeroize the output buffer because it may contain data from the previous invocation */ + memset(pub_key, 0, MBEDTLS_LMOTS_N_HASH_LEN(params->type)); + status = psa_hash_setup(&op, PSA_ALG_SHA_256); if (status != PSA_SUCCESS) { goto exit; @@ -352,10 +363,11 @@ static int public_key_from_hashed_digit_array(const mbedtls_lmots_parameters_t * MBEDTLS_LMOTS_N_HASH_LEN(params->type), &output_hash_len); if (status != PSA_SUCCESS) { + goto exit; + } exit: - psa_hash_abort(&op); - } + psa_hash_abort(&op); return PSA_TO_MBEDTLS_ERR(status); } diff --git a/library/lms.c b/library/lms.c index 41d34bfe16..c3ecea258f 100644 --- a/library/lms.c +++ b/library/lms.c @@ -178,6 +178,9 @@ static int create_merkle_internal_value(const mbedtls_lms_parameters_t *params, size_t output_hash_len; unsigned char r_node_idx_bytes[4]; + /* Always zeroize the output buffer because it may contain data from the previous invocation */ + memset(out, 0, MBEDTLS_LMS_M_NODE_BYTES(params->type)); + op = psa_hash_operation_init(); status = psa_hash_setup(&op, PSA_ALG_SHA_256); if (status != PSA_SUCCESS) { From d327b904b3eac4e40db7ef6c0f07ca4c4cbc6f3c Mon Sep 17 00:00:00 2001 From: Johnson Ajibi Date: Wed, 21 Oct 2026 20:24:11 +0100 Subject: [PATCH 2/2] lms: drop the aliasing memset in create_merkle_internal_value 0xiviel's review found that the memset added to create_merkle_internal_value() breaks LMS verification: in mbedtls_lms_verify(), Tc_candidate_root_node is passed as out while simultaneously being left_node or right_node as the Merkle path is walked bottom-up -- the memset wipes the running candidate before psa_hash_update() reads it as an input, on 100 of 4192 calls in test_suite_lms at this head, failing 9 of 25 tests closed (MBEDTLS_ERR_LMS_VERIFY_FAILED, no forgery route, per 0xiviel's analysis). None of the three functions touched in lmots.c alias this way: all three are static, and at every one of their seven call sites (verified by 0xiviel, and independently here) the destination is a distinct object from every input. Those three keep their memset. Only create_merkle_leaf_value's original memset (from 9b3051fb10, pre-dating this PR) and create_merkle_internal_value's new one were ever in question; create_merkle_leaf_value does not alias (pub_key and out are always distinct per RFC8554 Algorithm 1 step 2's tree construction) and also keeps its memset. Verified: tests/test_suite_lms back to PASSED (25 / 25), both with a plain build and with -fsanitize=address,undefined (no diagnostics either way). tests/test_suite_lmots unaffected, PASSED (17 / 17, 1 skipped) throughout. Signed-off-by: Johnson Ajibi --- library/lms.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/lms.c b/library/lms.c index c3ecea258f..41d34bfe16 100644 --- a/library/lms.c +++ b/library/lms.c @@ -178,9 +178,6 @@ static int create_merkle_internal_value(const mbedtls_lms_parameters_t *params, size_t output_hash_len; unsigned char r_node_idx_bytes[4]; - /* Always zeroize the output buffer because it may contain data from the previous invocation */ - memset(out, 0, MBEDTLS_LMS_M_NODE_BYTES(params->type)); - op = psa_hash_operation_init(); status = psa_hash_setup(&op, PSA_ALG_SHA_256); if (status != PSA_SUCCESS) {