From b01382c13b3554ab1fedab0a92f69299feda5b45 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Thu, 4 Jun 2026 09:26:43 -0400 Subject: [PATCH] Add unit tests that TLS resumption fails due to ALPN mismatch Fix F-4141 --- tests/api/test_tls.c | 153 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 4 ++ 2 files changed, 157 insertions(+) diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 4abf2b6206..970b98e47d 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -1128,6 +1128,159 @@ int test_tls13_resumption_with_alpn(void) return EXPECT_RESULT(); } +/* TLS 1.2 stateful (session-ID) resumption must fall back to a full + * handshake if the ALPN protocol negotiated for the resumed connection + * does not match the ALPN bound to the original session. Mirrors + * test_tls12_session_id_resumption_sni_mismatch but varies ALPN instead + * of SNI. */ +int test_tls12_session_id_resumption_alpn_mismatch(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_ALPN) && \ + defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + struct test_memio_ctx test_ctx; + const char alpnA[] = "h2"; + const char alpnB[] = "http/1.1"; + + /* Step 1: full TLS 1.2 handshake negotiating ALPN=h2, with the + * session ticket path disabled so resumption can only happen via the + * server's session-ID cache. The negotiated ALPN is retained on + * ssl->extensions by ALPN_Select, so SetupSession binds its hash to + * the cached session. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_NoTicketTLSv12(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_NoTicketTLSv12(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_c, (char*)alpnA, (word32)XSTRLEN(alpnA), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_s, (char*)alpnA, (word32)XSTRLEN(alpnA), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Sanity: the first handshake was not a resumption. */ + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + /* Step 2: new SSL objects on the SAME WOLFSSL_CTX (so the server's + * session cache still holds the entry from step 1). The client offers + * the saved session but both sides now advertise a *different* ALPN + * (http/1.1), so the handshake negotiates http/1.1. The server's cache + * lookup matches by session ID, but the server MUST NOT resume because + * the negotiated ALPN differs from the one bound to the original + * session. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + ExpectIntEQ(wolfSSL_NoTicketTLSv12(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_NoTicketTLSv12(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_c, (char*)alpnB, (word32)XSTRLEN(alpnB), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_s, (char*)alpnB, (word32)XSTRLEN(alpnB), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Expected behavior: server falls back to a full handshake because the + * negotiated ALPN does not match the ALPN bound to the cached session. + * Both sides should report no resumption. */ + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_c), 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* TLS 1.3 PSK resumption must fall back to a full handshake if the ALPN + * protocol negotiated for the resumed connection does not match the ALPN + * bound to the original session. Mirrors + * test_tls13_session_resumption_sni_mismatch but varies ALPN instead of + * SNI. */ +int test_tls13_session_resumption_alpn_mismatch(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_ALPN) && defined(HAVE_SESSION_TICKET) && \ + !defined(NO_SESSION_CACHE) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + struct test_memio_ctx test_ctx; + const char alpnA[] = "h2"; + const char alpnB[] = "http/1.1"; + byte readBuf[16]; + + /* Step 1: full TLS 1.3 handshake negotiating ALPN=h2 to obtain a + * session ticket. The negotiated ALPN is retained on ssl->extensions + * by ALPN_Select and bound to the ticket. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(wolfSSL_UseALPN(ssl_c, (char*)alpnA, (word32)XSTRLEN(alpnA), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_s, (char*)alpnA, (word32)XSTRLEN(alpnA), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Sanity: the first handshake was not a resumption. */ + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + /* Drive the post-handshake NewSessionTicket through to the client so + * the saved session is a real resumption ticket. */ + ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + /* Step 2: new SSL objects on the SAME WOLFSSL_CTX (so the server's + * ticket key still matches). The client offers the saved session but + * both sides now advertise a *different* ALPN (http/1.1). The server + * MUST NOT resume because the negotiated ALPN differs from the one + * bound to the original ticket. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + ExpectIntEQ(wolfSSL_UseALPN(ssl_c, (char*)alpnB, (word32)XSTRLEN(alpnB), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_s, (char*)alpnB, (word32)XSTRLEN(alpnB), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Expected behavior: server falls back to a full handshake because the + * negotiated ALPN does not match the ALPN bound to the cached ticket. + * Both sides should report no resumption. */ + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_c), 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + int test_tls_set_curves_list_ecc_fallback(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 4b4f5385c3..d883a05135 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -36,6 +36,8 @@ int test_tls_set_session_min_downgrade(void); int test_tls12_session_id_resumption_sni_mismatch(void); int test_tls13_session_resumption_sni_mismatch(void); int test_tls13_resumption_with_alpn(void); +int test_tls12_session_id_resumption_alpn_mismatch(void); +int test_tls13_session_resumption_alpn_mismatch(void); int test_tls_set_curves_list_ecc_fallback(void); int test_tls12_corrupted_finished(void); int test_tls12_peerauth_failsafe(void); @@ -57,6 +59,8 @@ int test_wolfSSL_alert_desc_string(void); TEST_DECL_GROUP("tls", test_tls12_session_id_resumption_sni_mismatch), \ TEST_DECL_GROUP("tls", test_tls13_session_resumption_sni_mismatch), \ TEST_DECL_GROUP("tls", test_tls13_resumption_with_alpn), \ + TEST_DECL_GROUP("tls", test_tls12_session_id_resumption_alpn_mismatch),\ + TEST_DECL_GROUP("tls", test_tls13_session_resumption_alpn_mismatch), \ TEST_DECL_GROUP("tls", test_tls_set_curves_list_ecc_fallback), \ TEST_DECL_GROUP("tls", test_tls12_corrupted_finished), \ TEST_DECL_GROUP("tls", test_tls12_peerauth_failsafe), \