From e23b6040774c72588f7d077e6d47d3b3b37fcfda Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 17 Sep 2026 12:26:48 -0600 Subject: [PATCH 1/3] PKCS7: enforce RFC 5084 GCM ICV size on AuthEnvelopedData decode --- tests/api/test_pkcs7.c | 149 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_pkcs7.h | 2 + wolfcrypt/src/pkcs7.c | 25 +++---- 3 files changed, 162 insertions(+), 14 deletions(-) diff --git a/tests/api/test_pkcs7.c b/tests/api/test_pkcs7.c index 982aab81bda..4d59c7f8bc8 100644 --- a/tests/api/test_pkcs7.c +++ b/tests/api/test_pkcs7.c @@ -3826,6 +3826,155 @@ int test_wc_PKCS7_DecodeAuthEnvelopedData_truncated(void) } /* END test_wc_PKCS7_DecodeAuthEnvelopedData_truncated() */ +#if defined(HAVE_PKCS7) && !defined(NO_RSA) && !defined(NO_AES) && \ + defined(WOLFSSL_AES_128) && (defined(HAVE_AESGCM) || defined(HAVE_AESCCM)) +/* Encode an AuthEnvelopedData bundle, then cut its tag down to tagSz bytes + * and fix up the stated tag size and the outer lengths. Returns the new size. */ +static int pkcs7_shortTagBundle(byte* out, word32 outSz, int encryptOID, + word32 tagSz, int contentOID) +{ + PKCS7* pkcs7 = NULL; + byte data[] = "short authTag authEnvelopedData test"; + int encSz; + word32 cut; + word32 lenIdx[3]; + word32 found = 0; + word32 i; + word32 n; + word32 len; + + if (tagSz == 0 || tagSz > (word32)WC_AES_BLOCK_SIZE) + return -1; + cut = (word32)WC_AES_BLOCK_SIZE - tagSz; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId); + if (pkcs7 == NULL) + return -1; + if (wc_PKCS7_InitWithCert(pkcs7, (byte*)client_cert_der_2048, + sizeof_client_cert_der_2048) != 0) { + wc_PKCS7_Free(pkcs7); + return -1; + } + pkcs7->content = data; + pkcs7->contentSz = (word32)sizeof(data); + /* a contentOID other than DATA makes the encoder add authenticated + * attributes, which the decoder walks in its own state */ + pkcs7->contentOID = contentOID; + pkcs7->encryptOID = encryptOID; + encSz = wc_PKCS7_EncodeAuthEnvelopedData(pkcs7, out, outSz); + wc_PKCS7_Free(pkcs7); + if (encSz <= 32) + return -1; + + /* Tag is last: 04 10 <16 bytes>. Keep only its first tagSz bytes. */ + if (out[encSz - (WC_AES_BLOCK_SIZE + 2)] != ASN_OCTET_STRING || + out[encSz - (WC_AES_BLOCK_SIZE + 1)] != WC_AES_BLOCK_SIZE) + return -1; + out[encSz - (WC_AES_BLOCK_SIZE + 1)] = (byte)tagSz; + encSz -= (int)cut; + + /* Tag size field follows the nonce: 04 02 01 10 */ + for (i = 0; i + 20 < (word32)encSz; i++) { + n = out[i + 1]; + /* 04 02 01 10, then the encryptedContent [0] tag */ + if (out[i] == 0x04 && n >= 7 && n <= 13 && + out[i + n + 2] == 0x02 && out[i + n + 3] == 0x01 && + out[i + n + 4] == 0x10 && + (out[i + n + 5] == 0x80 || out[i + n + 5] == 0xA0)) { + out[i + n + 4] = (byte)tagSz; + found = 1; + break; + } + } + if (!found) + return -1; + + /* Outer SEQUENCE, [0] and inner SEQUENCE lengths are 82 hi lo. */ + lenIdx[0] = 1; + lenIdx[1] = 6 + (word32)out[5] + 1; + lenIdx[2] = lenIdx[1] + 4; + for (i = 0; i < 3; i++) { + if (out[lenIdx[i]] != 0x82) + return -1; + len = ((word32)out[lenIdx[i] + 1] << 8) | out[lenIdx[i] + 2]; + len -= cut; + out[lenIdx[i] + 1] = (byte)(len >> 8); + out[lenIdx[i] + 2] = (byte)len; + } + + return encSz; +} + +/* Decode a bundle whose tag was cut short, expecting it to be refused. */ +static int pkcs7_decodeShortTag(byte* enveloped, int encSz) +{ + PKCS7* pkcs7 = NULL; + byte decoded[256]; + int ret; + + pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId); + if (pkcs7 == NULL) + return -1; + if (wc_PKCS7_InitWithCert(pkcs7, (byte*)client_cert_der_2048, + sizeof_client_cert_der_2048) != 0) { + wc_PKCS7_Free(pkcs7); + return -1; + } + pkcs7->privateKey = (byte*)client_key_der_2048; + pkcs7->privateKeySz = sizeof_client_key_der_2048; + ret = wc_PKCS7_DecodeAuthEnvelopedData(pkcs7, enveloped, (word32)encSz, + decoded, sizeof(decoded)); + wc_PKCS7_Free(pkcs7); + + return ret; +} +#endif + + +/* A GCM tag under 12 bytes must be refused, even when the build allows + * short tags for plain AES-GCM calls. */ +int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag(void) +{ + EXPECT_DECLS; +#if defined(HAVE_PKCS7) && defined(HAVE_AESGCM) && !defined(NO_RSA) && \ + !defined(NO_AES) && defined(WOLFSSL_AES_128) + byte enveloped[2048]; + int encSz = 0; + + ExpectIntGT(encSz = pkcs7_shortTagBundle(enveloped, sizeof(enveloped), + AES128GCMb, 8, DATA), 0); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(pkcs7_decodeShortTag(enveloped, encSz), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + } + +/* These accept a truncated tag, which needs the in-tree AES-GCM. A v5, v6 or + * selftest build pins an older aes.c, so they are left out there. */ +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && !defined(HAVE_SELFTEST) +#if WOLFSSL_MIN_AUTH_TAG_SZ <= 12 + /* a tag at the floor still decodes, a GCM tag being the leading bytes + * of the full one */ + ExpectIntGT(encSz = pkcs7_shortTagBundle(enveloped, sizeof(enveloped), + AES128GCMb, 12, DATA), 0); + if (EXPECT_SUCCESS()) { + ExpectIntGT(pkcs7_decodeShortTag(enveloped, encSz), 0); + } +#endif +#if WOLFSSL_MIN_AUTH_TAG_SZ <= 13 + /* SP 800-38D section 5.2.1.2 approves 104 bits, so an odd GCM ICV is + * allowed here where the CCM list has none */ + ExpectIntGT(encSz = pkcs7_shortTagBundle(enveloped, sizeof(enveloped), + AES128GCMb, 13, DATA), 0); + if (EXPECT_SUCCESS()) { + ExpectIntGT(pkcs7_decodeShortTag(enveloped, encSz), 0); + } +#endif +#endif /* in-tree AES-GCM */ +#endif + return EXPECT_RESULT(); +} /* END test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag() */ + + /* Tearing down a PKCS7 whose AuthEnvelopedData decode stopped part-way must * not leak the encryptedContent buffer. * diff --git a/tests/api/test_pkcs7.h b/tests/api/test_pkcs7.h index bf66118e08d..bd14c92a45d 100644 --- a/tests/api/test_pkcs7.h +++ b/tests/api/test_pkcs7.h @@ -76,6 +76,7 @@ int test_wc_PKCS7_DecodeEnvelopedData_multiple_recipients(void); int test_wc_PKCS7_DecodeEnvelopedData_forgedRecipientSetLen(void); int test_wc_PKCS7_DecodeEnvelopedData_constructedDefiniteOctet(void); int test_wc_PKCS7_DecodeAuthEnvelopedData_truncated(void); +int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag(void); int test_wc_PKCS7_AuthEnvelopedData_stream_leak(void); int test_wc_PKCS7_VerifySignedData_PKCS7ContentSeq(void); int test_wc_PKCS7_VerifySignedData_IndefLenOOB(void); @@ -171,6 +172,7 @@ int test_wc_PKCS7_VerifySignedData_NoDigestParams(void); TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeEnvelopedData_forgedRecipientSetLen), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeEnvelopedData_constructedDefiniteOctet), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_truncated), \ + TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_AuthEnvelopedData_stream_leak) #define TEST_PKCS7_SIGNED_ENCRYPTED_DATA_DECLS \ diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index 6f63ba28354..2627bf27df8 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -128,6 +128,9 @@ struct PKCS7SignerInfo { #define WOLFSSL_PKCS7_MAX_DECOMPRESSION 1031 #endif +/* RFC 5084 section 3.2: smallest ICV AES-GCM may carry in a bundle. */ +#define PKCS7_GCM_MIN_ICV_SZ 12 + #ifndef NO_PKCS7_STREAM /* Hard upper bound on a single PKCS7 streaming buffer allocation. Guards @@ -16318,22 +16321,16 @@ int wc_PKCS7_DecodeAuthEnvelopedData(wc_PKCS7* pkcs7, byte* in, WOLFSSL_MSG("AuthEnvelopedData authTag size mismatch"); ret = ASN_PARSE_E; } + /* RFC 5084 section 3.2: AES-GCM ICV is 12 to 16 bytes, and macSz + * already bounds the top. The floor is raised to the build minimum + * when that is larger. */ if (ret == 0 && (encOID == AES128GCMb || encOID == AES192GCMb || - encOID == AES256GCMb)) { - #if (defined(HAVE_FIPS) && FIPS_VERSION3_LT(7,0,0)) || \ - defined(HAVE_SELFTEST) || !defined(HAVE_AESGCM) - if (authTagSz < WOLFSSL_MIN_AUTH_TAG_SZ) { - WOLFSSL_MSG("AuthEnvelopedData GCM authTag too small"); - ret = ASN_PARSE_E; - } - #else - ret = wc_local_AesGcmCheckTagSz(authTagSz); - if (ret != 0) { - ret = ASN_PARSE_E; - WOLFSSL_MSG("AuthEnvelopedData GCM authTag invalid size"); - } - #endif + encOID == AES256GCMb) && + (authTagSz < PKCS7_GCM_MIN_ICV_SZ || + authTagSz < WOLFSSL_MIN_AUTH_TAG_SZ)) { + WOLFSSL_MSG("AuthEnvelopedData GCM authTag invalid size"); + ret = ASN_PARSE_E; } if (ret == 0 && (encOID == AES128CCMb || encOID == AES192CCMb || From a892b7676fe5042ea5fdd19d6aa9ce948ea03151 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 17 Sep 2026 12:26:48 -0600 Subject: [PATCH 2/3] PKCS7: enforce RFC 5084 CCM ICV size and SP 800-38C 8 byte floor --- tests/api/test_pkcs7.c | 40 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_pkcs7.h | 2 ++ wolfcrypt/src/pkcs7.c | 13 ++++++++++--- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/tests/api/test_pkcs7.c b/tests/api/test_pkcs7.c index 4d59c7f8bc8..5f12a7f011d 100644 --- a/tests/api/test_pkcs7.c +++ b/tests/api/test_pkcs7.c @@ -3975,6 +3975,46 @@ int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag(void) } /* END test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag() */ +/* A CCM tag under 8 bytes must be refused, even when the build allows + * short tags for plain AES-CCM calls. */ +int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm(void) +{ + EXPECT_DECLS; +#if defined(HAVE_PKCS7) && defined(HAVE_AESCCM) && !defined(NO_RSA) && \ + !defined(NO_AES) && defined(WOLFSSL_AES_128) + byte enveloped[2048]; + int encSz = 0; + + ExpectIntGT(encSz = pkcs7_shortTagBundle(enveloped, sizeof(enveloped), + AES128CCMb, 6, DATA), 0); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(pkcs7_decodeShortTag(enveloped, encSz), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + } + +/* Odd sizes are the only way to reach the parity rule, and RFC 5084 section + * 3.1 stops at 16, so 13 and 15 are the only candidates. */ +#if WOLFSSL_MIN_AUTH_TAG_SZ <= 13 + #define PKCS7_TEST_CCM_ODD_SZ 13 +#elif WOLFSSL_MIN_AUTH_TAG_SZ <= 15 + #define PKCS7_TEST_CCM_ODD_SZ 15 +#endif + +#ifdef PKCS7_TEST_CCM_ODD_SZ + /* RFC 5084 section 3.1 has no odd ICV size, so this is refused while + * parsing rather than later by the cipher */ + ExpectIntGT(encSz = pkcs7_shortTagBundle(enveloped, sizeof(enveloped), + AES128CCMb, PKCS7_TEST_CCM_ODD_SZ, DATA), 0); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(pkcs7_decodeShortTag(enveloped, encSz), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + } +#endif +#endif + return EXPECT_RESULT(); +} /* END test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm() */ + + /* Tearing down a PKCS7 whose AuthEnvelopedData decode stopped part-way must * not leak the encryptedContent buffer. * diff --git a/tests/api/test_pkcs7.h b/tests/api/test_pkcs7.h index bd14c92a45d..c564399713e 100644 --- a/tests/api/test_pkcs7.h +++ b/tests/api/test_pkcs7.h @@ -77,6 +77,7 @@ int test_wc_PKCS7_DecodeEnvelopedData_forgedRecipientSetLen(void); int test_wc_PKCS7_DecodeEnvelopedData_constructedDefiniteOctet(void); int test_wc_PKCS7_DecodeAuthEnvelopedData_truncated(void); int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag(void); +int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm(void); int test_wc_PKCS7_AuthEnvelopedData_stream_leak(void); int test_wc_PKCS7_VerifySignedData_PKCS7ContentSeq(void); int test_wc_PKCS7_VerifySignedData_IndefLenOOB(void); @@ -173,6 +174,7 @@ int test_wc_PKCS7_VerifySignedData_NoDigestParams(void); TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeEnvelopedData_constructedDefiniteOctet), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_truncated), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag), \ + TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_AuthEnvelopedData_stream_leak) #define TEST_PKCS7_SIGNED_ENCRYPTED_DATA_DECLS \ diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index 2627bf27df8..4d0c7d8df51 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -128,8 +128,10 @@ struct PKCS7SignerInfo { #define WOLFSSL_PKCS7_MAX_DECOMPRESSION 1031 #endif -/* RFC 5084 section 3.2: smallest ICV AES-GCM may carry in a bundle. */ +/* RFC 5084 section 3.2 and SP 800-38C appendix B.2: smallest ICV each mode + * may carry in a bundle. */ #define PKCS7_GCM_MIN_ICV_SZ 12 +#define PKCS7_CCM_MIN_ICV_SZ 8 #ifndef NO_PKCS7_STREAM @@ -16332,11 +16334,16 @@ int wc_PKCS7_DecodeAuthEnvelopedData(wc_PKCS7* pkcs7, byte* in, WOLFSSL_MSG("AuthEnvelopedData GCM authTag invalid size"); ret = ASN_PARSE_E; } + /* RFC 5084 section 3.1 lists even ICV sizes only, and SP 800-38C + * appendix B.2 wants 8 bytes or more. The floor is raised to the + * build minimum when that is larger. */ if (ret == 0 && (encOID == AES128CCMb || encOID == AES192CCMb || encOID == AES256CCMb) && - authTagSz < WOLFSSL_MIN_AUTH_TAG_SZ) { - WOLFSSL_MSG("AuthEnvelopedData CCM authTag too small"); + (authTagSz < PKCS7_CCM_MIN_ICV_SZ || + authTagSz < WOLFSSL_MIN_AUTH_TAG_SZ || + (authTagSz & 1) != 0)) { + WOLFSSL_MSG("AuthEnvelopedData CCM authTag invalid size"); ret = ASN_PARSE_E; } From 97dd37bf9224f66d4e2b2d5b8d95bdebef62cad0 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Thu, 17 Sep 2026 12:26:48 -0600 Subject: [PATCH 3/3] PKCS7: restore the cipher when the stream re-enters at the tag state --- tests/api/test_pkcs7.c | 47 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_pkcs7.h | 2 ++ wolfcrypt/src/pkcs7.c | 2 ++ 3 files changed, 51 insertions(+) diff --git a/tests/api/test_pkcs7.c b/tests/api/test_pkcs7.c index 5f12a7f011d..b45af81ef3d 100644 --- a/tests/api/test_pkcs7.c +++ b/tests/api/test_pkcs7.c @@ -4015,6 +4015,53 @@ int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm(void) } /* END test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm() */ +/* Feeding the bundle in chunks makes the decoder come back in at the tag + * state, where it must still refuse a short tag. */ +int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagChunked(void) +{ + EXPECT_DECLS; +#if defined(HAVE_PKCS7) && defined(HAVE_AESGCM) && !defined(NO_RSA) && \ + !defined(NO_AES) && defined(WOLFSSL_AES_128) && !defined(NO_PKCS7_STREAM) + PKCS7* pkcs7 = NULL; + byte enveloped[2048]; + byte decoded[256]; + int encSz = 0; + int ret = 0; + int idx; + int chunk = 1; + + /* authenticated attributes plus one byte at a time make the decoder stop + * in its own state, so it comes back in at the tag state instead of + * falling through to it with the cipher still in hand */ + ExpectIntGT(encSz = pkcs7_shortTagBundle(enveloped, sizeof(enveloped), + AES128GCMb, 8, FIRMWARE_PKG_DATA), 0); + + ExpectNotNull(pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId)); + ExpectIntEQ(wc_PKCS7_InitWithCert(pkcs7, (byte*)client_cert_der_2048, + sizeof_client_cert_der_2048), 0); + if (pkcs7 != NULL) { + pkcs7->privateKey = (byte*)client_key_der_2048; + pkcs7->privateKeySz = sizeof_client_key_der_2048; + } + + if (EXPECT_SUCCESS()) { + for (idx = 0; idx < encSz; idx += chunk) { + int sz = (encSz - idx < chunk) ? encSz - idx : chunk; + + ret = wc_PKCS7_DecodeAuthEnvelopedData(pkcs7, enveloped + idx, + (word32)sz, decoded, sizeof(decoded)); + if (ret != WC_NO_ERR_TRACE(WC_PKCS7_WANT_READ_E)) + break; + } + ExpectIntEQ(ret, WC_NO_ERR_TRACE(ASN_PARSE_E)); + } + + wc_PKCS7_Free(pkcs7); +#endif + return EXPECT_RESULT(); +} /* END test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagChunked() */ + + /* Tearing down a PKCS7 whose AuthEnvelopedData decode stopped part-way must * not leak the encryptedContent buffer. * diff --git a/tests/api/test_pkcs7.h b/tests/api/test_pkcs7.h index c564399713e..3c2194d20e7 100644 --- a/tests/api/test_pkcs7.h +++ b/tests/api/test_pkcs7.h @@ -78,6 +78,7 @@ int test_wc_PKCS7_DecodeEnvelopedData_constructedDefiniteOctet(void); int test_wc_PKCS7_DecodeAuthEnvelopedData_truncated(void); int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag(void); int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm(void); +int test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagChunked(void); int test_wc_PKCS7_AuthEnvelopedData_stream_leak(void); int test_wc_PKCS7_VerifySignedData_PKCS7ContentSeq(void); int test_wc_PKCS7_VerifySignedData_IndefLenOOB(void); @@ -175,6 +176,7 @@ int test_wc_PKCS7_VerifySignedData_NoDigestParams(void); TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_truncated), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_shortTag), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagCcm), \ + TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeAuthEnvelopedData_shortTagChunked), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_AuthEnvelopedData_stream_leak) #define TEST_PKCS7_SIGNED_ENCRYPTED_DATA_DECLS \ diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index 4d0c7d8df51..0dba93eb02e 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -16294,6 +16294,8 @@ int wc_PKCS7_DecodeAuthEnvelopedData(wc_PKCS7* pkcs7, byte* in, encodedAttribs = pkcs7->stream->aad; } macSz = (int)pkcs7->stream->icvSz; + /* re-entry here skips the earlier states, so get the cipher back */ + wc_PKCS7_StreamGetVar(pkcs7, &encOID, NULL, NULL); #endif