From 11f71108bab25013e683dd9e2ecab76db80a8621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 13:26:04 +0200 Subject: [PATCH] PKCS#7: support SignedData with absent eContent Allow encoding and verifying a CMS SignedData whose encapContentInfo carries no eContent, that is, a signed-attributes-only signature over empty content (RFC 5652 makes eContent OPTIONAL). This is required for SCEP CertRep PENDING and FAILURE messages (RFC 8894 section 3.2.2), which must omit the pkcsPKIEnvelope entirely. Encode: wc_PKCS7_EncodeSignedData computes the messageDigest over the empty content when detached is set and contentSz is 0, since there is no eContent to drive the normal content-hashing pass. Verify: PKCS7_VerifySignedData no longer rejects an absent eContent when no external content or hash was supplied. It is processed as a detached signature over empty content, and wc_PKCS7_VerifyContentMessageDigest computes the digest of zero-length content using the parsed digest algorithm. The messageDigest comparison still rejects a stripped non-empty eContent. Add pkcs7_signed_no_content_test, a round-trip over a CMS SignedData whose encapContentInfo carries no eContent (a detached signature over empty content, signed-attributes-only), as produced by SCEP CertRep PENDING/FAILURE messages. The encode omits the eContent and the verify accepts it without any caller-supplied content or hash, checking the messageDigest against the hash of empty content. Run for RSA/SHA-256. --- wolfcrypt/src/pkcs7.c | 48 ++++++++++++--- wolfcrypt/test/test.c | 138 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 10 deletions(-) diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index d37773cfa6..107c555077 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -3864,11 +3864,17 @@ int wc_PKCS7_EncodeSignedData(wc_PKCS7* pkcs7, byte* output, word32 outputSz) return BAD_FUNC_ARG; } - /* pre-calculate content hash for ECDSA and RSA-PSS (both sign digest directly) */ + /* pre-calculate content hash for ECDSA and RSA-PSS (both sign digest + * directly). + * A detached SignedData over empty content has no eContent to drive the + * normal content-hashing pass, so its messageDigest signed attribute is + * computed here as the hash of the empty content (e.g. an SCEP CertRep + * PENDING/FAILURE per RFC 8894) */ if (pkcs7->publicKeyOID == ECDSAk #ifdef WC_RSA_PSS || pkcs7->publicKeyOID == RSAPSSk #endif + || (pkcs7->detached && pkcs7->contentSz == 0) ) { int hashSz; enum wc_HashType hashType; @@ -3890,7 +3896,9 @@ int wc_PKCS7_EncodeSignedData(wc_PKCS7* pkcs7, byte* output, word32 outputSz) ret = wc_HashInit(hash, hashType); if (ret == 0) { ret = wc_HashUpdate(hash, hashType, - pkcs7->content, pkcs7->contentSz); + (pkcs7->content != NULL) ? pkcs7->content + : (const byte*)"", + (pkcs7->content != NULL) ? pkcs7->contentSz : 0); if (ret == 0) { ret = wc_HashFinal(hash, hashType, hashBuf); } @@ -5071,11 +5079,10 @@ static int wc_PKCS7_VerifyContentMessageDigest(wc_PKCS7* pkcs7, if (pkcs7 == NULL) return BAD_FUNC_ARG; - if ((pkcs7->content == NULL || pkcs7->contentSz == 0) && - (hashBuf == NULL || hashSz == 0)) { - WOLFSSL_MSG("SignedData bundle has no content or hash to verify"); - return BAD_FUNC_ARG; - } + /* An absent eContent with no caller-supplied hash is valid: the content + * is empty, so the messageDigest attribute is verified against the hash of + * zero-length content below (e.g. an SCEP CertRep PENDING/FAILURE per + * RFC 8894). Stripping a non-empty eContent still fails this comparison. */ /* lookup messageDigest attribute */ attrib = findAttrib(pkcs7, mdOid, sizeof(mdOid)); @@ -5112,7 +5119,14 @@ static int wc_PKCS7_VerifyContentMessageDigest(wc_PKCS7* pkcs7, content = pkcs7->content; contentLen = (int)pkcs7->contentSz; - if (pkcs7->contentIsPkcs7Type == 1) { + /* An absent eContent is hashed as empty content. Guard against a + * NULL content pointer paired with a non-zero size: hash zero-length + * content rather than dereferencing NULL or reading past the empty + * literal used at the wc_Hash call below. */ + if (content == NULL) + contentLen = 0; + + if (content != NULL && pkcs7->contentIsPkcs7Type == 1) { /* Content follows PKCS#7 RFC, which defines type as ANY. CMS * mandates OCTET_STRING which has already been stripped off. * For PKCS#7 message digest calculation, digest is calculated @@ -5130,8 +5144,10 @@ static int wc_PKCS7_VerifyContentMessageDigest(wc_PKCS7* pkcs7, } } - ret = wc_Hash(hashType, content + contentIdx, (word32)contentLen, digest, - MAX_PKCS7_DIGEST_SZ); + ret = wc_Hash(hashType, + (content != NULL) ? content + contentIdx + : (const byte*)"", + (word32)contentLen, digest, MAX_PKCS7_DIGEST_SZ); if (ret < 0) { WOLFSSL_MSG("Error hashing PKCS7 content for verification"); WC_FREE_VAR_EX(digest, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -6030,6 +6046,7 @@ static int PKCS7_VerifySignedData(wc_PKCS7* pkcs7, const byte* hashBuf, word32 certIdx, certIdx2; byte degenerate = 0; byte detached = 0; + byte noContent = 0; byte tag = 0; word16 contentIsPkcs7Type = 0; #ifdef ASN_BER_TO_DER @@ -6359,6 +6376,7 @@ static int PKCS7_VerifySignedData(wc_PKCS7* pkcs7, const byte* hashBuf, if ((encapContentInfoLen != 0) && ((word32)encapContentInfoLen - contentTypeSz == 0)) { ret = ASN_PARSE_E; + noContent = 1; #ifndef NO_PKCS7_STREAM pkcs7->stream->noContent = 1; #endif @@ -6542,6 +6560,16 @@ static int PKCS7_VerifySignedData(wc_PKCS7* pkcs7, const byte* hashBuf, detached = 1; } + /* eContent genuinely absent and no external content/hash + * supplied: verify as a detached signature over empty content. + * The messageDigest signed attribute is checked against the + * hash of zero-length content downstream, so a stripped + * non-empty eContent still fails. */ + if (!degenerate && !detached && noContent) { + WOLFSSL_MSG("Processing as detached signature, no content"); + detached = 1; + } + if (!degenerate && !detached && ret != 0) break; diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 0bf7595b06..9e026fc5b4 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -67613,6 +67613,136 @@ static wc_test_ret_t pkcs7signed_run_SingleShotVectors( } +#if !defined(NO_RSA) && !defined(NO_SHA256) +/* Round-trip test of a CMS SignedData whose encapContentInfo carries no + * eContent: a signed-attributes-only signature over empty content, as used by + * SCEP CertRep PENDING/FAILURE (RFC 8894 section 3.2.2). The encoder must omit + * the eContent and compute the messageDigest over the empty content; the + * verifier must accept the absent eContent and check that digest without any + * caller-supplied content or hash. */ +static wc_test_ret_t pkcs7_signed_no_content_test(byte* cert, word32 certSz, + byte* key, word32 keySz) +{ + wc_test_ret_t ret = 0; + wc_PKCS7* pkcs7 = NULL; + WC_RNG rng; + int rngInit = 0; + byte* out = NULL; + int encSz = 0; + const word32 outSz = FOURK_BUF; + static const byte content[] = "non-empty content that gets stripped"; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (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; + + 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 = NULL; /* no eContent */ + pkcs7->contentSz = 0; + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + + /* detached signature with empty content -> absent eContent on the wire */ + ret = wc_PKCS7_SetDetached(pkcs7, 1); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out_lbl); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* Verify with no caller-supplied content or hash. */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + /* the eContent must be reported absent after decode */ + if (pkcs7->content != NULL || pkcs7->contentSz != 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* Negative case: a signature made over non-empty content but transmitted + * with the eContent absent (as if stripped) must be rejected. The + * messageDigest signed attribute covers the real content, so a verifier + * that treats the absent eContent as empty content gets a digest mismatch + * (SIG_VERIFY_E). This locks in the documented security property that a + * stripped non-empty eContent still fails verification. */ + 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)XSTRLEN((const char*)content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + + /* detached over non-empty content -> eContent absent on the wire, while the + * messageDigest attribute still covers the real content */ + ret = wc_PKCS7_SetDetached(pkcs7, 1); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out_lbl); + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out_lbl); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* Verify without supplying the detached content: the absent eContent is + * hashed as empty content, which must not match the messageDigest computed + * over the real content. */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out_lbl); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != SIG_VERIFY_E) + ERROR_OUT(WC_TEST_RET_ENC_NC, out_lbl); + + ret = 0; + +out_lbl: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} +#endif /* !NO_RSA && !NO_SHA256 */ + + WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) { wc_test_ret_t ret = 0; @@ -67743,6 +67873,14 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); #endif +#if !defined(NO_RSA) && !defined(NO_SHA256) + /* SignedData with absent eContent (detached over empty content) */ + if (ret >= 0) + ret = pkcs7_signed_no_content_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); +#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);