From bc3288204e38d074e13b1bd8b2df9f1dbd15a6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 22 Jun 2026 15:43:24 +0200 Subject: [PATCH] PKCS#7: add ML-DSA (FIPS 204) SignedData support Add ML-DSA signing and verification for CMS/PKCS#7 SignedData, following RFC 9882. ML-DSA is used in CMS "pure" mode: the signature is computed over the complete message (the DER SET OF signed attributes, or the eContent when none are present) with an empty context string and absent signatureAlgorithm parameters, rather than over a pre-computed DigestInfo as with RSA/ECDSA. wolfcrypt/src/pkcs7.c: - New ML-DSA helpers: wc_PKCS7_MlDsaLevelFromOID, wc_PKCS7_BuildPureSigMessage, wc_PKCS7_MlDsaSign and wc_PKCS7_MlDsaVerify, wired into the per-algorithm switch sites (GetSignSize, SignedDataGetEncAlgoId, SetPublicKeyOID, CheckPublicKeyDer) and the sign/verify dispatchers. Only the final FIPS 204 ML-DSA OIDs are accepted; pre-standard draft Dilithium OIDs are not. - GetSignSize derives the ML-DSA signature length from the parameter set. - InitWithCert copies the signer public key into the RSA-sized publicKey buffer only for RSA/ECC certs (the raw-sign callback consumers); large PQC keys such as ML-DSA would overflow it and are never read back, so publicKeySz stays 0. - wc_MlDsaKey is always heap allocated (it embeds multi-KB key buffers); the accompanying DecodedCert uses the WC_DECLARE_VAR/WC_ALLOC_VAR_EX macros for stack-vs-heap handling under WOLFSSL_SMALL_STACK. - wc_PKCS7_SignedDataBuildSignature skips building the DigestInfo for ML-DSA, which signs the full message in pure mode and never consumes it. - wc_PKCS7_MlDsaSign wraps the ML-DSA private-key decode in PRIVATE_KEY_UNLOCK/PRIVATE_KEY_LOCK. Unlike RSA/ECC, the FIPS module gates wc_MlDsaKey_PrivateKeyDecode behind the private-key read lock, so signing would otherwise fail with FIPS_PRIVATE_KEY_LOCKED_E under --enable-fips. The macros are no-ops in non-FIPS builds. wolfssl/wolfcrypt/pkcs7.h: - Document that the fixed-size signer public key buffer (publicKey/publicKeySz) holds only RSA/ECC keys; it stays RSA-sized. wolfcrypt/src/hash.c: - Map the SHAKE128/SHAKE256 OIDs to their hash types in wc_OidGetHash(). certs/mldsa: - Add expanded-only PKCS#8 DER private keys (mldsa44/65/87-key.der) matching the self-signed ML-DSA certificates, with README and include.am updates. The expanded-only shape (no seed) decodes via wc_MlDsaKey_ImportPrivRaw without keygen-from-seed or the ASN template, so pkcs7signed_mldsa_test also passes in WOLFSSL_MLDSA_NO_MAKE_KEY and non-WOLFSSL_ASN_TEMPLATE builds. certs/renewcerts.sh: - Generate the mldsa-key.der files from the matching mldsa-key.pem in the expanded-only shape (openssl pkey -provparam ml-dsa.output_formats=priv), so a regeneration keeps the DER key in step with the cert. The OpenSSL detection probe now requires both ML-DSA keygen and that conversion across all three levels, so the block runs fully (matched cert+key) or is skipped entirely rather than aborting mid-way. wolfcrypt/test/test.c: - Add pkcs7signed_mldsa_test(): round-trip encode/verify of SignedData across ML-DSA-44/65/87, with and without signed attributes, including a check that the digest algorithm parameters are encoded as expected. The message-digest OID is selected from the enabled hash set (SHA-512, else SHA-256, else SHA-1) so the test builds when SHA-512 is disabled. A negative case confirms ML-DSA rejects a caller-supplied pre-computed content hash with BAD_FUNC_ARG. --- .github/workflows/pq-all.yml | 6 +- certs/mldsa/README.txt | 18 + certs/mldsa/include.am | 3 + certs/mldsa/mldsa44-key.der | Bin 0 -> 2588 bytes certs/mldsa/mldsa65-key.der | Bin 0 -> 4060 bytes certs/mldsa/mldsa87-key.der | Bin 0 -> 4924 bytes certs/renewcerts.sh | 47 ++- configure.ac | 5 +- wolfcrypt/src/hash.c | 14 + wolfcrypt/src/pkcs7.c | 697 +++++++++++++++++++++++++++++++++-- wolfcrypt/test/test.c | 387 +++++++++++++++++++ wolfssl/wolfcrypt/pkcs7.h | 3 + wolfssl/wolfcrypt/settings.h | 9 +- 13 files changed, 1148 insertions(+), 41 deletions(-) create mode 100644 certs/mldsa/mldsa44-key.der create mode 100644 certs/mldsa/mldsa65-key.der create mode 100644 certs/mldsa/mldsa87-key.der diff --git a/.github/workflows/pq-all.yml b/.github/workflows/pq-all.yml index 50b73005296..7805f91d876 100644 --- a/.github/workflows/pq-all.yml +++ b/.github/workflows/pq-all.yml @@ -233,7 +233,11 @@ jobs: {"name": "mldsa-no-asn1-fips204-draft-opensslextra", "minutes": 1.2, "configure": ["--enable-intelasm", "--enable-sp-asm", "--enable-dilithium=yes,draft", "--enable-opensslextra", - "CPPFLAGS=-DWOLFSSL_MLDSA_NO_ASN1"]} + "CPPFLAGS=-DWOLFSSL_MLDSA_NO_ASN1"]}, + {"name": "pkcs7-mldsa-only", "minutes": 1.2, + "comment": "PKCS#7 SignedData with ML-DSA as the only signature algorithm (no RSA, no ECC); guards the ML-DSA-only PKCS7 build path", + "configure": ["--enable-cryptonly", "--enable-mldsa", + "--enable-pkcs7", "--disable-rsa", "--disable-ecc"]} ] EOF .github/scripts/parallel-make-check.py \ diff --git a/certs/mldsa/README.txt b/certs/mldsa/README.txt index 9cb8a4dc9da..a5a7e11a6dd 100644 --- a/certs/mldsa/README.txt +++ b/certs/mldsa/README.txt @@ -9,6 +9,24 @@ File variants, per level N in {44, 65, 87}: mldsa_oqskeypair.der liboqs concatenated (priv || pub) format mldsa_pub-spki.der SubjectPublicKeyInfo wrapping the public key +Self-signed certificates and their matching keys (used by the PKCS#7/CMS +SignedData tests), per level N in {44, 65, 87}: + mldsa-cert.pem / mldsa-cert.der self-signed ML-DSA certificate + mldsa-key.pem matching private key (PEM, + seed-and-expanded PKCS#8) + mldsa-key.der matching private key (DER, + expanded-only PKCS#8) + +The mldsa-key.der files were derived from the matching mldsa-key.pem +using OpenSSL 3.5+, selecting the portable expanded-only private-key shape: + + openssl pkey -in mldsa-key.pem \ + -provparam ml-dsa.output_formats=priv -outform DER \ + -out mldsa-key.der + +Unlike the standalone mldsa_priv-only.der vectors above, these correspond +to the public key in mldsa-cert.der. + The *_pub-spki.der files were derived from the matching *_priv-only.der files using OpenSSL 3.5+: diff --git a/certs/mldsa/include.am b/certs/mldsa/include.am index 405a0859502..cf6489f9ef9 100644 --- a/certs/mldsa/include.am +++ b/certs/mldsa/include.am @@ -26,13 +26,16 @@ EXTRA_DIST += \ certs/mldsa/mldsa87_bare-seed.der \ certs/mldsa/mldsa87_bare-priv.der \ certs/mldsa/mldsa44-key.pem \ + certs/mldsa/mldsa44-key.der \ certs/mldsa/mldsa44-cert.pem \ certs/mldsa/mldsa44-cert.der \ certs/mldsa/ecc-leaf-mldsa44.pem \ certs/mldsa/mldsa65-key.pem \ + certs/mldsa/mldsa65-key.der \ certs/mldsa/mldsa65-cert.pem \ certs/mldsa/mldsa65-cert.der \ certs/mldsa/mldsa87-key.pem \ + certs/mldsa/mldsa87-key.der \ certs/mldsa/mldsa87-cert.pem \ certs/mldsa/mldsa87-cert.der \ certs/mldsa/bench_mldsa_44_key.der \ diff --git a/certs/mldsa/mldsa44-key.der b/certs/mldsa/mldsa44-key.der new file mode 100644 index 0000000000000000000000000000000000000000..f487a4378972ab6e6857ad61b696c51f008c7941 GIT binary patch literal 2588 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 literal 0 HcmV?d00001 diff --git a/certs/mldsa/mldsa65-key.der b/certs/mldsa/mldsa65-key.der new file mode 100644 index 0000000000000000000000000000000000000000..91ed244731ad56fa7e4b23be41ea6919c231b6d4 GIT binary patch literal 4060 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/dev/null; then + probe_key="$(mktemp)" || continue + # Probe every level the loop below generates (44/65/87), for both + # keygen and the expanded-only conversion, so a partially-capable + # binary is rejected up front rather than aborting mid-loop. + probe_ok=1 + for probe_level in 44 65 87; do + if ! "$candidate" genpkey -algorithm "mldsa${probe_level}" \ + -out "$probe_key" 2>/dev/null || \ + ! "$candidate" pkey -in "$probe_key" \ + -provparam ml-dsa.output_formats=priv -outform DER \ + -out /dev/null 2>/dev/null; then + probe_ok=0 + break + fi + done + rm -f "$probe_key" + if [ "$probe_ok" -eq 1 ]; then OPENSSL3="$candidate" break fi @@ -1230,6 +1252,19 @@ EOF -outform DER -out "mldsa/mldsa${level}-cert.der" check_result $? "ML-DSA-${level} DER conversion" + # Matching private key in the portable expanded-only PKCS#8 DER + # shape, used by the PKCS#7/CMS SignedData tests. Derived from the + # same mldsa${level}-key.pem so it corresponds to the public key in + # mldsa${level}-cert.der. The expanded-only form (no seed) decodes + # via ImportPrivRaw without keygen-from-seed or the ASN template, + # so the tests pass in WOLFSSL_MLDSA_NO_MAKE_KEY and non-template + # builds too; the seed-and-expanded default would not. The probe + # above already verified this binary supports the conversion. + "$OPENSSL3" pkey -in "mldsa/mldsa${level}-key.pem" \ + -provparam ml-dsa.output_formats=priv -outform DER \ + -out "mldsa/mldsa${level}-key.der" + check_result $? "ML-DSA-${level} key DER conversion" + echo "End of ML-DSA-${level} section" done @@ -1254,7 +1289,7 @@ EOF echo "End of ecc-leaf-mldsa44 section" echo "---------------------------------------------------------------------" else - echo "Skipping ML-DSA cert generation (no OpenSSL 3.3+ with ML-DSA support found)" + echo "Skipping ML-DSA cert generation (no OpenSSL 3.5+ built-in ML-DSA provider found)" echo "---------------------------------------------------------------------" fi diff --git a/configure.ac b/configure.ac index 25028f57580..e7b2a4e7868 100644 --- a/configure.ac +++ b/configure.ac @@ -11802,8 +11802,9 @@ AS_IF([test "x$ENABLED_INTEL_QA_SYNC" = "xyes" || test "x$ENABLED_OCTEON_SYNC" = # checks for pkcs7 needed enables AS_IF([test "x$ENABLED_PKCS7" = "xyes" && \ test "x$ENABLED_RSA" = "xno" && \ - test "x$ENABLED_ECC" = "xno"], - [AC_MSG_ERROR([please enable ecc or rsa if enabling pkcs7.])]) + test "x$ENABLED_ECC" = "xno" && \ + test "x$ENABLED_MLDSA" = "xno"], + [AC_MSG_ERROR([please enable ecc, rsa or mldsa if enabling pkcs7.])]) AS_IF([test "x$ENABLED_PKCS7" = "xyes" && \ test "x$ENABLED_SHA" = "xno" && \ diff --git a/wolfcrypt/src/hash.c b/wolfcrypt/src/hash.c index 8e8c33c7084..ed6342ae4fe 100644 --- a/wolfcrypt/src/hash.c +++ b/wolfcrypt/src/hash.c @@ -353,6 +353,20 @@ enum wc_HashType wc_OidGetHash(int oid) hash_type = WC_ERR_TRACE(WC_HASH_TYPE_NONE); #endif break; + case SHAKE128h: + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE128) + hash_type = WC_HASH_TYPE_SHAKE128; + #else + hash_type = WC_ERR_TRACE(WC_HASH_TYPE_NONE); + #endif + break; + case SHAKE256h: + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE256) + hash_type = WC_HASH_TYPE_SHAKE256; + #else + hash_type = WC_ERR_TRACE(WC_HASH_TYPE_NONE); + #endif + break; case SM3h: #ifdef WOLFSSL_SM3 hash_type = WC_HASH_TYPE_SM3; diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index d7667b99432..882a2af1ae2 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -73,6 +73,21 @@ #ifdef HAVE_ECC #include #endif +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) + #include + /* gates the ML-DSA SignedData support; see the design note below. */ + #define WC_PKCS7_HAVE_MLDSA + /* ML-DSA SignedData signing and verification compile independently, + * mirroring the private-/public-key availability of the ML-DSA backend. + * Helpers shared by only one side are gated by these so they are never + * compiled unused (which would trip -Werror=unused-function). */ + #if !defined(WOLFSSL_MLDSA_NO_SIGN) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) + #define WC_PKCS7_MLDSA_SIGN + #endif + #if !defined(WOLFSSL_MLDSA_NO_VERIFY) && defined(WOLFSSL_MLDSA_PUBLIC_KEY) + #define WC_PKCS7_MLDSA_VERIFY + #endif +#endif #ifdef HAVE_LIBZ #include #endif @@ -1045,6 +1060,37 @@ static int wc_PKCS7_RecipientListVersionsAllZero(wc_PKCS7* pkcs7) return 1; } +#if defined(WC_PKCS7_HAVE_MLDSA) && \ + (defined(WOLFSSL_MLDSA_PUBLIC_KEY) || defined(WC_PKCS7_MLDSA_SIGN)) +/* forward declaration; defined alongside the other ML-DSA helpers below. + * Used by CheckPublicKeyDer (public key) and the sign-size/sign paths. */ +static int wc_PKCS7_MlDsaLevelFromOID(word32 publicKeyOID, byte* level); +#endif + +/* Returns whether the parameters field of a CMS structural digest + * AlgorithmIdentifier (SignedData.digestAlgorithms / SignerInfo.digestAlgorithm) + * should be omitted (absent) rather than encoded as NULL. + * + * RFC 8702 requires the SHAKE128/SHAKE256 digest identifiers to have ABSENT + * parameters, so those are always forced absent. For the SHA-2 family RFC 5754 + * says the parameters SHOULD be absent but receivers MUST accept both forms; + * to preserve wolfSSL's long-standing output and interoperability the caller's + * pkcs7->hashParamsAbsent preference (default: NULL) is honored there. + * + * This applies only to the CMS structural AlgorithmIdentifiers; the PKCS#1 + * v1.5 DigestInfo used internally for RSA signatures keeps the NULL parameter + * that is conventional for that structure (RFC 8017) and is unaffected. */ +static byte wc_PKCS7_DigestParamsAbsent(const wc_PKCS7* pkcs7) +{ +#if defined(WOLFSSL_SHA3) && \ + (defined(WOLFSSL_SHAKE256) || defined(WOLFSSL_SHAKE128)) + if (pkcs7->hashOID == SHAKE256h || pkcs7->hashOID == SHAKE128h) { + return 1; + } +#endif + return pkcs7->hashParamsAbsent; +} + /* Verify RSA/ECC key is correctly formatted, used as sanity check after * import of key/cert. * @@ -1136,9 +1182,43 @@ static int wc_PKCS7_CheckPublicKeyDer(wc_PKCS7* pkcs7, int keyOID, wc_ecc_free(ecc); break; +#endif +#if defined(WC_PKCS7_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PUBLIC_KEY) + case ML_DSA_44k: + case ML_DSA_65k: + case ML_DSA_87k: + { + /* Sanity check: decode the ML-DSA public key from its SPKI. */ + byte level = 0; + wc_MlDsaKey* mldsa = (wc_MlDsaKey*)XMALLOC(sizeof(wc_MlDsaKey), + pkcs7->heap, DYNAMIC_TYPE_MLDSA); + if (mldsa == NULL) { + ret = MEMORY_E; + break; + } + ret = wc_PKCS7_MlDsaLevelFromOID((word32)keyOID, &level); + if (ret == 0) { + ret = wc_MlDsaKey_Init(mldsa, pkcs7->heap, pkcs7->devId); + } + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(mldsa, level); + if (ret == 0) { + ret = wc_MlDsaKey_PublicKeyDecode(mldsa, key, keySz, + &scratch); + } + wc_MlDsaKey_Free(mldsa); + } + XFREE(mldsa, pkcs7->heap, DYNAMIC_TYPE_MLDSA); + break; + } #endif } + /* scratch is only read inside the (conditionally compiled) switch cases; + * mark it used so a build with none of them enabled (e.g. sign-only ML-DSA + * with RSA and ECC disabled) stays -Werror clean. */ + (void)scratch; + #ifdef WOLFSSL_SMALL_STACK #ifndef NO_RSA XFREE(rsa, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -1249,16 +1329,32 @@ int wc_PKCS7_InitWithCert(wc_PKCS7* pkcs7, byte* derCert, word32 derCertSz) return ret; } - if (dCert->pubKeySize > (MAX_RSA_INT_SZ + MAX_RSA_E_SZ) || - dCert->serialSz > MAX_SN_SZ) { - WOLFSSL_MSG("Invalid size in certificate"); + if (dCert->serialSz > MAX_SN_SZ) { + WOLFSSL_MSG("Invalid serial size in certificate"); FreeDecodedCert(dCert); WC_FREE_VAR_EX(dCert, pkcs7->heap, DYNAMIC_TYPE_DCERT); return ASN_PARSE_E; } - XMEMCPY(pkcs7->publicKey, dCert->publicKey, dCert->pubKeySize); - pkcs7->publicKeySz = dCert->pubKeySize; + /* Store the signer public key only for RSA/ECC; it is consumed solely + * by the RSA/ECC raw-sign callback paths. PQC keys (e.g. ML-DSA) are + * large, never read back from here, and would overflow this RSA-sized + * buffer, so they are not stored. */ + if (dCert->keyOID == RSAk || dCert->keyOID == RSAPSSk || + dCert->keyOID == ECDSAk) { + /* guard the fixed-size buffer against an over-large key blob */ + if (dCert->pubKeySize > (MAX_RSA_INT_SZ + MAX_RSA_E_SZ)) { + WOLFSSL_MSG("Invalid public key size in certificate"); + FreeDecodedCert(dCert); + WC_FREE_VAR_EX(dCert, pkcs7->heap, DYNAMIC_TYPE_DCERT); + return ASN_PARSE_E; + } + XMEMCPY(pkcs7->publicKey, dCert->publicKey, dCert->pubKeySize); + pkcs7->publicKeySz = dCert->pubKeySize; + } + else { + pkcs7->publicKeySz = 0; + } pkcs7->publicKeyOID = dCert->keyOID; /* Do not derive publicKeyOID from cert signatureOID: the cert's * signature is how the cert was signed by its issuer; the signer @@ -1598,7 +1694,15 @@ typedef struct ESD { byte contentDigest[WC_MAX_DIGEST_SIZE + 2]; /* content only + ASN.1 heading */ WC_BITFIELD contentDigestSet:1; byte contentAttribsDigest[WC_MAX_DIGEST_SIZE]; - byte encContentDigest[MAX_ENCRYPTED_KEY_SZ]; + /* Signature buffer, heap allocated and right-sized to the signature + * algorithm before signing (the size is obtained from + * wc_PKCS7_GetSignSize). A single allocation serves every algorithm + * (RSA/ECDSA/RSA-PSS/ML-DSA), so signature storage is not special-cased by + * type. encContentDigestSz is the actual signature length and + * encContentDigestBufSz is the allocated capacity. Freed in the encode + * cleanup path. */ + byte* encContentDigest; + word32 encContentDigestBufSz; byte outerSeq[MAX_SEQ_SZ]; byte outerContent[MAX_EXP_SZ]; @@ -1978,7 +2082,7 @@ static int wc_PKCS7_RsaSign(wc_PKCS7* pkcs7, byte* in, word32 inSz, ESD* esd) #endif { ret = wc_RsaSSL_Sign(in, inSz, esd->encContentDigest, - sizeof(esd->encContentDigest), + esd->encContentDigestBufSz, privKey, pkcs7->rng); } #ifdef WOLFSSL_ASYNC_CRYPT @@ -2061,7 +2165,7 @@ static int wc_PKCS7_EcdsaSign(wc_PKCS7* pkcs7, byte* in, word32 inSz, ESD* esd) ret = wc_PKCS7_ImportECC(pkcs7, privKey); if (ret == 0) { - outSz = sizeof(esd->encContentDigest); + outSz = esd->encContentDigestBufSz; #ifdef WOLFSSL_ASYNC_CRYPT do { ret = wc_AsyncWait(ret, &privKey->asyncDev, @@ -2165,7 +2269,7 @@ static int wc_PKCS7_RsaPssSign(wc_PKCS7* pkcs7, byte* digest, word32 digestSz, ret = wc_PKCS7_ImportRSA(pkcs7, privKey); if (ret == 0) { - outSz = sizeof(esd->encContentDigest); + outSz = esd->encContentDigestBufSz; #ifdef WOLFSSL_ASYNC_CRYPT do { ret = wc_AsyncWait(ret, &privKey->asyncDev, @@ -2257,6 +2361,35 @@ static int wc_PKCS7_GetSignSize(wc_PKCS7* pkcs7) } break; #endif + + #if defined(WC_PKCS7_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) + case ML_DSA_44k: + case ML_DSA_65k: + case ML_DSA_87k: + { + /* ML-DSA signatures are a fixed size per parameter set, so the + * size is derived from the level alone - no private key or signing + * is required. */ + byte level = 0; + wc_MlDsaKey* key = (wc_MlDsaKey*)XMALLOC(sizeof(wc_MlDsaKey), + pkcs7->heap, DYNAMIC_TYPE_MLDSA); + if (key == NULL) + return MEMORY_E; + + ret = wc_PKCS7_MlDsaLevelFromOID(pkcs7->publicKeyOID, &level); + if (ret == 0) + ret = wc_MlDsaKey_Init(key, pkcs7->heap, pkcs7->devId); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(key, level); + if (ret == 0) + ret = wc_MlDsaKey_SigSize(key); + wc_MlDsaKey_Free(key); + } + XFREE(key, pkcs7->heap, DYNAMIC_TYPE_MLDSA); + } + break; + #endif } return ret; @@ -2566,6 +2699,17 @@ static int wc_PKCS7_SignedDataGetEncAlgoId(wc_PKCS7* pkcs7, int* digEncAlgoId, return NOT_COMPILED_IN; } #endif +#ifdef WC_PKCS7_HAVE_MLDSA + else if (pkcs7->publicKeyOID == ML_DSA_44k || + pkcs7->publicKeyOID == ML_DSA_65k || + pkcs7->publicKeyOID == ML_DSA_87k) { + /* RFC 9882: the signatureAlgorithm is the ML-DSA OID itself (no hash + * prefix), and its parameters field MUST be absent. The OID value is + * shared between the key and signature OID tables. */ + algoType = oidSigType; + algoId = (int)pkcs7->publicKeyOID; + } +#endif if (algoId == 0) { WOLFSSL_MSG("Invalid signature algorithm type"); @@ -2667,6 +2811,184 @@ static int wc_PKCS7_BuildDigestInfo(wc_PKCS7* pkcs7, byte* flatSignedAttribs, } +#ifdef WC_PKCS7_HAVE_MLDSA +/* + * ML-DSA (FIPS 204) SignedData support, per RFC 9882. + * + * Unlike RSA/ECDSA, ML-DSA is used in CMS "pure" mode: the signature is + * computed over the complete message (the DER SET OF signed attributes, or the + * eContent when none are present) rather than a pre-computed digest, with an + * empty context string and absent signatureAlgorithm parameters. + * + * The signature itself is stored like every other algorithm's, in the single + * right-sized esd->encContentDigest buffer; the only ML-DSA-specific code is + * the small set of helpers below (message construction, key level mapping, + * sign and verify), which the shared encode/verify dispatchers call. Adding a + * future "pure" PQC scheme (SLH-DSA, FN-DSA) means providing equivalent + * helpers and adding the OID to the per-algorithm switch sites + * (wc_PKCS7_GetSignSize, wc_PKCS7_SignedDataGetEncAlgoId, + * wc_PKCS7_SetPublicKeyOID, wc_PKCS7_CheckPublicKeyDer and the sign/verify + * dispatchers) rather than reworking the encode/decode control flow. + */ + +/* Map a public key OID to the corresponding ML-DSA parameter level. + * Returns 0 and sets *level on success, BAD_FUNC_ARG otherwise. + * + * Only the FIPS 204 final ML-DSA OIDs are handled; the pre-standard draft + * Dilithium OIDs (DILITHIUM_LEVEL2k/3k/5k) are intentionally not supported + * for CMS, as RFC 9882 is defined over final ML-DSA. */ +#if defined(WOLFSSL_MLDSA_PUBLIC_KEY) || defined(WC_PKCS7_MLDSA_SIGN) +static int wc_PKCS7_MlDsaLevelFromOID(word32 publicKeyOID, byte* level) +{ + switch (publicKeyOID) { + case ML_DSA_44k: + *level = WC_ML_DSA_44; + return 0; + case ML_DSA_65k: + *level = WC_ML_DSA_65; + return 0; + case ML_DSA_87k: + *level = WC_ML_DSA_87; + return 0; + default: + return BAD_FUNC_ARG; + } +} +#endif + +/* Build the exact octet string that a "pure" PQC signature is computed over, + * per RFC 9882 Section 4: + * - if signed attributes are present, the DER encoding of the SignedAttrs + * SET OF (i.e. the [0] IMPLICIT attributes re-tagged to a universal SET); + * - otherwise, the eContent of the SignedData directly. + * + * On success *outMsg / *outMsgSz reference the message to sign/verify. When + * signed attributes are present a buffer is allocated and *outAlloc is set to + * 1 (caller must XFREE *outMsg with DYNAMIC_TYPE_TMP_BUFFER); otherwise + * *outMsg points into pkcs7->content and *outAlloc is 0. + * + * attribs/attribsSz are the flattened attributes without the SET wrapper, as + * available on both the encode (flatSignedAttribs) and decode (signedAttrib) + * paths. */ +#if defined(WC_PKCS7_MLDSA_SIGN) || defined(WC_PKCS7_MLDSA_VERIFY) +static int wc_PKCS7_BuildPureSigMessage(wc_PKCS7* pkcs7, const byte* attribs, + word32 attribsSz, byte** outMsg, word32* outMsgSz, int* outAlloc) +{ + *outMsg = NULL; + *outMsgSz = 0; + *outAlloc = 0; + + if (attribsSz > 0) { + byte attribSet[MAX_SET_SZ]; + word32 attribSetSz; + byte* msg; + + if (attribs == NULL) { + return BAD_FUNC_ARG; + } + + attribSetSz = SetSet(attribsSz, attribSet); + + msg = (byte*)XMALLOC(attribSetSz + attribsSz, pkcs7->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (msg == NULL) { + return MEMORY_E; + } + + XMEMCPY(msg, attribSet, attribSetSz); + XMEMCPY(msg + attribSetSz, attribs, attribsSz); + + *outMsg = msg; + *outMsgSz = attribSetSz + attribsSz; + *outAlloc = 1; + } + else { + /* No signed attributes: the signature is over the eContent directly, + * which must be present. ML-DSA "pure" Sign/Verify require a non-NULL + * message pointer (a zero-length eContent is still passed by pointer), + * so a missing eContent with no signed attributes is rejected here + * rather than failing later with the same error. + * + * Scope: the eContent is signed verbatim. Unlike the RSA/ECC digest + * path (wc_PKCS7_SignedDataGetDigest), this does not strip the ASN.1 + * tag/length for legacy PKCS#7 ANY-typed content (contentIsPkcs7Type + * == 1). Only CMS (OCTET STRING) eContent is supported for the ML-DSA + * no-signed-attributes path; the common RFC 9882 case carries signed + * attributes and is unaffected. */ + if (pkcs7->content == NULL) { + return BAD_FUNC_ARG; + } + *outMsg = pkcs7->content; + *outMsgSz = pkcs7->contentSz; + } + + return 0; +} +#endif /* WC_PKCS7_MLDSA_SIGN || WC_PKCS7_MLDSA_VERIFY */ + +#if !defined(WOLFSSL_MLDSA_NO_SIGN) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) +/* Sign the supplied message with the ML-DSA private key in pkcs7->privateKey, + * writing the signature into the shared esd->encContentDigest buffer (which the + * caller has sized to the signature length). Uses pure ML-DSA with an empty + * context string, per RFC 9882. + * + * Returns the signature length on success, negative on error. */ +static int wc_PKCS7_MlDsaSign(wc_PKCS7* pkcs7, const byte* msg, word32 msgSz, + ESD* esd) +{ + int ret; + byte level = 0; + word32 idx = 0; + word32 sigSz; + wc_MlDsaKey* key; + + if (pkcs7 == NULL || esd == NULL || msg == NULL || + esd->encContentDigest == NULL || + pkcs7->privateKey == NULL || pkcs7->privateKeySz == 0) { + return BAD_FUNC_ARG; + } + + ret = wc_PKCS7_MlDsaLevelFromOID(pkcs7->publicKeyOID, &level); + if (ret != 0) { + return ret; + } + + key = (wc_MlDsaKey*)XMALLOC(sizeof(wc_MlDsaKey), pkcs7->heap, + DYNAMIC_TYPE_MLDSA); + if (key == NULL) { + return MEMORY_E; + } + + ret = wc_MlDsaKey_Init(key, pkcs7->heap, pkcs7->devId); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(key, level); + if (ret == 0) { + /* FIPS locks ML-DSA private-key reads by default; unlock only for + * the decode of the caller-provided key, then re-lock. */ + PRIVATE_KEY_UNLOCK(); + ret = wc_MlDsaKey_PrivateKeyDecode(key, pkcs7->privateKey, + pkcs7->privateKeySz, &idx); + PRIVATE_KEY_LOCK(); + } + if (ret == 0) { + /* RFC 9882: pure ML-DSA with an empty context string. */ + sigSz = esd->encContentDigestBufSz; + ret = wc_MlDsaKey_SignCtx(key, NULL, 0, esd->encContentDigest, + &sigSz, msg, msgSz, pkcs7->rng); + } + wc_MlDsaKey_Free(key); + } + XFREE(key, pkcs7->heap, DYNAMIC_TYPE_MLDSA); + + if (ret == 0) { + return (int)sigSz; + } + return ret; +} +#endif /* !WOLFSSL_MLDSA_NO_SIGN && WOLFSSL_MLDSA_PRIVATE_KEY */ +#endif /* WC_PKCS7_HAVE_MLDSA */ + + /* build SignedData signature over DigestInfo or content digest * * pkcs7 - pointer to initialized PKCS7 struct @@ -2681,6 +3003,7 @@ static int wc_PKCS7_SignedDataBuildSignature(wc_PKCS7* pkcs7, ESD* esd) { int ret = 0; + int skipDigestInfo = 0; #if defined(HAVE_ECC) || \ (defined(HAVE_PKCS7_RSA_RAW_SIGN_CALLBACK) && !defined(NO_RSA)) || \ (defined(WC_RSA_PSS) && !defined(NO_RSA)) @@ -2699,12 +3022,24 @@ static int wc_PKCS7_SignedDataBuildSignature(wc_PKCS7* pkcs7, DYNAMIC_TYPE_TMP_BUFFER, return MEMORY_E); XMEMSET(digestInfo, 0, digestInfoSz); - ret = wc_PKCS7_BuildDigestInfo(pkcs7, flatSignedAttribs, - flatSignedAttribsSz, esd, digestInfo, - &digestInfoSz); - if (ret < 0) { - WC_FREE_VAR_EX(digestInfo, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); - return ret; +#ifdef WC_PKCS7_HAVE_MLDSA + /* ML-DSA signs the full message in pure mode (RFC 9882); it consumes no + * DigestInfo or content digest, so skip building one. */ + if (pkcs7->publicKeyOID == ML_DSA_44k || + pkcs7->publicKeyOID == ML_DSA_65k || + pkcs7->publicKeyOID == ML_DSA_87k) { + skipDigestInfo = 1; + } +#endif + + if (!skipDigestInfo) { + ret = wc_PKCS7_BuildDigestInfo(pkcs7, flatSignedAttribs, + flatSignedAttribsSz, esd, digestInfo, + &digestInfoSz); + if (ret < 0) { + WC_FREE_VAR_EX(digestInfo, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } } #if defined(HAVE_ECC) || \ @@ -2731,9 +3066,13 @@ static int wc_PKCS7_SignedDataBuildSignature(wc_PKCS7* pkcs7, /* user signing plain digest, build DigestInfo themselves */ ret = pkcs7->rsaSignRawDigestCb(pkcs7, esd->contentAttribsDigest, hashSz, - esd->encContentDigest, sizeof(esd->encContentDigest), + esd->encContentDigest, esd->encContentDigestBufSz, pkcs7->privateKey, pkcs7->privateKeySz, pkcs7->devId, hashOID); + /* validate return value doesn't exceed buffer size */ + if (ret > 0 && (word32)ret > esd->encContentDigestBufSz) { + ret = BUFFER_E; + } break; } #endif @@ -2755,11 +3094,11 @@ static int wc_PKCS7_SignedDataBuildSignature(wc_PKCS7* pkcs7, /* user signing plain digest */ ret = pkcs7->eccSignRawDigestCb(pkcs7, esd->contentAttribsDigest, hashSz, - esd->encContentDigest, sizeof(esd->encContentDigest), + esd->encContentDigest, esd->encContentDigestBufSz, pkcs7->privateKey, pkcs7->privateKeySz, pkcs7->devId, eccHashOID); /* validate return value doesn't exceed buffer size */ - if (ret > 0 && (word32)ret > sizeof(esd->encContentDigest)) { + if (ret > 0 && (word32)ret > esd->encContentDigestBufSz) { ret = BUFFER_E; } break; @@ -2780,6 +3119,32 @@ static int wc_PKCS7_SignedDataBuildSignature(wc_PKCS7* pkcs7, break; #endif +#if defined(WC_PKCS7_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) + case ML_DSA_44k: + case ML_DSA_65k: + case ML_DSA_87k: + { + /* RFC 9882: ML-DSA signs the complete message (the DER SET OF + * signed attributes, or the eContent when none are present) in + * pure mode, not a DigestInfo or content digest. */ + byte* msg = NULL; + word32 msgSz = 0; + int msgAlloc = 0; + + ret = wc_PKCS7_BuildPureSigMessage(pkcs7, flatSignedAttribs, + flatSignedAttribsSz, &msg, + &msgSz, &msgAlloc); + if (ret == 0) { + ret = wc_PKCS7_MlDsaSign(pkcs7, msg, msgSz, esd); + if (msgAlloc) { + XFREE(msg, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + } + } + break; + } +#endif + default: WOLFSSL_MSG("Unsupported public key type"); ret = BAD_FUNC_ARG; @@ -3113,6 +3478,7 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, int idx = 0, ret = 0; int digEncAlgoId, digEncAlgoType; int keyIdSize; + int signNow = 0; byte* flatSignedAttribs = NULL; word32 flatSignedAttribsSz = 0; word32 defaultAttribCap; @@ -3157,6 +3523,29 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, } #endif +#ifdef WC_PKCS7_HAVE_MLDSA + /* ML-DSA signs in "pure" mode over the full message, not a pre-computed + * digest, so a caller-supplied content hash is not meaningful and would + * also undersize the signature buffer (it forces the immediate-sign path + * with the historical MAX_ENCRYPTED_KEY_SZ allocation). Reject it with a + * clear error rather than failing later in wc_MlDsaKey_SignCtx. + * + * Known limitation: wc_PKCS7_EncodeSignedData() pre-computes a content + * hash (passing a non-NULL hashBuf) for the detached-over-empty-content + * case (pkcs7->detached set with contentSz == 0, e.g. an SCEP CertRep + * PENDING/FAILURE). That specific combination is therefore unsupported for + * ML-DSA and returns BAD_FUNC_ARG here, whereas RSA/ECDSA support it. + * Detached-over-non-empty content and embedded ML-DSA SignedData both + * work. */ + if (hashBuf != NULL && + (pkcs7->publicKeyOID == ML_DSA_44k || + pkcs7->publicKeyOID == ML_DSA_65k || + pkcs7->publicKeyOID == ML_DSA_87k)) { + WOLFSSL_MSG("Pre-calculated content hash not supported for ML-DSA"); + return BAD_FUNC_ARG; + } +#endif + #if defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3) keyIdSize = wc_HashGetDigestSize(wc_HashTypeConvert(HashIdAlg( pkcs7->publicKeyOID))); @@ -3302,7 +3691,7 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, if (pkcs7->sidType != DEGENERATE_SID) { signerInfoSz += esd->signerVersionSz; esd->signerDigAlgoIdSz = SetAlgoIDEx(pkcs7->hashOID, esd->signerDigAlgoId, - oidHashType, 0, pkcs7->hashParamsAbsent); + oidHashType, 0, wc_PKCS7_DigestParamsAbsent(pkcs7)); signerInfoSz += esd->signerDigAlgoIdSz; /* set signatureAlgorithm */ @@ -3414,19 +3803,55 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, esd->signedAttribSetSz = 0; } - if (pkcs7->publicKeyOID != ECDSAk && hashBuf == NULL) { + /* Size and allocate the single signature buffer (one storage path for + * every algorithm). Deterministic-size algorithms (RSA and ML-DSA, when + * no caller hash is supplied) get their exact size from + * wc_PKCS7_GetSignSize() so the SignerInfo lengths can be reserved + * before the final signing pass, and the buffer is right-sized. + * + * ECDSA, RSA-PSS (which always requires a caller-supplied hash), and any + * other caller-supplied-hash case instead sign immediately + * and only learn the size afterwards; they allocate the historical + * maximum (MAX_ENCRYPTED_KEY_SZ) so no extra key import is needed just + * to size the buffer, and record the real length once signed. + * + * INVARIANT: for the reserve path the reserved length MUST equal the + * length the final signing pass produces, since the SignerInfo/SEQUENCE + * lengths are derived from it (enforced after the final signing pass). */ + signNow = (pkcs7->publicKeyOID == ECDSAk) || (hashBuf != NULL); + + if (!signNow) { ret = wc_PKCS7_GetSignSize(pkcs7); - esd->encContentDigestSz = (word32)ret; + if (ret <= 0) { + /* GetSignSize returns 0 for an unsupported signer key type */ + idx = (ret < 0) ? ret : BAD_FUNC_ARG; + goto out; + } + esd->encContentDigestBufSz = (word32)ret; } else { - ret = wc_PKCS7_SignedDataBuildSignature(pkcs7, flatSignedAttribs, - flatSignedAttribsSz, esd); + esd->encContentDigestBufSz = MAX_ENCRYPTED_KEY_SZ; } - if (ret < 0) { - idx = ret; + + esd->encContentDigest = (byte*)XMALLOC(esd->encContentDigestBufSz, + pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + if (esd->encContentDigest == NULL) { + idx = MEMORY_E; goto out; } + if (!signNow) { + esd->encContentDigestSz = esd->encContentDigestBufSz; + } + else { + ret = wc_PKCS7_SignedDataBuildSignature(pkcs7, flatSignedAttribs, + flatSignedAttribsSz, esd); + if (ret < 0) { + idx = ret; + goto out; + } + } + signerInfoSz += flatSignedAttribsSz + esd->signedAttribSetSz; esd->signerDigestSz = SetOctetString(esd->encContentDigestSz, @@ -3455,7 +3880,7 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, if (pkcs7->sidType != DEGENERATE_SID) { esd->singleDigAlgoIdSz = SetAlgoIDEx(pkcs7->hashOID, esd->singleDigAlgoId, - oidHashType, 0, pkcs7->hashParamsAbsent); + oidHashType, 0, wc_PKCS7_DigestParamsAbsent(pkcs7)); } esd->digAlgoIdSetSz = SetSet(esd->singleDigAlgoIdSz, esd->digAlgoIdSet); @@ -3758,6 +4183,13 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, } if (hashBuf == NULL && pkcs7->sidType != DEGENERATE_SID) { + /* Only RSA (PKCS#1 v1.5) and ML-DSA reach this final signing pass. + * ECDSA and RSA-PSS both require a pre-supplied hash and sign earlier + * via the immediate-sign (signNow) path. The signature is now produced + * over the finalized attributes, and the size reserved during the + * sizing pass above is baked into the SignerInfo/SEQUENCE lengths. */ + word32 reservedSigSz = esd->encContentDigestSz; + /* Calculate the final hash and encrypt it. */ WOLFSSL_MSG("Recreating signature with new hash"); ret = wc_PKCS7_SignedDataBuildSignature(pkcs7, flatSignedAttribs, @@ -3766,6 +4198,14 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, idx = ret; goto out; } + + /* Enforce the fixed-size invariant: a signature length that differs + * from the reserved size would corrupt the already-encoded lengths. */ + if (esd->encContentDigestSz != reservedSigSz) { + WOLFSSL_MSG("Signature size changed between sizing and signing"); + idx = BUFFER_E; + goto out; + } } wc_PKCS7_WriteOut(pkcs7, (output2)? (output2 + idx) : NULL, @@ -3812,14 +4252,20 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, XFREE(flatSignedAttribs, pkcs7->heap, DYNAMIC_TYPE_PKCS7); /* free the working attribute array only if it was heap-allocated (i.e. it - * is not the inline buffer) before freeing esd. In small-stack builds esd - * is heap-allocated and may be NULL here. */ + * is not the inline buffer), and the heap-allocated signature buffer, + * before freeing esd. In small-stack builds esd is heap-allocated and may + * be NULL here. */ if (WC_VAR_OK(esd)) { if (esd->signedAttribs != NULL && esd->signedAttribs != esd->signedAttribsInline) { XFREE(esd->signedAttribs, pkcs7->heap, DYNAMIC_TYPE_PKCS7); } esd->signedAttribs = NULL; + + if (esd->encContentDigest != NULL) { + XFREE(esd->encContentDigest, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + esd->encContentDigest = NULL; + } } WC_FREE_VAR_EX(esd, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -4497,7 +4943,7 @@ int wc_PKCS7_SetEccSignRawDigestCb(wc_PKCS7* pkcs7, CallbackEccSignRawDigest cb) #endif /* HAVE_ECC */ -#if !defined(NO_RSA) || defined(HAVE_ECC) +#if !defined(NO_RSA) || defined(HAVE_ECC) || defined(WC_PKCS7_MLDSA_VERIFY) /* Check whether the given decoded certificate matches the SignerIdentifier * (sid) field of the currently parsed SignerInfo. Per RFC 5652 Section 5.3, * the sid selects which certificate's public key must be used to verify the @@ -4583,7 +5029,7 @@ static int wc_PKCS7_CertMatchesSignerInfo(wc_PKCS7* pkcs7, DecodedCert* dCert) return 0; } -#endif /* !NO_RSA || HAVE_ECC */ +#endif /* !NO_RSA || HAVE_ECC || WC_PKCS7_MLDSA_VERIFY */ #ifndef NO_RSA @@ -5048,6 +5494,145 @@ static int wc_PKCS7_EcdsaVerify(wc_PKCS7* pkcs7, byte* sig, int sigSz, #endif /* HAVE_ECC */ +#if defined(WC_PKCS7_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + defined(WOLFSSL_MLDSA_PUBLIC_KEY) +/* Verify a "pure" ML-DSA SignedData signature (RFC 9882) over the supplied + * message (the DER SET OF signed attributes, or the eContent when none are + * present). Tries each certificate in the bundle, mirroring the RSA/ECDSA + * verify helpers, and uses an empty context string. + * + * returns 0 on success, negative on error */ +static int wc_PKCS7_MlDsaVerify(wc_PKCS7* pkcs7, byte* sig, int sigSz, + const byte* msg, word32 msgSz) +{ + int ret = 0, i; + int res = 0; + int verified = 0; + byte level = 0; + /* wc_MlDsaKey embeds full key buffers (several KB), so it is always heap + * allocated rather than placed on the stack even in non-WOLFSSL_SMALL_STACK + * builds, to keep the stack footprint small on constrained targets. This + * matches the ML-DSA key handling in asn.c. */ + wc_MlDsaKey* key = NULL; + WC_DECLARE_VAR(dCert, DecodedCert, 1, 0); + word32 idx; + + if (pkcs7 == NULL || sig == NULL || msg == NULL) { + return BAD_FUNC_ARG; + } + + key = (wc_MlDsaKey*)XMALLOC(sizeof(wc_MlDsaKey), pkcs7->heap, + DYNAMIC_TYPE_MLDSA); + if (key == NULL) { + return MEMORY_E; + } + + WC_ALLOC_VAR_EX(dCert, DecodedCert, 1, pkcs7->heap, DYNAMIC_TYPE_DCERT, + { XFREE(key, pkcs7->heap, DYNAMIC_TYPE_MLDSA); + return MEMORY_E; }); + + /* loop over certs received in certificates set, try to find one + * that will validate signature */ + for (i = 0; i < MAX_PKCS7_CERTS; i++) { + + verified = 0; + idx = 0; + + if (pkcs7->certSz[i] == 0) + continue; + + ret = wc_MlDsaKey_Init(key, pkcs7->heap, pkcs7->devId); + if (ret != 0) { + /* Hard internal failure (e.g. MEMORY_E): return it directly so it + * is not masked as SIG_VERIFY_E by the post-loop check. */ + XFREE(key, pkcs7->heap, DYNAMIC_TYPE_MLDSA); + WC_FREE_VAR_EX(dCert, pkcs7->heap, DYNAMIC_TYPE_DCERT); + return ret; + } + + InitDecodedCert(dCert, pkcs7->cert[i], pkcs7->certSz[i], pkcs7->heap); + +#ifdef WC_ASN_UNKNOWN_EXT_CB + if (pkcs7->unknownExtCallback != NULL) + wc_SetUnknownExtCallback(dCert, pkcs7->unknownExtCallback); +#endif + + /* not verifying, only using this to extract public key */ + ret = ParseCert(dCert, CA_TYPE, NO_VERIFY, 0); + if (ret < 0) { + WOLFSSL_MSG("ASN ML-DSA cert parse error"); + FreeDecodedCert(dCert); + wc_MlDsaKey_Free(key); + continue; + } + + /* Only try the certificate identified by the SignerInfo sid. */ + if (pkcs7->signerInfo != NULL && pkcs7->signerInfo->sid != NULL && + !wc_PKCS7_CertMatchesSignerInfo(pkcs7, dCert)) { + FreeDecodedCert(dCert); + wc_MlDsaKey_Free(key); + continue; + } + + /* Defense in depth: reject SPKIs that are not the expected ML-DSA + * type before attempting the key decode. */ + if (dCert->keyOID != pkcs7->publicKeyOID || + wc_PKCS7_MlDsaLevelFromOID(dCert->keyOID, &level) != 0) { + FreeDecodedCert(dCert); + wc_MlDsaKey_Free(key); + continue; + } + + ret = wc_MlDsaKey_SetParams(key, level); + if (ret == 0) { + ret = wc_MlDsaKey_PublicKeyDecode(key, dCert->publicKey, + dCert->pubKeySize, &idx); + } + if (ret < 0) { + /* Try the next candidate cert on any decode error. A hard failure + * (e.g. MEMORY_E) is intentionally masked as SIG_VERIFY_E by the + * post-loop check, matching the RSA/ECC verify helpers. */ + WOLFSSL_MSG("ASN ML-DSA key decode error"); + FreeDecodedCert(dCert); + wc_MlDsaKey_Free(key); + continue; + } + + /* RFC 9882: pure ML-DSA with an empty context string. */ + res = 0; + ret = wc_MlDsaKey_VerifyCtx(key, sig, (word32)sigSz, NULL, 0, + msg, msgSz, &res); + + if (ret == 0 && res == 1) { + /* found signer that successfully verified signature */ + verified = 1; + XMEMCPY(pkcs7->issuerSubjKeyId, dCert->extSubjKeyId, KEYID_SIZE); + pkcs7->verifyCert = pkcs7->cert[i]; + pkcs7->verifyCertSz = pkcs7->certSz[i]; + } + + wc_MlDsaKey_Free(key); + FreeDecodedCert(dCert); + + if (ret == 0 && res == 1) { + break; + } + } + + if (verified == 0) { + ret = SIG_VERIFY_E; + } + + XFREE(key, pkcs7->heap, DYNAMIC_TYPE_MLDSA); + WC_FREE_VAR_EX(dCert, pkcs7->heap, DYNAMIC_TYPE_DCERT); + + return ret; +} + +#endif /* WC_PKCS7_HAVE_MLDSA && !WOLFSSL_MLDSA_NO_VERIFY && + * WOLFSSL_MLDSA_PUBLIC_KEY */ + + /* build SignedData digest, both in PKCS#7 DigestInfo format and * as plain digest for CMS. * @@ -5382,6 +5967,23 @@ static int wc_PKCS7_SignedDataVerifySignature(wc_PKCS7* pkcs7, byte* sig, if (!haveCert) { WOLFSSL_MSG("No certificates in bundle to verify signature"); +#if defined(WC_PKCS7_HAVE_MLDSA) + /* ML-DSA (RFC 9882) signs the complete message in pure mode, not a + * digest. The stored plainDigest/pkcs7Digest is therefore not + * enough for an external verifier to reconstruct and check the + * signature, so reject here instead of returning unusable material + * via PKCS7_SIGNEEDS_CHECK. */ + if (pkcs7->publicKeyOID == ML_DSA_44k || + pkcs7->publicKeyOID == ML_DSA_65k || + pkcs7->publicKeyOID == ML_DSA_87k) { + WOLFSSL_MSG("ML-DSA external verify without a certificate in " + "the bundle is not supported"); + WC_FREE_VAR_EX(pkcs7Digest, pkcs7->heap, + DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } +#endif + /* store signature */ XFREE(pkcs7->signature, pkcs7->heap, DYNAMIC_TYPE_SIGNATURE); pkcs7->signature = NULL; @@ -5461,6 +6063,31 @@ static int wc_PKCS7_SignedDataVerifySignature(wc_PKCS7* pkcs7, byte* sig, break; #endif +#if defined(WC_PKCS7_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + defined(WOLFSSL_MLDSA_PUBLIC_KEY) + case ML_DSA_44k: + case ML_DSA_65k: + case ML_DSA_87k: + { + /* RFC 9882: ML-DSA verifies over the complete message, not a + * digest. Rebuild the same octet string that was signed. */ + byte* msg = NULL; + word32 msgSz = 0; + int msgAlloc = 0; + + ret = wc_PKCS7_BuildPureSigMessage(pkcs7, signedAttrib, + signedAttribSz, &msg, &msgSz, + &msgAlloc); + if (ret == 0) { + ret = wc_PKCS7_MlDsaVerify(pkcs7, sig, (int)sigSz, msg, msgSz); + if (msgAlloc) { + XFREE(msg, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + } + } + break; + } +#endif + default: WOLFSSL_MSG("Unsupported public key type"); ret = BAD_FUNC_ARG; @@ -5543,6 +6170,16 @@ static int wc_PKCS7_SetPublicKeyOID(wc_PKCS7* pkcs7, int sigOID) break; #endif + #ifdef WC_PKCS7_HAVE_MLDSA + /* RFC 9882: the ML-DSA signatureAlgorithm OID is the key OID itself + * (CTC_ML_DSA_* and ML_DSA_*k share the same OID sum value). */ + case ML_DSA_44k: + case ML_DSA_65k: + case ML_DSA_87k: + pkcs7->publicKeyOID = (word32)sigOID; + break; + #endif + default: WOLFSSL_MSG("Unsupported public key algorithm"); return ASN_SIG_KEY_E; diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index ed80cd4e96d..fe0ffe96dfe 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -67060,6 +67060,12 @@ static wc_test_ret_t pkcs7signed_run_vectors( XMEMSET(&rng, 0, sizeof(rng)); + /* These attribute blobs are only referenced by the RSA/ECC test vectors + * below. Keep them used so a build without RSA and ECC (e.g. ML-DSA only) + * does not warn on unused variables. */ + (void)attribs; + (void)customContentType; + testVectors = (pkcs7SignedVector *)XMALLOC(MAX_TESTVECTORS_LEN * sizeof(*testVectors), HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (testVectors == NULL) { @@ -67566,6 +67572,15 @@ static wc_test_ret_t pkcs7signed_run_SingleShotVectors( XMEMSET(&rng, 0, sizeof(rng)); + /* These blobs are only referenced by the RSA/ECC test vectors below. Keep + * them used so a build without RSA and ECC (e.g. ML-DSA only) does not + * warn on unused variables. */ + (void)attribs; +#if !defined(NO_PKCS7_ENCRYPTED_DATA) && \ + defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_256) + (void)aes256Key; +#endif + testVectors = (pkcs7SignedVector *)XMALLOC(MAX_TESTVECTORS_LEN * sizeof(*testVectors), HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (testVectors == NULL) { @@ -69046,6 +69061,370 @@ 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) && \ + !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + !defined(NO_FILESYSTEM) && !defined(NO_ASN) + +typedef struct { + const char* certFile; /* signer certificate, DER */ + const char* keyFile; /* matching ML-DSA private key, PKCS#8 DER */ + int hashOID; /* message-digest algorithm for signed attrs */ + int noSignedAttribs; /* 1: exercise the pure-eContent path */ +} pkcs7MlDsaVector; + +#if defined(WOLFSSL_SHA3) && \ + (defined(WOLFSSL_SHAKE256) || defined(WOLFSSL_SHAKE128)) +/* RFC 8702: a SHAKE digest AlgorithmIdentifier MUST omit the parameters + * field. A digest algorithm appears in a CMS SignedData both in the + * SignedData.digestAlgorithms SET and in the SignerInfo.digestAlgorithm, so + * every occurrence must be checked. Walk the buffer for each + * "SEQUENCE { OID oidDer }" (anchored on the SEQUENCE tag to avoid matching + * the OID bytes inside signatures/keys) and confirm the SEQUENCE holds the + * OID only, i.e. the (short-form) SEQUENCE length equals the OID length so the + * parameters field is absent. *found receives the number of digest + * AlgorithmIdentifiers inspected; the return value is the number that carry a + * (non-absent) parameters field. */ +static int pkcs7_digest_oid_params_present(const byte* buf, word32 bufSz, + const byte* oidDer, word32 oidDerSz, int* found) +{ + int present = 0; + word32 i; + *found = 0; + if (oidDerSz == 0 || bufSz < oidDerSz + 2) + return 0; + for (i = 2; i + oidDerSz <= bufSz; i++) { + word32 seqLen; + /* AlgorithmIdentifier ::= SEQUENCE { OBJECT IDENTIFIER, params } is + * encoded as 0x30 [params]; oidDer begins with its own + * 0x06 tag, so the SEQUENCE tag sits two octets before the match. */ + if (buf[i - 2] != 0x30 || XMEMCMP(buf + i, oidDer, oidDerSz) != 0) + continue; + /* Validate the wrapper so the OID bytes appearing by chance inside a + * signature or key are not counted as a digest AlgorithmIdentifier: + * the short-form SEQUENCE length must span exactly the OID (params + * absent) or the OID plus a 2-byte NULL (05 00, the non-compliant + * shape this check is meant to catch). */ + seqLen = buf[i - 1]; + if (seqLen == oidDerSz) { + (*found)++; /* params absent - RFC 8702 compliant */ + } + else if (seqLen == oidDerSz + 2 && i + oidDerSz + 2 <= bufSz && + buf[i + oidDerSz] == 0x05 && buf[i + oidDerSz + 1] == 0x00) { + (*found)++; + present++; /* explicit NULL params - non-compliant */ + } + } + return present; +} +#endif + +/* Round-trip (encode then verify) test of CMS/PKCS#7 SignedData using ML-DSA + * signatures, per RFC 9882. Exercises each enabled ML-DSA parameter set with + * the strongest enabled non-SHAKE digest (SHA-512 when available) and, when + * available, a SHAKE256 message digest. */ +static wc_test_ret_t pkcs7signed_mldsa_test(void) +{ + wc_test_ret_t ret = 0; + WC_RNG rng; + wc_PKCS7* pkcs7 = NULL; + XFILE f = NULL; + byte* cert = NULL; + byte* key = NULL; + byte* out = NULL; + word32 certSz, keySz; + word32 outHeadSz, outFootSz; + int encodedSz; + int i, testSz; + int rngInit = 0; + byte preHash[WC_MAX_DIGEST_SIZE]; + + /* "Hello PQC" */ + WOLFSSL_SMALL_STACK_STATIC const byte content[] = { + 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x50,0x51,0x43 + }; + + #define MLDSA_CERT(n) CERT_ROOT "mldsa" CERT_PATH_SEP "mldsa" n "-cert.der" + #define MLDSA_KEY(n) CERT_ROOT "mldsa" CERT_PATH_SEP "mldsa" n "-key.der" + + /* RFC 9882 recommends SHA-512, but ML-DSA uses SHA-3/SHAKE internally and + * does not require SHA-512, and PKCS#7 only requires SHA-1 or SHA-256. + * Track the strongest non-SHAKE digest actually enabled so the test builds + * in configurations that disable SHA-512. */ + #if defined(WOLFSSL_SHA512) + #define MLDSA_TEST_HASH SHA512h + #elif !defined(NO_SHA256) + #define MLDSA_TEST_HASH SHA256h + #else + #define MLDSA_TEST_HASH SHAh + #endif + + /* one row per (level, message-digest) combination that is enabled; + * MLDSA_TEST_HASH (SHA-512 when enabled, per the RFC 9882 recommendation) + * always, plus SHAKE256 to exercise the SHAKE digest-OID path. SHAKE128 + * (128-bit collision strength) is only + * paired with ML-DSA-44 (NIST level 2), where the message-digest strength + * matches the signature; pairing it with the stronger levels would weaken + * the content binding, so it is intentionally not used there. */ + pkcs7MlDsaVector vectors[12]; + testSz = 0; + XMEMSET(vectors, 0, sizeof(vectors)); + +#ifndef WOLFSSL_NO_ML_DSA_44 + vectors[testSz].certFile = MLDSA_CERT("44"); + vectors[testSz].keyFile = MLDSA_KEY("44"); + vectors[testSz].hashOID = MLDSA_TEST_HASH; + testSz++; + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE256) + vectors[testSz].certFile = MLDSA_CERT("44"); + vectors[testSz].keyFile = MLDSA_KEY("44"); + vectors[testSz].hashOID = SHAKE256h; + testSz++; + #endif + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE128) + vectors[testSz].certFile = MLDSA_CERT("44"); + vectors[testSz].keyFile = MLDSA_KEY("44"); + vectors[testSz].hashOID = SHAKE128h; + testSz++; + #endif +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 + vectors[testSz].certFile = MLDSA_CERT("65"); + vectors[testSz].keyFile = MLDSA_KEY("65"); + vectors[testSz].hashOID = MLDSA_TEST_HASH; + testSz++; + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE256) + vectors[testSz].certFile = MLDSA_CERT("65"); + vectors[testSz].keyFile = MLDSA_KEY("65"); + vectors[testSz].hashOID = SHAKE256h; + testSz++; + #endif +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 + vectors[testSz].certFile = MLDSA_CERT("87"); + vectors[testSz].keyFile = MLDSA_KEY("87"); + vectors[testSz].hashOID = MLDSA_TEST_HASH; + testSz++; +#endif + + /* Additionally exercise the no-signed-attributes (pure eContent) encode and + * verify path (RFC 9882), which signs over pkcs7->content directly instead + * of a reconstructed signed-attributes SET. One enabled level is enough to + * cover the branch. */ +#if !defined(WOLFSSL_NO_ML_DSA_44) || !defined(WOLFSSL_NO_ML_DSA_65) || \ + !defined(WOLFSSL_NO_ML_DSA_87) + #ifndef WOLFSSL_NO_ML_DSA_44 + vectors[testSz].certFile = MLDSA_CERT("44"); + vectors[testSz].keyFile = MLDSA_KEY("44"); + #elif !defined(WOLFSSL_NO_ML_DSA_65) + vectors[testSz].certFile = MLDSA_CERT("65"); + vectors[testSz].keyFile = MLDSA_KEY("65"); + #else + vectors[testSz].certFile = MLDSA_CERT("87"); + vectors[testSz].keyFile = MLDSA_KEY("87"); + #endif + vectors[testSz].hashOID = MLDSA_TEST_HASH; + vectors[testSz].noSignedAttribs = 1; + testSz++; +#endif + + XMEMSET(&rng, 0, sizeof(rng)); + + cert = (byte*)XMALLOC(FOURK_BUF * 2, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + key = (byte*)XMALLOC(FOURK_BUF * 2, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + out = (byte*)XMALLOC(FOURK_BUF * 5, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (cert == NULL || key == NULL || out == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + rngInit = 1; + + /* at least one ML-DSA level must be enabled, else the test would pass + * without exercising anything */ + if (testSz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + + for (i = 0; i < testSz; i++) { + /* load signer certificate (DER) */ + f = XFOPEN(vectors[i].certFile, "rb"); + if (f == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + certSz = (word32)XFREAD(cert, 1, FOURK_BUF * 2, f); + XFCLOSE(f); + f = NULL; + if (certSz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + + /* load matching ML-DSA private key (PKCS#8 DER) */ + f = XFOPEN(vectors[i].keyFile, "rb"); + if (f == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + keySz = (word32)XFREAD(key, 1, FOURK_BUF * 2, f); + XFCLOSE(f); + f = NULL; + if (keySz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + + /* --- encode SignedData --- */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + pkcs7->rng = &rng; + pkcs7->content = (byte*)content; + pkcs7->contentSz = (word32)sizeof(content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = vectors[i].hashOID; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + + if (vectors[i].noSignedAttribs) { + ret = wc_PKCS7_SetDefaultSignedAttribs(pkcs7, WOLFSSL_NO_ATTRIBUTES); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + } + + encodedSz = wc_PKCS7_EncodeSignedData(pkcs7, out, FOURK_BUF * 5); + if (encodedSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encodedSz), out_lbl); + + /* RFC 8702: a SHAKE digest algorithm must be encoded with absent + * parameters, not NULL. Confirm both the SignedData.digestAlgorithms + * and the SignerInfo.digestAlgorithm occurrences comply. */ + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE256) + if (vectors[i].hashOID == SHAKE256h) { + static const byte shake256OidDer[] = { 0x06,0x09,0x60,0x86,0x48, + 0x01,0x65,0x03,0x04,0x02,0x0c }; + int found = 0; + if (pkcs7_digest_oid_params_present(out, (word32)encodedSz, + shake256OidDer, (word32)sizeof(shake256OidDer), &found) != 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + if (found < 2) /* digestAlgorithms SET + SignerInfo must be seen */ + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + } + #endif + #if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE128) + if (vectors[i].hashOID == SHAKE128h) { + static const byte shake128OidDer[] = { 0x06,0x09,0x60,0x86,0x48, + 0x01,0x65,0x03,0x04,0x02,0x0b }; + int found = 0; + if (pkcs7_digest_oid_params_present(out, (word32)encodedSz, + shake128OidDer, (word32)sizeof(shake128OidDer), &found) != 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + if (found < 2) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + } + #endif + + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* --- verify SignedData (signer cert is embedded in the bundle) --- */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + + ret = wc_PKCS7_InitWithCert(pkcs7, NULL, 0); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encodedSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + /* content should be recovered and match what was signed */ + if (pkcs7->contentSz != (word32)sizeof(content) || + XMEMCMP(pkcs7->content, content, sizeof(content)) != 0) { + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + } + + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* --- negative case: tamper with the signature and confirm the + * verifier rejects it with SIG_VERIFY_E rather than accepting it. + * The ML-DSA signature value is the last element of the bundle, so + * flipping its final byte corrupts the signature while leaving the + * ASN.1 structure and the signed content intact, exercising the + * wc_PKCS7_MlDsaVerify rejection path. */ + out[encodedSz - 1] ^= 0xFF; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + + ret = wc_PKCS7_InitWithCert(pkcs7, NULL, 0); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encodedSz); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + ret = 0; + + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + } + + /* negative: ML-DSA signs the full message in pure mode (RFC 9882), so a + * caller-supplied pre-computed content hash is meaningless and must be + * rejected. Confirm wc_PKCS7_EncodeSignedData_ex returns BAD_FUNC_ARG + * instead of producing a signature. Reuses the last vector's cert/key. */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + pkcs7->rng = &rng; + pkcs7->content = (byte*)content; + pkcs7->contentSz = (word32)sizeof(content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = MLDSA_TEST_HASH; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + + XMEMSET(preHash, 0, sizeof(preHash)); + outHeadSz = FOURK_BUF * 5; + outFootSz = FOURK_BUF * 5; + ret = wc_PKCS7_EncodeSignedData_ex(pkcs7, preHash, (word32)sizeof(preHash), + out, &outHeadSz, out, &outFootSz); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + ret = 0; + +out_lbl: + if (f != NULL) + XFCLOSE(f); + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(cert, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + #undef MLDSA_CERT + #undef MLDSA_KEY + #undef MLDSA_TEST_HASH + + return ret; +} + +#endif /* WOLFSSL_HAVE_MLDSA && sign && verify && filesystem */ + + WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) { wc_test_ret_t ret = 0; @@ -69253,6 +69632,14 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) rsaServerCertBuf, (word32)rsaServerCertBufSz, rsaCaCertBuf, (word32)rsaCaCertBufSz); #endif + +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + !defined(NO_FILESYSTEM) && !defined(NO_ASN) + if (ret >= 0) + ret = pkcs7signed_mldsa_test(); +#endif + XFREE(rsaClientCertBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); XFREE(rsaClientPrivKeyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); XFREE(rsaServerCertBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/wolfssl/wolfcrypt/pkcs7.h b/wolfssl/wolfcrypt/pkcs7.h index 43d305597d7..6c37344c0b7 100644 --- a/wolfssl/wolfcrypt/pkcs7.h +++ b/wolfssl/wolfcrypt/pkcs7.h @@ -318,6 +318,9 @@ struct wc_PKCS7 { int devId; /* device ID for HW based private key */ byte issuerHash[KEYID_SIZE]; /* hash of all alt Names */ byte issuerSn[MAX_SN_SZ]; /* singleCert's serial number */ + /* Signer public key, stored only for RSA/ECC (consumed by the raw-sign + * callback paths). PQC keys (e.g. ML-DSA) are large and never read back + * from here, so they are not stored and this stays RSA-sized. */ byte publicKey[MAX_RSA_INT_SZ + MAX_RSA_E_SZ]; /* MAX RSA key size (m + e)*/ word32 certSz[MAX_PKCS7_CERTS]; diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h index f17c69d6dd5..4c449512698 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -5454,8 +5454,13 @@ blinding by defining WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS." #if defined(HAVE_OCSP) && defined(NO_RSA) && !defined(HAVE_ECC) #error "OCSP (HAVE_OCSP) requires RSA or ECC (HAVE_ECC)" #endif -#if defined(HAVE_PKCS7) && defined(NO_RSA) && !defined(HAVE_ECC) - #error "PKCS7 (HAVE_PKCS7) requires RSA or ECC (HAVE_ECC)" +#if defined(HAVE_PKCS7) && defined(NO_RSA) && !defined(HAVE_ECC) && \ + (!defined(WOLFSSL_HAVE_MLDSA) || defined(WOLFSSL_MLDSA_NO_ASN1)) + /* The ML-DSA-only escape hatch requires ML-DSA ASN.1 support: pkcs7.c only + * compiles its ML-DSA SignedData code when WC_PKCS7_HAVE_MLDSA is defined, + * which needs !WOLFSSL_MLDSA_NO_ASN1. Without it, PKCS7 would build with no + * usable signature algorithm. */ + #error "PKCS7 (HAVE_PKCS7) requires RSA, ECC (HAVE_ECC), or ML-DSA with ASN.1 (not WOLFSSL_MLDSA_NO_ASN1)" #endif #if defined(HAVE_PKCS7) && defined(NO_SHA) && defined(NO_SHA256) #error "PKCS7 (HAVE_PKCS7) requires SHA or SHA-256"