diff --git a/tests/api/test_lms_xmss.c b/tests/api/test_lms_xmss.c index 56e568460a..d5ae22227e 100644 --- a/tests/api/test_lms_xmss.c +++ b/tests/api/test_lms_xmss.c @@ -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. * diff --git a/tests/api/test_lms_xmss.h b/tests/api/test_lms_xmss.h index 5b578b32db..2a3ab55d3d 100644 --- a/tests/api/test_lms_xmss.h +++ b/tests/api/test_lms_xmss.h @@ -25,6 +25,7 @@ #include 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), \ diff --git a/wolfcrypt/src/wc_lms.c b/wolfcrypt/src/wc_lms.c index 2b119a369a..6ddb9164ec 100644 --- a/wolfcrypt/src/wc_lms.c +++ b/wolfcrypt/src/wc_lms.c @@ -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; }