From 5c44b2afc0065a509de339bd2de1b362bcfcd96f 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. - 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. wolfssl/wolfcrypt/pkcs7.h: - Gate the cached signer public key buffer (publicKey/publicKeySz) behind the new WOLFSSL_PKCS7_HAVE_RAW_SIGN_CALLBACK macro. It is consumed only by the RSA/ECC raw-sign callback paths, so large PQC keys are no longer copied into the fixed RSA-sized buffer. 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. 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. --- 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 wolfcrypt/src/hash.c | 14 + wolfcrypt/src/pkcs7.c | 620 ++++++++++++++++++++++++++++++++++-- wolfcrypt/test/test.c | 294 +++++++++++++++++ wolfssl/wolfcrypt/pkcs7.h | 3 + 9 files changed, 932 insertions(+), 20 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/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 1d6588ee053..bb614419e55 100644 --- a/certs/mldsa/include.am +++ b/certs/mldsa/include.am @@ -26,12 +26,15 @@ 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/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..0db2300615b3a6dcdb19729fec25b1812cfa1d36 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..9254fb452a877c3c560c2bbd54d62f0cd27aac70 GIT binary patch literal 4060 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 literal 0 HcmV?d00001 diff --git a/certs/mldsa/mldsa87-key.der b/certs/mldsa/mldsa87-key.der new file mode 100644 index 0000000000000000000000000000000000000000..08cb34b89f9e4088b49c05694296ae1d4ad8b798 GIT binary patch literal 4924 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 literal 0 HcmV?d00001 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 b04504a0054..4817591f479 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 @@ -1033,6 +1048,36 @@ static int wc_PKCS7_RecipientListVersionsAllZero(wc_PKCS7* pkcs7) return 1; } +#if 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. * @@ -1124,6 +1169,35 @@ 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 } @@ -1237,16 +1311,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 @@ -1586,7 +1676,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]; @@ -1953,7 +2051,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 @@ -2036,7 +2134,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, @@ -2140,7 +2238,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, @@ -2232,6 +2330,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; @@ -2495,6 +2622,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"); @@ -2596,6 +2734,173 @@ 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. */ + 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) { + ret = wc_MlDsaKey_PrivateKeyDecode(key, pkcs7->privateKey, + pkcs7->privateKeySz, &idx); + } + 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 @@ -2660,7 +2965,7 @@ 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); break; @@ -2684,11 +2989,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; @@ -2709,6 +3014,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; @@ -3042,6 +3373,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; @@ -3078,6 +3410,21 @@ 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. */ + 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))); @@ -3223,7 +3570,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 */ @@ -3291,19 +3638,54 @@ 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, RSA-PSS, 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, and any 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, @@ -3332,7 +3714,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); @@ -3635,6 +4017,13 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, } if (hashBuf == NULL && pkcs7->sidType != DEGENERATE_SID) { + /* Only the deterministic-size algorithms (RSA, RSA-PSS, ML-DSA) reach + * this final signing pass; ECDSA requires a pre-supplied hash and signs + * earlier. 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, @@ -3643,6 +4032,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, @@ -3688,6 +4085,18 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, XFREE(flatSignedAttribs, pkcs7->heap, DYNAMIC_TYPE_PKCS7); + /* free the heap-allocated signature buffer. esd is a stack object unless + * WOLFSSL_SMALL_STACK, where it is heap allocated and may be NULL if an + * early allocation failed. */ +#ifdef WOLFSSL_SMALL_STACK + if (esd != NULL && esd->encContentDigest != NULL) +#else + if (esd->encContentDigest != NULL) +#endif + { + XFREE(esd->encContentDigest, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + esd->encContentDigest = NULL; + } WC_FREE_VAR_EX(esd, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); WC_FREE_VAR_EX(signedDataOid, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -4902,6 +5311,142 @@ 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) { + 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. * @@ -5307,6 +5852,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; @@ -5389,6 +5959,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 e95bba5ae5e..c0bf4109877 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -67438,6 +67438,293 @@ static wc_test_ret_t pkcs7signed_run_SingleShotVectors( } +#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 */ +} 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 + * both a SHA-512 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; + int encodedSz; + int i, testSz; + int rngInit = 0; + + /* "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" + + /* one row per (level, message-digest) combination that is enabled; + * SHA-512 always (RFC 9882 recommendation), 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; + +#ifndef WOLFSSL_NO_ML_DSA_44 + vectors[testSz].certFile = MLDSA_CERT("44"); + vectors[testSz].keyFile = MLDSA_KEY("44"); + vectors[testSz].hashOID = SHA512h; + 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 = SHA512h; + 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 = SHA512h; + 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; + + 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; + } + + 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 + + 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; @@ -67568,6 +67855,13 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); #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 93c9c6a5d79..20c863b3bce 100644 --- a/wolfssl/wolfcrypt/pkcs7.h +++ b/wolfssl/wolfcrypt/pkcs7.h @@ -293,6 +293,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];