From 5cf136d15a63f80208915fd8392895730e4c7818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 09:02:39 +0200 Subject: [PATCH 1/8] F-6426 - Reject Camellia cipher ops when no key is set The Camellia encrypt and decrypt operations used the key schedule without checking that a key had ever been configured. A zeroed or otherwise unkeyed context has a keySz that does not match 128, 192, or 256, so the underlying block transform hit the default no-op case and CBC emitted an easily reversible XOR chain while still returning success. A caller who forgot wc_CamelliaSetKey received a success code with effectively unencrypted output. Add a key-state check that accepts only valid Camellia key sizes and have wc_CamelliaEncryptDirect, wc_CamelliaDecryptDirect, wc_CamelliaCbcEncrypt, and wc_CamelliaCbcDecrypt return MISSING_KEY when no key has been set. Mirrors the existing 3DES keySet guard. Add a regression test covering the unkeyed and garbage key-size paths. --- tests/api/test_camellia.c | 42 +++++++++++++++++++++++++++++++++++++++ tests/api/test_camellia.h | 2 ++ wolfcrypt/src/camellia.c | 19 ++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/tests/api/test_camellia.c b/tests/api/test_camellia.c index cc1845e5ed..a5ef9e90cf 100644 --- a/tests/api/test_camellia.c +++ b/tests/api/test_camellia.c @@ -254,6 +254,48 @@ int test_wc_CamelliaCbcEncryptDecrypt(void) } /* END test_wc_CamelliaCbcEncryptDecrypt */ +/* + * Cipher operations must fail safely when no key has been configured. + */ +int test_wc_Camellia_MissingKey(void) +{ + EXPECT_DECLS; +#ifdef HAVE_CAMELLIA + wc_Camellia camellia; + static const byte plainT[] = { + 0x6B, 0xC1, 0xBE, 0xE2, 0x2E, 0x40, 0x9F, 0x96, + 0xE9, 0x3D, 0x7E, 0x11, 0x73, 0x93, 0x17, 0x2A + }; + byte out[WC_CAMELLIA_BLOCK_SIZE]; + + XMEMSET(out, 0, sizeof(out)); + + /* Zeroed context, never keyed: every op must reject with MISSING_KEY + * rather than silently emitting unencrypted output. */ + XMEMSET(&camellia, 0, sizeof(camellia)); + + ExpectIntEQ(wc_CamelliaEncryptDirect(&camellia, out, plainT), + WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_CamelliaDecryptDirect(&camellia, out, plainT), + WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_CamelliaCbcEncrypt(&camellia, out, plainT, + WC_CAMELLIA_BLOCK_SIZE), WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_CamelliaCbcDecrypt(&camellia, out, plainT, + WC_CAMELLIA_BLOCK_SIZE), WC_NO_ERR_TRACE(MISSING_KEY)); + + /* Garbage key size that is not 128/192/256 must also be rejected. */ + XMEMSET(&camellia, 0, sizeof(camellia)); + camellia.keySz = 64; + + ExpectIntEQ(wc_CamelliaEncryptDirect(&camellia, out, plainT), + WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_CamelliaCbcEncrypt(&camellia, out, plainT, + WC_CAMELLIA_BLOCK_SIZE), WC_NO_ERR_TRACE(MISSING_KEY)); +#endif + return EXPECT_RESULT(); +} /* END test_wc_Camellia_MissingKey */ + + #include #define MC_CIPHER_TEST_COUNT 100 diff --git a/tests/api/test_camellia.h b/tests/api/test_camellia.h index 387faf73cc..d440810f10 100644 --- a/tests/api/test_camellia.h +++ b/tests/api/test_camellia.h @@ -29,6 +29,7 @@ int test_wc_CamelliaSetIV(void); int test_wc_CamelliaFree(void); int test_wc_CamelliaEncryptDecryptDirect(void); int test_wc_CamelliaCbcEncryptDecrypt(void); +int test_wc_Camellia_MissingKey(void); int test_wc_CamelliaCbc_MonteCarlo(void); #define TEST_CAMELLIA_DECLS \ @@ -37,6 +38,7 @@ int test_wc_CamelliaCbc_MonteCarlo(void); TEST_DECL_GROUP("camellia", test_wc_CamelliaFree), \ TEST_DECL_GROUP("camellia", test_wc_CamelliaEncryptDecryptDirect), \ TEST_DECL_GROUP("camellia", test_wc_CamelliaCbcEncryptDecrypt), \ + TEST_DECL_GROUP("camellia", test_wc_Camellia_MissingKey), \ TEST_DECL_GROUP("camellia", test_wc_CamelliaCbc_MonteCarlo) #endif /* WOLFCRYPT_TEST_CAMELLIA_H */ diff --git a/wolfcrypt/src/camellia.c b/wolfcrypt/src/camellia.c index 49541144e0..f89975ffd4 100644 --- a/wolfcrypt/src/camellia.c +++ b/wolfcrypt/src/camellia.c @@ -1562,11 +1562,21 @@ int wc_CamelliaSetIV(wc_Camellia* cam, const byte* iv) } +/* Returns 1 when a valid key has been configured, 0 otherwise. */ +static int CamelliaKeyIsSet(const wc_Camellia* cam) +{ + return (cam->keySz == 128 || cam->keySz == 192 || cam->keySz == 256); +} + + int wc_CamelliaEncryptDirect(wc_Camellia* cam, byte* out, const byte* in) { if (cam == NULL || out == NULL || in == NULL) { return BAD_FUNC_ARG; } + if (!CamelliaKeyIsSet(cam)) { + return MISSING_KEY; + } Camellia_EncryptBlock(cam->keySz, in, cam->key, out); return 0; @@ -1578,6 +1588,9 @@ int wc_CamelliaDecryptDirect(wc_Camellia* cam, byte* out, const byte* in) if (cam == NULL || out == NULL || in == NULL) { return BAD_FUNC_ARG; } + if (!CamelliaKeyIsSet(cam)) { + return MISSING_KEY; + } Camellia_DecryptBlock(cam->keySz, in, cam->key, out); return 0; @@ -1593,6 +1606,9 @@ int wc_CamelliaCbcEncrypt(wc_Camellia* cam, byte* out, const byte* in, word32 sz if (sz % WC_CAMELLIA_BLOCK_SIZE != 0) { return BAD_LENGTH_E; } + if (!CamelliaKeyIsSet(cam)) { + return MISSING_KEY; + } blocks = sz / WC_CAMELLIA_BLOCK_SIZE; while (blocks--) { @@ -1618,6 +1634,9 @@ int wc_CamelliaCbcDecrypt(wc_Camellia* cam, byte* out, const byte* in, word32 sz if (sz % WC_CAMELLIA_BLOCK_SIZE != 0) { return BAD_LENGTH_E; } + if (!CamelliaKeyIsSet(cam)) { + return MISSING_KEY; + } blocks = sz / WC_CAMELLIA_BLOCK_SIZE; while (blocks--) { From 7562ae5e371af35a65e9eb8bf1943536c0164ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 09:14:51 +0200 Subject: [PATCH 2/8] F-6427 - Reject RC2 cipher ops when no key is set The RC2 encrypt and decrypt operations used the expanded key schedule without checking that a key had ever been configured. On a zeroed or otherwise unkeyed context the ECB ops ran over an all-zero schedule and returned success, and the CBC wrappers inherited the same behavior, so a caller who skipped wc_Rc2SetKey received ciphertext under an unintended key with no error signalled. Guard wc_Rc2EcbEncrypt and wc_Rc2EcbDecrypt on a zero keylen and return MISSING_KEY when no key has been set. The CBC wrappers call these and propagate the error. Mirrors the existing 3DES keySet guard. Add a regression test covering the unkeyed path for all four ops. --- tests/api/test_rc2.c | 31 +++++++++++++++++++++++++++++++ tests/api/test_rc2.h | 2 ++ wolfcrypt/src/rc2.c | 24 ++++++++++++++++-------- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/tests/api/test_rc2.c b/tests/api/test_rc2.c index 10b7194e4c..19f59e786a 100644 --- a/tests/api/test_rc2.c +++ b/tests/api/test_rc2.c @@ -228,6 +228,37 @@ int test_wc_Rc2CbcEncryptDecrypt(void) } /* END test_wc_Rc2CbcEncryptDecrypt */ +/* + * Cipher operations must fail safely when no key has been configured. + */ +int test_wc_Rc2_MissingKey(void) +{ + EXPECT_DECLS; +#ifdef WC_RC2 + Rc2 rc2; + byte out[RC2_BLOCK_SIZE]; + byte in[RC2_BLOCK_SIZE]; + + XMEMSET(out, 0, sizeof(out)); + XMEMSET(in, 0, sizeof(in)); + + /* Zeroed context, never keyed: every op must reject with MISSING_KEY + * rather than ciphering under an all-zero key schedule. */ + XMEMSET(&rc2, 0, sizeof(rc2)); + + ExpectIntEQ(wc_Rc2EcbEncrypt(&rc2, out, in, RC2_BLOCK_SIZE), + WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_Rc2EcbDecrypt(&rc2, out, in, RC2_BLOCK_SIZE), + WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_Rc2CbcEncrypt(&rc2, out, in, RC2_BLOCK_SIZE), + WC_NO_ERR_TRACE(MISSING_KEY)); + ExpectIntEQ(wc_Rc2CbcDecrypt(&rc2, out, in, RC2_BLOCK_SIZE), + WC_NO_ERR_TRACE(MISSING_KEY)); +#endif + return EXPECT_RESULT(); +} /* END test_wc_Rc2_MissingKey */ + + #define MC_CIPHER_TEST_COUNT 100 #define MC_RC2_MAX_DATA_SZ 1024 diff --git a/tests/api/test_rc2.h b/tests/api/test_rc2.h index acdd08e3ae..a9219494af 100644 --- a/tests/api/test_rc2.h +++ b/tests/api/test_rc2.h @@ -28,6 +28,7 @@ int test_wc_Rc2SetKey(void); int test_wc_Rc2SetIV(void); int test_wc_Rc2EcbEncryptDecrypt(void); int test_wc_Rc2CbcEncryptDecrypt(void); +int test_wc_Rc2_MissingKey(void); int test_wc_Rc2Cbc_MonteCarlo(void); int test_wc_Rc2Free(void); @@ -36,6 +37,7 @@ int test_wc_Rc2Free(void); TEST_DECL_GROUP("rc2", test_wc_Rc2SetIV), \ TEST_DECL_GROUP("rc2", test_wc_Rc2EcbEncryptDecrypt), \ TEST_DECL_GROUP("rc2", test_wc_Rc2CbcEncryptDecrypt), \ + TEST_DECL_GROUP("rc2", test_wc_Rc2_MissingKey), \ TEST_DECL_GROUP("rc2", test_wc_Rc2Cbc_MonteCarlo), \ TEST_DECL_GROUP("rc2", test_wc_Rc2Free) diff --git a/wolfcrypt/src/rc2.c b/wolfcrypt/src/rc2.c index 3fd9ecbbac..f4050b97bf 100644 --- a/wolfcrypt/src/rc2.c +++ b/wolfcrypt/src/rc2.c @@ -171,6 +171,10 @@ int wc_Rc2EcbEncrypt(Rc2* rc2, byte* out, const byte* in, word32 sz) return BUFFER_E; } + if (rc2->keylen == 0) { + return MISSING_KEY; + } + r10 = (word16)((word16)in[1] << 8) | in[0]; /* R[0] */ r32 = (word16)((word16)in[3] << 8) | in[2]; /* R[1] */ r54 = (word16)((word16)in[5] << 8) | in[4]; /* R[2] */ @@ -236,6 +240,10 @@ int wc_Rc2EcbDecrypt(Rc2* rc2, byte* out, const byte* in, word32 sz) return BUFFER_E; } + if (rc2->keylen == 0) { + return MISSING_KEY; + } + r0 = (word16)((word16)in[1] << 8) | in[0]; r1 = (word16)((word16)in[3] << 8) | in[2]; r2 = (word16)((word16)in[5] << 8) | in[4]; @@ -285,14 +293,14 @@ int wc_Rc2CbcEncrypt(Rc2* rc2, byte* out, const byte* in, word32 sz) return BAD_FUNC_ARG; } - if (sz == 0) { - return 0; - } - if (sz % RC2_BLOCK_SIZE != 0) { return BAD_LENGTH_E; } + if (rc2->keylen == 0) { + return MISSING_KEY; + } + blocks = sz / RC2_BLOCK_SIZE; while (blocks--) { @@ -320,14 +328,14 @@ int wc_Rc2CbcDecrypt(Rc2* rc2, byte* out, const byte* in, word32 sz) return BAD_FUNC_ARG; } - if (sz == 0) { - return 0; - } - if (sz % RC2_BLOCK_SIZE != 0) { return BAD_LENGTH_E; } + if (rc2->keylen == 0) { + return MISSING_KEY; + } + blocks = sz / RC2_BLOCK_SIZE; while (blocks--) { From 845a3a93b552d563be246423e183d5d83bac1f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 10:11:34 +0200 Subject: [PATCH 3/8] F-6345 - Reject oversized length in memory BIO write wolfSSL_BIO_write rejected negative lengths but allowed a large positive length through to wolfSSL_BIO_MEMORY_write. On a fresh buffer an INT_MAX length overflowed the 4/3 buffer growth calculation, so the grow reported success with a short allocation and the following copy read far past the small source buffer. Add an upper bound check that rejects lengths large enough to overflow the growth math before any allocation or copy, and add a regression test that drives a huge length through the public BIO_write entry point. --- src/bio.c | 10 ++++++++++ tests/api/test_ossl_bio.c | 33 +++++++++++++++++++++++++++++++++ tests/api/test_ossl_bio.h | 2 ++ 3 files changed, 45 insertions(+) diff --git a/src/bio.c b/src/bio.c index b9dfe6b7dd..9d94144f57 100644 --- a/src/bio.c +++ b/src/bio.c @@ -631,6 +631,16 @@ static int wolfSSL_BIO_MEMORY_write(WOLFSSL_BIO* bio, const void* data, if (len <= 0) return 0; /* Nothing to write */ + /* Reject sizes that would overflow the buffer growth calculation, which + * rounds the requested size up to the next multiple of 4/3 via + * (size + 3) / 3 * 4. The extra rounding slack means the safe ceiling is + * one below (INT_MAX / 4) * 3. Without this an oversized length grows a + * short buffer and copies past the source. */ + if (len > ((INT_MAX / 4) * 3) - 1 - bio->wrSz) { + WOLFSSL_MSG("write length too large"); + return WOLFSSL_BIO_ERROR; + } + if (wolfSSL_BUF_MEM_grow_ex(bio->mem_buf, ((size_t)bio->wrSz) + ((size_t)len), 0) == 0) { WOLFSSL_MSG("Error growing memory area"); diff --git a/tests/api/test_ossl_bio.c b/tests/api/test_ossl_bio.c index b111fd4468..6223ee8f93 100644 --- a/tests/api/test_ossl_bio.c +++ b/tests/api/test_ossl_bio.c @@ -1019,6 +1019,39 @@ int test_wolfSSL_BIO_read_negative_len(void) return EXPECT_RESULT(); } +/* A length larger than the source buffer must never reach XMEMCPY in the + * memory-BIO write path. A huge positive length on a fresh buffer would + * otherwise overflow the buffer growth calculation, allocate a short + * destination, and copy far past the small source. Verify such a length is + * rejected with an error, nothing is buffered, and normal writes still work. */ +int test_wolfSSL_BIO_write_large_len(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) + BIO* bio = NULL; + char msg[] = "large length test"; + int msgLen = (int)XSTRLEN(msg); + char out[64]; + + ExpectNotNull(bio = BIO_new(BIO_s_mem())); + + /* Oversized length on a fresh buffer: must be rejected with an error, not + * a wild copy from the small source buffer. */ + ExpectIntLT(BIO_write(bio, msg, (int)0x7FFFFFFF), 0); + /* Nothing should have been buffered. */ + ExpectIntEQ(BIO_pending(bio), 0); + + /* A normal write then read still returns the intact message. */ + ExpectIntEQ(BIO_write(bio, msg, msgLen), msgLen); + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(BIO_read(bio, out, (int)sizeof(out)), msgLen); + ExpectIntEQ(XMEMCMP(out, msg, msgLen), 0); + + BIO_free(bio); +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_BIO_printf(void) { diff --git a/tests/api/test_ossl_bio.h b/tests/api/test_ossl_bio.h index acf13fb776..7aac4f4add 100644 --- a/tests/api/test_ossl_bio.h +++ b/tests/api/test_ossl_bio.h @@ -36,6 +36,7 @@ int test_wolfSSL_BIO_s_null(void); int test_wolfSSL_BIO_accept(void); int test_wolfSSL_BIO_write(void); int test_wolfSSL_BIO_read_negative_len(void); +int test_wolfSSL_BIO_write_large_len(void); int test_wolfSSL_BIO_printf(void); int test_wolfSSL_BIO_f_md(void); int test_wolfSSL_BIO_up_ref(void); @@ -57,6 +58,7 @@ int test_wolfSSL_BIO_get_init(void); TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_s_null), \ TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_write), \ TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_read_negative_len), \ + TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_write_large_len), \ TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_printf), \ TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_f_md), \ TEST_DECL_GROUP("ossl_bio", test_wolfSSL_BIO_up_ref), \ From 2943ee6a69d9058e3fe325daef8284910a6e927a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 10:26:23 +0200 Subject: [PATCH 4/8] F-6346 - Reject oversized length in EVP_EncodeBlock wolfSSL_EVP_EncodeBlock rejected negative input lengths but passed any large positive length straight to Base64_Encode_NoNl, which read that many bytes from the caller input buffer and ran past its allocation. Reject input lengths whose base64 output would overflow a positive int, which also bounds the read against the caller allocation. The encoded length is the int return value, so the safe maximum input is (INT_MAX / 4) * 3. --- tests/api/test_evp.c | 4 ++++ wolfcrypt/src/evp.c | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/tests/api/test_evp.c b/tests/api/test_evp.c index 172bf3d2c1..133b91bfd5 100644 --- a/tests/api/test_evp.c +++ b/tests/api/test_evp.c @@ -203,6 +203,10 @@ int test_wolfSSL_EVP_EncodeUpdate(void) sizeof(encBlock0)-1); ExpectStrEQ(encOutBuff, encBlock0); + /* oversized length must be rejected, not read past the input buffer */ + XMEMSET( encOutBuff,0, sizeof(encOutBuff)); + ExpectIntEQ(EVP_EncodeBlock(encOutBuff, plain0, 0x7FFFFFFF), -1); + /* pass small size( < 48bytes ) input, then make sure they are not * encoded and just stored in ctx */ diff --git a/wolfcrypt/src/evp.c b/wolfcrypt/src/evp.c index 5001288338..4503917564 100644 --- a/wolfcrypt/src/evp.c +++ b/wolfcrypt/src/evp.c @@ -13266,6 +13266,11 @@ int wolfSSL_EVP_EncodeBlock(unsigned char *out, const unsigned char *in, if (inLen < 0) return WOLFSSL_FATAL_ERROR; + /* Reject lengths whose base64 output would overflow a positive int. This + * also guards against reads far past the caller's input allocation. */ + if (inLen > (INT_MAX / 4) * 3) + return WOLFSSL_FATAL_ERROR; + if (Base64_Encode_NoNl(in, (word32)inLen, out, &ret) == 0) return (int)ret; else From d88ac76fda44bda80d708a19ee83d6be122a7c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 10:56:36 +0200 Subject: [PATCH 5/8] F-6347 - Reject negative and oversized length in EVP_EncodeUpdate wolfSSL_EVP_EncodeUpdate did not validate the input length. A large inl caused the block loop and the residual copy to read far past the caller's input buffer, and a negative inl was silently treated as success. Reject negative lengths and lengths whose base64 output would overflow a positive int before processing any data. --- tests/api/test_evp.c | 15 +++++++++++++++ wolfcrypt/src/evp.c | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/api/test_evp.c b/tests/api/test_evp.c index 133b91bfd5..4935669791 100644 --- a/tests/api/test_evp.c +++ b/tests/api/test_evp.c @@ -207,6 +207,21 @@ int test_wolfSSL_EVP_EncodeUpdate(void) XMEMSET( encOutBuff,0, sizeof(encOutBuff)); ExpectIntEQ(EVP_EncodeBlock(encOutBuff, plain0, 0x7FFFFFFF), -1); + /* oversized length must be rejected by EVP_EncodeUpdate as well, rather + * than reading far past the caller's input buffer */ + EVP_EncodeInit(ctx); + outl = 1; + XMEMSET( encOutBuff,0, sizeof(encOutBuff)); + ExpectIntEQ( + EVP_EncodeUpdate( + ctx, + encOutBuff, + &outl, + plain0, + 0x7FFFFFFF), + 0); + ExpectIntEQ(outl, 0); + /* pass small size( < 48bytes ) input, then make sure they are not * encoded and just stored in ctx */ diff --git a/wolfcrypt/src/evp.c b/wolfcrypt/src/evp.c index 4503917564..66e66381b7 100644 --- a/wolfcrypt/src/evp.c +++ b/wolfcrypt/src/evp.c @@ -13329,6 +13329,15 @@ int wolfSSL_EVP_EncodeUpdate(WOLFSSL_EVP_ENCODE_CTX* ctx, *outl = 0; + if (inl < 0) + return 0; + + /* Reject lengths whose base64 output would overflow a positive int. This + * also guards against reads far past the caller's input allocation. */ + if (inl > (INT_MAX / (BASE64_ENCODE_RESULT_BLOCK_SIZE + 1)) * + BASE64_ENCODE_BLOCK_SIZE) + return 0; + /* if the remaining data exists in the ctx, add input data to them * to create a block(48bytes) for encoding */ From 3c5ae182a6d00b7d6539ab3225919c0280fa5a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 11:28:31 +0200 Subject: [PATCH 6/8] F-6350 - Cap d2i_ASN1_OBJECT parse window to OID size An oversized length argument was passed straight to GetASNHeader as the buffer bound. A caller supplying a length larger than the real buffer let the OBJECT_ID header claim more content than was present, driving the OID validation read past the end of the allocation. Since an ASN1_OBJECT is an OID, clamp the parse window to the maximum OID encoding so the header decode cannot read beyond a sane bound. --- src/ssl_asn1.c | 13 ++++++++++++- tests/api/test_ossl_asn1.c | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ssl_asn1.c b/src/ssl_asn1.c index 2b7c25f01b..2e02ecfd87 100644 --- a/src/ssl_asn1.c +++ b/src/ssl_asn1.c @@ -2222,6 +2222,7 @@ WOLFSSL_ASN1_OBJECT *wolfSSL_d2i_ASN1_OBJECT(WOLFSSL_ASN1_OBJECT **a, WOLFSSL_ASN1_OBJECT* ret = NULL; int len = 0; word32 idx = 0; + word32 maxIdx; WOLFSSL_ENTER("wolfSSL_d2i_ASN1_OBJECT"); @@ -2231,7 +2232,17 @@ WOLFSSL_ASN1_OBJECT *wolfSSL_d2i_ASN1_OBJECT(WOLFSSL_ASN1_OBJECT **a, return NULL; } - if (GetASNHeader(*der, ASN_OBJECT_ID, &idx, &len, (word32)length) < 0) { + /* An ASN.1 OBJECT is an OID, whose DER encoding cannot exceed the OID + * ceiling: a tag byte, a single short-form length byte (content is at + * most MAX_OID_SZ, which is below the long-form threshold) and the OID + * content. Cap the parse window so an oversized length argument cannot + * drive the header decode to read past the end of the actual buffer. */ + maxIdx = (word32)length; + if (maxIdx > (word32)(MAX_OID_SZ + 2)) { + maxIdx = (word32)(MAX_OID_SZ + 2); + } + + if (GetASNHeader(*der, ASN_OBJECT_ID, &idx, &len, maxIdx) < 0) { WOLFSSL_MSG("error getting tag"); return NULL; } diff --git a/tests/api/test_ossl_asn1.c b/tests/api/test_ossl_asn1.c index f1b56c3f04..2d9c7540ed 100644 --- a/tests/api/test_ossl_asn1.c +++ b/tests/api/test_ossl_asn1.c @@ -916,6 +916,8 @@ int test_wolfSSL_ASN1_get_object(void) const unsigned char objDerBadLen[] = { 0x30, 0x04 }; const unsigned char objDerNotObj[] = { 0x02, 0x01, 0x00 }; const unsigned char objDerNoData[] = { 0x06, 0x00 }; + /* OBJECT_ID header claims 126 content bytes but only one is present. */ + const unsigned char objDerOobLen[] = { 0x06, 0x7e, 0x2a }; const unsigned char* p; unsigned char objDer[10]; unsigned char* der; @@ -1018,6 +1020,10 @@ int test_wolfSSL_ASN1_get_object(void) ExpectNull(d2i_ASN1_OBJECT(&a, &p, sizeof(objDerNotObj))); p = objDerNoData; ExpectNull(d2i_ASN1_OBJECT(&a, &p, sizeof(objDerNoData))); + /* Oversized length must not let the header's claimed content length drive + * a read past the end of the actual buffer. */ + p = objDerOobLen; + ExpectNull(d2i_ASN1_OBJECT(&a, &p, INT_MAX)); /* Create an ASN OBJECT from content */ p = derBuf + 2; From e8865748f2113b274014c5eb1c7094d409b8a23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 11:47:47 +0200 Subject: [PATCH 7/8] F-6351 - Fix use after free in wolfSSL_ASN1_STRING_set self-alias When the caller passes the object's own data pointer as the source, wolfSSL_ASN1_STRING_set freed the existing buffer before copying from it, reading freed memory in the dynamic case and copying cleared bytes in the fixed-buffer case. Duplicate the source into a temporary buffer when it aliases the object before disposing of the old buffer, then free the temporary once the copy completes. --- src/ssl_asn1.c | 64 ++++++++++++++++++++++---------------- tests/api/test_ossl_asn1.c | 16 ++++++++++ 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/ssl_asn1.c b/src/ssl_asn1.c index 2e02ecfd87..7badd8e5f5 100644 --- a/src/ssl_asn1.c +++ b/src/ssl_asn1.c @@ -3174,42 +3174,52 @@ int wolfSSL_ASN1_STRING_set(WOLFSSL_ASN1_STRING* asn1, const void* data, int sz) * when sz == INT_MAX. By this point sz >= 0 (negative sz is * handled above as OpenSSL -1/strlen compat). */ size_t allocSz = (size_t)sz + 1; + char* oldData = asn1->data; + int oldDynamic = asn1->isDynamic; + char* dst; - /* Dispose of any existing dynamic data. */ - if (asn1->isDynamic) { - XFREE(asn1->data, NULL, DYNAMIC_TYPE_OPENSSL); - asn1->data = NULL; - } - - /* Check string will fit - including NUL. */ + /* Select the destination buffer WITHOUT disposing of the existing + * data yet. Deferring the free keeps the copy below safe even when + * the source aliases the object's own buffer (data == asn1->data), + * avoiding a use-after-free or clear-before-copy without needing an + * ephemeral allocation. */ if (allocSz > CTC_NAME_SIZE) { /* Allocate new buffer. */ - asn1->data = (char*)XMALLOC(allocSz, NULL, - DYNAMIC_TYPE_OPENSSL); - if (asn1->data == NULL) { + dst = (char*)XMALLOC(allocSz, NULL, DYNAMIC_TYPE_OPENSSL); + if (dst == NULL) { ret = 0; } - else { - /* Ensure buffer will be freed. */ - asn1->isDynamic = 1; - } } else { - /* Clear out fixed array and use it for data. */ - XMEMSET(asn1->strData, 0, CTC_NAME_SIZE); - asn1->data = asn1->strData; - asn1->isDynamic = 0; + /* Use the fixed array for data. */ + dst = asn1->strData; } - } - if (ret == 1) { - /* Check if there is a string to copy. */ - if (data != NULL) { - /* Copy string and append NUL. */ - XMEMCPY(asn1->data, data, (size_t)sz); - asn1->data[sz] = '\0'; + + if (ret == 1) { + /* Copy string and append NUL. XMEMMOVE handles the case where + * data aliases the fixed array (source and destination overlap). */ + if (data != NULL) { + XMEMMOVE(dst, data, (size_t)sz); + } + dst[sz] = '\0'; + + /* Clear any remainder of the fixed array (matches prior behavior + * of zeroing the whole array). Done after the copy so it never + * disturbs an aliased source. */ + if (dst == asn1->strData) { + XMEMSET(dst + sz + 1, 0, CTC_NAME_SIZE - (size_t)sz - 1); + } + + /* Dispose of any old dynamic buffer now the copy is complete. */ + if (oldDynamic && (oldData != dst)) { + XFREE(oldData, NULL, DYNAMIC_TYPE_OPENSSL); + } + + /* Commit the new buffer and its properties. */ + asn1->data = dst; + asn1->isDynamic = (allocSz > CTC_NAME_SIZE) ? 1 : 0; + asn1->length = sz; } - /* Set size of string. */ - asn1->length = sz; } return ret; diff --git a/tests/api/test_ossl_asn1.c b/tests/api/test_ossl_asn1.c index 2d9c7540ed..fc4a9d28e6 100644 --- a/tests/api/test_ossl_asn1.c +++ b/tests/api/test_ossl_asn1.c @@ -1194,6 +1194,22 @@ int test_wolfSSL_ASN1_STRING(void) ExpectIntEQ(ASN1_STRING_length(NULL), 0); ExpectIntGT(ASN1_STRING_length(str), 0); + /* Setting from the object's own buffer must not read freed/cleared data. + * Fixed-buffer case: data is held in the small array. */ + ExpectIntEQ(ASN1_STRING_set(str, (const void*)data, (int)XSTRLEN(data)), 1); + ExpectIntEQ(ASN1_STRING_set(str, ASN1_STRING_get0_data(str), + ASN1_STRING_length(str)), 1); + ExpectIntEQ(ASN1_STRING_length(str), (int)XSTRLEN(data)); + ExpectIntEQ(XMEMCMP(ASN1_STRING_get0_data(str), data, XSTRLEN(data)), 0); + /* Dynamic-buffer case: data is held in a heap allocation. */ + ExpectIntEQ(ASN1_STRING_set(str, (const void*)longData, + (int)XSTRLEN(longData)), 1); + ExpectIntEQ(ASN1_STRING_set(str, ASN1_STRING_get0_data(str), + ASN1_STRING_length(str)), 1); + ExpectIntEQ(ASN1_STRING_length(str), (int)XSTRLEN(longData)); + ExpectIntEQ(XMEMCMP(ASN1_STRING_get0_data(str), longData, + XSTRLEN(longData)), 0); + ASN1_STRING_free(c); ASN1_STRING_free(str); ASN1_STRING_free(NULL); From 154f2e2ea41b9bc1b6ac6e7867bd1fe527fb13c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 13:14:24 +0200 Subject: [PATCH 8/8] F-6547 - Reject TLS KeyUpdate on QUIC connections QUIC performs key updates at the packet-protection layer via the Key Phase bit, so RFC 9001 section 6 requires a QUIC endpoint to reject any received TLS KeyUpdate handshake message as a fatal unexpected_message connection error and to never send one. The TLS 1.3 receive path processed the message normally, rotating traffic secrets and possibly emitting a prohibited KeyUpdate response, and the send path allowed a QUIC connection to originate a KeyUpdate. Guard the key_update case in SanityCheckTls13MsgReceived so a QUIC connection aborts with a fatal unexpected_message alert, and guard Tls13UpdateKeys so a QUIC connection cannot send a KeyUpdate. Add a QUIC unit test that feeds a post-handshake KeyUpdate and confirms the connection is refused. --- src/tls13.c | 20 +++++++++++++++++++- tests/quic.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/tls13.c b/src/tls13.c index 0778cc186b..92e89cecb3 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13568,6 +13568,16 @@ static int SanityCheckTls13MsgReceived(WOLFSSL* ssl, byte type) case key_update: /* Valid on both sides. */ +#ifdef WOLFSSL_QUIC + /* RFC 9001 Section 6: QUIC performs key updates at the QUIC + * packet-protection layer, so a TLS KeyUpdate message must be + * rejected as a fatal unexpected_message connection error. */ + if (WOLFSSL_IS_QUIC(ssl)) { + WOLFSSL_MSG("KeyUpdate received over QUIC"); + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; + } +#endif /* Check state. * Client and server must have received finished message from other * side. @@ -14974,6 +14984,13 @@ int Tls13UpdateKeys(WOLFSSL* ssl) if (ssl == NULL || !IsAtLeastTLSv1_3(ssl->version)) return BAD_FUNC_ARG; +#ifdef WOLFSSL_QUIC + /* RFC 9001 Section 6: a QUIC connection must not send a TLS KeyUpdate; + * key updates are handled at the QUIC packet-protection layer. */ + if (WOLFSSL_IS_QUIC(ssl)) + return BAD_FUNC_ARG; +#endif + #ifdef WOLFSSL_DTLS13 /* we are already waiting for the ack of a sent key update message. We can't send another one before receiving its ack. Either wolfSSL_update_keys() @@ -14993,7 +15010,8 @@ int Tls13UpdateKeys(WOLFSSL* ssl) * calling wolfSSL_write() will have the message sent when ready. * * ssl The SSL/TLS object. - * returns BAD_FUNC_ARG when ssl is NULL, or not using TLS v1.3, + * returns BAD_FUNC_ARG when ssl is NULL, not using TLS v1.3, or running over + * QUIC (RFC 9001 handles key updates at the QUIC packet-protection layer), * WOLFSSL_ERROR_WANT_WRITE when non-blocking I/O is not ready to write, * WOLFSSL_SUCCESS on success and otherwise failure. */ diff --git a/tests/quic.c b/tests/quic.c index 3e8ed2a428..e11f7bfb93 100644 --- a/tests/quic.c +++ b/tests/quic.c @@ -1470,6 +1470,56 @@ static int test_quic_server_hello_fail(int verbose) { return EXPECT_RESULT(); } +static int test_quic_key_update_rejected(int verbose) { + EXPECT_DECLS; + WOLFSSL_CTX * ctx_c = NULL; + WOLFSSL_CTX * ctx_s = NULL; + QuicTestContext tclient, tserver; + QuicConversation conv; + uint8_t lbuffer[16]; + size_t len; + int ret; + + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectTrue(wolfSSL_CTX_use_certificate_file(ctx_s, svrCertFile, + WOLFSSL_FILETYPE_PEM)); + ExpectTrue(wolfSSL_CTX_use_PrivateKey_file(ctx_s, svrKeyFile, + WOLFSSL_FILETYPE_PEM)); + + /* complete a normal QUIC handshake */ + QuicTestContext_init(&tclient, ctx_c, "client", verbose); + QuicTestContext_init(&tserver, ctx_s, "server", verbose); + QuicConversation_init(&conv, &tclient, &tserver); + QuicConversation_do(&conv); + + /* RFC 9001 section 6: a QUIC connection must not send a TLS KeyUpdate; + * key updates are handled at the QUIC packet-protection layer. The + * public wolfSSL_update_keys() must refuse on a QUIC connection. */ + ExpectIntEQ(wolfSSL_update_keys(tserver.ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* RFC 9001 section 6: a TLS KeyUpdate handshake message must be rejected + * as a fatal unexpected_message connection error when running over QUIC. + * Feed a key_update (update_not_requested) as post-handshake CRYPTO data + * and confirm the server refuses to process it. */ + len = fake_record(key_update, OPAQUE8_LEN, lbuffer); + lbuffer[HANDSHAKE_HEADER_SZ] = update_not_requested; + ExpectIntEQ(wolfSSL_provide_quic_data(tserver.ssl, + wolfssl_encryption_application, lbuffer, len), WOLFSSL_SUCCESS); + ret = wolfSSL_process_quic_post_handshake(tserver.ssl); + ExpectIntEQ(ret, WC_NO_ERR_TRACE(SANITY_MSG_E)); + + QuicTestContext_free(&tclient); + QuicTestContext_free(&tserver); + + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + printf(" test_quic_key_update_rejected: %s\n", + EXPECT_RESULT() ? pass : fail); + return EXPECT_RESULT(); +} + /* This has gotten a bit out of hand. */ #if (defined(OPENSSL_ALL) || (defined(OPENSSL_EXTRA) && \ (defined(HAVE_STUNNEL) || defined(WOLFSSL_NGINX) || \ @@ -2005,6 +2055,7 @@ int QuicTest(void) #if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) if ((ret = test_quic_server_hello(verbose)) != TEST_SUCCESS) goto leave; if ((ret = test_quic_server_hello_fail(verbose)) != TEST_SUCCESS) goto leave; + if ((ret = test_quic_key_update_rejected(verbose)) != TEST_SUCCESS) goto leave; #ifdef REALLY_HAVE_ALPN_AND_SNI if ((ret = test_quic_alpn(verbose)) != TEST_SUCCESS) goto leave; #endif /* REALLY_HAVE_ALPN_AND_SNI */