diff options
| author | Alberto Maschietto <[email protected]> | 2026-10-19 12:54:46 +0200 |
|---|---|---|
| committer | Eugene Syromiatnikov <[email protected]> | 2026-10-19 13:07:56 +0200 |
| commit | ad66de34323492829f7b43b6fe06411f9467fd91 (patch) | |
| tree | bf8ac7437cfb7f6fdb681e611f7d87f8bb14f2a8 /test | |
| parent | ml_dsa_mu_prov.c: initialize ML-DSA-MU before empty-message finalization (diff) | |
| download | openssl-ad66de34323492829f7b43b6fe06411f9467fd91.tar.gz openssl-ad66de34323492829f7b43b6fe06411f9467fd91.zip | |
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 <[email protected]>
Reviewed-by: Nikola Pajkovsky <[email protected]>
Reviewed-by: Tomas Mraz <[email protected]>
Reviewed-by: Eugene Syromiatnikov <[email protected]>
Merge-date: Mon Oct 19 11:08:03 2026
Merged-from: https://github.com/openssl/openssl/pull/32447
Diffstat (limited to 'test')
| -rw-r--r-- | test/ml_dsa_test.c | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/test/ml_dsa_test.c b/test/ml_dsa_test.c index 3ae236b70ff..e910fcbc3eb 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 |
