]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Add an initial ML-DSA fuzzer
authorAndrew Dinh <andrewd@openssl.org>
Fri, 14 Feb 2025 12:15:50 +0000 (19:15 +0700)
committerNeil Horman <nhorman@openssl.org>
Sat, 15 Feb 2025 16:13:38 +0000 (11:13 -0500)
Add an initial version of an ML-DSA fuzzer.  Exercises various ML-DSA
appropriate APIs. Currently it is able to randomly:

1. Attempt to create raw public private keys of various valid and invalid sizes
2. Generate legitimate keys of various sizes using the keygen api
3. Perform sign/verify operations using real generated keys
4. Perform digest sign/verify operations using real generated keys
5. Do an export and import of a key using todata/fromdata
6. Do a comparison of two equal and unequal keys

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/26685)

doc/man7/EVP_SIGNATURE-ML-DSA.pod
fuzz/build.info
fuzz/corpora
fuzz/ml-dsa.c [new file with mode: 0644]
test/recipes/99-test_fuzz_ml-dsa.t [new file with mode: 0644]

index ad7569eff7859a8a141371dec303b412e254c3a2..3e7cc41b242421ad105281d88c9ec26a06754163 100644 (file)
@@ -100,7 +100,7 @@ To sign a message using an ML-DSA EVP_PKEY structure:
         size_t sig_len;
         unsigned char *sig = NULL;
         const OSSL_PARAM params[] = {
-            OSSL_PARAM_octet_string("context-string", (unsigned char *)"A context string", 33),
+            OSSL_PARAM_octet_string("context-string", (unsigned char *)"A context string", 16),
             OSSL_PARAM_END
         };
         EVP_PKEY_CTX *sctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
index 21f647471fb191c3e8a07a607dd0c876dbf60a0b..af0a62a36784458a83d1c1c4f919ed4b1eadf780 100644 (file)
@@ -13,10 +13,15 @@ IF[{- !$disabled{"fuzz-afl"} || !$disabled{"fuzz-libfuzzer"} -}]
   PROGRAMS{noinst}=punycode pem decoder hashtable acert
   PROGRAMS{noinst}=v3name
   PROGRAMS{noinst}=provider
+
   IF[{- !$disabled{"ml-kem"} -}]
     PROGRAMS{noinst}=ml-kem
   ENDIF
 
+  IF[{- !$disabled{"ml-dsa"} -}]
+    PROGRAMS{noinst}=ml-dsa
+  ENDIF
+
   IF[{- !$disabled{"cmp"} -}]
     PROGRAMS{noinst}=cmp
   ENDIF
@@ -148,6 +153,10 @@ IF[{- !$disabled{"fuzz-afl"} || !$disabled{"fuzz-libfuzzer"} -}]
   SOURCE[ml-kem]=ml-kem.c driver.c
   INCLUDE[ml-kem]=../include {- $ex_inc -}
   DEPEND[ml-kem]=../libcrypto {- $ex_lib -}
+
+  SOURCE[ml-dsa]=ml-dsa.c driver.c
+  INCLUDE[ml-dsa]=../include {- $ex_inc -}
+  DEPEND[ml-dsa]=../libcrypto {- $ex_lib -}
 ENDIF
 
 IF[{- !$disabled{tests} -}]
@@ -160,6 +169,10 @@ IF[{- !$disabled{tests} -}]
     PROGRAMS{noinst}=ml-kem-test
   ENDIF
 
+  IF[{- !$disabled{"ml-dsa"} -}]
+    PROGRAMS{noinst}=ml-dsa-test
+  ENDIF
+
   IF[{- !$disabled{"cmp"} -}]
     PROGRAMS{noinst}=cmp-test
   ENDIF
@@ -213,6 +226,10 @@ IF[{- !$disabled{tests} -}]
   INCLUDE[ml-kem-test]=../include
   DEPEND[ml-kem-test]=../libcrypto.a
 
