Invalidate LMS key when private key write fails

wc_LmsKey_Sign computes the signature with the one-time key at the
current leaf and only afterwards advances the leaf index and writes it
to non-volatile storage. When that write failed the signature was erased
but the key state was left as OK, so the same in-memory key could sign
again while storage still recorded the already consumed leaf. A restart
followed by wc_LmsKey_Reload would then resume from that leaf and sign a
second message with the same LM-OTS private key.

Set the key state to bad before signing and only restore it to OK once
the advanced private key has been committed to storage, matching what
wc_xmsskey_signupdate already does for XMSS. Exhausted keys keep their
existing NOSIGS state.

Fixes F-7411.
This commit is contained in:
Tobias Frauenschläger
2026-08-07 08:11:23 +02:00
parent be59d5af49
commit af0eb33fff
3 changed files with 115 additions and 1 deletions
+84 -1
View File
@@ -71,10 +71,19 @@ static const char* lms_test_priv_key_file(void)
}
#define LMS_TEST_PRIV_KEY_FILE lms_test_priv_key_file()
/* Set to 1 to make test_lms_write_key() simulate a non-volatile storage write
* that never completes. */
static int lms_write_key_fail = 0;
static int test_lms_write_key(const byte* priv, word32 privSz, void* context)
{
FILE* f = fopen((const char*)context, "wb");
FILE* f;
int ret = WC_LMS_RC_SAVED_TO_NV_MEMORY;
if (lms_write_key_fail)
return -1;
f = fopen((const char*)context, "wb");
if (f == NULL)
return -1;
if (fwrite(priv, 1, privSz, f) != privSz)
@@ -177,6 +186,80 @@ int test_wc_LmsKey_sign_verify(void)
return EXPECT_RESULT();
}
/*
* Test that a failed private key write permanently invalidates the key.
*
* RFC 8554 section 5.4.1 requires the advanced leaf index to be stored before
* the signature is released. The signature is computed with the one-time key
* at the current leaf and only then is the index advanced and written out, so
* a failed write leaves storage pointing at a leaf that has already been used.
* The key must refuse to sign again rather than hand out a second signature
* from the same one-time key.
*/
int test_wc_LmsKey_write_fail(void)
{
EXPECT_DECLS;
#if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY)
LmsKey key;
WC_RNG rng;
byte msg[] = "test message for LMS signing";
byte sig[2048];
word32 sigSz;
word32 goodSigSz = 0;
word32 i;
int nonZero;
/* Zero so cleanup is safe if an early alloc failure skips init. */
XMEMSET(&key, 0, sizeof(key));
XMEMSET(&rng, 0, sizeof(rng));
ExpectIntEQ(wc_InitRng(&rng), 0);
(void)remove(LMS_TEST_PRIV_KEY_FILE);
ExpectIntEQ(test_lms_init_key(&key, &rng), 0);
ExpectIntEQ(wc_LmsKey_MakeKey(&key, &rng), 0);
sigSz = sizeof(sig);
ExpectIntEQ(wc_LmsKey_Sign(&key, sig, &sigSz, msg, sizeof(msg)), 0);
goodSigSz = sigSz;
/* A signature was really produced, so the check below is not vacuous. */
nonZero = 0;
for (i = 0; i < goodSigSz; i++) {
if (sig[i] != 0)
nonZero++;
}
ExpectIntGT(nonZero, 0);
/* Fail the write of the advanced private key. */
lms_write_key_fail = 1;
sigSz = sizeof(sig);
ExpectIntEQ(wc_LmsKey_Sign(&key, sig, &sigSz, msg, sizeof(msg)),
WC_NO_ERR_TRACE(IO_FAILED_E));
lms_write_key_fail = 0;
/* The signature has to be erased as well, not just reported as failed.
* The leaf index was never stored, so releasing it would allow the same
* one-time key to sign twice. */
nonZero = 0;
for (i = 0; i < goodSigSz; i++) {
if (sig[i] != 0)
nonZero++;
}
ExpectIntEQ(nonZero, 0);
/* Storage works again but the key must stay unusable. */
sigSz = sizeof(sig);
ExpectIntEQ(wc_LmsKey_Sign(&key, sig, &sigSz, msg, sizeof(msg)),
WC_NO_ERR_TRACE(BAD_STATE_E));
wc_LmsKey_Free(&key);
wc_FreeRng(&rng);
(void)remove(LMS_TEST_PRIV_KEY_FILE);
#endif
return EXPECT_RESULT();
}
/*
* Test LMS key reload after advancing past the leaf cache window.
*
+2
View File
@@ -25,6 +25,7 @@
#include <tests/api/api_decl.h>
int test_wc_LmsKey_sign_verify(void);
int test_wc_LmsKey_write_fail(void);
int test_wc_LmsKey_reload_cache(void);
int test_wc_LmsKey_reload_devid(void);
int test_wc_XmssKey_reload_devid(void);
@@ -40,6 +41,7 @@ int test_wc_XmssFeatureCoverage(void);
/* LMS, and RFC 9802 (HSS/LMS and XMSS/XMSS^MT in X.509). */
#define TEST_LMS_XMSS_DECLS \
TEST_DECL_GROUP("lms", test_wc_LmsKey_sign_verify), \
TEST_DECL_GROUP("lms", test_wc_LmsKey_write_fail), \
TEST_DECL_GROUP("lms", test_wc_LmsKey_reload_cache), \
TEST_DECL_GROUP("lms", test_wc_LmsKey_reload_devid), \
TEST_DECL_GROUP("xmss", test_wc_XmssKey_reload_devid), \
+29
View File
@@ -1420,6 +1420,10 @@ int wc_LmsKey_GetPrivLen(const LmsKey* key, word32* len)
}
/* Sign a message.
*
* The one-time key at the current leaf is consumed before the advanced private
* key is written to storage. If either step fails then the key state is left
* bad and no further signatures can be created with this key.
*
* @param [in, out] key LMS key to sign with.
* @param [out] sig Signature data. Buffer must be big enough to hold
@@ -1434,6 +1438,7 @@ int wc_LmsKey_GetPrivLen(const LmsKey* key, word32* len)
* @return BAD_FUNC_ARG when a read/write private key context is not set.
* @return BUFFER_E when sigSz is too small.
* @return BAD_STATE_E when wrong state for operation.
* @return KEY_EXHAUSTED_E when no signatures are left in the key.
* @return IO_FAILED_E when reading or writing private key failed.
*/
int wc_LmsKey_Sign(LmsKey* key, byte* sig, word32* sigSz, const byte* msg,
@@ -1475,6 +1480,16 @@ int wc_LmsKey_Sign(LmsKey* key, byte* sig, word32* sigSz, const byte* msg,
ret = wc_CryptoCb_PqcStatefulSigSign(msg, (word32)msgSz, sig, sigSz,
WC_PQC_STATEFUL_SIG_TYPE_LMS, key);
if (ret != WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE)) {
if (ret == WC_NO_ERR_TRACE(KEY_EXHAUSTED_E)) {
/* Signature space exhausted. */
WOLFSSL_MSG("error: no LMS signatures remaining");
key->state = WC_LMS_STATE_NOSIGS;
}
else if (ret != 0) {
/* The device may have consumed the one-time key without
* committing the advanced state, so presume the key is bad. */
key->state = WC_LMS_STATE_BAD;
}
return ret;
}
ret = 0; /* fall through to software path */
@@ -1498,6 +1513,9 @@ int wc_LmsKey_Sign(LmsKey* key, byte* sig, word32* sigSz, const byte* msg,
/* Initialize working state for use. */
ret = wc_lmskey_state_init(state, key->params);
if (ret == 0) {
/* Set the key state to bad by default. State is presumed bad
* unless a correct sign and write operation happen together. */
key->state = WC_LMS_STATE_BAD;
/* Sign message. */
ret = wc_hss_sign(state, key->priv_raw, &key->priv,
key->priv_data, msg, (word32)msgSz, sig);
@@ -1507,6 +1525,11 @@ int wc_LmsKey_Sign(LmsKey* key, byte* sig, word32* sigSz, const byte* msg,
WC_FREE_VAR_EX(state, NULL, DYNAMIC_TYPE_TMP_BUFFER);
}
}
if (ret == WC_NO_ERR_TRACE(KEY_EXHAUSTED_E)) {
/* Signature space exhausted. */
WOLFSSL_MSG("error: no LMS signatures remaining");
key->state = WC_LMS_STATE_NOSIGS;
}
if (ret == 0) {
*sigSz = (word32)key->params->sig_len;
}
@@ -1532,10 +1555,16 @@ int wc_LmsKey_Sign(LmsKey* key, byte* sig, word32* sigSz, const byte* msg,
if (rv != WC_LMS_RC_SAVED_TO_NV_MEMORY) {
/* Write to NV storage failed. Erase the signature from
* memory to prevent OTS key reuse if state is rolled back. */
WOLFSSL_MSG("error: LmsKey write_private_key failed");
ForceZero(sig, key->params->sig_len);
ret = IO_FAILED_E;
}
}
if (ret == 0) {
/* The advanced private key was committed to storage. The key is safe
* to sign with again. */
key->state = WC_LMS_STATE_OK;
}
return ret;
}