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 00000000000..0db2300615b 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 00000000000..9254fb452a8 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 00000000000..08cb34b89f9 Binary files /dev/null and b/certs/mldsa/mldsa87-key.der differ 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];