mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-07-10 22:30:50 +02:00
Merge pull request #10759 from Frauschi/pkcs7_mldsa
PKCS#7: add ML-DSA support
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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+:
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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" && \
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user