From 574215bcd96615c12245fd67fb6ba2153c4d7285 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Fri, 30 Oct 2026 18:49:38 -0700 Subject: [PATCH] mldsa: reject a HashML-DSA pre-hash weaker than the parameter set --- include/wolfprovider/alg_funcs.h | 1 + src/wp_mldsa_kmgmt.c | 16 ++++ src/wp_mldsa_sig.c | 13 +++- test/test_mldsa.c | 121 ++++++++++++++++++------------- 4 files changed, 99 insertions(+), 52 deletions(-) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 771a1a76..1711a2a7 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -333,6 +333,7 @@ void* wp_mldsa_get_key(wp_MlDsa* mldsa); int wp_mldsa_get_sig_size(const wp_MlDsa* mldsa); int wp_mldsa_has_private(const wp_MlDsa* mldsa); int wp_mldsa_get_level(wp_MlDsa* mldsa); +int wp_mldsa_get_security_bits(const wp_MlDsa* mldsa); /* Internal SLH-DSA types and functions. */ typedef struct wp_SlhDsa wp_SlhDsa; diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 4a47fc1d..b5a3ba5b 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -192,6 +192,22 @@ int wp_mldsa_get_level(wp_MlDsa* mldsa) return level; } +/** + * Get the security strength in bits of the key's parameter set. + * + * @param [in] mldsa ML-DSA key object. + * @return Security bits, or 0 when not available. + */ +int wp_mldsa_get_security_bits(const wp_MlDsa* mldsa) +{ + int bits = 0; + + if ((mldsa != NULL) && (mldsa->data != NULL)) { + bits = mldsa->data->securityBits; + } + return bits; +} + /** * Get the maximum signature size for the key. * diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 59380a1f..928960ce 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -767,7 +767,7 @@ static int wp_mldsa_stream_verify_final(wp_MlDsaSigCtx* ctx, /* Digests permitted for FIPS 204 HashML-DSA. Restricting to the SHA2/SHA3 * families keeps the generic name mapper from admitting legacy/undersized * digests (MD5, SHA-1, SHA-512/224) as a HashML-DSA pre-hash. */ -static int wp_mldsa_hash_allowed(enum wc_HashType hashType) +static int wp_mldsa_hash_allowed(enum wc_HashType hashType, int secBits) { int allowed = 0; @@ -783,6 +783,14 @@ static int wp_mldsa_hash_allowed(enum wc_HashType hashType) (hashType == WC_HASH_TYPE_SHA3_512)) { allowed = 1; } +#ifndef WOLFSSL_MLDSA_ALLOW_WEAK_PREHASH + /* FIPS 204 sec 5.4: collision strength must cover the parameter set. */ + if (allowed && (wc_HashGetDigestSize(hashType) * 8 / 2 < secBits)) { + allowed = 0; + } +#else + (void)secBits; +#endif /* !WOLFSSL_MLDSA_ALLOW_WEAK_PREHASH */ return allowed; } @@ -796,7 +804,8 @@ static int wp_mldsa_setup_prehash(wp_MlDsaSigCtx* ctx, const char* mdName) OSSL_LIB_CTX* libCtx = (ctx->provCtx != NULL) ? ctx->provCtx->libCtx : NULL; ctx->hashType = wp_name_to_wc_hash_type(libCtx, mdName, NULL); - if (!wp_mldsa_hash_allowed(ctx->hashType)) { + if (!wp_mldsa_hash_allowed(ctx->hashType, + wp_mldsa_get_security_bits(ctx->mldsa))) { ctx->hashType = WC_HASH_TYPE_NONE; ok = 0; } diff --git a/test/test_mldsa.c b/test/test_mldsa.c index ab94a277..d9025fb9 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -743,66 +743,87 @@ int test_mldsa_get_params(void* data) return err; } -/* DigestSignInit with non-empty mdName must fail (ML-DSA is pure). */ +/* HashML-DSA digest choices and whether init must accept them. */ +typedef struct mldsa_prehash_case { + const char* keyName; + const char* mdName; + int ok; +} mldsa_prehash_case; + +static const mldsa_prehash_case mldsa_prehash_cases[] = { + { "ML-DSA-44", "SHA-256", 1 }, +#ifdef WP_HAVE_SHA384 + { "ML-DSA-65", "SHA-384", 1 }, +#endif /* WP_HAVE_SHA384 */ +#ifdef WP_HAVE_SHA512 + { "ML-DSA-87", "SHA-512", 1 }, +#endif /* WP_HAVE_SHA512 */ + { "ML-DSA-65", "SHA-1", 0 }, +#ifndef WOLFSSL_MLDSA_ALLOW_WEAK_PREHASH + /* Digest weaker than the parameter set (FIPS 204 sec 5.4). */ + { "ML-DSA-65", "SHA-256", 0 }, + { "ML-DSA-87", "SHA-384", 0 }, +#endif /* !WOLFSSL_MLDSA_ALLOW_WEAK_PREHASH */ +}; +#define MLDSA_PREHASH_CASE_COUNT \ + (sizeof(mldsa_prehash_cases) / sizeof(mldsa_prehash_cases[0])) + +/* A digest name selects HashML-DSA: allowed digests round-trip, others fail + * at init. */ int test_mldsa_digest_sign_prehash(void* data) { int err = 0; + size_t i; EVP_PKEY* k = NULL; EVP_MD_CTX* mdctx = NULL; - unsigned char sig[5000]; - size_t sigLen = sizeof(sig); - const unsigned char msg[] = "wolfProvider ML-DSA pre-hash message"; + unsigned char sig[ML_DSA_LEVEL5_SIG_SIZE]; + size_t sigLen = 0; int rc; (void)data; - PRINT_MSG("DigestSign/Verify pre-hash (HashML-DSA) round-trip"); + for (i = 0; (err == 0) && (i < MLDSA_PREHASH_CASE_COUNT); i++) { + const mldsa_prehash_case* c = &mldsa_prehash_cases[i]; + PRINT_MSG("HashML-DSA %s with %s", c->keyName, c->mdName); - err = mldsa_keygen("ML-DSA-65", &k); - if (err == 0) { - mdctx = EVP_MD_CTX_new(); - err = (mdctx == NULL); - } - /* A digest name selects FIPS 204 HashML-DSA; signing must succeed. */ - if (err == 0) { - rc = EVP_DigestSignInit_ex(mdctx, NULL, "SHA-256", wpLibCtx, NULL, k, - NULL); - err = (rc != 1); - if (err) PRINT_ERR_MSG("DigestSignInit with mdName failed"); - } - if (err == 0) { - err = EVP_DigestSign(mdctx, sig, &sigLen, msg, sizeof(msg)) != 1; - if (err) PRINT_ERR_MSG("EVP_DigestSign (pre-hash) failed"); - } - EVP_MD_CTX_free(mdctx); - mdctx = NULL; - if (err == 0) { - mdctx = EVP_MD_CTX_new(); - err = (mdctx == NULL); - } - if (err == 0) { - rc = EVP_DigestVerifyInit_ex(mdctx, NULL, "SHA-256", wpLibCtx, NULL, k, - NULL); - err = (rc != 1); - } - if (err == 0) { - err = EVP_DigestVerify(mdctx, sig, sigLen, msg, sizeof(msg)) != 1; - if (err) PRINT_ERR_MSG("EVP_DigestVerify (pre-hash) failed"); - } - EVP_MD_CTX_free(mdctx); - mdctx = NULL; - /* A weak/legacy digest is not allowed for HashML-DSA. */ - if (err == 0) { - mdctx = EVP_MD_CTX_new(); - err = (mdctx == NULL); - } - if (err == 0) { - rc = EVP_DigestSignInit_ex(mdctx, NULL, "SHA-1", wpLibCtx, NULL, k, - NULL); - err = (rc == 1); - if (err) PRINT_ERR_MSG("DigestSignInit accepted SHA-1 for ML-DSA"); + err = mldsa_keygen(c->keyName, &k); + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + rc = EVP_DigestSignInit_ex(mdctx, NULL, c->mdName, wpLibCtx, NULL, + k, NULL); + err = ((rc == 1) != c->ok); + if (err) PRINT_ERR_MSG("DigestSignInit result unexpected"); + } + if ((err == 0) && c->ok) { + sigLen = sizeof(sig); + err = EVP_DigestSign(mdctx, sig, &sigLen, mldsa_test_msg, + MLDSA_TEST_MSG_LEN) != 1; + if (err) PRINT_ERR_MSG("EVP_DigestSign (pre-hash) failed"); + } + EVP_MD_CTX_free(mdctx); + mdctx = NULL; + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + rc = EVP_DigestVerifyInit_ex(mdctx, NULL, c->mdName, wpLibCtx, + NULL, k, NULL); + err = ((rc == 1) != c->ok); + if (err) PRINT_ERR_MSG("DigestVerifyInit result unexpected"); + } + if ((err == 0) && c->ok) { + err = EVP_DigestVerify(mdctx, sig, sigLen, mldsa_test_msg, + MLDSA_TEST_MSG_LEN) != 1; + if (err) PRINT_ERR_MSG("EVP_DigestVerify (pre-hash) failed"); + } + EVP_MD_CTX_free(mdctx); + mdctx = NULL; + EVP_PKEY_free(k); + k = NULL; } - EVP_MD_CTX_free(mdctx); - EVP_PKEY_free(k); return err; }