From 81b4679dedb2376c8fd80662e1b860c4834fc0b4 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Wed, 26 Aug 2026 22:19:43 -0700 Subject: [PATCH 1/8] Add CB_ONLY mode for ML-DSA --- .github/workflows/cryptocb-only.yml | 15 +- tests/api.c | 1 + tests/api/test_mldsa.c | 37 +- tests/api/test_mldsa_legacy.c | 3 +- wolfcrypt/src/cryptocb.c | 1 + wolfcrypt/src/wc_mldsa.c | 158 ++++++++- wolfcrypt/src/wc_mldsa_asm.S | 12 +- wolfcrypt/test/test.c | 508 +++++++++++++++++++++++++++- wolfssl/wolfcrypt/settings.h | 11 + wolfssl/wolfcrypt/wc_mldsa.h | 11 + 10 files changed, 728 insertions(+), 29 deletions(-) diff --git a/.github/workflows/cryptocb-only.yml b/.github/workflows/cryptocb-only.yml index c31f875d76f..98df0a3bf5f 100644 --- a/.github/workflows/cryptocb-only.yml +++ b/.github/workflows/cryptocb-only.yml @@ -151,12 +151,21 @@ jobs: {"name": "falcon-onlycb-no-swdev", "minutes": 1.0, "comment": "WOLF_CRYPTO_CB_ONLY_FALCON without swdev, which has no Falcon handlers: builds the Falcon key API that a callback-only build keeps, including its TLS and ASN callers, and runs the tests that need no device.", "configure": ["--disable-swdev", "--enable-falcon", "--enable-experimental", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_FALCON"]}, + {"name": "mldsa", "minutes": 4.0, + "comment": "WOLF_CRYPTO_CB_ONLY_MLDSA: strips the software ML-DSA core (NTT, matrix expansion, rejection sampling, the sign/verify workers and the x86 assembly); key import/export, the size queries and the DER paths stay, and the cached matrix/vector fields stay in the key struct so its layout is unchanged. The in-tree cryptocb test registers a callback-only ML-DSA device and drives key generation, signing, verifying and the private-key check through it, so the successful dispatch path is covered and not just the no-device refusal.", + "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_MLDSA"]}, + {"name": "mldsa-verify-only", "minutes": 4.0, + "comment": "WOLF_CRYPTO_CB_ONLY_MLDSA against a verify-only ML-DSA build. That combination drops WOLFSSL_MLDSA_CHECK_KEY and the signing entry points, so it catches a callback-only path that assumes an API which this configuration does not compile.", + "configure": ["--enable-mldsa=verify-only", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_MLDSA"]}, + {"name": "mldsa-no-verify", "minutes": 4.0, + "comment": "WOLF_CRYPTO_CB_ONLY_MLDSA against a sign-only ML-DSA build (WOLFSSL_MLDSA_NO_VERIFY), the mirror of the verify-only entry.", + "configure": ["--enable-mldsa=sign", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_MLDSA"]}, {"name": "shake-xof", "minutes": 4.0, "comment": "WOLF_CRYPTO_CB_SHAKE_XOF: swdev handles SHAKE absorb and squeeze. No ONLY_* strip exists for SHAKE, so software SHAKE stays in. ML-KEM and ML-DSA reach swdev_shake through WOLF_CRYPTO_CB_FIND, and cryptocb_test runs shake_cb_xof_test.", "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_SHAKE_XOF"]}, - {"name": "all", "minutes": 19, - "comment": "All nine ONLY_* macros at once: every supported software primitive is stripped and dispatched through cryptocb. Catches any cross-algorithm call that a single-strip entry would still resolve via the remaining software paths.", - "configure": ["--enable-slhdsa=yes,sha2", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519 -DWOLF_CRYPTO_CB_ONLY_CURVE448 -DWOLF_CRYPTO_CB_ONLY_SLHDSA"]}, + {"name": "all", "minutes": 20, + "comment": "All ten ONLY_* macros at once: every supported software primitive is stripped and dispatched through cryptocb. Catches any cross-algorithm call that a single-strip entry would still resolve via the remaining software paths.", + "configure": ["--enable-slhdsa=yes,sha2", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519 -DWOLF_CRYPTO_CB_ONLY_CURVE448 -DWOLF_CRYPTO_CB_ONLY_SLHDSA -DWOLF_CRYPTO_CB_ONLY_MLDSA"]}, {"name": "only", "minutes": 4.0, "comment": "Same coverage as the \"all\" entry above, but driven by ./configure --enable-cryptocb=only instead of a hand-written CPPFLAGS list. This is the regression test for the configure option: it must emit exactly the WOLF_CRYPTO_CB_ONLY_* set that \"all\" passes by hand, for the algorithms this base enables. The \"all\" entry deliberately stays on explicit CPPFLAGS so a bug in the configure logic cannot silently weaken both. Note the base already passes --enable-cryptocb; this entry's flags are appended after the base, so --enable-cryptocb=only wins.", "configure": ["--enable-cryptocb=only"]}, diff --git a/tests/api.c b/tests/api.c index 8a09b804278..d1a65a62baa 100644 --- a/tests/api.c +++ b/tests/api.c @@ -29908,6 +29908,7 @@ static int test_wc_SignCRL_mldsa(void) EXPECT_DECLS; #if defined(WOLFSSL_CERT_GEN) && defined(HAVE_CRL) && !defined(NO_FILESYSTEM) && \ !defined(NO_ASN) && defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ defined(WOLFSSL_PEM_TO_DER) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ !defined(WOLFSSL_MLDSA_NO_ASN1) static const struct { diff --git a/tests/api/test_mldsa.c b/tests/api/test_mldsa.c index f3749d684b4..05664c67919 100644 --- a/tests/api/test_mldsa.c +++ b/tests/api/test_mldsa.c @@ -711,7 +711,8 @@ int test_mldsa(void) int test_mldsa_sign_pubonly_fails(void) { EXPECT_DECLS; -#if !defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0) +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(WC_MLDSA_HAVE_NATIVE) #if defined(WOLFSSL_HAVE_MLDSA) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ @@ -804,6 +805,7 @@ int test_mldsa_make_key(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) wc_MlDsaKey* key; WC_RNG rng; @@ -844,7 +846,7 @@ int test_mldsa_make_key(void) int test_mldsa_sign(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLDSA) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && defined(WOLFSSL_MLDSA_NO_CTX) wc_MlDsaKey* key; wc_MlDsaKey* importKey = NULL; @@ -1031,7 +1033,7 @@ int test_mldsa_sign(void) int test_mldsa_verify(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLDSA) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) && defined(WOLFSSL_MLDSA_NO_CTX) && \ (!defined(WOLFSSL_NO_ML_DSA_44) || !defined(WOLFSSL_MLDSA_NO_SIGN)) wc_MlDsaKey* key; @@ -1264,6 +1266,7 @@ int test_mldsa_sign_vfy(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY) wc_MlDsaKey* key; @@ -1352,6 +1355,7 @@ int test_mldsa_check_key(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ defined(WOLFSSL_MLDSA_CHECK_KEY) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) wc_MlDsaKey* checkKey; @@ -3024,6 +3028,7 @@ int test_mldsa_der(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) wc_MlDsaKey* key; @@ -3228,6 +3233,7 @@ int test_mldsa_der(void) } #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) /* Decode, re-export, byte-compare. Asserts version=1 on the bundled form and @@ -3296,6 +3302,7 @@ int test_mldsa_oneasymkey_version(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) #ifndef WOLFSSL_NO_ML_DSA_44 ExpectIntEQ(mldsa_oneasymkey_version_check(WC_ML_DSA_44), @@ -3317,6 +3324,7 @@ int test_mldsa_make_key_from_seed(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) wc_MlDsaKey* key; #ifndef WOLFSSL_NO_ML_DSA_44 @@ -7784,7 +7792,7 @@ int test_mldsa_make_key_from_seed(void) int test_mldsa_sig_kats(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLDSA) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && defined(WOLFSSL_MLDSA_NO_CTX) wc_MlDsaKey* key; #ifndef WOLFSSL_NO_ML_DSA_44 @@ -12604,6 +12612,7 @@ int test_mldsa_sign_ctx_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) wc_MlDsaKey* key; word32 sigLen; @@ -16842,6 +16851,7 @@ int test_mldsa_verify_ctx_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) wc_MlDsaKey* key; int res; @@ -20363,7 +20373,7 @@ int test_mldsa_verify_ctx_kats(void) int test_mldsa_verify_kats(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLDSA) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) && defined(WOLFSSL_MLDSA_NO_CTX) wc_MlDsaKey* key; int res; @@ -24635,6 +24645,7 @@ int test_mldsa_sign_mu_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) wc_MlDsaKey* key = NULL; word32 sigLen; @@ -27499,6 +27510,7 @@ int test_mldsa_verify_mu_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) wc_MlDsaKey* key = NULL; byte* sigBuf = NULL; @@ -29695,6 +29707,7 @@ int test_mldsa_verify_mu_kats(void) } #if !defined(NO_ASN) && defined(HAVE_PKCS8) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ defined(WOLFSSL_HAVE_MLDSA) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_ASN_TEMPLATE) @@ -29763,6 +29776,7 @@ int test_mldsa_PrivateKeyDecode_OpenSSL_form(void) EXPECT_DECLS; #if !defined(NO_ASN) && defined(HAVE_PKCS8) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ defined(WOLFSSL_HAVE_MLDSA) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_ASN_TEMPLATE) @@ -29856,6 +29870,7 @@ int test_mldsa_pkcs8_import_OpenSSL_form(void) { EXPECT_DECLS; #if !defined(NO_ASN) && defined(HAVE_PKCS8) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ defined(WOLFSSL_HAVE_MLDSA) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && \ @@ -29930,6 +29945,7 @@ int test_mldsa_pkcs8_export_import_wolfSSL_form(void) { EXPECT_DECLS; #if !defined(NO_ASN) && defined(HAVE_PKCS8) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ defined(WOLFSSL_HAVE_MLDSA) && !defined(NO_TLS) && \ (!defined(NO_WOLFSSL_CLIENT) || !defined(NO_WOLFSSL_SERVER)) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ @@ -30044,7 +30060,7 @@ int test_mldsa_pkcs8_export_import_wolfSSL_form(void) int test_mldsa_encode_w1_large_values(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLDSA) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ (!defined(WOLFSSL_MLDSA_NO_SIGN) || \ !defined(WOLFSSL_MLDSA_NO_VERIFY)) @@ -30177,7 +30193,8 @@ int test_mldsa_pkcs12(void) { EXPECT_DECLS; #if !defined(NO_ASN) && defined(HAVE_PKCS12) && \ - defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \ + defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(NO_TLS) && !defined(NO_PWDBASED) && !defined(NO_HMAC) && \ !defined(NO_CERTS) && !defined(NO_DES3) && \ @@ -30562,6 +30579,7 @@ int test_mldsa_verify_hash(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) wc_MlDsaKey key; @@ -30602,6 +30620,7 @@ int test_dilithium_hash(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) wc_MlDsaKey key; @@ -30815,6 +30834,7 @@ int test_wc_MldsaFeatureCoverage(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && \ @@ -30904,7 +30924,8 @@ int test_wc_MldsaFeatureCoverage(void) int test_wc_MldsaDecisionCoverage2(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLDSA) +#if defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WC_MLDSA_HAVE_NATIVE) wc_MlDsaKey key; int inited = 0; diff --git a/tests/api/test_mldsa_legacy.c b/tests/api/test_mldsa_legacy.c index a8f1c131f47..6de7cd0db52 100644 --- a/tests/api/test_mldsa_legacy.c +++ b/tests/api/test_mldsa_legacy.c @@ -373,7 +373,8 @@ int test_mldsa_legacy_shim(void) #if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) && !defined(WOLFSSL_NO_ML_DSA_44) && \ defined(WOLFSSL_MLDSA_PUBLIC_KEY) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \ - !defined(WC_NO_RNG) + !defined(WC_NO_RNG) && \ + defined(WC_MLDSA_HAVE_NATIVE) { dilithium_key key; /* legacy typedef */ WC_RNG rng; diff --git a/wolfcrypt/src/cryptocb.c b/wolfcrypt/src/cryptocb.c index d706d8a608f..3e1e586aed8 100644 --- a/wolfcrypt/src/cryptocb.c +++ b/wolfcrypt/src/cryptocb.c @@ -66,6 +66,7 @@ Crypto Callback Build Options: * WOLF_CRYPTO_CB_ONLY_AES: Use only callbacks for AES default: off * WOLF_CRYPTO_CB_ONLY_ED25519: Use only callbacks for Ed25519 default: off * WOLF_CRYPTO_CB_ONLY_CURVE25519: Use only callbacks for X25519 default: off + * WOLF_CRYPTO_CB_ONLY_MLDSA: Use only callbacks for ML-DSA default: off * WOLF_CRYPTO_CB_SHAKE_XOF: Dispatch SHAKE absorb and squeeze default: off * as well as update and final. Off by * default because a callback that predates diff --git a/wolfcrypt/src/wc_mldsa.c b/wolfcrypt/src/wc_mldsa.c index 8fade7cfb5f..ab7152cec9d 100644 --- a/wolfcrypt/src/wc_mldsa.c +++ b/wolfcrypt/src/wc_mldsa.c @@ -493,6 +493,7 @@ static int mldsa_alloc_pub_buf(wc_MlDsaKey* key) } #endif +#ifndef WOLF_CRYPTO_CB_ONLY_MLDSA /****************************************************************************** * Hash operations ******************************************************************************/ @@ -11362,6 +11363,7 @@ static int mldsa_verify_ctx_hash(wc_MlDsaKey* key, const byte* ctx, return ret; } #endif /* WOLFSSL_MLDSA_NO_VERIFY */ +#endif /* !WOLF_CRYPTO_CB_ONLY_MLDSA */ #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng) @@ -11389,6 +11391,11 @@ int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng) } #endif +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Check the level or parameters have been set. */ if (key->params == NULL) { @@ -11399,6 +11406,7 @@ int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng) ret = mldsa_make_key(key, rng); } } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ /* No key-pair test here: wc_MlDsaKey_MakeKeyFromSeed(), reached from * mldsa_make_key() above, already runs it on every generation path. @@ -11408,6 +11416,7 @@ int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng) return ret; } +#ifndef WOLF_CRYPTO_CB_ONLY_MLDSA /* Expand a seed into an ML-DSA key pair and test it. * * @param [in, out] key ML-DSA key to fill in. @@ -11449,10 +11458,24 @@ static int mldsa_key_from_seed_checked(wc_MlDsaKey* key, const byte* seed, return ret; } +#endif /* !WOLF_CRYPTO_CB_ONLY_MLDSA */ int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed) { +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + /* Validate as the software path does, so the reported error stays the + * same for a bad call. */ + if ((key == NULL) || (seed == NULL)) { + return BAD_FUNC_ARG; + } + /* Not NO_VALID_DEVID: no device can service this one. Expanding a seed + * is a local computation and the callback protocol has no seed to hand + * over, so registering a device would not help. The seed expansion is + * part of the software core this build removes. */ + return NOT_COMPILED_IN; +#else return mldsa_key_from_seed_checked(key, seed, 1); +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ } #endif @@ -11515,11 +11538,17 @@ int wc_MlDsaKey_SignCtx(wc_MlDsaKey* key, const byte* ctx, byte ctxLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Sign message. */ ret = mldsa_sign_ctx_msg(key, rng, ctx, ctxLen, msg, msgLen, sig, sigLen); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11577,10 +11606,16 @@ int wc_MlDsaKey_Sign(wc_MlDsaKey* key, byte* sig, word32 *sigLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Sign message. */ ret = mldsa_sign_msg(key, rng, msg, msgLen, sig, sigLen); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11636,11 +11671,17 @@ int wc_MlDsaKey_SignCtxHash(wc_MlDsaKey* key, const byte* ctx, byte ctxLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Sign message. */ ret = mldsa_sign_ctx_hash(key, rng, ctx, ctxLen, hashAlg, hash, hashLen, sig, sigLen); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11685,11 +11726,18 @@ int wc_MlDsaKey_SignCtxWithSeed(wc_MlDsaKey* key, const byte* ctx, byte ctxLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NOT_COMPILED_IN; + } + (void)msgLen; +#else if (ret == 0) { /* Sign message. */ ret = mldsa_sign_ctx_msg_with_seed(key, seed, ctx, ctxLen, msg, msgLen, sig, sigLen); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11730,10 +11778,17 @@ int wc_MlDsaKey_SignWithSeed(wc_MlDsaKey* key, byte* sig, word32 *sigLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + (void)msgLen; + if (ret == 0) { + ret = NOT_COMPILED_IN; + } +#else if (ret == 0) { /* Sign message. */ ret = mldsa_sign_msg_with_seed(key, seed, msg, msgLen, sig, sigLen); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11773,11 +11828,19 @@ int wc_MlDsaKey_SignCtxHashWithSeed(wc_MlDsaKey* key, const byte* ctx, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NOT_COMPILED_IN; + } + (void)hashLen; + (void)hashAlg; +#else if (ret == 0) { /* Sign message. */ ret = mldsa_sign_ctx_hash_with_seed(key, seed, ctx, ctxLen, hashAlg, hash, hashLen, sig, sigLen); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11817,6 +11880,11 @@ int wc_MlDsaKey_SignMuWithSeed(wc_MlDsaKey* key, byte* sig, word32 *sigLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NOT_COMPILED_IN; + } +#else if (ret == 0) { /* Build [seed||mu] buffer and call internal sign function. */ byte seedMu[MLDSA_RND_SZ + MLDSA_MU_SZ]; @@ -11832,6 +11900,7 @@ int wc_MlDsaKey_SignMuWithSeed(wc_MlDsaKey* key, byte* sig, word32 *sigLen, wc_MemZero_Check(seedMu, sizeof(seedMu)); #endif } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11894,11 +11963,17 @@ int wc_MlDsaKey_VerifyCtx(wc_MlDsaKey* key, const byte* sig, word32 sigLen, } #endif +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Verify message with signature. */ ret = mldsa_verify_ctx_msg(key, ctx, ctxLen, msg, msgLen, sig, sigLen, res); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -11952,10 +12027,16 @@ int wc_MlDsaKey_Verify(wc_MlDsaKey* key, const byte* sig, word32 sigLen, } #endif +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Verify message with signature. */ ret = mldsa_verify_msg(key, msg, msgLen, sig, sigLen, res); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -12007,11 +12088,17 @@ int wc_MlDsaKey_VerifyCtxHash(wc_MlDsaKey* key, const byte* sig, word32 sigLen, } #endif +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if (ret == 0) { /* Verify message with signature. */ ret = mldsa_verify_ctx_hash(key, ctx, ctxLen, hashAlg, hash, hashLen, sig, sigLen, res); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -12081,9 +12168,16 @@ int wc_MlDsaKey_VerifyMu(wc_MlDsaKey* key, const byte* sig, word32 sigLen, ret = BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NOT_COMPILED_IN; + } + (void)sigLen; +#else if (ret == 0) { ret = mldsa_verify_with_mu(key, mu, sig, sigLen, res); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ return ret; } @@ -12682,6 +12776,7 @@ int wc_MlDsaKey_GetSigLen(wc_MlDsaKey* key, int* len) int wc_MlDsaKey_CheckKey(wc_MlDsaKey* key) { int ret = 0; +#ifndef WOLF_CRYPTO_CB_ONLY_MLDSA const wc_MlDsaParams* params = NULL; sword32* a = NULL; sword32* s1 = NULL; @@ -12689,11 +12784,54 @@ int wc_MlDsaKey_CheckKey(wc_MlDsaKey* key) sword32* t = NULL; sword32* t0 = NULL; sword32* t1 = NULL; +#endif /* Validate parameter. */ if (key == NULL) { ret = BAD_FUNC_ARG; } + +#ifdef WOLF_CRYPTO_CB + /* A device-backed key holds no local private material, so dispatch before + * the prvKeySet check the software path makes. */ + if (ret == 0) { + #ifndef WOLF_CRYPTO_CB_FIND + if (key->devId != INVALID_DEVID) + #endif + { + const byte* pub = NULL; + word32 pubSz = 0; + /* Read through a pointer: key->p is an array in the default + * layout and a pointer in the dynamic and assign-key ones. */ + const byte* keyPub = key->p; + + /* pubKeySet only says a public key exists, not that this object + * holds its bytes: a device-generated key has none locally. Send + * a length only alongside a pointer to go with it. */ + if (key->pubKeySet && (keyPub != NULL)) { + int sz = wc_MlDsaKey_PubSize(key); + if (sz > 0) { + pub = keyPub; + pubSz = (word32)sz; + } + } + ret = wc_CryptoCb_PqcSignatureCheckPrivKey(key, + WC_PQC_SIG_TYPE_MLDSA, pub, pubSz); + if (ret != WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE)) + return ret; + /* fall-through when unavailable */ + ret = 0; + } + } +#endif /* WOLF_CRYPTO_CB */ + +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + /* No software fallback: the check recomputes the public key from the + * private key, which only the device holding it can do. */ + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else if ((ret == 0) && (!key->prvKeySet)) { ret = BAD_FUNC_ARG; } @@ -12830,6 +12968,8 @@ int wc_MlDsaKey_CheckKey(wc_MlDsaKey* key) /* Dispose of allocated memory. */ XFREE(s1, key->heap, DYNAMIC_TYPE_MLDSA); } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ + return ret; } #endif /* WOLFSSL_MLDSA_CHECK_KEY */ @@ -13013,7 +13153,8 @@ int wc_MlDsaKey_ImportPubRaw(wc_MlDsaKey* key, const byte* in, word32 inLen) XMEMCPY(key->p, in, inLen); #endif -#ifdef WC_MLDSA_CACHE_PUB_VECTORS +#if defined(WC_MLDSA_CACHE_PUB_VECTORS) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) + /* The caches only feed the native signer and verifier. */ #ifndef WC_MLDSA_FIXED_ARRAY /* Allocate t1 if required. */ if (key->t1 == NULL) { @@ -13032,7 +13173,7 @@ int wc_MlDsaKey_ImportPubRaw(wc_MlDsaKey* key, const byte* in, word32 inLen) /* Compute t1 from public key data. */ mldsa_make_pub_vec(key, key->t1); #endif -#ifdef WC_MLDSA_CACHE_MATRIX_A +#if defined(WC_MLDSA_CACHE_MATRIX_A) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) #ifndef WC_MLDSA_FIXED_ARRAY /* Allocate matrix a if required. */ if (key->a == NULL) { @@ -13133,7 +13274,9 @@ static int mldsa_set_priv_key(const byte* priv, word32 privSz, { int ret = 0; int expPrivSz; -#ifdef WC_MLDSA_CACHE_MATRIX_A +#if (defined(WC_MLDSA_CACHE_MATRIX_A) || \ + defined(WC_MLDSA_CACHE_PRIV_VECTORS)) && \ + !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) const wc_MlDsaParams* params = key->params; #endif @@ -13173,8 +13316,9 @@ static int mldsa_set_priv_key(const byte* priv, word32 privSz, #endif } - /* Allocate and create cached values. */ -#ifdef WC_MLDSA_CACHE_MATRIX_A + /* Allocate and create cached values. The caches only feed the + * native signer and verifier. */ +#if defined(WC_MLDSA_CACHE_MATRIX_A) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) #ifndef WC_MLDSA_FIXED_ARRAY if (ret == 0) { /* Allocate matrix a if required. */ @@ -13199,7 +13343,7 @@ static int mldsa_set_priv_key(const byte* priv, word32 privSz, } } #endif -#ifdef WC_MLDSA_CACHE_PRIV_VECTORS +#if defined(WC_MLDSA_CACHE_PRIV_VECTORS) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) #ifndef WC_MLDSA_FIXED_ARRAY if ((ret == 0) && (key->s1 == NULL)) { /* Allocate L vector s1, K vector s2 and K vector t0 if required. */ @@ -13650,7 +13794,7 @@ int wc_MlDsaKey_PrivateKeyDecode(wc_MlDsaKey* key, const byte* input, if (ret == 0) { /* Generate a key pair if seed exists and decoded key pair is ignored */ if (seedLen != 0) { -#if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) +#if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) if (seedLen == MLDSA_SEED_SZ) { /* runPct 0: this is an import, not a generation. */ ret = mldsa_key_from_seed_checked(key, seed, 0); diff --git a/wolfcrypt/src/wc_mldsa_asm.S b/wolfcrypt/src/wc_mldsa_asm.S index 26669947a60..072ecd02744 100644 --- a/wolfcrypt/src/wc_mldsa_asm.S +++ b/wolfcrypt/src/wc_mldsa_asm.S @@ -43,7 +43,7 @@ #endif /* HAVE_INTEL_AVX512 */ #endif /* NO_AVX512_SUPPORT */ -#ifdef WOLFSSL_HAVE_MLDSA +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) #ifdef HAVE_INTEL_AVX2 #ifndef __APPLE__ .data @@ -39712,8 +39712,8 @@ _wc_mldsa_poly_make_pos_avx2: .size wc_mldsa_poly_make_pos_avx2,.-wc_mldsa_poly_make_pos_avx2 #endif /* __APPLE__ */ #endif /* HAVE_INTEL_AVX2 */ -#endif /* WOLFSSL_HAVE_MLDSA */ -#ifdef WOLFSSL_HAVE_MLDSA +#endif /* WOLFSSL_HAVE_MLDSA && !WOLF_CRYPTO_CB_ONLY_MLDSA */ +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) #ifndef NO_AVX512_SUPPORT #ifndef HAVE_INTEL_AVX512 #define HAVE_INTEL_AVX512 @@ -63818,8 +63818,8 @@ _wc_mldsa_poly_make_pos_avx512: .size wc_mldsa_poly_make_pos_avx512,.-wc_mldsa_poly_make_pos_avx512 #endif /* __APPLE__ */ #endif /* HAVE_INTEL_AVX512 */ -#endif /* WOLFSSL_HAVE_MLDSA */ -#ifdef WOLFSSL_HAVE_MLDSA +#endif /* WOLFSSL_HAVE_MLDSA && !WOLF_CRYPTO_CB_ONLY_MLDSA */ +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) #ifndef NO_AVX512_SUPPORT #ifndef NO_AVX512_VBMI_SUPPORT #ifndef HAVE_INTEL_AVX512_VBMI @@ -64425,7 +64425,7 @@ _wc_mldsa_encode_t0_t1_avx512_vbmi: .size wc_mldsa_encode_t0_t1_avx512_vbmi,.-wc_mldsa_encode_t0_t1_avx512_vbmi #endif /* __APPLE__ */ #endif /* HAVE_INTEL_AVX512_VBMI */ -#endif /* WOLFSSL_HAVE_MLDSA */ +#endif /* WOLFSSL_HAVE_MLDSA && !WOLF_CRYPTO_CB_ONLY_MLDSA */ #if defined(__linux__) && defined(__ELF__) .section .note.GNU-stack,"",%progbits diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 260b6e94d22..648a5dedb11 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -65324,6 +65324,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t frodokem_test(void) #endif /* WOLFSSL_HAVE_FRODOKEM */ #ifdef WOLFSSL_HAVE_MLDSA +#ifdef WC_MLDSA_HAVE_NATIVE #ifndef WOLFSSL_MLDSA_NO_VERIFY static wc_test_ret_t mldsa_param_vfy_test(int param, const byte* pubKey, word32 pubKeyLen, const byte* sig, word32 sigLen) @@ -69018,6 +69019,7 @@ static wc_test_ret_t mldsa_decode_test(void) } #endif /* (WOLFSSL_MLDSA_PUBLIC_KEY && !WOLFSSL_MLDSA_NO_VERIFY) || * (WOLFSSL_MLDSA_PRIVATE_KEY && !WOLFSSL_MLDSA_NO_SIGN) */ +#endif /* WC_MLDSA_HAVE_NATIVE */ #endif /* WOLFSSL_HAVE_MLDSA - Falcon test below is independent of ML-DSA */ @@ -69687,9 +69689,22 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t falcon_test(void) #if defined(WOLFSSL_HAVE_MLDSA) +#ifndef WC_MLDSA_HAVE_NATIVE +/* Any compiled-in level proves the dispatch behaviour; which one is + * irrelevant, so pick the first that is actually built. */ +#ifndef WOLFSSL_NO_ML_DSA_44 + #define MLDSA_CB_ONLY_LEVEL WC_ML_DSA_44 +#elif !defined(WOLFSSL_NO_ML_DSA_65) + #define MLDSA_CB_ONLY_LEVEL WC_ML_DSA_65 +#elif !defined(WOLFSSL_NO_ML_DSA_87) + #define MLDSA_CB_ONLY_LEVEL WC_ML_DSA_87 +#endif +#endif /* !WC_MLDSA_HAVE_NATIVE */ + WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) { wc_test_ret_t ret; +#ifdef WC_MLDSA_HAVE_NATIVE WC_RNG rng; #ifndef HAVE_FIPS @@ -69779,6 +69794,277 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) #endif wc_FreeRng(&rng); return ret; +#else /* !WC_MLDSA_HAVE_NATIVE */ + /* Software ML-DSA is compiled out. Walk the public API with a key that + * has no device behind it and confirm every entry point refuses rather + * than silently doing nothing: NO_VALID_DEVID where a registered device + * could have serviced the call, NOT_COMPILED_IN where the callback + * protocol cannot express the operation at all. */ + ret = 0; +#ifdef MLDSA_CB_ONLY_LEVEL + { + /* wc_MlDsaKey embeds the key buffers, so keep it off the stack as the + * parameter-set helpers in this file do. */ + WC_DECLARE_VAR(key, wc_MlDsaKey, 1, HEAP_HINT); + int key_inited = 0; + int r; +#if !defined(WOLFSSL_MLDSA_NO_SIGN) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) + byte* priv = NULL; +#endif + + WC_ALLOC_VAR(key, wc_MlDsaKey, 1, HEAP_HINT); + if (!WC_VAR_OK(key)) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + if (ret == 0) { + r = wc_MlDsaKey_Init(key, HEAP_HINT, INVALID_DEVID); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + key_inited = 1; + } + if (ret == 0) { + r = wc_MlDsaKey_SetParams(key, MLDSA_CB_ONLY_LEVEL); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } +#ifndef WOLFSSL_MLDSA_NO_MAKE_KEY + if (ret == 0) { + WC_RNG kgRng; + + r = wc_InitRng_ex(&kgRng, HEAP_HINT, INVALID_DEVID); + if (r != 0) { + ret = WC_TEST_RET_ENC_EC(r); + } + else { + r = wc_MlDsaKey_MakeKey(key, &kgRng); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + wc_FreeRng(&kgRng); + } + } + if (ret == 0) { + byte seed[MLDSA_SEED_SZ]; + + XMEMSET(seed, 0, sizeof(seed)); + /* Expanding a seed is local work and the callback carries no + * seed, so no device could take this one. */ + r = wc_MlDsaKey_MakeKeyFromSeed(key, seed); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } +#endif /* !WOLFSSL_MLDSA_NO_MAKE_KEY */ +#ifndef WOLFSSL_MLDSA_NO_SIGN + if (ret == 0) { + byte sig[4]; + word32 sigLen = (word32)sizeof(sig); + + /* Argument checks still run ahead of the dispatch report: no + * private key is set, so this is refused before the question of + * a device even arises. */ + r = wc_MlDsaKey_SignCtx(key, NULL, 0, sig, &sigLen, + (const byte*)"m", 1, NULL); + if (r != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + ret = WC_TEST_RET_ENC_NC; + } +#ifdef WOLFSSL_MLDSA_PRIVATE_KEY + /* Give the key private material so the refusals below come from the + * dispatch rather than from the argument check above. An all-zero + * private key imports cleanly: every s1 and s2 coefficient of that + * encoding reads as in range. */ + if (ret == 0) { + int privSz = wc_MlDsaKey_Size(key); + + if (privSz <= 0) { + ret = WC_TEST_RET_ENC_NC; + } + else { + priv = (byte*)XMALLOC((size_t)privSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (priv == NULL) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + else + XMEMSET(priv, 0, (size_t)privSz); + } + if (ret == 0) { + r = wc_MlDsaKey_ImportPrivRaw(key, priv, (word32)privSz); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + } + if (ret == 0) { + byte sig[4]; + word32 sigLen = (word32)sizeof(sig); + + r = wc_MlDsaKey_SignCtx(key, NULL, 0, sig, &sigLen, + (const byte*)"m", 1, NULL); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + byte sig[4]; + byte hash[32]; + word32 sigLen = (word32)sizeof(sig); + + XMEMSET(hash, 0, sizeof(hash)); + r = wc_MlDsaKey_SignCtxHash(key, NULL, 0, sig, &sigLen, hash, + (word32)sizeof(hash), WC_HASH_TYPE_SHA256, NULL); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } + /* The deterministic entry points take a caller-supplied seed that + * the callback has no field for, so they report NOT_COMPILED_IN. */ + if (ret == 0) { + byte sig[4]; + byte seed[MLDSA_SEED_SZ]; + word32 sigLen = (word32)sizeof(sig); + + XMEMSET(seed, 0, sizeof(seed)); + r = wc_MlDsaKey_SignCtxWithSeed(key, NULL, 0, sig, &sigLen, + (const byte*)"m", 1, seed); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + byte sig[4]; + byte hash[32]; + byte seed[MLDSA_SEED_SZ]; + word32 sigLen = (word32)sizeof(sig); + + XMEMSET(hash, 0, sizeof(hash)); + XMEMSET(seed, 0, sizeof(seed)); + r = wc_MlDsaKey_SignCtxHashWithSeed(key, NULL, 0, sig, &sigLen, + hash, (word32)sizeof(hash), WC_HASH_TYPE_SHA256, seed); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + byte sig[4]; + byte mu[MLDSA_MU_SZ]; + byte seed[MLDSA_SEED_SZ]; + word32 sigLen = (word32)sizeof(sig); + + XMEMSET(mu, 0, sizeof(mu)); + XMEMSET(seed, 0, sizeof(seed)); + r = wc_MlDsaKey_SignMuWithSeed(key, sig, &sigLen, mu, + (word32)sizeof(mu), seed); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } +#ifdef WOLFSSL_MLDSA_NO_CTX + if (ret == 0) { + byte sig[4]; + word32 sigLen = (word32)sizeof(sig); + + r = wc_MlDsaKey_Sign(key, sig, &sigLen, (const byte*)"m", 1, NULL); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + byte sig[4]; + byte seed[MLDSA_SEED_SZ]; + word32 sigLen = (word32)sizeof(sig); + + XMEMSET(seed, 0, sizeof(seed)); + r = wc_MlDsaKey_SignWithSeed(key, sig, &sigLen, (const byte*)"m", + 1, seed); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } +#endif /* WOLFSSL_MLDSA_NO_CTX */ +#endif /* WOLFSSL_MLDSA_PRIVATE_KEY */ +#endif /* !WOLFSSL_MLDSA_NO_SIGN */ +#ifndef WOLFSSL_MLDSA_NO_VERIFY + if (ret == 0) { + byte sig[4]; + word32 sigLen = (word32)sizeof(sig); + int res = 0; + + r = wc_MlDsaKey_VerifyCtx(key, sig, sigLen, NULL, 0, + (const byte*)"m", 1, &res); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + byte sig[4]; + byte hash[32]; + word32 sigLen = (word32)sizeof(sig); + int res = 0; + + XMEMSET(hash, 0, sizeof(hash)); + r = wc_MlDsaKey_VerifyCtxHash(key, sig, sigLen, NULL, 0, hash, + (word32)sizeof(hash), WC_HASH_TYPE_SHA256, &res); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + byte sig[4]; + byte mu[MLDSA_MU_SZ]; + word32 sigLen = (word32)sizeof(sig); + int res = 0; + + XMEMSET(mu, 0, sizeof(mu)); + /* The callback has no way to say the input is mu rather than a + * message, so no device could take this one either. */ + r = wc_MlDsaKey_VerifyMu(key, sig, sigLen, mu, + (word32)sizeof(mu), &res); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } +#ifdef WOLFSSL_MLDSA_NO_CTX + if (ret == 0) { + byte sig[4]; + word32 sigLen = (word32)sizeof(sig); + int res = 0; + + r = wc_MlDsaKey_Verify(key, sig, sigLen, (const byte*)"m", 1, + &res); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } +#endif /* WOLFSSL_MLDSA_NO_CTX */ +#endif /* !WOLFSSL_MLDSA_NO_VERIFY */ +#ifdef WOLFSSL_MLDSA_CHECK_KEY + if (ret == 0) { + r = wc_MlDsaKey_CheckKey(key); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } +#endif +#if !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \ + !defined(WOLFSSL_NO_ML_DSA_44) + if (ret == 0) { + /* A OneAsymmetricKey holding only the 32 byte seed for + * ML-DSA-44. Decoding it means expanding the seed, which this + * build cannot do and no callback can be asked to do. */ + static const byte seedOnlyKey[] = { + 0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, + 0x03, 0x11, 0x04, 0x22, 0x80, 0x20, 0x67, 0x1b, + 0x9a, 0x54, 0xb4, 0x72, 0x81, 0xbd, 0x1d, 0xb1, + 0x97, 0xab, 0x6d, 0x64, 0x38, 0x0b, 0x0c, 0x1b, + 0x17, 0x9d, 0xcb, 0x46, 0x20, 0xc9, 0x46, 0x3d, + 0xd4, 0x4f, 0x80, 0x87, 0xa2, 0x16 + }; + word32 idx = 0; + + r = wc_MlDsaKey_PrivateKeyDecode(key, seedOnlyKey, + (word32)sizeof(seedOnlyKey), &idx); + if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + ret = WC_TEST_RET_ENC_NC; + } +#endif + if (key_inited) + wc_MlDsaKey_Free(key); +#if !defined(WOLFSSL_MLDSA_NO_SIGN) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) + /* Freed after the key: WOLFSSL_MLDSA_ASSIGN_KEY builds keep the + * pointer instead of copying the bytes. */ + XFREE(priv, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); +#endif + WC_FREE_VAR(key, HEAP_HINT); + } +#endif /* MLDSA_CB_ONLY_LEVEL */ + return ret; +#endif /* WC_MLDSA_HAVE_NATIVE */ } #endif /* WOLFSSL_HAVE_MLDSA */ @@ -82724,7 +83010,8 @@ static wc_test_ret_t pkcs7_signed_no_content_test(byte* cert, word32 certSz, #endif /* !NO_RSA && !NO_SHA256 */ -#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ + !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ !defined(NO_FILESYSTEM) && !defined(NO_ASN) @@ -83296,7 +83583,8 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) rsaCaCertBuf, (word32)rsaCaCertBufSz); #endif -#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) && \ + !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ !defined(NO_FILESYSTEM) && !defined(NO_ASN) if (ret >= 0) @@ -88593,6 +88881,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t blob_test(void) /* Example custom context for crypto callback */ typedef struct { int exampleVar; /* flag for testing if only crypt is enabled. */ +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) + int mldsaCount; /* ML-DSA callback invocations */ + int mldsaFail; /* when set, the ML-DSA handler returns this error */ +#endif #ifdef HAVE_ECC int eccMakePubCount; /* EC make-pub callback invocations */ int eccCheckPubCount; /* EC check-pubkey callback invocations */ @@ -89754,6 +90046,28 @@ static int myCryptoCbExportPointX963(const ecc_set_type* dp, ecc_point* pub, #endif /* HAVE_ECC && !WOLFSSL_NO_MALLOC && HAVE_ECC_KEY_EXPORT */ /* Example crypto dev callback function that calls software version */ +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) +#define MLDSA_CB_SIG_LEN 32 +/* Deterministic stand-in signature for the callback-only ML-DSA device: it + * covers the message, the context and the pre-hash selector, so a dispatch + * that loses any of them fails the matching verify. */ +static void mldsa_cb_sign(const byte* msg, word32 msgLen, const byte* ctx, + byte ctxLen, word32 preHashType, byte* out) +{ + word32 i; + + for (i = 0; i < MLDSA_CB_SIG_LEN; i++) { + byte b = (byte)(0x5a ^ (byte)i ^ (byte)ctxLen ^ (byte)preHashType); + + if (msgLen > 0) + b ^= msg[i % msgLen]; + if ((ctx != NULL) && (ctxLen > 0)) + b ^= ctx[i % ctxLen]; + out[i] = b; + } +} +#endif + static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) { int ret = WC_NO_ERR_TRACE(NOT_COMPILED_IN); /* return this to bypass HW and @@ -90927,7 +91241,7 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) myCtx->exampleVar++; } #endif /* HAVE_FALCON && !WOLF_CRYPTO_CB_ONLY_FALCON */ - #ifdef WOLFSSL_HAVE_MLDSA + #if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY if (info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) { if ((info->pk.pqc_sig_kg.type == WC_PQC_SIG_TYPE_MLDSA) && @@ -91028,7 +91342,77 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) } } #endif - #endif /* WOLFSSL_HAVE_MLDSA */ + #endif /* WOLFSSL_HAVE_MLDSA && WC_MLDSA_HAVE_NATIVE */ + #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) + /* The software core is stripped, so this device cannot delegate to the + * public API the way the other handlers do; it would dispatch straight + * back here. It answers with its own deterministic signature instead, + * which is enough to prove the dispatch reaches a device for all four + * ML-DSA operations, that the signature and the verify result travel + * back to the caller, and that a device error is reported as-is. The + * signature covers the message and the context, so a call site that + * drops either is caught by the verify step. */ + if ((info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) && + (info->pk.pqc_sig_kg.type == WC_PQC_SIG_TYPE_MLDSA)) { + wc_MlDsaKey* dk = (wc_MlDsaKey*)info->pk.pqc_sig_kg.key; + + myCtx->mldsaCount++; + if (myCtx->mldsaFail != 0) { + ret = myCtx->mldsaFail; + } + else { + /* The device holds the key material; the caller's object only + * records that it now has one. */ + dk->pubKeySet = 1; + dk->prvKeySet = 1; + ret = 0; + } + } + else if ((info->pk.type == WC_PK_TYPE_PQC_SIG_SIGN) && + (info->pk.pqc_sign.type == WC_PQC_SIG_TYPE_MLDSA)) { + myCtx->mldsaCount++; + if (myCtx->mldsaFail != 0) { + ret = myCtx->mldsaFail; + } + else if (*info->pk.pqc_sign.outlen < MLDSA_CB_SIG_LEN) { + ret = BUFFER_E; + } + else { + mldsa_cb_sign(info->pk.pqc_sign.in, info->pk.pqc_sign.inlen, + info->pk.pqc_sign.context, info->pk.pqc_sign.contextLen, + info->pk.pqc_sign.preHashType, info->pk.pqc_sign.out); + *info->pk.pqc_sign.outlen = MLDSA_CB_SIG_LEN; + ret = 0; + } + } + else if ((info->pk.type == WC_PK_TYPE_PQC_SIG_VERIFY) && + (info->pk.pqc_verify.type == WC_PQC_SIG_TYPE_MLDSA)) { + myCtx->mldsaCount++; + if (myCtx->mldsaFail != 0) { + ret = myCtx->mldsaFail; + } + else { + byte expected[MLDSA_CB_SIG_LEN]; + + mldsa_cb_sign(info->pk.pqc_verify.msg, + info->pk.pqc_verify.msglen, info->pk.pqc_verify.context, + info->pk.pqc_verify.contextLen, + info->pk.pqc_verify.preHashType, expected); + if (info->pk.pqc_verify.res != NULL) { + *info->pk.pqc_verify.res = + ((info->pk.pqc_verify.siglen == MLDSA_CB_SIG_LEN) && + (XMEMCMP(info->pk.pqc_verify.sig, expected, + MLDSA_CB_SIG_LEN) == 0)) ? 1 : 0; + } + ret = 0; + } + } + else if ((info->pk.type == WC_PK_TYPE_PQC_SIG_CHECK_PRIV_KEY) && + (info->pk.pqc_sig_check.type == WC_PQC_SIG_TYPE_MLDSA)) { + myCtx->mldsaCount++; + ret = (myCtx->mldsaFail != 0) ? myCtx->mldsaFail : 0; + } + #endif /* WOLFSSL_HAVE_MLDSA && !WC_MLDSA_HAVE_NATIVE */ #ifdef WOLFSSL_HAVE_MLKEM #ifndef WOLFSSL_MLKEM_NO_MAKE_KEY if (info->pk.type == WC_PK_TYPE_PQC_KEM_KEYGEN) { @@ -93957,6 +94341,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) /* example data for callback */ myCtx.exampleVar = 1; +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) + myCtx.mldsaCount = 0; + myCtx.mldsaFail = 0; +#endif #ifdef HAVE_ECC myCtx.eccMakePubCount = 0; myCtx.eccCheckPubCount = 0; @@ -94639,6 +95027,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) ret = mldsa_test(); + #ifdef WC_MLDSA_HAVE_NATIVE #if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && !defined(HAVE_FIPS) if ((ret == 0) && (myCtx.mldsaKeyGenCount == 0)) ret = WC_TEST_RET_ENC_NC; @@ -94666,7 +95055,118 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if ((ret == 0) && (myCtx.mldsaVerifyHashCount == 0)) ret = WC_TEST_RET_ENC_NC; #endif + #endif /* WC_MLDSA_HAVE_NATIVE */ + } +#if !defined(WC_MLDSA_HAVE_NATIVE) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + !defined(WC_NO_RNG) && defined(MLDSA_CB_ONLY_LEVEL) + /* With the software core stripped, an ML-DSA operation can only succeed + * through a registered device. Drive key generation, signing, verifying + * and the private-key check that way and confirm the results came back, + * so a dispatch regression cannot hide behind the NO_VALID_DEVID checks + * in mldsa_test(). */ + if (ret == 0) { + WC_DECLARE_VAR(key, wc_MlDsaKey, 1, HEAP_HINT); + WC_DECLARE_VAR(mldsaRng, WC_RNG, 1, HEAP_HINT); + byte sig[MLDSA_CB_SIG_LEN]; + word32 sigLen = (word32)sizeof(sig); + static const byte msg[] = "wolfSSL ML-DSA callback-only dispatch"; + static const byte sigCtx[] = { 0x01, 0x02, 0x03 }; + int key_inited = 0; + int rng_inited = 0; + int baseline = myCtx.mldsaCount; + int res = 0; + int r; + + WC_ALLOC_VAR(key, wc_MlDsaKey, 1, HEAP_HINT); + WC_ALLOC_VAR(mldsaRng, WC_RNG, 1, HEAP_HINT); + if ((!WC_VAR_OK(key)) || (!WC_VAR_OK(mldsaRng))) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + if (ret == 0) { + /* The device ignores the RNG; keep it off the callback path. */ + r = wc_InitRng_ex(mldsaRng, HEAP_HINT, INVALID_DEVID); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + rng_inited = 1; + } + if (ret == 0) { + r = wc_MlDsaKey_Init(key, HEAP_HINT, devId); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + key_inited = 1; + } + if (ret == 0) { + r = wc_MlDsaKey_SetParams(key, MLDSA_CB_ONLY_LEVEL); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + r = wc_MlDsaKey_MakeKey(key, mldsaRng); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + r = wc_MlDsaKey_SignCtx(key, sigCtx, (byte)sizeof(sigCtx), sig, + &sigLen, msg, (word32)sizeof(msg), mldsaRng); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else if (sigLen != MLDSA_CB_SIG_LEN) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + r = wc_MlDsaKey_VerifyCtx(key, sig, sigLen, sigCtx, + (byte)sizeof(sigCtx), msg, (word32)sizeof(msg), &res); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else if (res != 1) + ret = WC_TEST_RET_ENC_NC; + } + /* A different context must not verify: proves the context reached the + * device rather than being dropped on the way. */ + if (ret == 0) { + static const byte otherCtx[] = { 0x09, 0x09, 0x09 }; + + res = 1; + r = wc_MlDsaKey_VerifyCtx(key, sig, sigLen, otherCtx, + (byte)sizeof(otherCtx), msg, (word32)sizeof(msg), &res); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else if (res != 0) + ret = WC_TEST_RET_ENC_NC; + } +#ifdef WOLFSSL_MLDSA_CHECK_KEY + /* This is the operation a no-device test cannot cover: before the key + * check dispatched, it could only ever fail. */ + if (ret == 0) { + r = wc_MlDsaKey_CheckKey(key); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } +#endif + /* Every operation above went through the callback. */ + if ((ret == 0) && (myCtx.mldsaCount <= baseline)) + ret = WC_TEST_RET_ENC_NC; + /* A device error must reach the caller unchanged. */ + if (ret == 0) { + myCtx.mldsaFail = WC_NO_ERR_TRACE(WC_HW_E); + sigLen = (word32)sizeof(sig); + r = wc_MlDsaKey_SignCtx(key, sigCtx, (byte)sizeof(sigCtx), sig, + &sigLen, msg, (word32)sizeof(msg), mldsaRng); + myCtx.mldsaFail = 0; + if (r != WC_NO_ERR_TRACE(WC_HW_E)) + ret = WC_TEST_RET_ENC_NC; + } + if (key_inited) + wc_MlDsaKey_Free(key); + if (rng_inited) + wc_FreeRng(mldsaRng); + WC_FREE_VAR(mldsaRng, HEAP_HINT); + WC_FREE_VAR(key, HEAP_HINT); } +#endif /* !WC_MLDSA_HAVE_NATIVE && sign && verify && !WC_NO_RNG */ #endif #ifdef WOLFSSL_HAVE_SLHDSA if (ret == 0) { diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h index 85f98891574..1a88d9c1969 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -6047,6 +6047,17 @@ blinding by defining WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS." #error "WOLF_CRYPTO_CB_ONLY_ED25519 with " \ "WOLFSSL_ED25519_STREAMING_VERIFY is not supported" #endif +#if defined(WOLF_CRYPTO_CB_ONLY_MLDSA) && !defined(WOLF_CRYPTO_CB) + #error "WOLF_CRYPTO_CB_ONLY_MLDSA requires WOLF_CRYPTO_CB" +#endif +#if defined(WOLF_CRYPTO_CB_ONLY_MLDSA) && !defined(WOLFSSL_HAVE_MLDSA) + #error "WOLF_CRYPTO_CB_ONLY_MLDSA requires WOLFSSL_HAVE_MLDSA" +#endif +#if defined(WOLF_CRYPTO_CB_ONLY_MLDSA) && defined(HAVE_FIPS) + /* Key generation runs a pairwise consistency test that the callback would + * have to service; not validated. */ + #error "WOLF_CRYPTO_CB_ONLY_MLDSA is incompatible with FIPS builds" +#endif #if defined(WOLF_CRYPTO_CB_ONLY_CURVE25519) && !defined(WOLF_CRYPTO_CB) #error "WOLF_CRYPTO_CB_ONLY_CURVE25519 requires WOLF_CRYPTO_CB" #endif diff --git a/wolfssl/wolfcrypt/wc_mldsa.h b/wolfssl/wolfcrypt/wc_mldsa.h index 34ba33efc26..a50b5bee851 100644 --- a/wolfssl/wolfcrypt/wc_mldsa.h +++ b/wolfssl/wolfcrypt/wc_mldsa.h @@ -1114,5 +1114,16 @@ WOLFSSL_TEST_VIS void wc_mldsa_encode_w1_32(const sword32* w1, byte* w1e); } /* extern "C" */ #endif +/* Native implementation core (internal). The public wc_MlDsaKey_* functions + * in wc_mldsa.c wrap it with cryptocb dispatch and argument checking. With + * WOLF_CRYPTO_CB_ONLY_MLDSA the native core is not compiled: all operations go + * through the crypto callback. */ +#ifndef WOLF_CRYPTO_CB_ONLY_MLDSA +/* Signals that native key generation, signing and verifying are available. + * Tests gate on this rather than on the build switch, so a test says what it + * needs rather than which configuration removed it. */ +#define WC_MLDSA_HAVE_NATIVE +#endif + #endif /* WOLFSSL_HAVE_MLDSA */ #endif /* WOLF_CRYPT_WC_MLDSA_H */ From 4f7567b573e158be37f2b182001525be4bfb62ff Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Thu, 27 Aug 2026 10:50:00 -0700 Subject: [PATCH 2/8] PR feedback: rework pubkey detection, pass seed to device (as done in SLHDSA PR) --- wolfcrypt/src/wc_mldsa.c | 99 ++++++++++++++++++++++-------- wolfcrypt/test/test.c | 113 +++++++++++++++++++++++++++++++---- wolfssl/wolfcrypt/wc_mldsa.h | 2 + 3 files changed, 175 insertions(+), 39 deletions(-) diff --git a/wolfcrypt/src/wc_mldsa.c b/wolfcrypt/src/wc_mldsa.c index ab7152cec9d..0b978ac7311 100644 --- a/wolfcrypt/src/wc_mldsa.c +++ b/wolfcrypt/src/wc_mldsa.c @@ -9222,6 +9222,11 @@ static int mldsa_pct(wc_MlDsaKey* key) * @return Other negative when an error occurs. */ +/* Forward declaration: the software path below calls this directly so it + * does not re-enter the crypto callback. */ +static int mldsa_key_from_seed_checked(wc_MlDsaKey* key, const byte* seed, + int runPct); + static int mldsa_make_key(wc_MlDsaKey* key, WC_RNG* rng) { int ret; @@ -9236,8 +9241,10 @@ static int mldsa_make_key(wc_MlDsaKey* key, WC_RNG* rng) #endif /* Step 2: Check for error. */ if (ret == 0) { - /* Step 5: Make key with random seed. */ - ret = wc_MlDsaKey_MakeKeyFromSeed(key, seed); + /* Step 5: Make key with random seed. Straight to the helper: + * wc_MlDsaKey_MakeKeyFromSeed() would offer the seed to the + * device that already declined this generation. */ + ret = mldsa_key_from_seed_checked(key, seed, 1); } ForceZero(seed, sizeof(seed)); @@ -11408,7 +11415,7 @@ int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng) } #endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ - /* No key-pair test here: wc_MlDsaKey_MakeKeyFromSeed(), reached from + /* No key-pair test here: mldsa_key_from_seed_checked(), reached from * mldsa_make_key() above, already runs it on every generation path. * Guarded on the version, not HAVE_FIPS: src/include.am only compiles * this file under BUILD_FIPS_V7_PLUS, so the two are equivalent here. */ @@ -11460,22 +11467,56 @@ static int mldsa_key_from_seed_checked(wc_MlDsaKey* key, const byte* seed, } #endif /* !WOLF_CRYPTO_CB_ONLY_MLDSA */ -int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed) +/* Expand a seed into a key pair, offering it to a device first. + * + * A device that generates ML-DSA keys already expands a seed of its own + * choosing, so it can take this one. runPct is 0 for the ASN.1 decode path. + * The software key generation path calls mldsa_key_from_seed_checked() + * directly rather than coming back through here. + */ +static int mldsa_key_from_seed_dev(wc_MlDsaKey* key, const byte* seed, + int runPct) { -#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA - /* Validate as the software path does, so the reported error stays the - * same for a bad call. */ + int ret = 0; + + /* Validate parameters. */ if ((key == NULL) || (seed == NULL)) { - return BAD_FUNC_ARG; + ret = BAD_FUNC_ARG; + } + +#ifdef WOLF_CRYPTO_CB + if (ret == 0) { + #ifndef WOLF_CRYPTO_CB_FIND + if (key->devId != INVALID_DEVID) + #endif + { + ret = wc_CryptoCb_MakePqcSignatureKeyEx(NULL, + WC_PQC_SIG_TYPE_MLDSA, key->level, seed, MLDSA_SEED_SZ, key); + if (ret != WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE)) + return ret; + /* fall-through when unavailable */ + ret = 0; + } } - /* Not NO_VALID_DEVID: no device can service this one. Expanding a seed - * is a local computation and the callback protocol has no seed to hand - * over, so registering a device would not help. The seed expansion is - * part of the software core this build removes. */ - return NOT_COMPILED_IN; +#endif + +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + if (ret == 0) { + ret = NO_VALID_DEVID; + } + (void)runPct; #else - return mldsa_key_from_seed_checked(key, seed, 1); + if (ret == 0) { + ret = mldsa_key_from_seed_checked(key, seed, runPct); + } #endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ + + return ret; +} + +int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed) +{ + return mldsa_key_from_seed_dev(key, seed, 1); } #endif @@ -12801,17 +12842,21 @@ int wc_MlDsaKey_CheckKey(wc_MlDsaKey* key) { const byte* pub = NULL; word32 pubSz = 0; - /* Read through a pointer: key->p is an array in the default - * layout and a pointer in the dynamic and assign-key ones. */ - const byte* keyPub = key->p; - - /* pubKeySet only says a public key exists, not that this object - * holds its bytes: a device-generated key has none locally. Send - * a length only alongside a pointer to go with it. */ - if (key->pubKeySet && (keyPub != NULL)) { + + /* pubKeySet means this object holds the public key bytes. A key + * the device generated has none here, so nothing is sent and the + * device works from its own copy. */ + if (key->pubKeySet) { int sz = wc_MlDsaKey_PubSize(key); - if (sz > 0) { - pub = keyPub; + int have = (sz > 0); + + #if defined(WOLFSSL_MLDSA_DYNAMIC_KEYS) || \ + defined(WOLFSSL_MLDSA_ASSIGN_KEY) + /* p is a pointer in these layouts and may be unset. */ + have = have && (key->p != NULL); + #endif + if (have) { + pub = key->p; pubSz = (word32)sz; } } @@ -13794,10 +13839,12 @@ int wc_MlDsaKey_PrivateKeyDecode(wc_MlDsaKey* key, const byte* input, if (ret == 0) { /* Generate a key pair if seed exists and decoded key pair is ignored */ if (seedLen != 0) { -#if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && !defined(WOLF_CRYPTO_CB_ONLY_MLDSA) +#ifndef WOLFSSL_MLDSA_NO_MAKE_KEY + /* The seed is the source of truth and any expanded key alongside + * it is discarded, so both build modes derive the same key. */ if (seedLen == MLDSA_SEED_SZ) { /* runPct 0: this is an import, not a generation. */ - ret = mldsa_key_from_seed_checked(key, seed, 0); + ret = mldsa_key_from_seed_dev(key, seed, 0); } else { ret = ASN_PARSE_E; diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 648a5dedb11..353de4129da 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -69846,10 +69846,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) byte seed[MLDSA_SEED_SZ]; XMEMSET(seed, 0, sizeof(seed)); - /* Expanding a seed is local work and the callback carries no - * seed, so no device could take this one. */ + /* The seed goes to a device now, so with none registered this + * reports a missing device rather than missing code. */ r = wc_MlDsaKey_MakeKeyFromSeed(key, seed); - if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) ret = WC_TEST_RET_ENC_NC; } #endif /* !WOLFSSL_MLDSA_NO_MAKE_KEY */ @@ -70034,8 +70034,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) !defined(WOLFSSL_NO_ML_DSA_44) if (ret == 0) { /* A OneAsymmetricKey holding only the 32 byte seed for - * ML-DSA-44. Decoding it means expanding the seed, which this - * build cannot do and no callback can be asked to do. */ + * ML-DSA-44. Decoding it means expanding the seed: a device is + * asked to do that, so with none registered the report is a + * missing device -- unless key generation is compiled out, when + * there is no seed path at all. */ static const byte seedOnlyKey[] = { 0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, @@ -70049,7 +70051,11 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) r = wc_MlDsaKey_PrivateKeyDecode(key, seedOnlyKey, (word32)sizeof(seedOnlyKey), &idx); +#ifndef WOLFSSL_MLDSA_NO_MAKE_KEY + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) +#else if (r != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) +#endif ret = WC_TEST_RET_ENC_NC; } #endif @@ -90048,6 +90054,15 @@ static int myCryptoCbExportPointX963(const ecc_set_type* dp, ecc_point* pub, /* Example crypto dev callback function that calls software version */ #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) #define MLDSA_CB_SIG_LEN 32 + +/* Seed the device is asked to generate from, shared with the test below so + * the handler can confirm it arrived unchanged. */ +static const byte mldsa_cb_seed[MLDSA_SEED_SZ] = { + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, + 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf +}; /* Deterministic stand-in signature for the callback-only ML-DSA device: it * covers the message, the context and the pre-hash selector, so a dispatch * that loses any of them fails the matching verify. */ @@ -91354,17 +91369,24 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) * drops either is caught by the verify step. */ if ((info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) && (info->pk.pqc_sig_kg.type == WC_PQC_SIG_TYPE_MLDSA)) { - wc_MlDsaKey* dk = (wc_MlDsaKey*)info->pk.pqc_sig_kg.key; - myCtx->mldsaCount++; if (myCtx->mldsaFail != 0) { ret = myCtx->mldsaFail; } + else if (info->pk.pqc_sig_kg.seed != NULL) { + /* Seeded generation: the seed must arrive whole. A real + * device would expand it; this one only checks it. */ + ret = ((info->pk.pqc_sig_kg.seedSz == + (word32)sizeof(mldsa_cb_seed)) && + (XMEMCMP(info->pk.pqc_sig_kg.seed, mldsa_cb_seed, + sizeof(mldsa_cb_seed)) == 0)) ? + 0 : WC_NO_ERR_TRACE(BAD_STATE_E); + } else { - /* The device holds the key material; the caller's object only - * records that it now has one. */ - dk->pubKeySet = 1; - dk->prvKeySet = 1; + /* The device keeps the key material. The key flags say + * whether this object holds the bytes, and it holds neither, + * so both stay clear. Signing and verifying dispatch before + * those flags are looked at. */ ret = 0; } } @@ -91409,8 +91431,32 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) } else if ((info->pk.type == WC_PK_TYPE_PQC_SIG_CHECK_PRIV_KEY) && (info->pk.pqc_sig_check.type == WC_PQC_SIG_TYPE_MLDSA)) { + wc_MlDsaKey* ck = (wc_MlDsaKey*)info->pk.pqc_sig_check.key; + myCtx->mldsaCount++; - ret = (myCtx->mldsaFail != 0) ? myCtx->mldsaFail : 0; + if (myCtx->mldsaFail != 0) { + ret = myCtx->mldsaFail; + } + else if (ck == NULL) { + ret = BAD_FUNC_ARG; + } + else if (!ck->pubKeySet) { + /* No local public key: a real device compares against its own + * copy, so anything sent here would be material the caller + * never had. */ + ret = ((info->pk.pqc_sig_check.pubKey == NULL) && + (info->pk.pqc_sig_check.pubKeySz == 0)) ? + 0 : WC_NO_ERR_TRACE(BAD_STATE_E); + } + else { + /* Public key present: it must arrive whole. */ + int ckSz = wc_MlDsaKey_PubSize(ck); + + ret = ((ckSz > 0) && + (info->pk.pqc_sig_check.pubKey != NULL) && + (info->pk.pqc_sig_check.pubKeySz == (word32)ckSz)) ? + 0 : WC_NO_ERR_TRACE(BAD_STATE_E); + } } #endif /* WOLFSSL_HAVE_MLDSA && !WC_MLDSA_HAVE_NATIVE */ #ifdef WOLFSSL_HAVE_MLKEM @@ -95108,6 +95154,15 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if (r != 0) ret = WC_TEST_RET_ENC_EC(r); } +#ifndef WOLFSSL_MLDSA_NO_MAKE_KEY + /* Generating from a caller supplied seed is the device's job too: + * the handler fails the call if the seed does not arrive whole. */ + if (ret == 0) { + r = wc_MlDsaKey_MakeKeyFromSeed(key, mldsa_cb_seed); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } +#endif if (ret == 0) { r = wc_MlDsaKey_SignCtx(key, sigCtx, (byte)sizeof(sigCtx), sig, &sigLen, msg, (word32)sizeof(msg), mldsaRng); @@ -95139,12 +95194,44 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) } #ifdef WOLFSSL_MLDSA_CHECK_KEY /* This is the operation a no-device test cannot cover: before the key - * check dispatched, it could only ever fail. */ + * check dispatched, it could only ever fail. The handler asserts that + * a key generated on the device sends no public key with it. */ if (ret == 0) { r = wc_MlDsaKey_CheckKey(key); if (r != 0) ret = WC_TEST_RET_ENC_EC(r); } +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + /* Import a public key and check again: now the bytes are local, so + * the handler asserts they arrive whole. */ + if (ret == 0) { + int pubSz = wc_MlDsaKey_PubSize(key); + byte* pubRaw = NULL; + + if (pubSz <= 0) { + ret = WC_TEST_RET_ENC_NC; + } + else { + pubRaw = (byte*)XMALLOC((word32)pubSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if (pubRaw == NULL) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + else + XMEMSET(pubRaw, 0x5a, (word32)pubSz); + } + if (ret == 0) { + r = wc_MlDsaKey_ImportPubRaw(key, pubRaw, (word32)pubSz); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + r = wc_MlDsaKey_CheckKey(key); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + XFREE(pubRaw, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ #endif /* Every operation above went through the callback. */ if ((ret == 0) && (myCtx.mldsaCount <= baseline)) diff --git a/wolfssl/wolfcrypt/wc_mldsa.h b/wolfssl/wolfcrypt/wc_mldsa.h index a50b5bee851..e93c40f16be 100644 --- a/wolfssl/wolfcrypt/wc_mldsa.h +++ b/wolfssl/wolfcrypt/wc_mldsa.h @@ -592,6 +592,8 @@ typedef struct wc_MlDsaParams MlDsaParams; #endif struct wc_MlDsaKey { + /* Set when this object holds the public key bytes in p. A key generated + * on a crypto-callback device has none here and leaves it clear. */ byte pubKeySet; byte prvKeySet; byte level; /* 2,3 or 5 */ From 0fc43f9b22b3af7456e214f0def765516836d5f7 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Wed, 2 Sep 2026 20:37:59 -0700 Subject: [PATCH 3/8] Update configure.ac for ml-dsa --- configure.ac | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configure.ac b/configure.ac index ffb92d5e842..ec519e9b43b 100644 --- a/configure.ac +++ b/configure.ac @@ -12118,6 +12118,9 @@ then if test "$ENABLED_SLHDSA" != "no"; then AM_CFLAGS="$AM_CFLAGS -DWOLF_CRYPTO_CB_ONLY_SLHDSA" fi + if test "$ENABLED_MLDSA" != "no"; then + AM_CFLAGS="$AM_CFLAGS -DWOLF_CRYPTO_CB_ONLY_MLDSA" + fi fi if test "$ENABLED_CRYPTOCB_SW_TEST" = "no" From 968e29918abb43797fef23988780af5cd8f4ba91 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Tue, 8 Sep 2026 15:17:42 -0700 Subject: [PATCH 4/8] PR feedback: seed dispatch, precompiler guards --- wolfcrypt/test/test.c | 74 +++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 353de4129da..d9f15b11c13 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -69689,7 +69689,6 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t falcon_test(void) #if defined(WOLFSSL_HAVE_MLDSA) -#ifndef WC_MLDSA_HAVE_NATIVE /* Any compiled-in level proves the dispatch behaviour; which one is * irrelevant, so pick the first that is actually built. */ #ifndef WOLFSSL_NO_ML_DSA_44 @@ -69699,7 +69698,6 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t falcon_test(void) #elif !defined(WOLFSSL_NO_ML_DSA_87) #define MLDSA_CB_ONLY_LEVEL WC_ML_DSA_87 #endif -#endif /* !WC_MLDSA_HAVE_NATIVE */ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) { @@ -88887,9 +88885,11 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t blob_test(void) /* Example custom context for crypto callback */ typedef struct { int exampleVar; /* flag for testing if only crypt is enabled. */ -#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) - int mldsaCount; /* ML-DSA callback invocations */ - int mldsaFail; /* when set, the ML-DSA handler returns this error */ +#if defined(WOLFSSL_HAVE_MLDSA) + int mldsaCount; /* ML-DSA callback invocations */ + int mldsaFail; /* when set, the ML-DSA handler returns this error */ + int mldsaCbActive; /* when clear, the handler declines so a native build + * still runs the real ML-DSA test */ #endif #ifdef HAVE_ECC int eccMakePubCount; /* EC make-pub callback invocations */ @@ -90052,7 +90052,7 @@ static int myCryptoCbExportPointX963(const ecc_set_type* dp, ecc_point* pub, #endif /* HAVE_ECC && !WOLFSSL_NO_MALLOC && HAVE_ECC_KEY_EXPORT */ /* Example crypto dev callback function that calls software version */ -#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) +#if defined(WOLFSSL_HAVE_MLDSA) #define MLDSA_CB_SIG_LEN 32 /* Seed the device is asked to generate from, shared with the test below so @@ -91258,7 +91258,8 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) #endif /* HAVE_FALCON && !WOLF_CRYPTO_CB_ONLY_FALCON */ #if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY - if (info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) { + if ((info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) && + !myCtx->mldsaCbActive) { if ((info->pk.pqc_sig_kg.type == WC_PQC_SIG_TYPE_MLDSA) && (info->pk.pqc_sig_kg.key != NULL)) { wc_MlDsaKey* key = (wc_MlDsaKey*)info->pk.pqc_sig_kg.key; @@ -91280,7 +91281,8 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) #if !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_CTX) /* WOLFSSL_MLDSA_NO_CTX makes Sign() and SignCtx() with an empty * context indistinguishable here, so leave both to software. */ - if (info->pk.type == WC_PK_TYPE_PQC_SIG_SIGN) { + if ((info->pk.type == WC_PK_TYPE_PQC_SIG_SIGN) && + !myCtx->mldsaCbActive) { if ((info->pk.pqc_sign.type == WC_PQC_SIG_TYPE_MLDSA) && (info->pk.pqc_sign.key != NULL)) { wc_MlDsaKey* key = (wc_MlDsaKey*)info->pk.pqc_sign.key; @@ -91319,7 +91321,8 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) #endif #if !defined(WOLFSSL_MLDSA_NO_VERIFY) && !defined(WOLFSSL_MLDSA_NO_CTX) /* Omitted under WOLFSSL_MLDSA_NO_CTX; see the sign branch. */ - if (info->pk.type == WC_PK_TYPE_PQC_SIG_VERIFY) { + if ((info->pk.type == WC_PK_TYPE_PQC_SIG_VERIFY) && + !myCtx->mldsaCbActive) { if ((info->pk.pqc_verify.type == WC_PQC_SIG_TYPE_MLDSA) && (info->pk.pqc_verify.key != NULL)) { wc_MlDsaKey* key = (wc_MlDsaKey*)info->pk.pqc_verify.key; @@ -91358,22 +91361,26 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) } #endif #endif /* WOLFSSL_HAVE_MLDSA && WC_MLDSA_HAVE_NATIVE */ - #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) - /* The software core is stripped, so this device cannot delegate to the - * public API the way the other handlers do; it would dispatch straight - * back here. It answers with its own deterministic signature instead, - * which is enough to prove the dispatch reaches a device for all four - * ML-DSA operations, that the signature and the verify result travel - * back to the caller, and that a device error is reported as-is. The - * signature covers the message and the context, so a call site that - * drops either is caught by the verify step. */ - if ((info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) && + #if defined(WOLFSSL_HAVE_MLDSA) + /* This device cannot delegate to the public API the way the other + * handlers do; it would dispatch straight back here, and under + * CB_ONLY there is no software core to reach. It answers with its + * own deterministic signature instead, which proves the dispatch + * reaches a device for all four ML-DSA operations, that the signature + * and the verify result travel back, and that a device error is + * reported as-is. The signature covers the message and the context, + * so a call site that drops either is caught by the verify step. */ + if (!myCtx->mldsaCbActive) { + /* Leave ret unchanged */ + } + else if (((info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) || + (info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN_SEED)) && (info->pk.pqc_sig_kg.type == WC_PQC_SIG_TYPE_MLDSA)) { myCtx->mldsaCount++; if (myCtx->mldsaFail != 0) { ret = myCtx->mldsaFail; } - else if (info->pk.pqc_sig_kg.seed != NULL) { + else if (info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN_SEED) { /* Seeded generation: the seed must arrive whole. A real * device would expand it; this one only checks it. */ ret = ((info->pk.pqc_sig_kg.seedSz == @@ -91387,7 +91394,10 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) * whether this object holds the bytes, and it holds neither, * so both stay clear. Signing and verifying dispatch before * those flags are looked at. */ - ret = 0; + /* A plain keygen carrying a seed means a seeded call reached + * the device as a request for an unrelated random key. */ + ret = (info->pk.pqc_sig_kg.seed == NULL) ? 0 : + WC_NO_ERR_TRACE(BAD_STATE_E); } } else if ((info->pk.type == WC_PK_TYPE_PQC_SIG_SIGN) && @@ -91429,6 +91439,7 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) ret = 0; } } + #ifdef WOLFSSL_MLDSA_CHECK_KEY else if ((info->pk.type == WC_PK_TYPE_PQC_SIG_CHECK_PRIV_KEY) && (info->pk.pqc_sig_check.type == WC_PQC_SIG_TYPE_MLDSA)) { wc_MlDsaKey* ck = (wc_MlDsaKey*)info->pk.pqc_sig_check.key; @@ -91458,7 +91469,8 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) 0 : WC_NO_ERR_TRACE(BAD_STATE_E); } } - #endif /* WOLFSSL_HAVE_MLDSA && !WC_MLDSA_HAVE_NATIVE */ + #endif /* WOLFSSL_MLDSA_CHECK_KEY */ + #endif /* WOLFSSL_HAVE_MLDSA */ #ifdef WOLFSSL_HAVE_MLKEM #ifndef WOLFSSL_MLKEM_NO_MAKE_KEY if (info->pk.type == WC_PK_TYPE_PQC_KEM_KEYGEN) { @@ -94387,9 +94399,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) /* example data for callback */ myCtx.exampleVar = 1; -#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WC_MLDSA_HAVE_NATIVE) +#if defined(WOLFSSL_HAVE_MLDSA) myCtx.mldsaCount = 0; myCtx.mldsaFail = 0; + myCtx.mldsaCbActive = 0; #endif #ifdef HAVE_ECC myCtx.eccMakePubCount = 0; @@ -95103,15 +95116,15 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) #endif #endif /* WC_MLDSA_HAVE_NATIVE */ } -#if !defined(WC_MLDSA_HAVE_NATIVE) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ +#if !defined(WOLFSSL_MLDSA_NO_SIGN) && \ !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ !defined(WC_NO_RNG) && defined(MLDSA_CB_ONLY_LEVEL) - /* With the software core stripped, an ML-DSA operation can only succeed - * through a registered device. Drive key generation, signing, verifying - * and the private-key check that way and confirm the results came back, - * so a dispatch regression cannot hide behind the NO_VALID_DEVID checks - * in mldsa_test(). */ + /* Drive key generation, seeded generation, signing, verifying and the + * private-key check through a registered device and confirm the results + * came back. Runs in native builds too, so the seeded dispatch is covered + * where a software path exists to hide a regression. */ + myCtx.mldsaCbActive = 1; if (ret == 0) { WC_DECLARE_VAR(key, wc_MlDsaKey, 1, HEAP_HINT); WC_DECLARE_VAR(mldsaRng, WC_RNG, 1, HEAP_HINT); @@ -95253,7 +95266,8 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) WC_FREE_VAR(mldsaRng, HEAP_HINT); WC_FREE_VAR(key, HEAP_HINT); } -#endif /* !WC_MLDSA_HAVE_NATIVE && sign && verify && !WC_NO_RNG */ + myCtx.mldsaCbActive = 0; +#endif /* sign && verify && make key && !WC_NO_RNG */ #endif #ifdef WOLFSSL_HAVE_SLHDSA if (ret == 0) { From 7bcc20d79dce84bfc20028229797c5f2d9ab9f98 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Wed, 16 Sep 2026 23:06:20 -0700 Subject: [PATCH 5/8] Address CI failure and note that swdev.c needs updating as a future improvement --- .github/workflows/cryptocb-only.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cryptocb-only.yml b/.github/workflows/cryptocb-only.yml index 98df0a3bf5f..2b3699e7ec7 100644 --- a/.github/workflows/cryptocb-only.yml +++ b/.github/workflows/cryptocb-only.yml @@ -163,9 +163,12 @@ jobs: {"name": "shake-xof", "minutes": 4.0, "comment": "WOLF_CRYPTO_CB_SHAKE_XOF: swdev handles SHAKE absorb and squeeze. No ONLY_* strip exists for SHAKE, so software SHAKE stays in. ML-KEM and ML-DSA reach swdev_shake through WOLF_CRYPTO_CB_FIND, and cryptocb_test runs shake_cb_xof_test.", "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_SHAKE_XOF"]}, - {"name": "all", "minutes": 20, - "comment": "All ten ONLY_* macros at once: every supported software primitive is stripped and dispatched through cryptocb. Catches any cross-algorithm call that a single-strip entry would still resolve via the remaining software paths.", - "configure": ["--enable-slhdsa=yes,sha2", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519 -DWOLF_CRYPTO_CB_ONLY_CURVE448 -DWOLF_CRYPTO_CB_ONLY_SLHDSA -DWOLF_CRYPTO_CB_ONLY_MLDSA"]}, + {"name": "all", "minutes": 19, + "comment": "All nine ONLY_* macros that can share a build: every supported software primitive is stripped and dispatched through cryptocb. Catches any cross-algorithm call that a single-strip entry would still resolve via the remaining software paths. MLDSA is not here because this entry also enables SLH-DSA, and the TLS 1.3 suite then runs an SLH-DSA root with an ML-DSA-44 entity certificate: with the ML-DSA software path gone that handshake needs a registered device, which swdev does not provide yet. The all-mldsa entry below covers MLDSA alongside the rest.", + "configure": ["--enable-slhdsa=yes,sha2", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519 -DWOLF_CRYPTO_CB_ONLY_CURVE448 -DWOLF_CRYPTO_CB_ONLY_SLHDSA"]}, + {"name": "all-mldsa", "minutes": 4.5, + "comment": "The 'all' set with MLDSA in place of SLHDSA, so a stripped ML-DSA still meets the other stripped primitives. SLH-DSA is left out because only that combination pulls in the ML-DSA-44 entity certificate test described above.", + "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519 -DWOLF_CRYPTO_CB_ONLY_CURVE448 -DWOLF_CRYPTO_CB_ONLY_MLDSA"]}, {"name": "only", "minutes": 4.0, "comment": "Same coverage as the \"all\" entry above, but driven by ./configure --enable-cryptocb=only instead of a hand-written CPPFLAGS list. This is the regression test for the configure option: it must emit exactly the WOLF_CRYPTO_CB_ONLY_* set that \"all\" passes by hand, for the algorithms this base enables. The \"all\" entry deliberately stays on explicit CPPFLAGS so a bug in the configure logic cannot silently weaken both. Note the base already passes --enable-cryptocb; this entry's flags are appended after the base, so --enable-cryptocb=only wins.", "configure": ["--enable-cryptocb=only"]}, From 2213a1dda8e74432efeaa2c4048e2bae31c4f3b6 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Mon, 28 Sep 2026 17:25:13 -0700 Subject: [PATCH 6/8] Fix RNG test --- wolfcrypt/test/test.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index d9f15b11c13..452eb6b8aab 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -69829,7 +69829,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) if (ret == 0) { WC_RNG kgRng; - r = wc_InitRng_ex(&kgRng, HEAP_HINT, INVALID_DEVID); + r = wc_InitRng_ex(&kgRng, HEAP_HINT, devId); if (r != 0) { ret = WC_TEST_RET_ENC_EC(r); } @@ -95143,8 +95143,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if ((!WC_VAR_OK(key)) || (!WC_VAR_OK(mldsaRng))) ret = WC_TEST_RET_ENC_EC(MEMORY_E); if (ret == 0) { - /* The device ignores the RNG; keep it off the callback path. */ - r = wc_InitRng_ex(mldsaRng, HEAP_HINT, INVALID_DEVID); + r = wc_InitRng_ex(mldsaRng, HEAP_HINT, devId); if (r != 0) ret = WC_TEST_RET_ENC_EC(r); else From 9a14e25e7ced5a25e404be942a35f2afe2ac5ade Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Thu, 1 Oct 2026 18:58:38 -0700 Subject: [PATCH 7/8] PR feedback: add test coverage for MakeKeyFromSeed --- wolfcrypt/test/test.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 452eb6b8aab..4a820555a14 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -95134,7 +95134,6 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) static const byte sigCtx[] = { 0x01, 0x02, 0x03 }; int key_inited = 0; int rng_inited = 0; - int baseline = myCtx.mldsaCount; int res = 0; int r; @@ -95161,18 +95160,28 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if (r != 0) ret = WC_TEST_RET_ENC_EC(r); } + /* Key generation has a software fallback in native builds, so require + * that each call reached the device. */ if (ret == 0) { + int before = myCtx.mldsaCount; + r = wc_MlDsaKey_MakeKey(key, mldsaRng); if (r != 0) ret = WC_TEST_RET_ENC_EC(r); + else if (myCtx.mldsaCount != before + 1) + ret = WC_TEST_RET_ENC_NC; } #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY /* Generating from a caller supplied seed is the device's job too: * the handler fails the call if the seed does not arrive whole. */ if (ret == 0) { + int before = myCtx.mldsaCount; + r = wc_MlDsaKey_MakeKeyFromSeed(key, mldsa_cb_seed); if (r != 0) ret = WC_TEST_RET_ENC_EC(r); + else if (myCtx.mldsaCount != before + 1) + ret = WC_TEST_RET_ENC_NC; } #endif if (ret == 0) { @@ -95245,9 +95254,6 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) } #endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ #endif - /* Every operation above went through the callback. */ - if ((ret == 0) && (myCtx.mldsaCount <= baseline)) - ret = WC_TEST_RET_ENC_NC; /* A device error must reach the caller unchanged. */ if (ret == 0) { myCtx.mldsaFail = WC_NO_ERR_TRACE(WC_HW_E); From 0ae42d8329a01618a952ae127da09b1d7ee0349e Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Fri, 2 Oct 2026 13:04:28 -0700 Subject: [PATCH 8/8] Add WC_MLDSA_HAVE_NATIVE guard in test_mldsa.c --- tests/api/test_mldsa.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/api/test_mldsa.c b/tests/api/test_mldsa.c index 05664c67919..f722308c16c 100644 --- a/tests/api/test_mldsa.c +++ b/tests/api/test_mldsa.c @@ -31778,6 +31778,7 @@ int test_wc_MlDsaKey_seed_service_indicator(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + defined(WC_MLDSA_HAVE_NATIVE) && \ !defined(WOLFSSL_MLDSA_VERIFY_ONLY) wc_MlDsaKey key; byte seed[MLDSA_SEED_SZ];