diff --git a/.github/workflows/pq-all.yml b/.github/workflows/pq-all.yml index 50b7300529..7805f91d87 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 9cb8a4dc9d..a5a7e11a6d 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 405a085950..cf6489f9ef 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 0000000000..f487a43789 Binary files /dev/null and b/certs/mldsa/mldsa44-key.der differ diff --git a/certs/mldsa/mldsa65-key.der b/certs/mldsa/mldsa65-key.der new file mode 100644 index 0000000000..91ed244731 Binary files /dev/null and b/certs/mldsa/mldsa65-key.der differ diff --git a/certs/mldsa/mldsa87-key.der b/certs/mldsa/mldsa87-key.der new file mode 100644 index 0000000000..73b06b07b4 Binary files /dev/null and b/certs/mldsa/mldsa87-key.der differ diff --git a/certs/renewcerts.sh b/certs/renewcerts.sh index 1cdff5feb4..e0dc5bd010 100755 --- a/certs/renewcerts.sh +++ b/certs/renewcerts.sh @@ -1187,13 +1187,19 @@ EOF ############################################################ #### ML-DSA (FIPS 204) self-signed certificates ### ############################################################ - # ML-DSA requires an OpenSSL 3.x binary with ML-DSA support - # (via oqsprovider or built-in). Detect support by probing candidates. + # ML-DSA requires an OpenSSL 3.5+ binary with the built-in ML-DSA provider. + # Besides key/cert generation the block also produces the expanded-only + # PKCS#8 key.der (-provparam ml-dsa.output_formats=priv, a 3.5+ built-in + # construct) that the PKCS#7 tests decode without keygen-from-seed. The + # probe below requires both keygen and that conversion, so the common + # unsuitable binaries (oqsprovider or pre-3.5, which lack the expanded-only + # conversion) are rejected here and the block is skipped cleanly rather + # than aborting after writing a cert.der but no matching key.der. OPENSSL3="" for candidate in \ - "/usr/local/opt/openssl@3.2/bin/openssl" \ + "/usr/local/opt/openssl@3.5/bin/openssl" \ "/usr/local/opt/openssl@3/bin/openssl" \ - "/opt/homebrew/opt/openssl@3.2/bin/openssl" \ + "/opt/homebrew/opt/openssl@3.5/bin/openssl" \ "/opt/homebrew/opt/openssl@3/bin/openssl" \ "openssl"; do if [ "$candidate" = "openssl" ]; then @@ -1203,7 +1209,23 @@ EOF # Skip non-existent or non-executable absolute paths. [ -x "$candidate" ] || continue fi - if "$candidate" genpkey -algorithm mldsa44 -out /dev/null 2>/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 25028f5758..e7b2a4e786 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 8e8c33c708..ed6342ae4f 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 d7667b9943..882a2af1ae 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 ed80cd4e96..fe0ffe96df 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 43d305597d..6c37344c0b 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 f17c69d6dd..4c44951269 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"