Commit ad66de3432 for openssl.org
commit ad66de34323492829f7b43b6fe06411f9467fd91
Author: Alberto Maschietto <albertomaschietto9@gmail.com>
Date: Mon Oct 19 12:54:46 2026 +0200
test/ml_dsa_test.c: add a check for empty message finalisation
The regression test covers four cases: pure ML-DSA and SHA-512 prehash
mode, each with either no update or an explicit
EVP_DigestUpdate(mdctx, NULL, 0). Pure mode must succeed and return
the independently calculated
SHAKE256(SHAKE256(pub, 64) || 0x00 || 0x00, 64). Prehash mode must
reject the missing prehash input.
The test is gated on FIPS provider version because unfixed providers
would crash. The default provider always runs the regression.
References: https://github.com/openssl/openssl/issues/32445
Assisted-by: Claude Code
Assisted-by: Codex:gpt-6
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Merge-date: Mon Oct 19 11:08:03 2026
Merged-from: https://github.com/openssl/openssl/pull/32447
diff --git a/test/ml_dsa_test.c b/test/ml_dsa_test.c
index 3ae236b70f..e910fcbc3e 100644
--- a/test/ml_dsa_test.c
+++ b/test/ml_dsa_test.c
@@ -646,6 +646,86 @@ err:
return ret;
}
+/*
+ * Finalizing an ML-DSA-MU digest that has had no message data supplied.
+ *
+ * For pure ML-DSA (no "digest" parameter) the empty message is a valid input,
+ * so mu is well defined and the final must succeed.
+ *
+ * For HASH-ML-DSA (a "digest" parameter is set) the input must be a prehashed
+ * message whose size matches the digest, so a prehash of the empty message is
+ * never itself empty and the final must fail.
+ */
+static const char *ml_dsa_mu_empty_message_digest[] = {
+ NULL,
+ "SHA-512",
+};
+
+static int ml_dsa_mu_empty_message_final_test(int tst)
+{
+ const char *digestname = ml_dsa_mu_empty_message_digest[tst / 2];
+ int zero_length_update = tst % 2;
+ int expected = (digestname == NULL);
+ int ret = 0;
+ EVP_PKEY *key = NULL;
+ EVP_MD *md = NULL, *shake = NULL;
+ EVP_MD_CTX *mdctx = NULL, *refctx = NULL;
+ uint8_t pub[ML_DSA_44_PUB_LEN];
+ uint8_t mu[ML_DSA_MU_BYTES], expected_mu[ML_DSA_MU_BYTES];
+ uint8_t tr[64];
+ static const uint8_t prefix[] = { 0, 0 };
+ size_t publen = 0;
+ OSSL_PARAM params[3], *p = params;
+
+ if (!TEST_ptr(key = do_gen_key("ML-DSA-44", NULL, 0))
+ || !TEST_true(EVP_PKEY_get_octet_string_param(key,
+ OSSL_PKEY_PARAM_PUB_KEY, pub, sizeof(pub), &publen)))
+ goto err;
+
+ *p++ = OSSL_PARAM_construct_octet_string(OSSL_DIGEST_PARAM_MU_PUB_KEY,
+ pub, publen);
+ if (digestname != NULL)
+ *p++ = OSSL_PARAM_construct_utf8_string(OSSL_DIGEST_PARAM_MU_DIGEST,
+ (char *)digestname, 0);
+ *p = OSSL_PARAM_construct_end();
+
+ if (!TEST_ptr(md = EVP_MD_fetch(lib_ctx, "ML-DSA-MU", NULL))
+ || !TEST_ptr(mdctx = EVP_MD_CTX_new())
+ || !TEST_true(EVP_DigestInit_ex2(mdctx, md, params)))
+ goto err;
+
+ /* EVP returns early for a zero-length update without calling mu_update. */
+ if (zero_length_update && !TEST_true(EVP_DigestUpdate(mdctx, NULL, 0)))
+ goto err;
+
+ if (!TEST_int_eq(EVP_DigestFinalXOF(mdctx, mu, sizeof(mu)), expected))
+ goto err;
+
+ if (expected) {
+ /* Pure ML-DSA: mu = SHAKE256(SHAKE256(pub, 64) || 0 || 0, 64). */
+ if (!TEST_ptr(shake = EVP_MD_fetch(lib_ctx, "SHAKE256", NULL))
+ || !TEST_ptr(refctx = EVP_MD_CTX_new())
+ || !TEST_true(EVP_DigestInit_ex2(refctx, shake, NULL))
+ || !TEST_true(EVP_DigestUpdate(refctx, pub, publen))
+ || !TEST_true(EVP_DigestFinalXOF(refctx, tr, sizeof(tr)))
+ || !TEST_true(EVP_DigestInit_ex2(refctx, shake, NULL))
+ || !TEST_true(EVP_DigestUpdate(refctx, tr, sizeof(tr)))
+ || !TEST_true(EVP_DigestUpdate(refctx, prefix, sizeof(prefix)))
+ || !TEST_true(EVP_DigestFinalXOF(refctx, expected_mu, sizeof(expected_mu)))
+ || !TEST_mem_eq(mu, sizeof(mu), expected_mu, sizeof(expected_mu)))
+ goto err;
+ }
+
+ ret = 1;
+err:
+ EVP_MD_CTX_free(refctx);
+ EVP_MD_free(shake);
+ EVP_MD_CTX_free(mdctx);
+ EVP_MD_free(md);
+ EVP_PKEY_free(key);
+ return ret;
+}
+
static int ml_dsa_priv_pub_bad_t0_test(void)
{
int ret = 0;
@@ -831,6 +911,15 @@ int setup_tests(void)
ADD_TEST(ml_dsa_digest_sign_verify_test);
ADD_TEST(ml_dsa_priv_pub_bad_t0_test);
ADD_TEST(ml_dsa_newdata_bad_propq_test);
+ /*
+ * mu_final() dereferenced an uninitialised digest context for an empty
+ * message, so this test crashes against a FIPS provider that predates
+ * the fix. Only run it where the provider has it.
+ */
+ if (fips_provider_version_ge(lib_ctx, 4, 0, 4)) {
+ ADD_ALL_TESTS(ml_dsa_mu_empty_message_final_test,
+ 2 * OSSL_NELEM(ml_dsa_mu_empty_message_digest));
+ }
/*
* Tested only in the default configuration, with a non-default provider