Merge pull request #10759 from Frauschi/pkcs7_mldsa

PKCS#7: add ML-DSA support
This commit is contained in:
David Garske
2026-07-09 10:32:26 -07:00
committed by GitHub
13 changed files with 1148 additions and 41 deletions
+5 -1
View File
@@ -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 \
+18
View File
@@ -9,6 +9,24 @@ File variants, per level N in {44, 65, 87}:
mldsa<N>_oqskeypair.der liboqs concatenated (priv || pub) format
mldsa<N>_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<N>-cert.pem / mldsa<N>-cert.der self-signed ML-DSA certificate
mldsa<N>-key.pem matching private key (PEM,
seed-and-expanded PKCS#8)
mldsa<N>-key.der matching private key (DER,
expanded-only PKCS#8)
The mldsa<N>-key.der files were derived from the matching mldsa<N>-key.pem
using OpenSSL 3.5+, selecting the portable expanded-only private-key shape:
openssl pkey -in mldsa<N>-key.pem \
-provparam ml-dsa.output_formats=priv -outform DER \
-out mldsa<N>-key.der
Unlike the standalone mldsa<N>_priv-only.der vectors above, these correspond
to the public key in mldsa<N>-cert.der.
The *_pub-spki.der files were derived from the matching *_priv-only.der files
using OpenSSL 3.5+:
+3
View File
@@ -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 \
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41 -6
View File
@@ -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
+3 -2
View File
@@ -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" && \
+14
View File
@@ -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;
+667 -30
View File
@@ -73,6 +73,21 @@
#ifdef HAVE_ECC
#include <wolfssl/wolfcrypt/ecc.h>
#endif
#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1)
#include <wolfssl/wolfcrypt/wc_mldsa.h>
/* 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 <wolfssl/wolfcrypt/compress.h>
#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,17 +3803,53 @@ 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 {
esd->encContentDigestBufSz = MAX_ENCRYPTED_KEY_SZ;
}
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;
if (ret < 0) {
idx = ret;
goto out;
}
}
signerInfoSz += flatSignedAttribsSz + esd->signedAttribSetSz;
@@ -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;
+387
View File
@@ -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 @@ out_lbl:
#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 <len> <oidDer> [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);
+3
View File
@@ -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];
+7 -2
View File
@@ -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"