From 366000eec23ad8cd2209278591053c0462021fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 22 Jun 2026 19:22:58 +0200 Subject: [PATCH] PKCS#7: support degenerate certs-only encode and harden signed-attribute handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server-side PKCS#7 encode improvements that let downstream EST/SCEP enrollment code (wolfCert) drive the existing encoder through the public API rather than hand-rolling DER. Everything is gated under the existing HAVE_PKCS7 — no new build options and no new public functions; the convenience wrappers live caller-side. Allow degenerate (certs-only) SignedData encode Relax the hashOID != 0 requirement in PKCS7_EncodeSigned() when sidType == DEGENERATE_SID, so a caller can produce a certs-only bundle (no signer, attributes, or eContent — the form used by EST /cacerts and SCEP GetCACert) by selecting DEGENERATE_SID via wc_PKCS7_SetSignerIdentifierType() and calling wc_PKCS7_EncodeSignedData(). The output round-trips through wc_PKCS7_VerifySignedData(). Size the signed-attribute array to the actual count The SignerInfo attribute working array is now sized to the real attribute count instead of a fixed [7] array. An inline buffer (sized MAX_SIGNED_ATTRIBS_SZ, the historical footprint) covers the common allocation-free case; a heap buffer is used only when the count exceeds it. The default-attribute count comes from a single helper (wc_PKCS7_GetDefaultSignedAttribCount) so the sizing matches the emission logic exactly, and the canned-attribute write is bound-checked against the array capacity. This also fixes a latent overflow where the backing array was hardcoded [7] while the bound check used MAX_SIGNED_ATTRIBS_SZ. The macro is retained for source compatibility but no longer caps the count. Document the decoded-attribute value shape Documented the stable shape of PKCS7DecodedAttrib.value (the contents of the SET OF AttributeValue, outer SET tag stripped) so callers can rely on it. No behavior change. Fix multi-certificate decode in non-streaming builds Bound the additional-certificate loop in wc_PKCS7_VerifySignedData against the absolute end of the certificate set (idx + length) rather than the relative length. In NO_PKCS7_STREAM builds the old bound dropped trailing certificates (all but the first when a large eContent preceded the set), failing verification when the signer cert was among those dropped. Streaming builds were unaffected. Tests Added coverage in pkcs7signed_test: degenerate certs-only encode via the public API, nine-attribute encode (beyond the inline capacity), decoded attribute value shape for PrintableString and OCTET STRING, and a multi-certificate decode regression with large content that triggers the bound bug under NO_PKCS7_STREAM. Added a signed-attribute selection round-trip covering a messageDigest-only subset and the no-attributes case via wc_PKCS7_SetDefaultSignedAttribs/wc_PKCS7_NoDefaultSignedAttribs, a WOLFSSL_NO_MALLOC over-capacity case that must return BUFFER_E instead of overrunning the inline buffer, and a malformed certificate-set length that exercises the certSetEnd clamp in the verifier. Config-sensitive cases are guarded. --- .github/workflows/os-check.yml | 3 + wolfcrypt/src/pkcs7.c | 161 +++++- wolfcrypt/test/test.c | 944 +++++++++++++++++++++++++++++++++ wolfssl/wolfcrypt/pkcs7.h | 25 + 4 files changed, 1119 insertions(+), 14 deletions(-) diff --git a/.github/workflows/os-check.yml b/.github/workflows/os-check.yml index d9e70e0845..1073762444 100644 --- a/.github/workflows/os-check.yml +++ b/.github/workflows/os-check.yml @@ -162,6 +162,9 @@ jobs: "configure": ["--enable-all", "CPPFLAGS=-DWOLFSSL_DEBUG_CERTS"]}, {"name": "all-hash-keep", "minutes": 7.8, "configure": ["--enable-all", "CPPFLAGS=-DWOLFSSL_HASH_KEEP"]}, + {"name": "all-no-pkcs7-stream", "minutes": 7.0, + "comment": "Non-streaming PKCS#7 exercises the multi-certificate decode bound in pkcs7_signed_multi_cert_test(); the streaming default cannot reach that off-by-idx path.", + "configure": ["--enable-all", "CPPFLAGS=-DNO_PKCS7_STREAM"]}, {"name": "all-no-aes-192-256", "minutes": 7.5, "configure": ["--enable-all", "CPPFLAGS=-DNO_AES_192 -DNO_AES_256"]}, {"name": "all-writedup", "minutes": 6.9, diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index 61035e2fa2..5e829783a0 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -1618,7 +1618,14 @@ typedef struct ESD { byte digEncAlgoId[MAX_ALGO_SZ]; #endif byte signedAttribSet[MAX_SET_SZ]; - EncodedAttrib signedAttribs[7]; + /* Working attribute array. signedAttribs points at + * the inline buffer for the common case (no heap use, + * important for no-malloc/static-memory builds) and is + * redirected to a heap allocation only when the + * attribute count exceeds MAX_SIGNED_ATTRIBS_SZ. + * signedAttribsCap holds the usable entry count. */ + EncodedAttrib signedAttribsInline[MAX_SIGNED_ATTRIBS_SZ]; + EncodedAttrib* signedAttribs; byte signerDigest[MAX_OCTET_STR_SZ]; word32 innerOctetsSz, innerContSeqSz, contentInfoSeqSz; word32 outerSeqSz, outerContentSz, innerSeqSz, versionSz, digAlgoIdSetSz, @@ -1627,7 +1634,7 @@ typedef struct ESD { issuerSnSeqSz, issuerNameSz, issuerSnSz, issuerSKIDSz, issuerSKIDSeqSz, signerDigAlgoIdSz, digEncAlgoIdSz, signerDigestSz; word32 encContentDigestSz, signedAttribsSz, signedAttribsCount, - signedAttribSetSz; + signedAttribSetSz, signedAttribsCap; } ESD; @@ -2244,11 +2251,46 @@ static int wc_PKCS7_GetSignSize(wc_PKCS7* pkcs7) } +/* Number of default ("canned") signed attributes that + * wc_PKCS7_BuildSignedAttributes() will emit for the current + * pkcs7->defaultSignedAttribs selection. This is the single source of truth for + * that count: it must stay in lock step with the emission logic in + * wc_PKCS7_BuildSignedAttributes() below, and is used by PKCS7_EncodeSigned() to + * size the working attribute array to the exact count. */ +static word32 wc_PKCS7_GetDefaultSignedAttribCount(const wc_PKCS7* pkcs7) +{ + word32 cnt = 0; + word16 flags; + + if (pkcs7 == NULL) + return 0; + + flags = pkcs7->defaultSignedAttribs; + if (flags == WOLFSSL_NO_ATTRIBUTES) + return 0; + + if ((flags & WOLFSSL_CONTENT_TYPE_ATTRIBUTE) || flags == 0) + cnt++; +#ifndef NO_ASN_TIME + if ((flags & WOLFSSL_SIGNING_TIME_ATTRIBUTE) || flags == 0) + cnt++; +#endif + if ((flags & WOLFSSL_MESSAGE_DIGEST_ATTRIBUTE) || flags == 0) + cnt++; + + return cnt; +} + + /* builds up SignedData signed attributes, including default ones. * * pkcs7 - pointer to initialized PKCS7 structure * esd - pointer to initialized ESD structure, used for output * + * The number of default attributes emitted here must match + * wc_PKCS7_GetDefaultSignedAttribCount(), which sizes the working array; the + * runtime bound-check below turns any drift into BUFFER_E rather than overflow. + * * return 0 on success, negative on error */ static int wc_PKCS7_BuildSignedAttributes(wc_PKCS7* pkcs7, ESD* esd, const byte* contentType, word32 contentTypeSz, @@ -2321,6 +2363,13 @@ static int wc_PKCS7_BuildSignedAttributes(wc_PKCS7* pkcs7, ESD* esd, idx++; } + /* the working array is sized for the canned count by PKCS7_EncodeSigned() + * via wc_PKCS7_GetDefaultSignedAttribCount(); bound-check here so a + * future drift between the two can never overflow it */ + if (esd->signedAttribs == NULL || + atrIdx + idx > esd->signedAttribsCap) + return BUFFER_E; + esd->signedAttribsCount += idx; encAttribsSz = EncodeAttributes(&esd->signedAttribs[atrIdx], (int)idx, cannedAttribs, (int)idx); @@ -2335,9 +2384,13 @@ static int wc_PKCS7_BuildSignedAttributes(wc_PKCS7* pkcs7, ESD* esd, /* add custom signed attributes if set */ if (pkcs7->signedAttribsSz > 0 && pkcs7->signedAttribs != NULL) { - word32 availableSpace = MAX_SIGNED_ATTRIBS_SZ - atrIdx; + /* esd->signedAttribs was allocated to hold all attributes, but guard + * against writing past it in case the working array was undersized */ + word32 availableSpace = (esd->signedAttribsCap > atrIdx) ? + (esd->signedAttribsCap - atrIdx) : 0; - if (pkcs7->signedAttribsSz > availableSpace) + if (esd->signedAttribs == NULL || + pkcs7->signedAttribsSz > availableSpace) return BUFFER_E; esd->signedAttribsCount += pkcs7->signedAttribsSz; @@ -3050,6 +3103,8 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, int keyIdSize; byte* flatSignedAttribs = NULL; word32 flatSignedAttribsSz = 0; + word32 defaultAttribCap; + word32 neededCap; WC_DECLARE_VAR(esd, ESD, 1, 0); #ifdef ASN_BER_TO_DER @@ -3060,7 +3115,10 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, byte signingTime[MAX_TIME_STRING_SZ]; - if (pkcs7 == NULL || pkcs7->hashOID == 0 || + /* hashOID is unused on the degenerate (certs-only) path, so only require + * it when an actual signer is present */ + if (pkcs7 == NULL || + (pkcs7->hashOID == 0 && pkcs7->sidType != DEGENERATE_SID) || outputSz == NULL) { WOLFSSL_MSG("PKCS7 struct / outputSz null, or hashOID is 0"); return BAD_FUNC_ARG; @@ -3071,9 +3129,12 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, } /* signature size varies with ECDSA; RSA-PSS signs digest directly like - * ECDSA. For both, content hash must be known to build ASN.1 before signing */ + * ECDSA. For both, content hash must be known to build ASN.1 before signing. + * The degenerate (certs-only) path has no signer, so this does not apply + * even though InitWithCert may have parsed an ECDSA/RSA-PSS cert and left + * publicKeyOID set. */ #if defined(HAVE_ECC) || defined(WC_RSA_PSS) - if (hashBuf == NULL && + if (pkcs7->sidType != DEGENERATE_SID && hashBuf == NULL && (pkcs7->publicKeyOID == ECDSAk #ifdef WC_RSA_PSS || pkcs7->publicKeyOID == RSAPSSk @@ -3266,6 +3327,50 @@ static int PKCS7_EncodeSigned(wc_PKCS7* pkcs7, } signerInfoSz += esd->digEncAlgoIdSz; + /* Point the working attribute array at the inline buffer, sized for the + * actual attribute count: the user-supplied attributes plus the exact + * number of CMS auto-defaults that will be emitted for this + * pkcs7->defaultSignedAttribs selection. Only fall back to a heap + * allocation when more attributes are needed than fit inline, so the + * common case stays allocation-free. */ + defaultAttribCap = wc_PKCS7_GetDefaultSignedAttribCount(pkcs7); + neededCap = defaultAttribCap + pkcs7->signedAttribsSz; + + /* detect addition overflow */ + if (neededCap < pkcs7->signedAttribsSz) { + idx = BUFFER_E; + goto out; + } + + if (neededCap <= MAX_SIGNED_ATTRIBS_SZ) { + esd->signedAttribs = esd->signedAttribsInline; + esd->signedAttribsCap = MAX_SIGNED_ATTRIBS_SZ; + } + else { + #ifdef WOLFSSL_NO_MALLOC + /* cannot grow beyond the inline array without a heap */ + idx = BUFFER_E; + goto out; + #else + /* detect multiplication overflow */ + if (neededCap > ((word32)WC_MAX_SINT_OF(int) / + (word32)sizeof(EncodedAttrib))) { + idx = BUFFER_E; + goto out; + } + esd->signedAttribs = (EncodedAttrib*)XMALLOC( + neededCap * (word32)sizeof(EncodedAttrib), + pkcs7->heap, DYNAMIC_TYPE_PKCS7); + if (esd->signedAttribs == NULL) { + idx = MEMORY_E; + goto out; + } + esd->signedAttribsCap = neededCap; + #endif + } + XMEMSET(esd->signedAttribs, 0, + esd->signedAttribsCap * (word32)sizeof(EncodedAttrib)); + /* build up signed attributes, include contentType, signingTime, and messageDigest by default */ ret = wc_PKCS7_BuildSignedAttributes(pkcs7, esd, pkcs7->contentType, @@ -3694,6 +3799,17 @@ 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. */ + if (WC_VAR_OK(esd)) { + if (esd->signedAttribs != NULL && + esd->signedAttribs != esd->signedAttribsInline) { + XFREE(esd->signedAttribs, pkcs7->heap, DYNAMIC_TYPE_PKCS7); + } + esd->signedAttribs = NULL; + } + WC_FREE_VAR_EX(esd, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); WC_FREE_VAR_EX(signedDataOid, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -3865,17 +3981,21 @@ int wc_PKCS7_EncodeSignedData(wc_PKCS7* pkcs7, byte* output, word32 outputSz) } /* pre-calculate content hash for ECDSA and RSA-PSS (both sign digest - * directly). + * directly). Skipped on the degenerate (certs-only) path: there is no + * signer to hash for, and hashOID is 0 so deriving a hash type would fail + * even though InitWithCert may have left publicKeyOID set to the cert's + * OID. * 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 + * PENDING/FAILURE per RFC 8894). */ + if (pkcs7->sidType != DEGENERATE_SID && + (pkcs7->publicKeyOID == ECDSAk #ifdef WC_RSA_PSS - || pkcs7->publicKeyOID == RSAPSSk + || pkcs7->publicKeyOID == RSAPSSk #endif - || (pkcs7->detached && pkcs7->contentSz == 0) - ) { + || (pkcs7->detached && pkcs7->contentSz == 0) + )) { int hashSz; enum wc_HashType hashType; byte hashBuf[WC_MAX_DIGEST_SIZE]; @@ -7055,6 +7175,19 @@ static int PKCS7_VerifySignedData(wc_PKCS7* pkcs7, const byte* hashBuf, if (ret == 0 && MAX_PKCS7_CERTS > 0) { int sz = 0; int i; + /* Absolute end of the certificate set within pkiMsg2. + * idx is the start of the set, so the set spans + * [idx, idx + length). In non-streaming mode idx is the + * absolute offset into the message; in streaming mode it + * is typically 0 (the set was copied to a standalone + * buffer). Bounding the loop with the relative length + * alone stops short by idx bytes in non-streaming mode + * and can drop the last certificate. Clamp to pkiMsg2Sz + * to guard against overflow/over-long length (reads stay + * bounded by the certIdx + 1 < pkiMsg2Sz check below). */ + word32 certSetEnd = idx + (word32)length; + if (certSetEnd < idx || certSetEnd > pkiMsg2Sz) + certSetEnd = pkiMsg2Sz; pkcs7->cert[0] = cert; pkcs7->certSz[0] = (word32)certSz; @@ -7062,7 +7195,7 @@ static int PKCS7_VerifySignedData(wc_PKCS7* pkcs7, const byte* hashBuf, for (i = 1; i < MAX_PKCS7_CERTS && certIdx + 1 < pkiMsg2Sz && - certIdx + 1 < (word32)length; i++) { + certIdx + 1 < certSetEnd; i++) { localIdx = certIdx; if (ret == 0 && GetASNTag(pkiMsg2, &certIdx, &tag, diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 867e802bc2..627da162bc 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -67767,6 +67767,881 @@ static wc_test_ret_t pkcs7signed_run_SingleShotVectors( } +#if !defined(NO_RSA) || defined(HAVE_ECC) +/* Exercise the degenerate (certs-only) encode path through the public API: a + * SignedData with DEGENERATE_SID and no signer (hashOID left 0). This covers + * the hashOID==0 relaxation in PKCS7_EncodeSigned(); the output round-trips + * through wc_PKCS7_VerifySignedData(), which repopulates pkcs7->cert[]. + * + * Called for both an RSA and an ECDSA cert: InitWithCert parses the cert and + * sets pkcs7->publicKeyOID, and the ECDSA case additionally covers the + * DEGENERATE_SID exclusion of the "pre-calculated content hash is needed" + * publicKeyOID==ECDSAk/RSAPSSk check (a degenerate bundle has no signer). */ +static wc_test_ret_t pkcs7_degenerate_encode_test(byte* cert, word32 certSz) +{ + wc_test_ret_t ret = 0; + wc_PKCS7* pkcs7 = NULL; + byte* out = NULL; + int encSz = 0; + const word32 outSz = FOURK_BUF * 2; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_ERRNO; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + /* degenerate: no signer, no eContent, hashOID deliberately left 0 */ + ret = wc_PKCS7_SetSignerIdentifierType(pkcs7, DEGENERATE_SID); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + pkcs7->detached = 1; + pkcs7->contentOID = DATA; + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* round-trip: degenerate verify repopulates the certificate */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + wc_PKCS7_AllowDegenerate(pkcs7, 1); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + if (pkcs7->certSz[0] != certSz || + XMEMCMP(pkcs7->cert[0], cert, certSz) != 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} +#endif /* !NO_RSA || HAVE_ECC */ + + +#if !defined(NO_RSA) && !defined(NO_SHA256) +/* The nine-attribute case below needs more slots than the inline array holds + * (MAX_SIGNED_ATTRIBS_SZ), which requires a heap allocation; skip it on + * no-malloc builds unless the inline array was enlarged at build time. */ +#if !defined(WOLFSSL_NO_MALLOC) || (MAX_SIGNED_ATTRIBS_SZ >= 9) +/* Test that a SignedData carrying six user signed attributes plus the CMS + * auto-defaults (nine total, or eight when NO_ASN_TIME drops the signing-time + * default) encodes and verifies. This exceeds the default inline capacity + * (MAX_SIGNED_ATTRIBS_SZ == 7). */ +static wc_test_ret_t pkcs7_signed_attribs_count_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 * 2; + int i; + byte content[] = "signed attribs count test"; + + /* six distinct user attribute OIDs (arbitrary, well-formed OID TLVs) and + * PrintableString values */ + static const byte oid0[] = { 0x06,0x03, 0x55,0x04,0x03 }; + static const byte oid1[] = { 0x06,0x03, 0x55,0x04,0x04 }; + static const byte oid2[] = { 0x06,0x03, 0x55,0x04,0x05 }; + static const byte oid3[] = { 0x06,0x03, 0x55,0x04,0x06 }; + static const byte oid4[] = { 0x06,0x03, 0x55,0x04,0x07 }; + static const byte oid5[] = { 0x06,0x03, 0x55,0x04,0x08 }; + static const byte val0[] = { 0x13,0x01, 0x30 }; + static const byte val1[] = { 0x13,0x01, 0x31 }; + static const byte val2[] = { 0x13,0x01, 0x32 }; + static const byte val3[] = { 0x13,0x01, 0x33 }; + static const byte val4[] = { 0x13,0x01, 0x34 }; + static const byte val5[] = { 0x13,0x01, 0x35 }; + + PKCS7Attrib attribs[6] = { + { oid0, sizeof(oid0), val0, sizeof(val0) }, + { oid1, sizeof(oid1), val1, sizeof(val1) }, + { oid2, sizeof(oid2), val2, sizeof(val2) }, + { oid3, sizeof(oid3), val3, sizeof(val3) }, + { oid4, sizeof(oid4), val4, sizeof(val4) }, + { oid5, sizeof(oid5), val5, sizeof(val5) } + }; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_ERRNO; + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = (word32)XSTRLEN((char*)content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + pkcs7->signedAttribs = attribs; + pkcs7->signedAttribsSz = (word32)(sizeof(attribs) / sizeof(attribs[0])); + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* verify */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + /* all six user attributes should be present in the decoded list. + * wc_PKCS7_GetAttributeValue() matches on the OID content (no tag/len), + * so skip the 2-byte OID TLV header. */ + for (i = 0; i < 6; i++) { + word32 vSz = 0; + ret = wc_PKCS7_GetAttributeValue(pkcs7, attribs[i].oid + 2, + attribs[i].oidSz - 2, NULL, &vSz); + if (ret != WC_NO_ERR_TRACE(LENGTH_ONLY_E) || vSz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + } + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} +#endif /* !WOLFSSL_NO_MALLOC || MAX_SIGNED_ATTRIBS_SZ >= 9 */ + +/* Number of CMS auto-defaults PKCS7_EncodeSigned() emits: contentType and + * messageDigest always, plus signingTime unless NO_ASN_TIME. Mirrors + * wc_PKCS7_GetDefaultSignedAttribCount(); used to size the inline-full case. */ +#ifdef NO_ASN_TIME + #define PKCS7_TEST_DEFAULT_ATTRIB_CNT 2 +#else + #define PKCS7_TEST_DEFAULT_ATTRIB_CNT 3 +#endif + +/* Exercise the exact inline-full boundary: pick the user-attribute count so the + * total (user + auto-defaults) equals MAX_SIGNED_ATTRIBS_SZ, i.e. neededCap == + * MAX_SIGNED_ATTRIBS_SZ and the working array stays inline (no heap allocation). + * The over-capacity count test only reaches neededCap > MAX_SIGNED_ATTRIBS_SZ + * and the decoded-shape test stays well under it, so this pins the '==' edge. + * Unlike the count test this also runs on WOLFSSL_NO_MALLOC builds. */ +#if (MAX_SIGNED_ATTRIBS_SZ - PKCS7_TEST_DEFAULT_ATTRIB_CNT) >= 1 && \ + (MAX_SIGNED_ATTRIBS_SZ - PKCS7_TEST_DEFAULT_ATTRIB_CNT) <= 5 +static wc_test_ret_t pkcs7_signed_attribs_boundary_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 * 2; + const word32 userCnt = + (word32)(MAX_SIGNED_ATTRIBS_SZ - PKCS7_TEST_DEFAULT_ATTRIB_CNT); + byte content[] = "signed attribs boundary test"; + + /* up to five distinct user attribute OIDs (arbitrary, well-formed OID TLVs) + * and PrintableString values; only the first userCnt are submitted */ + static const byte oid0[] = { 0x06,0x03, 0x55,0x04,0x03 }; + static const byte oid1[] = { 0x06,0x03, 0x55,0x04,0x04 }; + static const byte oid2[] = { 0x06,0x03, 0x55,0x04,0x05 }; + static const byte oid3[] = { 0x06,0x03, 0x55,0x04,0x06 }; + static const byte oid4[] = { 0x06,0x03, 0x55,0x04,0x07 }; + static const byte val0[] = { 0x13,0x01, 0x30 }; + static const byte val1[] = { 0x13,0x01, 0x31 }; + static const byte val2[] = { 0x13,0x01, 0x32 }; + static const byte val3[] = { 0x13,0x01, 0x33 }; + static const byte val4[] = { 0x13,0x01, 0x34 }; + + PKCS7Attrib attribs[5] = { + { oid0, sizeof(oid0), val0, sizeof(val0) }, + { oid1, sizeof(oid1), val1, sizeof(val1) }, + { oid2, sizeof(oid2), val2, sizeof(val2) }, + { oid3, sizeof(oid3), val3, sizeof(val3) }, + { oid4, sizeof(oid4), val4, sizeof(val4) } + }; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_ERRNO; + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = (word32)XSTRLEN((char*)content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + pkcs7->signedAttribs = attribs; + pkcs7->signedAttribsSz = userCnt; + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* round-trip verify with the working array filled to exact inline capacity */ + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} +#endif /* inline-full boundary fits attribs[5] */ + +/* Negative test for the WOLFSSL_NO_MALLOC over-capacity guard: request one more + * signed attribute than the inline array can hold alongside the CMS + * auto-defaults, so neededCap == MAX_SIGNED_ATTRIBS_SZ + 1. Without a heap the + * encoder cannot grow the working array, so wc_PKCS7_EncodeSignedData() must + * fail with BUFFER_E instead of overrunning the inline buffer. Only meaningful + * on WOLFSSL_NO_MALLOC builds; malloc builds heap-allocate and succeed, which + * pkcs7_signed_attribs_count_test() covers. */ +#if defined(WOLFSSL_NO_MALLOC) && (MAX_SIGNED_ATTRIBS_SZ < 9) && \ + (MAX_SIGNED_ATTRIBS_SZ >= PKCS7_TEST_DEFAULT_ATTRIB_CNT) +static wc_test_ret_t pkcs7_signed_attribs_nomalloc_overflow_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 * 2; + /* one past the inline capacity once the auto-defaults are counted */ + const word32 userCnt = (word32)(MAX_SIGNED_ATTRIBS_SZ + - PKCS7_TEST_DEFAULT_ATTRIB_CNT + 1); + byte content[] = "signed attribs no-malloc overflow test"; + + /* up to eight distinct user attribute OIDs (arbitrary, well-formed OID TLVs) + * and PrintableString values; only the first userCnt are submitted */ + static const byte oid0[] = { 0x06,0x03, 0x55,0x04,0x03 }; + static const byte oid1[] = { 0x06,0x03, 0x55,0x04,0x04 }; + static const byte oid2[] = { 0x06,0x03, 0x55,0x04,0x05 }; + static const byte oid3[] = { 0x06,0x03, 0x55,0x04,0x06 }; + static const byte oid4[] = { 0x06,0x03, 0x55,0x04,0x07 }; + static const byte oid5[] = { 0x06,0x03, 0x55,0x04,0x08 }; + static const byte oid6[] = { 0x06,0x03, 0x55,0x04,0x09 }; + static const byte oid7[] = { 0x06,0x03, 0x55,0x04,0x0A }; + static const byte val0[] = { 0x13,0x01, 0x30 }; + static const byte val1[] = { 0x13,0x01, 0x31 }; + static const byte val2[] = { 0x13,0x01, 0x32 }; + static const byte val3[] = { 0x13,0x01, 0x33 }; + static const byte val4[] = { 0x13,0x01, 0x34 }; + static const byte val5[] = { 0x13,0x01, 0x35 }; + static const byte val6[] = { 0x13,0x01, 0x36 }; + static const byte val7[] = { 0x13,0x01, 0x37 }; + + PKCS7Attrib attribs[8] = { + { oid0, sizeof(oid0), val0, sizeof(val0) }, + { oid1, sizeof(oid1), val1, sizeof(val1) }, + { oid2, sizeof(oid2), val2, sizeof(val2) }, + { oid3, sizeof(oid3), val3, sizeof(val3) }, + { oid4, sizeof(oid4), val4, sizeof(val4) }, + { oid5, sizeof(oid5), val5, sizeof(val5) }, + { oid6, sizeof(oid6), val6, sizeof(val6) }, + { oid7, sizeof(oid7), val7, sizeof(val7) } + }; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_ERRNO; + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = (word32)XSTRLEN((char*)content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + pkcs7->signedAttribs = attribs; + pkcs7->signedAttribsSz = userCnt; + + /* over-capacity on a no-malloc build must be refused, not overrun */ + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz != WC_NO_ERR_TRACE(BUFFER_E)) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} +#endif /* WOLFSSL_NO_MALLOC && inline over-capacity reachable */ + +/* Test that decoded signed-attribute values follow the documented, stable + * shape (the contents of the SET OF AttributeValue, outer SET tag stripped) + * for both PrintableString and OCTET STRING value types, as fetched via + * wc_PKCS7_GetAttributeValue(). */ +static wc_test_ret_t pkcs7_decoded_attrib_shape_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 * 2; + byte getBuf[32]; + word32 getBufSz; + byte content[] = "decoded attrib shape test"; + + /* one PrintableString-valued and one OCTET STRING-valued attribute */ + static const byte oidPS[] = { 0x06,0x03, 0x55,0x04,0x0A }; + static const byte oidOS[] = { 0x06,0x03, 0x55,0x04,0x0B }; + static const byte valPS[] = { 0x13,0x03, 0x61,0x62,0x63 }; /* "abc" */ + static const byte valOS[] = { 0x04,0x04, 0xDE,0xAD,0xBE,0xEF }; + + PKCS7Attrib attribs[2] = { + { oidPS, sizeof(oidPS), valPS, sizeof(valPS) }, + { oidOS, sizeof(oidOS), valOS, sizeof(valOS) } + }; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_ERRNO; + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = (word32)XSTRLEN((char*)content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + pkcs7->signedAttribs = attribs; + pkcs7->signedAttribsSz = 2; + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + /* PrintableString attribute: decoded value is the SET contents, i.e. the + * AttributeValue TLV (0x13 ...), NOT the outer SET (0x31 ...). Lookups + * match on the OID content, so skip the 2-byte OID TLV header. */ + getBufSz = sizeof(getBuf); + /* return value is the value length; *outSz is only set on the size probe */ + ret = wc_PKCS7_GetAttributeValue(pkcs7, oidPS + 2, sizeof(oidPS) - 2, + getBuf, &getBufSz); + if (ret != (int)sizeof(valPS) || XMEMCMP(getBuf, valPS, sizeof(valPS)) != 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + if (getBuf[0] != 0x13) /* value tag, not 0x31 SET */ + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + /* OCTET STRING attribute */ + getBufSz = sizeof(getBuf); + ret = wc_PKCS7_GetAttributeValue(pkcs7, oidOS + 2, sizeof(oidOS) - 2, + getBuf, &getBufSz); + if (ret != (int)sizeof(valOS) || XMEMCMP(getBuf, valOS, sizeof(valOS)) != 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + if (getBuf[0] != 0x04) /* value tag, not 0x31 SET */ + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} + +/* Exercise the subset and no-defaults branches of + * wc_PKCS7_GetDefaultSignedAttribCount(), the single source of truth that sizes + * the working attribute array. Every other signed-attribute test leaves + * defaultSignedAttribs at 0 (all defaults), so only the flags==0 branch is + * otherwise covered. + * + * Case 0: select only the messageDigest default (the one the verifier requires + * when signed attributes are present) alongside two user attributes, + * then confirm contentType and signingTime are absent. + * Case 1: select no default attributes and supply none, so the bundle carries + * no signed attributes at all and the signature covers the content. */ +static wc_test_ret_t pkcs7_signed_attribs_subset_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; + int caseIdx; + word32 vSz; + const word32 outSz = FOURK_BUF * 2; + byte content[] = "signed attribs subset test"; + + /* two user attributes (arbitrary well-formed OID TLVs + PrintableString) */ + static const byte uoid0[] = { 0x06,0x03, 0x55,0x04,0x03 }; + static const byte uoid1[] = { 0x06,0x03, 0x55,0x04,0x04 }; + static const byte uval0[] = { 0x13,0x01, 0x41 }; + static const byte uval1[] = { 0x13,0x01, 0x42 }; + + /* CMS default-attribute OIDs, content only (no tag/len), as matched by + * wc_PKCS7_GetAttributeValue() */ + static const byte contentTypeOid[] = + { 0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x09,0x03 }; + static const byte messageDigestOid[] = + { 0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x09,0x04 }; +#ifndef NO_ASN_TIME + static const byte signingTimeOid[] = + { 0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x09,0x05 }; +#endif + + PKCS7Attrib attribs[2] = { + { uoid0, sizeof(uoid0), uval0, sizeof(uval0) }, + { uoid1, sizeof(uoid1), uval1, sizeof(uval1) } + }; + + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return WC_TEST_RET_ENC_ERRNO; + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + for (caseIdx = 0; caseIdx < 2; caseIdx++) { + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert, certSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + if (caseIdx == 0) + ret = wc_PKCS7_SetDefaultSignedAttribs(pkcs7, + WOLFSSL_MESSAGE_DIGEST_ATTRIBUTE); + else + ret = wc_PKCS7_NoDefaultSignedAttribs(pkcs7); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = (word32)XSTRLEN((char*)content); + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + if (caseIdx == 0) { + pkcs7->signedAttribs = attribs; + pkcs7->signedAttribsSz = + (word32)(sizeof(attribs) / sizeof(attribs[0])); + } + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + /* contentType and signingTime defaults are selected in neither case */ + vSz = 0; + if (wc_PKCS7_GetAttributeValue(pkcs7, contentTypeOid, + sizeof(contentTypeOid), NULL, &vSz) + != WC_NO_ERR_TRACE(ASN_PARSE_E)) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + #ifndef NO_ASN_TIME + vSz = 0; + if (wc_PKCS7_GetAttributeValue(pkcs7, signingTimeOid, + sizeof(signingTimeOid), NULL, &vSz) + != WC_NO_ERR_TRACE(ASN_PARSE_E)) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + #endif + + vSz = 0; + ret = wc_PKCS7_GetAttributeValue(pkcs7, messageDigestOid, + sizeof(messageDigestOid), NULL, &vSz); + if (caseIdx == 0) { + /* subset selection keeps the required messageDigest default and the + * two user attributes must round-trip */ + if (ret != WC_NO_ERR_TRACE(LENGTH_ONLY_E) || vSz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + vSz = 0; + if (wc_PKCS7_GetAttributeValue(pkcs7, uoid0 + 2, sizeof(uoid0) - 2, + NULL, &vSz) != WC_NO_ERR_TRACE(LENGTH_ONLY_E) || vSz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + vSz = 0; + if (wc_PKCS7_GetAttributeValue(pkcs7, uoid1 + 2, sizeof(uoid1) - 2, + NULL, &vSz) != WC_NO_ERR_TRACE(LENGTH_ONLY_E) || vSz == 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + } + else { + /* no defaults and no user attribs: no signed attributes present */ + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + } + + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + } + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +} + +/* Regression test for the multi-certificate decode bound. A SignedData whose + * eContent is larger than the certificate set pushes the cert set to a large + * offset within the message; the decoder must still parse every certificate. + * + * IMPORTANT - build dependency: this only guards the bound fix under + * NO_PKCS7_STREAM. In the default streaming build wc_PKCS7_VerifySignedData() + * copies the cert set into a standalone buffer and resets idx to 0, so + * certSetEnd == length and both the pre-fix and post-fix code parse every + * certificate - the test passes either way and is just a general multi-cert + * round-trip sanity check. Only a NO_PKCS7_STREAM build actually exercises + * the off-by-idx bound this fix corrects. */ +static wc_test_ret_t pkcs7_signed_multi_cert_test( + byte* cert1, word32 cert1Sz, byte* key, word32 keySz, + byte* cert2, word32 cert2Sz, byte* cert3, word32 cert3Sz) +{ +#if MAX_PKCS7_CERTS < 3 + /* needs at least three certificate slots to exercise the bound */ + (void)cert1; (void)cert1Sz; (void)key; (void)keySz; + (void)cert2; (void)cert2Sz; (void)cert3; (void)cert3Sz; + return 0; +#else + wc_test_ret_t ret = 0; + wc_PKCS7* pkcs7 = NULL; + WC_RNG rng; + int rngInit = 0; + byte* out = NULL; + byte* content = NULL; + int encSz = 0; + const word32 outSz = FOURK_BUF * 4; + const word32 contentSz = FOURK_BUF; /* larger than the certificate set */ + int found1 = 0, found2 = 0, found3 = 0, j; + + content = (byte*)XMALLOC(contentSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (content == NULL || out == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + XMEMSET(content, 0xA5, contentSz); + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert1, cert1Sz); + if (ret == 0) + ret = wc_PKCS7_AddCertificate(pkcs7, cert2, cert2Sz); + if (ret == 0) + ret = wc_PKCS7_AddCertificate(pkcs7, cert3, cert3Sz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = contentSz; + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + ret = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + /* all three certificates must be decoded (SET OF is unordered) */ + for (j = 0; j < MAX_PKCS7_CERTS; j++) { + if (pkcs7->certSz[j] == cert1Sz && + XMEMCMP(pkcs7->cert[j], cert1, cert1Sz) == 0) + found1 = 1; + if (pkcs7->certSz[j] == cert2Sz && + XMEMCMP(pkcs7->cert[j], cert2, cert2Sz) == 0) + found2 = 1; + if (pkcs7->certSz[j] == cert3Sz && + XMEMCMP(pkcs7->cert[j], cert3, cert3Sz) == 0) + found3 = 1; + } + if (!found1 || !found2 || !found3) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(content, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +#endif /* MAX_PKCS7_CERTS < 3 */ +} + +/* Malformed-input companion to pkcs7_signed_multi_cert_test: after encoding a + * valid three-certificate SignedData, inflate the certificate-set length so it + * claims far more bytes than the message holds. This drives the certSetEnd + * clamp in wc_PKCS7_VerifySignedData() (the certSetEnd > pkiMsg2Sz branch), a + * branch the well-formed round-trip cannot reach, and confirms the decoder + * stays within pkiMsg2Sz and rejects the input rather than reading past the + * buffer (an ASAN target). + * + * Guarded to NO_PKCS7_STREAM: only there is idx the absolute message offset the + * clamp corrects. The streaming path copies the cert set into a standalone + * buffer and resets idx, a different code path. */ +static wc_test_ret_t pkcs7_signed_multi_cert_malformed_test( + byte* cert1, word32 cert1Sz, byte* key, word32 keySz, + byte* cert2, word32 cert2Sz, byte* cert3, word32 cert3Sz) +{ +#if !defined(NO_PKCS7_STREAM) || (MAX_PKCS7_CERTS < 3) + /* needs the non-streaming absolute-offset path and three cert slots */ + (void)cert1; (void)cert1Sz; (void)key; (void)keySz; + (void)cert2; (void)cert2Sz; (void)cert3; (void)cert3Sz; + return 0; +#else + wc_test_ret_t ret = 0; + wc_PKCS7* pkcs7 = NULL; + WC_RNG rng; + int rngInit = 0; + byte* out = NULL; + byte* content = NULL; + int encSz = 0; + int verifyRet; + word32 p; + word32 setStart = 0; + const word32 outSz = FOURK_BUF * 4; + const word32 contentSz = FOURK_BUF; /* pushes the cert set to a high offset */ + + content = (byte*)XMALLOC(contentSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + out = (byte*)XMALLOC(outSz, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (content == NULL || out == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + XMEMSET(content, 0xA5, contentSz); + + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + rngInit = 1; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + + ret = wc_PKCS7_InitWithCert(pkcs7, cert1, cert1Sz); + if (ret == 0) + ret = wc_PKCS7_AddCertificate(pkcs7, cert2, cert2Sz); + if (ret == 0) + ret = wc_PKCS7_AddCertificate(pkcs7, cert3, cert3Sz); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + pkcs7->rng = &rng; + pkcs7->content = content; + pkcs7->contentSz = contentSz; + pkcs7->contentOID = DATA; + pkcs7->hashOID = SHA256h; + pkcs7->encryptOID = RSAk; + pkcs7->privateKey = key; + pkcs7->privateKeySz = keySz; + + encSz = wc_PKCS7_EncodeSignedData(pkcs7, out, outSz); + if (encSz <= 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(encSz), out); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* Find the certificate set: it is the only place a full certificate follows + * the [0] IMPLICIT header (0xA0) with a 2-byte definite length (0x82 hi lo), + * so locate that header immediately before one of the three certificates. + * The three RSA test certs total several KB, guaranteeing the 2-byte length + * form. If the layout is not as expected, skip rather than corrupt the + * wrong bytes. */ + for (p = 4; p < (word32)encSz; p++) { + if (out[p - 4] != (ASN_CONSTRUCTED | ASN_CONTEXT_SPECIFIC) || + out[p - 3] != 0x82) + continue; + if ((p + cert1Sz <= (word32)encSz && + XMEMCMP(&out[p], cert1, cert1Sz) == 0) || + (p + cert2Sz <= (word32)encSz && + XMEMCMP(&out[p], cert2, cert2Sz) == 0) || + (p + cert3Sz <= (word32)encSz && + XMEMCMP(&out[p], cert3, cert3Sz) == 0)) { + setStart = p; + break; + } + } + if (setStart == 0) { + /* expected layout not present; nothing to corrupt safely */ + ret = 0; + goto out; + } + + /* inflate the 2-byte certificate-set length to 0xFFFF, well past the message + * end, so certSetEnd = idx + length exceeds pkiMsg2Sz and the clamp fires */ + out[setStart - 2] = 0xFF; + out[setStart - 1] = 0xFF; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, devId); + if (pkcs7 == NULL) + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, out); + verifyRet = wc_PKCS7_VerifySignedData(pkcs7, out, (word32)encSz); + /* the over-long cert-set length must be rejected, not accepted or overrun */ + if (verifyRet >= 0) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + ret = 0; + +out: + if (pkcs7 != NULL) + wc_PKCS7_Free(pkcs7); + if (rngInit) + wc_FreeRng(&rng); + XFREE(out, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(content, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + + return ret; +#endif /* NO_PKCS7_STREAM && MAX_PKCS7_CERTS >= 3 */ +} +#endif /* !NO_RSA && !NO_SHA256 */ + + #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 @@ -68027,14 +68902,83 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); #endif +#ifndef NO_RSA + /* degenerate (certs-only) encode via the public API */ + if (ret >= 0) + ret = pkcs7_degenerate_encode_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz); +#endif +#ifdef HAVE_ECC + /* same path with an ECDSA cert: covers the DEGENERATE_SID exclusion of the + * ECDSAk/RSAPSSk pre-hash check that InitWithCert would otherwise trip */ + if (ret >= 0) + ret = pkcs7_degenerate_encode_test( + eccClientCertBuf, (word32)eccClientCertBufSz); +#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); + +#if !defined(WOLFSSL_NO_MALLOC) || (MAX_SIGNED_ATTRIBS_SZ >= 9) + /* signed-attribute count beyond the inline capacity */ + if (ret >= 0) + ret = pkcs7_signed_attribs_count_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); #endif +#if defined(WOLFSSL_NO_MALLOC) && (MAX_SIGNED_ATTRIBS_SZ < 9) && \ + (MAX_SIGNED_ATTRIBS_SZ >= PKCS7_TEST_DEFAULT_ATTRIB_CNT) + /* over-capacity signed-attribute count refused on no-malloc builds */ + if (ret >= 0) + ret = pkcs7_signed_attribs_nomalloc_overflow_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); +#endif + +#if (MAX_SIGNED_ATTRIBS_SZ - PKCS7_TEST_DEFAULT_ATTRIB_CNT) >= 1 && \ + (MAX_SIGNED_ATTRIBS_SZ - PKCS7_TEST_DEFAULT_ATTRIB_CNT) <= 5 + /* signed-attribute count exactly filling the inline capacity */ + if (ret >= 0) + ret = pkcs7_signed_attribs_boundary_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); +#endif + + /* subset and no-defaults signed-attribute selections */ + if (ret >= 0) + ret = pkcs7_signed_attribs_subset_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); + + /* decoded signed-attribute value shape */ + if (ret >= 0) + ret = pkcs7_decoded_attrib_shape_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz); + + /* multi-certificate decode with large content (cert-set bound). NOTE: only + * validates the bound fix under NO_PKCS7_STREAM; in streaming builds it is a + * general multi-cert round-trip check (see the function comment). */ + if (ret >= 0) + ret = pkcs7_signed_multi_cert_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz, + rsaServerCertBuf, (word32)rsaServerCertBufSz, + rsaCaCertBuf, (word32)rsaCaCertBufSz); + + /* malformed cert-set length (drives the certSetEnd clamp) */ + if (ret >= 0) + ret = pkcs7_signed_multi_cert_malformed_test( + rsaClientCertBuf, (word32)rsaClientCertBufSz, + rsaClientPrivKeyBuf, (word32)rsaClientPrivKeyBufSz, + rsaServerCertBuf, (word32)rsaServerCertBufSz, + rsaCaCertBuf, (word32)rsaCaCertBufSz); +#endif XFREE(rsaClientCertBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); XFREE(rsaClientPrivKeyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); XFREE(rsaServerCertBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/wolfssl/wolfcrypt/pkcs7.h b/wolfssl/wolfcrypt/pkcs7.h index 93c9c6a5d7..43d305597d 100644 --- a/wolfssl/wolfcrypt/pkcs7.h +++ b/wolfssl/wolfcrypt/pkcs7.h @@ -63,6 +63,15 @@ #define MAX_ORI_VALUE_SZ 512 #endif +/* Bound on the number of signed attributes the encoder can place in a + * SignedData SignerInfo without a heap allocation. At encode time the working + * attribute array is sized to the actual attribute count (user-supplied + * attributes plus up to three CMS auto-defaults: contentType, messageDigest, + * signingTime); counts up to this bound use an inline buffer, and only larger + * counts fall back to a heap allocation (or fail under WOLFSSL_NO_MALLOC). + * The macro therefore no longer caps the attribute count, but it does set the + * size of that inline working buffer (in the transient encode-time ESD + * structure, not in wc_PKCS7), so raising it increases that buffer. */ #ifndef MAX_SIGNED_ATTRIBS_SZ #define MAX_SIGNED_ATTRIBS_SZ 7 #endif @@ -175,6 +184,22 @@ typedef struct PKCS7Attrib { } PKCS7Attrib; +/* A single decoded signed/unsigned attribute, as produced when verifying a + * SignedData. The fields follow a stable, guaranteed shape: + * + * oid, oidSz: + * The attribute OID, encoded as (the full OBJECT + * IDENTIFIER TLV). + * + * value, valueSz: + * The contents of the SET OF AttributeValue, i.e. the bytes inside the + * attribute's SET with the outer SET (tag 0x31) and its length removed. + * A CMS attribute is a SET OF AttributeValue and may legitimately carry + * more than one value, so this is the concatenation of all AttributeValue + * TLVs. For the common single-valued case it is exactly one AttributeValue + * encoded as (for example 0x13 PrintableString or + * 0x04 OCTET STRING). + */ typedef struct PKCS7DecodedAttrib { struct PKCS7DecodedAttrib* next; byte* oid;