From 6449ca003a923dce51b8f40416cd160b3f1a6209 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. Config-sensitive cases are guarded. --- wolfcrypt/src/pkcs7.c | 159 +++++++++++++- wolfcrypt/test/test.c | 429 ++++++++++++++++++++++++++++++++++++++ wolfssl/wolfcrypt/pkcs7.h | 25 +++ 3 files changed, 602 insertions(+), 11 deletions(-) diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index b04504a0054..8178c4363e8 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; @@ -2238,11 +2245,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(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, @@ -2315,6 +2357,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); @@ -2329,9 +2378,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; @@ -3054,7 +3107,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; @@ -3065,9 +3121,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 @@ -3260,6 +3319,53 @@ 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. */ + { + word32 defaultAttribCap = + wc_PKCS7_GetDefaultSignedAttribCount(pkcs7); + word32 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, @@ -3688,6 +3794,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. */ +#ifdef WOLFSSL_SMALL_STACK + if (esd != NULL) +#endif + { + 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); @@ -3858,12 +3978,16 @@ 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) */ - if (pkcs7->publicKeyOID == ECDSAk + /* pre-calculate content hash for ECDSA and RSA-PSS (both sign digest + * 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. */ + if (pkcs7->sidType != DEGENERATE_SID && + (pkcs7->publicKeyOID == ECDSAk #ifdef WC_RSA_PSS || pkcs7->publicKeyOID == RSAPSSk #endif - ) { + )) { int hashSz; enum wc_HashType hashType; byte hashBuf[WC_MAX_DIGEST_SIZE]; @@ -7021,6 +7145,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; @@ -7028,7 +7165,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 e95bba5ae5e..b3acd05d081 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -67438,6 +67438,395 @@ 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 three + * CMS auto-defaults (nine total) 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 */ + +/* 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; +} + +/* 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 */ +} +#endif /* !NO_RSA && !NO_SHA256 */ + + WOLFSSL_TEST_SUBROUTINE wc_test_ret_t pkcs7signed_test(void) { wc_test_ret_t ret = 0; @@ -67568,6 +67957,46 @@ 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) +#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 + + /* 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); +#endif + XFREE(rsaClientCertBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); XFREE(rsaClientPrivKeyBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); XFREE(rsaServerCertBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/wolfssl/wolfcrypt/pkcs7.h b/wolfssl/wolfcrypt/pkcs7.h index 93c9c6a5d79..43d305597d7 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;