From d97d0370d142de87fc5cff9b8de91f0fa7d2e064 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:53:38 +0200 Subject: [PATCH] tests: add TLS 1.3 null cipher HMAC negative test (F-2916) Tls13IntegrityOnly_Decrypt was completely untouched by existing tests, so any mutation of its ConstantCompare would pass CI. Add a memio TLS 1.3 handshake over TLS13-SHA256-SHA256 (integrity-only NULL cipher), then corrupt the final byte of the next record body via an IORecv wrapper and assert the server surfaces DECRYPT_ERROR. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 65 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 67 insertions(+) diff --git a/tests/api.c b/tests/api.c index 8590ad18d4..8e4b76d029 100644 --- a/tests/api.c +++ b/tests/api.c @@ -37505,6 +37505,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_tls_ems_downgrade), TEST_DECL(test_tls_ems_resumption_downgrade), TEST_DECL(test_tls12_chacha20_poly1305_bad_tag), + TEST_DECL(test_tls13_null_cipher_bad_hmac), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 6643b38565..976a3d9490 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -219,6 +219,71 @@ int test_tls12_chacha20_poly1305_bad_tag(void) } +#if defined(WOLFSSL_TLS13) && defined(HAVE_NULL_CIPHER) && \ + defined(BUILD_TLS_SHA256_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) +static int test_tls13_null_bad_hmac_trigger = 0; + +static int test_tls13_null_bad_hmac_io_recv(WOLFSSL* ssl, char* buf, int sz, + void* ctx) +{ + int ret = test_memio_read_cb(ssl, buf, sz, ctx); + /* Tamper with a byte from the encrypted record payload on the first + * read that spans past the 5-byte TLS record header, so the HMAC tag + * check in Tls13IntegrityOnly_Decrypt no longer matches. */ + if (test_tls13_null_bad_hmac_trigger && ret > 5) { + buf[ret - 1] ^= 0xFF; + test_tls13_null_bad_hmac_trigger = 0; + } + return ret; +} +#endif + +/* F-2916: TLS 1.3 integrity-only decryption must surface DECRYPT_ERROR + * when the HMAC tag is corrupted. */ +int test_tls13_null_cipher_bad_hmac(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_NULL_CIPHER) && \ + defined(BUILD_TLS_SHA256_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + const char msg[] = "integrity only"; + char recvBuf[32]; + int ret; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_ciphers = test_ctx.s_ciphers = "TLS13-SHA256-SHA256"; + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + wolfSSL_SSLSetIORecv(ssl_s, test_tls13_null_bad_hmac_io_recv); + + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)XSTRLEN(msg)), + (int)XSTRLEN(msg)); + + test_tls13_null_bad_hmac_trigger = 1; + ret = wolfSSL_read(ssl_s, recvBuf, sizeof(recvBuf)); + ExpectIntLE(ret, 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, ret), + WC_NO_ERR_TRACE(DECRYPT_ERROR)); + + test_tls13_null_bad_hmac_trigger = 0; + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index fb8a23d1a2..275888adf0 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -25,6 +25,7 @@ int test_tls_ems_downgrade(void); int test_tls_ems_resumption_downgrade(void); int test_tls12_chacha20_poly1305_bad_tag(void); +int test_tls13_null_cipher_bad_hmac(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void);