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