+  SOURCE[ml-dsa-test]=ml-dsa.c test-corpus.c fuzz_rand.c
+  INCLUDE[ml-dsa-test]=../include
+  DEPEND[ml-dsa-test]=../libcrypto.a
+
   # referring to static lib allows using non-exported functions
 
   SOURCE[cms-test]=cms.c test-corpus.c
index 93072ea488b1d9035e0ae29c7d6c5fab8f3471af..ce771805c094d098c25a218bc8e9f7344eccbc5a 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 93072ea488b1d9035e0ae29c7d6c5fab8f3471af
+Subproject commit ce771805c094d098c25a218bc8e9f7344eccbc5a
diff --git a/fuzz/ml-dsa.c b/fuzz/ml-dsa.c
new file mode 100644 (file)
index 0000000..1088f9d
--- /dev/null
@@ -0,0 +1,679 @@
+/*
+ * Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.openssl.org/source/license.html
+ * or in the file LICENSE in the source distribution.
+ */
+
+/* Test ML-DSA operation.  */
+#include <string.h>
+#include <openssl/evp.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/byteorder.h>
+#include "internal/nelem.h"
+#include "fuzzer.h"
+#include "crypto/ml_dsa.h"
+
+/**
+ * @brief Consumes an 8-bit unsigned integer from a buffer.
+ *
+ * This function extracts an 8-bit unsigned integer from the provided buffer,
+ * updates the buffer pointer, and adjusts the remaining length.
+ *
+ * @param buf  Pointer to the input buffer.
+ * @param len  Pointer to the size of the remaining buffer; updated after consumption.
+ * @param val  Pointer to store the extracted 8-bit value.
+ *
+ * @return Pointer to the updated buffer position after reading the value,
+ *         or NULL if the buffer does not contain enough data.
+ */
+static uint8_t *consume_uint8_t(const uint8_t *buf, size_t *len, uint8_t *val)
+{
+    if (*len < sizeof(uint8_t))
+        return NULL;
+    *val = *buf;
+    *len -= sizeof(uint8_t);
+    return (uint8_t *)buf + 1;
+}
+
+/**
+ * @brief Consumes a size_t from a buffer.
+ *
+ * This function extracts a size_t from the provided buffer, updates the buffer
+ * pointer, and adjusts the remaining length.
+ *
+ * @param buf  Pointer to the input buffer.
+ * @param len  Pointer to the size of the remaining buffer; updated after consumption.
+ * @param val  Pointer to store the extracted size_t value.
+ *
+ * @return Pointer to the updated buffer position after reading the value,
+ *         or NULL if the buffer does not contain enough data.
+ */
+static uint8_t *consume_size_t(const uint8_t *buf, size_t *len, size_t *val)
+{
+    if (*len < sizeof(size_t))
+        return NULL;
+    *val = *buf;
+    *len -= sizeof(size_t);
+    return (uint8_t *)buf + sizeof(size_t);
+}
+
+/**
+ * @brief Selects a key type and size from a buffer.
+ *
+ * This function reads a key size value from the buffer, determines the
+ * corresponding key type and length, and updates the buffer pointer
+ * accordingly. If `only_valid` is set, it restricts selection to valid key
+ * sizes; otherwise, it includes some invalid sizes for testing.
+ *
+ * @param buf       Pointer to the buffer pointer; updated after reading.
+ * @param len       Pointer to the remaining buffer size; updated accordingly.
+ * @param keytype   Pointer to store the selected key type string.
+ * @param keylen    Pointer to store the selected key length.
+ * @param only_valid Flag to restrict selection to valid key sizes.
+ *
+ * @return 1 if a key type is successfully selected, 0 on failure.
+ */
+static int select_keytype_and_size(uint8_t **buf, size_t *len,
+                                   char **keytype, size_t *keylen,
+                                   int only_valid)
+{
+    uint16_t keysize;
+    uint16_t modulus = 6;
+
+    /*
+     * Note: We don't really care about endianness here, we just want a random
+     * 16 bit value
+     */
+    *buf = (uint8_t *)OPENSSL_load_u16_le(&keysize, *buf);
+    *len -= sizeof(uint16_t);
+
+    if (*buf == NULL)
+        return 0;
+
+    /*
+     * If `only_valid` is set, select only ML-DSA-44, ML-DSA-65, and ML-DSA-87.
+     * Otherwise, include some invalid sizes to trigger error paths.
+     */
+
+    if (only_valid)
+        modulus = 3;
+
+    /*
+     * Note, keylens for valid values (cases 0-2) are taken based on input
+     * values from our unit tests
+     */
+    switch (keysize % modulus) {
+    case 0:
+        *keytype = "ML-DSA-44";
+        *keylen = ML_DSA_44_PUB_LEN;
+        break;
+    case 1:
+        *keytype = "ML-DSA-65";
+        *keylen = ML_DSA_65_PUB_LEN;
+        break;
+    case 2:
+        *keytype = "ML-DSA-87";
+        *keylen = ML_DSA_87_PUB_LEN;
+        break;
+    case 3:
+        /* select invalid alg */
+        *keytype = "ML-DSA-33";
+        *keylen = 33;
+        break;
+    case 4:
+        /* Select valid alg, but bogus size */
+        *keytype = "ML-DSA-87";
+        *buf = (uint8_t *)OPENSSL_load_u16_le(&keysize, *buf);
+        *len -= sizeof(uint16_t);
+        *keylen = (size_t)keysize;
+        *keylen %= ML_DSA_87_PUB_LEN; /* size to our key buffer */
+        break;
+    default:
+        *keytype = NULL;
+        *keylen = 0;
+        break;
+    }
+    return 1;
+}
+
+/**
+ * @brief Creates an ML-DSA raw key from a buffer.
+ *
+ * This function selects a key type and size from the buffer, generates a random
+ * key of the appropriate length, and creates either a public or private ML-DSA
+ * key using OpenSSL's EVP_PKEY interface.
+ *
+ * @param buf   Pointer to the buffer pointer; updated after reading.
+ * @param len   Pointer to the remaining buffer size; updated accordingly.
+ * @param key1  Pointer to store the generated EVP_PKEY key (public or private).
+ * @param key2  Unused parameter (reserved for future use).
+ *
+ * @note The generated key is allocated using OpenSSL's EVP_PKEY functions
+ *       and should be freed appropriately using `EVP_PKEY_free()`.
+ */
+static void create_ml_dsa_raw_key(uint8_t **buf, size_t *len,
+                                  void **key1, void **key2)
+{
+    EVP_PKEY *pubkey;
+    char *keytype = NULL;
+    size_t keylen = 0;
+    /* MAX_ML_DSA_PRIV_LEN is longer of that and ML_DSA_87_PUB_LEN */
+    uint8_t key[MAX_ML_DSA_PRIV_LEN];
+    int pub = 0;
+
+    if (!select_keytype_and_size(buf, len, &keytype, &keylen, 0))
+        return;
+
+    /*
+     * Select public or private key creation based on the low order bit of the
+     * next buffer value.
+     * Note that keylen as returned from select_keytype_and_size is a public key
+     * length, so make the adjustment to private key lengths here.
+     */
+    if ((*buf)[0] & 0x1) {
+        pub = 1;
+    } else {
+        switch (keylen) {
+        case (ML_DSA_44_PUB_LEN):
+            keylen = ML_DSA_44_PRIV_LEN;
+            break;
+        case (ML_DSA_65_PUB_LEN):
+            keylen = ML_DSA_65_PRIV_LEN;
+            break;
+        case (ML_DSA_87_PUB_LEN):
+            keylen = ML_DSA_87_PRIV_LEN;
+            break;
+        default:
+            return;
+        }
+    }
+
+    /*
+     * libfuzzer provides by default up to 4096 bit input buffers, but it's
+     * typically much less (between 1 and 100 bytes) so use RAND_bytes here
+     * instead
+     */
+    if (!RAND_bytes(key, keylen))
+        return;
+
+    /*
+     * Try to generate either a raw public or private key using random data
+     * Because the input is completely random, it's effectively certain this
+     * operation will fail, but it will still exercise the code paths below,
+     * which is what we want the fuzzer to do
+     */
+    if (pub == 1)
+        pubkey = EVP_PKEY_new_raw_public_key_ex(NULL, keytype, NULL, key, keylen);
+    else
+        pubkey = EVP_PKEY_new_raw_private_key_ex(NULL, keytype, NULL, key, keylen);
+
+    *key1 = pubkey;
+    return;
+}
+
+static int keygen_ml_dsa_real_key_helper(uint8_t **buf, size_t *len,
+                                         EVP_PKEY **key)
+{
+    char *keytype = NULL;
+    size_t keylen = 0;
+    EVP_PKEY_CTX *ctx = NULL;
+    int ret = 0;
+
+    /*
+     * Only generate valid key types and lengths. Note, no adjustment is made to
+     * keylen here, as the provider is responsible for selecting the keys and
+     * sizes for us during the EVP_PKEY_keygen call
+     */
+    if (!select_keytype_and_size(buf, len, &keytype, &keylen, 1))
+        goto err;
+
+    ctx = EVP_PKEY_CTX_new_from_name(NULL, keytype, NULL);
+    if (!ctx) {
+        fprintf(stderr, "Failed to generate ctx\n");
+        goto err;
+    }
+
+    if (!EVP_PKEY_keygen_init(ctx)) {
+        fprintf(stderr, "Failed to init keygen ctx\n");
+        goto err;
+    }
+
+    *key = EVP_PKEY_new();
+    if (*key == NULL)
+        goto err;
+
+    if (!EVP_PKEY_generate(ctx, key)) {
+        fprintf(stderr, "Failed to generate new real key\n");
+        goto err;
+    }
+
+    ret = 1;
+err:
+    EVP_PKEY_CTX_free(ctx);
+    return ret;
+}
+
+/**
+ * @brief Generates a valid ML-DSA key using OpenSSL.
+ *
+ * This function selects a valid ML-DSA key type and size from the buffer,
+ * initializes an OpenSSL EVP_PKEY context, and generates a cryptographic key
+ * accordingly.
+ *
+ * @param buf    Pointer to the buffer pointer; updated after reading.
+ * @param len    Pointer to the remaining buffer size; updated accordingly.
+ * @param key1   Pointer to store the first generated EVP_PKEY key.
+ * @param key2   Pointer to store the second generated EVP_PKEY key.
+ *
+ * @note The generated key is allocated using OpenSSL's EVP_PKEY functions
+ *       and should be freed using `EVP_PKEY_free()`.
+ */
+static void keygen_ml_dsa_real_key(uint8_t **buf, size_t *len,
+                                   void **key1, void **key2)
+{
+    if (!keygen_ml_dsa_real_key_helper(buf, len, (EVP_PKEY **)key1)
+        || !keygen_ml_dsa_real_key_helper(buf, len, (EVP_PKEY **)key2))
+        fprintf(stderr, "Unable to generate valid keys");
+}
+
+/**
+ * @brief Performs key sign and verify using an EVP_PKEY.
+ *
+ * This function generates a random key, signs random data using the provided
+ * public key, then verifies it. It makes use of OpenSSL's EVP_PKEY API for
+ * encryption and decryption.
+ *
+ * @param[out] buf   Unused output buffer (reserved for future use).
+ * @param[out] len   Unused length parameter (reserved for future use).
+ * @param[in]  key1  Pointer to an EVP_PKEY structure used for key operations.
+ * @param[in]  in2   Unused input parameter (reserved for future use).
+ * @param[out] out1  Unused output parameter (reserved for future use).
+ * @param[out] out2  Unused output parameter (reserved for future use).
+ */
+static void ml_dsa_sign_verify(uint8_t **buf, size_t *len, void *key1,
+                               void *in2, void **out1, void **out2)
+{
+    EVP_PKEY *key = (EVP_PKEY *)key1;
+    EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_pkey(NULL, key, NULL);
+    EVP_SIGNATURE *sig_alg = NULL;
+    unsigned char *sig = NULL;
+    size_t sig_len = 0, tbslen;
+    unsigned char *tbs = NULL;
+    /* Ownership of alg is retained by the pkey object */
+    const char *alg = EVP_PKEY_get0_type_name(key);
+    const OSSL_PARAM params[] = {
+        OSSL_PARAM_octet_string("context-string",
+                                (unsigned char *)"A context string", 16),
+        OSSL_PARAM_END
+    };
+
+    if (!consume_size_t(*buf, len, &tbslen)) {
+        fprintf(stderr, "Failed to set tbslen");
+        goto err;
+    }
+    /* Keep tbslen within a reasonable value we can malloc */
+    tbslen = (tbslen % 2048) + 1;
+
+    if ((tbs = OPENSSL_malloc(tbslen)) == NULL
+        || ctx == NULL || alg == NULL
+        || !RAND_bytes_ex(NULL, tbs, tbslen, 0)) {
+        fprintf(stderr, "Failed basic initialization\n");
+        goto err;
+    }
+
+    /*
+     * Because ML-DSA is fundamentally a one-shot algorithm like "pure" Ed25519
+     * and Ed448, we don't have any immediate plans to implement intermediate
+     * sign/verify functions. Therefore, we only test the one-shot functions.
+     */
+
+    if ((sig_alg = EVP_SIGNATURE_fetch(NULL, alg, NULL)) == NULL
+        || EVP_PKEY_sign_message_init(ctx, sig_alg, params) <= 0
+        || EVP_PKEY_sign(ctx, NULL, &sig_len, tbs, tbslen) <= 0
+        || (sig = OPENSSL_zalloc(sig_len)) == NULL
+        || EVP_PKEY_sign(ctx, sig, &sig_len, tbs, tbslen) <= 0) {
+        fprintf(stderr, "Failed to sign message\n");
+        goto err;
+    }
+
+    /* Verify signature */
+    EVP_PKEY_CTX_free(ctx);
+    ctx = NULL;
+
+    if ((ctx = EVP_PKEY_CTX_new_from_pkey(NULL, key, NULL)) == NULL
+        || EVP_PKEY_verify_message_init(ctx, sig_alg, params) <= 0
+        || EVP_PKEY_verify(ctx, sig, sig_len, tbs, tbslen) <= 0) {
+        fprintf(stderr, "Failed to verify message\n");
+        goto err;
+    }
+
+err:
+    OPENSSL_free(tbs);
+    EVP_PKEY_CTX_free(ctx);
+    EVP_SIGNATURE_free(sig_alg);
+    OPENSSL_free(sig);
+    return;
+}
+
+/**
+ * @brief Performs key sign and verify using an EVP_PKEY.
+ *
+ * This function generates a random key, signs random data using the provided
+ * public key, then verifies it. It makes use of OpenSSL's EVP_PKEY API for
+ * encryption and decryption.
+ *
+ * @param[out] buf   Unused output buffer (reserved for future use).
+ * @param[out] len   Unused length parameter (reserved for future use).
+ * @param[in]  key1  Pointer to an EVP_PKEY structure used for key operations.
+ * @param[in]  in2   Unused input parameter (reserved for future use).
+ * @param[out] out1  Unused output parameter (reserved for future use).
+ * @param[out] out2  Unused output parameter (reserved for future use).
+ */
+static void ml_dsa_digest_sign_verify(uint8_t **buf, size_t *len, void *key1,
+                                      void *in2, void **out1, void **out2)
+{
+    EVP_PKEY *key = (EVP_PKEY *)key1;
+    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
+    EVP_SIGNATURE *sig_alg = NULL;
+    unsigned char *sig = NULL;
+    size_t sig_len, tbslen;
+    unsigned char *tbs = NULL;
+    const OSSL_PARAM params[] = {
+        OSSL_PARAM_octet_string("context-string",
+                                (unsigned char *)"A context string", 16),
+        OSSL_PARAM_END
+    };
+
+    if (!consume_size_t(*buf, len, &tbslen)) {
+        fprintf(stderr, "Failed to set tbslen");
+        goto err;
+    }
+    /* Keep tbslen within a reasonable value we can malloc */
+    tbslen = (tbslen % 2048) + 1;
+
+    if ((tbs = OPENSSL_malloc(tbslen)) == NULL
+        || ctx == NULL
+        || !RAND_bytes_ex(NULL, tbs, tbslen, 0)) {
+        fprintf(stderr, "Failed basic initialization\n");
+        goto err;
+    }
+
+    /*
+     * Because ML-DSA is fundamentally a one-shot algorithm like "pure" Ed25519
+     * and Ed448, we don't have any immediate plans to implement intermediate
+     * sign/verify functions. Therefore, we only test the one-shot functions.
+     */
+
+    if (!EVP_DigestSignInit_ex(ctx, NULL, NULL, NULL, "?fips=true", key, params)
+        || EVP_DigestSign(ctx, NULL, &sig_len, tbs, tbslen) <= 0
+        || (sig = OPENSSL_malloc(sig_len)) == NULL
+        || EVP_DigestSign(ctx, sig, &sig_len, tbs, tbslen) <= 0) {
+        fprintf(stderr, "Failed to sign digest with EVP_DigestSign\n");
+        goto err;
+    }
+
+    /* Verify signature */
+    EVP_MD_CTX_free(ctx);
+    ctx = NULL;
+
+    if ((ctx = EVP_MD_CTX_new()) == NULL
+        || EVP_DigestVerifyInit_ex(ctx, NULL, NULL, NULL, "?fips=true", key,
+                                   params) <= 0
+        || EVP_DigestVerify(ctx, sig, sig_len, tbs, tbslen) <= 0) {
+        fprintf(stderr, "Failed to verify digest with EVP_DigestVerify\n");
+        goto err;
+    }
+
+err:
+    OPENSSL_free(tbs);
+    EVP_MD_CTX_free(ctx);
+    EVP_SIGNATURE_free(sig_alg);
+    OPENSSL_free(sig);
+    return;
+}
+
+/**
+ * @brief Exports and imports an ML-DSA key.
+ *
+ * This function extracts key material from the given key (`key1`), exports it
+ * as parameters, and then attempts to reconstruct a new key from those
+ * parameters. It uses OpenSSL's `EVP_PKEY_todata()` and `EVP_PKEY_fromdata()`
+ * functions for this process.
+ *
+ * @param[out] buf Unused output buffer (reserved for future use).
+ * @param[out] len Unused output length (reserved for future use).
+ * @param[in] key1 The key to be exported and imported.
+ * @param[in] key2 Unused input key (reserved for future use).
+ * @param[out] out1 Unused output parameter (reserved for future use).
+ * @param[out] out2 Unused output parameter (reserved for future use).
+ *
+ * @note If any step in the export-import process fails, the function
+ *       logs an error and cleans up allocated resources.
+ */
+static void ml_dsa_export_import(uint8_t **buf, size_t *len, void *key1,
+                                 void *key2, void **out1, void **out2)
+{
+    EVP_PKEY *alice = (EVP_PKEY *)key1;
+    EVP_PKEY *new_key = NULL;
+    EVP_PKEY_CTX *ctx = NULL;
+    OSSL_PARAM *params = NULL;
+
+    if (!EVP_PKEY_todata(alice, EVP_PKEY_KEYPAIR, &params)) {
+        fprintf(stderr, "Failed todata\n");
+        goto err;
+    }
+
+    ctx = EVP_PKEY_CTX_new_from_pkey(NULL, alice, NULL);
+    if (ctx == NULL) {
+        fprintf(stderr, "Failed new ctx\n");
+        goto err;
+    }
+
+    if (!EVP_PKEY_fromdata(ctx, &new_key, EVP_PKEY_KEYPAIR, params)) {
+        fprintf(stderr, "Failed fromdata\n");
+        goto err;
+    }
+
+err:
+    EVP_PKEY_CTX_free(ctx);
+    EVP_PKEY_free(new_key);
+    OSSL_PARAM_free(params);
+}
+
+/**
+ * @brief Compares two cryptographic keys and performs equality checks.
+ *
+ * This function takes in two cryptographic keys, casts them to `EVP_PKEY`
+ * structures, and checks their equality using `EVP_PKEY_eq()`. The purpose of
+ * `buf`, `len`, `out1`, and `out2` parameters is not clear from the function's
+ * current implementation.
+ *
+ * @param buf   Unused parameter (purpose unclear).
+ * @param len   Unused parameter (purpose unclear).
+ * @param key1  First key, expected to be an `EVP_PKEY *`.
+ * @param key2  Second key, expected to be an `EVP_PKEY *`.
+ * @param out1  Unused parameter (purpose unclear).
+ * @param out2  Unused parameter (purpose unclear).
+ */
+static void ml_dsa_compare(uint8_t **buf, size_t *len, void *key1,
+                           void *key2, void **out1, void **out2)
+{
+    EVP_PKEY *alice = (EVP_PKEY *)key1;
+    EVP_PKEY *bob = (EVP_PKEY *)key2;
+
+    EVP_PKEY_eq(alice, alice);
+    EVP_PKEY_eq(alice, bob);
+}
+
+/**
+ * @brief Frees allocated ML-DSA keys.
+ *
+ * This function releases memory associated with up to four EVP_PKEY objects by
+ * calling `EVP_PKEY_free()` on each provided key.
+ *
+ * @param key1 Pointer to the first key to be freed.
+ * @param key2 Pointer to the second key to be freed.
+ * @param key3 Pointer to the third key to be freed.
+ * @param key4 Pointer to the fourth key to be freed.
+ *
+ * @note This function assumes that each key is either a valid EVP_PKEY
+ *       object or NULL. Passing NULL is safe and has no effect.
+ */
+static void cleanup_ml_dsa_keys(void *key1, void *key2,
+                                void *key3, void *key4)
+{
+    EVP_PKEY_free((EVP_PKEY *)key1);
+    EVP_PKEY_free((EVP_PKEY *)key2);
+    EVP_PKEY_free((EVP_PKEY *)key3);
+    EVP_PKEY_free((EVP_PKEY *)key4);
+}
+
+/**
+ * @brief Represents an operation table entry for cryptographic operations.
+ *
+ * This structure defines a table entry containing function pointers for setting
+ * up, executing, and cleaning up cryptographic operations, along with
+ * associated metadata such as a name and description.
+ *
+ * @struct op_table_entry
+ */
+struct op_table_entry {
+    /** Name of the operation. */
+    char *name;
+
+    /** Description of the operation. */
+    char *desc;
+
+    /**
+     * @brief Function pointer for setting up the operation.
+     *
+     * @param buf   Pointer to the buffer pointer; may be updated.
+     * @param len   Pointer to the remaining buffer size; may be updated.
+     * @param out1  Pointer to store the first output of the setup function.
+     * @param out2  Pointer to store the second output of the setup function.
+     */
+    void (*setup)(uint8_t **buf, size_t *len, void **out1, void **out2);
+
+    /**
+     * @brief Function pointer for executing the operation.
+     *
+     * @param buf   Pointer to the buffer pointer; may be updated.
+     * @param len   Pointer to the remaining buffer size; may be updated.
+     * @param in1   First input parameter for the operation.
+     * @param in2   Second input parameter for the operation.
+     * @param out1  Pointer to store the first output of the operation.
+     * @param out2  Pointer to store the second output of the operation.
+     */
+    void (*doit)(uint8_t **buf, size_t *len, void *in1, void *in2,
+                 void **out1, void **out2);
+
+    /**
+     * @brief Function pointer for cleaning up after the operation.
+     *
+     * @param in1   First input parameter to be cleaned up.
+     * @param in2   Second input parameter to be cleaned up.
+     * @param out1  First output parameter to be cleaned up.
+     * @param out2  Second output parameter to be cleaned up.
+     */
+    void (*cleanup)(void *in1, void *in2, void *out1, void *out2);
+};
+
+static struct op_table_entry ops[] = {
+    {
+        "Generate ML-DSA raw key",
+        "Try generate a raw keypair using random data. Usually fails",
+        create_ml_dsa_raw_key,
+        NULL,
+        cleanup_ml_dsa_keys
+    }, {
+        "Generate ML-DSA keypair, using EVP_PKEY_keygen",
+        "Generates a real ML-DSA keypair, should always work",
+        keygen_ml_dsa_real_key,
+        NULL,
+        cleanup_ml_dsa_keys
+    }, {
+        "Do a sign/verify operation on a key",
+        "Generate key, sign random data, verify it, should work",
+        keygen_ml_dsa_real_key,
+        ml_dsa_sign_verify,
+        cleanup_ml_dsa_keys
+    }, {
+        "Do a digest sign/verify operation on a key",
+        "Generate key, digest sign random data, verify it, should work",
+        keygen_ml_dsa_real_key,
+        ml_dsa_digest_sign_verify,
+        cleanup_ml_dsa_keys
+    }, {
+        "Do an export/import of key data",
+        "Exercise EVP_PKEY_todata/fromdata",
+        keygen_ml_dsa_real_key,
+        ml_dsa_export_import,
+        cleanup_ml_dsa_keys
+    }, {
+        "Compare keys for equality",
+        "Compare key1/key1 and key1/key2 for equality",
+        keygen_ml_dsa_real_key,
+        ml_dsa_compare,
+        cleanup_ml_dsa_keys
+    }
+};
+
+int FuzzerInitialize(int *argc, char ***argv)
+{
+    return 0;
+}
+
+/**
+ * @brief Processes a fuzzing input by selecting and executing an operation.
+ *
+ * This function interprets the first byte of the input buffer to determine an
+ * operation to execute. It then follows a setup, execution, and cleanup
+ * sequence based on the selected operation.
+ *
+ * @param buf Pointer to the input buffer.
+ * @param len Length of the input buffer.
+ *
+ * @return 0 on successful execution, -1 if the input is too short.
+ *
+ * @note The function requires at least 32 bytes in the buffer to proceed.
+ *       It utilizes the `ops` operation table to dynamically determine and
+ *       execute the selected operation.
+ */
+int FuzzerTestOneInput(const uint8_t *buf, size_t len)
+{
+    uint8_t operation;
+    uint8_t *buffer_cursor;
+    void *in1 = NULL, *in2 = NULL;
+    void *out1 = NULL, *out2 = NULL;
+
+    if (len < 32)
+        return -1;
+
+    /* Get the first byte of the buffer to tell us what operation to perform */
+    buffer_cursor = consume_uint8_t(buf, &len, &operation);
+    if (buffer_cursor == NULL)
+        return -1;
+
+    /* Adjust for operational array size */
+    operation %= OSSL_NELEM(ops);
+
+    /* And run our setup/doit/cleanup sequence */
+    if (ops[operation].setup != NULL)
+        ops[operation].setup(&buffer_cursor, &len, &in1, &in2);
+    if (ops[operation].doit != NULL)
+        ops[operation].doit(&buffer_cursor, &len, in1, in2, &out1, &out2);
+    if (ops[operation].cleanup != NULL)
+        ops[operation].cleanup(in1, in2, out1, out2);
+
+    return 0;
+}
+
+void FuzzerCleanup(void)
+{
+    OPENSSL_cleanup();
+}
diff --git a/test/recipes/99-test_fuzz_ml-dsa.t b/test/recipes/99-test_fuzz_ml-dsa.t
new file mode 100644 (file)
index 0000000..d17595f
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License").  You may not use
+# this file except in compliance with the License.  You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use strict;
+use warnings;
+
+use OpenSSL::Test qw/:DEFAULT srctop_file/;
+use OpenSSL::Test::Utils;
+
+my $fuzzer = "ml-dsa";
+setup("test_fuzz_${fuzzer}");
+
+plan skip_all => "This test requires ML-DSA support"
+    if disabled("ml-dsa");
+
+plan tests => 2; # one more due to below require_ok(...)
+
+require_ok(srctop_file('test','recipes','fuzz.pl'));
+
+fuzz_ok($fuzzer);