Skip to content

Commit 08052b9

Browse files
Check SNI/ALPN in TLS 1.3 session resumption
1 parent a05745b commit 08052b9

4 files changed

Lines changed: 170 additions & 9 deletions

File tree

src/internal.c

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38761,8 +38761,22 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl)
3876138761
#endif
3876238762
#if defined(HAVE_SESSION_TICKET) && \
3876338763
(defined(HAVE_SNI) || defined(HAVE_ALPN))
38764-
if((ret=VerifyTicketBinding(ssl)))
38765-
goto out;
38764+
/* Only verify here for TLS 1.2 ticket-based resumption.
38765+
* For stateful (session-ID) resumption ssl->session is
38766+
* not loaded until HandleTlsResumption runs below, which
38767+
* performs its own binding check against the cached
38768+
* session. On mismatch decline the resumption (RFC 6066
38769+
* Section 3) but proceed with a full handshake; leave
38770+
* useTicket set so the server still issues a fresh
38771+
* ticket to the client. */
38772+
if (ssl->options.useTicket &&
38773+
VerifyTicketBinding(ssl) != 0) {
38774+
WOLFSSL_MSG("Ticket binding mismatch, "
38775+
"declining resumption and falling back "
38776+
"to full handshake");
38777+
ssl->options.resuming = 0;
38778+
ssl->options.peerAuthGood = 0;
38779+
}
3876638780
#endif
3876738781

3876838782
i += totalExtSz;
@@ -39594,13 +39608,23 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl)
3959439608
#if defined(HAVE_SNI) || defined(HAVE_ALPN)
3959539609
/* Server-side: verify the SNI/ALPN bindings carried on a resumed
3959639610
* session match what was negotiated for the current connection.
39597-
* Must be called after extension parsing and ALPN_Select.
39598-
* Returns 0 on match, WOLFSSL_FATAL_ERROR on mismatch. */
39611+
* Must be called after extension parsing and ALPN_Select, and only
39612+
* once ssl->session has been loaded with the resumed session's
39613+
* binding hashes (i.e. after DoClientTicketFinalize for TLS 1.2
39614+
* tickets / TLS 1.3 PSK resumption). For TLS 1.2 session-ID
39615+
* resumption the binding check is performed in HandleTlsResumption
39616+
* against the cached session directly, so this function should not
39617+
* be called for that path.
39618+
* Returns 0 on match, WOLFSSL_FATAL_ERROR on mismatch. Callers
39619+
* decide whether a mismatch is fatal or whether to decline the
39620+
* resumption and fall back to a full handshake -- RFC 6066 Section 3
39621+
* mandates the latter for TLS 1.2 SNI, and the same policy is
39622+
* applied to TLS 1.3 PSK resumption for consistency. */
3959939623
int VerifyTicketBinding(WOLFSSL* ssl)
3960039624
{
3960139625
byte curHash[TICKET_BINDING_HASH_SZ];
3960239626

39603-
if (!ssl->options.resuming || !ssl->options.useTicket)
39627+
if (!ssl->options.resuming)
3960439628
return 0;
3960539629

3960639630
#ifdef HAVE_SNI

src/tls13.c

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7725,8 +7725,72 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
77257725
goto exit_dch;
77267726
#endif
77277727
#if defined(HAVE_SESSION_TICKET) && (defined(HAVE_SNI) || defined(HAVE_ALPN))
7728-
if ((ret = VerifyTicketBinding(ssl)) != 0)
7729-
goto exit_dch;
7728+
/* Only verify when a PSK actually matched, since that's when
7729+
* DoClientTicketFinalize populated ssl->session with the resumed
7730+
* session's binding hashes. resuming may be set optimistically by
7731+
* CheckPreSharedKeys before any PSK has been validated, so gating
7732+
* on usingPSK avoids comparing against an uninitialized session. */
7733+
if (args->usingPSK && VerifyTicketBinding(ssl) != 0) {
7734+
/* RFC 6066 / common practice: decline the PSK resumption and
7735+
* fall back to a full handshake rather than aborting. Unwind
7736+
* the PSK state set up by CheckPreSharedKeys/DoPreSharedKeys
7737+
* so the rest of DoTls13ClientHello proceeds as if no PSK had
7738+
* been selected. */
7739+
TLSX* pskExt;
7740+
7741+
WOLFSSL_MSG("PSK binding mismatch, declining PSK and falling "
7742+
"back to full handshake");
7743+
args->usingPSK = 0;
7744+
ssl->options.resuming = 0;
7745+
ssl->options.peerAuthGood = 0;
7746+
ssl->options.noPskDheKe = 0;
7747+
/* DoPreSharedKeys cleared sendVerify because PSK auth obviates
7748+
* the server's Certificate/CertificateVerify. Restore the
7749+
* pre-PSK default (set at the top of DoTls13ClientHello) so the
7750+
* full handshake sends them. */
7751+
ssl->options.sendVerify = SEND_CERT;
7752+
/* DoPreSharedKeys derived the resumption PSK into psk_key and
7753+
* called DeriveEarlySecret with it; drop both so the FINALIZE
7754+
* block's DeriveEarlySecret call (now reached via !usingPSK)
7755+
* re-derives the early secret from zeros. */
7756+
if (ssl->arrays != NULL) {
7757+
ForceZero(ssl->arrays->psk_key,
7758+
sizeof(ssl->arrays->psk_key));
7759+
ssl->arrays->psk_keySz = 0;
7760+
}
7761+
/* Clear the selected-PSK response flag so the server does NOT
7762+
* echo a pre_shared_key extension in ServerHello, which is what
7763+
* tells the client the resumption was accepted. */
7764+
pskExt = TLSX_Find(ssl->extensions, TLSX_PRE_SHARED_KEY);
7765+
if (pskExt != NULL)
7766+
pskExt->resp = 0;
7767+
#ifdef WOLFSSL_EARLY_DATA
7768+
{
7769+
TLSX* extEarlyData = TLSX_Find(ssl->extensions,
7770+
TLSX_EARLY_DATA);
7771+
if (extEarlyData != NULL)
7772+
extEarlyData->resp = 0;
7773+
ssl->earlyData = no_early_data;
7774+
}
7775+
#endif
7776+
/* The KeyShare/SigAlgs validation in the !usingPSK block above
7777+
* was skipped because PSK had been selected. Re-check now to
7778+
* confirm a full handshake is actually possible. */
7779+
#ifndef NO_CERTS
7780+
if (TLSX_Find(ssl->extensions, TLSX_KEY_SHARE) == NULL) {
7781+
WOLFSSL_MSG("Client did not send a KeyShare extension; "
7782+
"cannot fall back to full handshake");
7783+
ERROR_OUT(INCOMPLETE_DATA, exit_dch);
7784+
}
7785+
if (ssl->clSuites->hashSigAlgoSz == 0) {
7786+
WOLFSSL_MSG("Client did not send a SignatureAlgorithms "
7787+
"extension; cannot fall back to full handshake");
7788+
ERROR_OUT(INCOMPLETE_DATA, exit_dch);
7789+
}
7790+
#else
7791+
ERROR_OUT(INVALID_PARAMETER, exit_dch);
7792+
#endif
7793+
}
77307794
#endif
77317795
} /* case TLS_ASYNC_BEGIN */
77327796
FALL_THROUGH;

tests/api/test_tls.c

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -906,8 +906,9 @@ int test_tls_set_session_min_downgrade(void)
906906
}
907907

908908
#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
909-
!defined(WOLFSSL_NO_TLS12) && defined(HAVE_SNI) && \
910-
defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE)
909+
(!defined(WOLFSSL_NO_TLS12) || defined(WOLFSSL_TLS13)) && \
910+
defined(HAVE_SNI) && defined(HAVE_SESSION_TICKET) && \
911+
!defined(NO_SESSION_CACHE)
911912
/* Accept-all SNI callback. */
912913
static int accept_any_sni_cb(WOLFSSL* ssl, int* ret, void* arg)
913914
{
@@ -987,6 +988,76 @@ int test_tls12_session_id_resumption_sni_mismatch(void)
987988
return EXPECT_RESULT();
988989
}
989990

991+
/* TLS 1.3 PSK resumption must fall back to a full handshake if the SNI in
992+
* the resumed ClientHello does not match the SNI bound to the original
993+
* session (RFC 6066 Section 3 / RFC 8446 Section 4.6.1). */
994+
int test_tls13_session_resumption_sni_mismatch(void)
995+
{
996+
EXPECT_DECLS;
997+
#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \
998+
defined(HAVE_SNI) && defined(HAVE_SESSION_TICKET) && \
999+
!defined(NO_SESSION_CACHE)
1000+
WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL;
1001+
WOLFSSL *ssl_c = NULL, *ssl_s = NULL;
1002+
WOLFSSL_SESSION *sess = NULL;
1003+
struct test_memio_ctx test_ctx;
1004+
const char* sniA = "public.example";
1005+
const char* sniB = "admin.example";
1006+
byte readBuf[16];
1007+
1008+
/* Step 1: full TLS 1.3 handshake under SNI=public.example to obtain a
1009+
* session ticket. The server-side SNI callback ensures ssl->extensions
1010+
* retains the client's SNI in builds that don't compile in
1011+
* WOLFSSL_ALWAYS_KEEP_SNI. */
1012+
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
1013+
ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s,
1014+
wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0);
1015+
wolfSSL_CTX_set_servername_callback(ctx_s, accept_any_sni_cb);
1016+
ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME,
1017+
sniA, (word16)XSTRLEN(sniA)), WOLFSSL_SUCCESS);
1018+
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
1019+
/* Sanity: the first handshake was not a resumption. */
1020+
ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0);
1021+
/* Drive the post-handshake NewSessionTicket through to the client so
1022+
* the saved session is a real resumption ticket. */
1023+
ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1);
1024+
ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ);
1025+
ExpectNotNull(sess = wolfSSL_get1_session(ssl_c));
1026+
1027+
wolfSSL_free(ssl_c); ssl_c = NULL;
1028+
wolfSSL_free(ssl_s); ssl_s = NULL;
1029+
1030+
/* Step 2: new SSL objects on the SAME WOLFSSL_CTX (so the server's
1031+
* ticket key still matches). The client offers the saved session but
1032+
* advertises a *different* SNI. The server MUST NOT resume because the
1033+
* SNI differs from the original. */
1034+
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
1035+
ExpectNotNull(ssl_c = wolfSSL_new(ctx_c));
1036+
wolfSSL_SetIOReadCtx(ssl_c, &test_ctx);
1037+
wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx);
1038+
ExpectNotNull(ssl_s = wolfSSL_new(ctx_s));
1039+
wolfSSL_SetIOReadCtx(ssl_s, &test_ctx);
1040+
wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx);
1041+
ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME,
1042+
sniB, (word16)XSTRLEN(sniB)), WOLFSSL_SUCCESS);
1043+
ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS);
1044+
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
1045+
1046+
/* Desired behavior: server falls back to a full handshake because the
1047+
* SNI in the ClientHello does not match the SNI bound to the cached
1048+
* ticket. Both sides should report no resumption. */
1049+
ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0);
1050+
ExpectIntEQ(wolfSSL_session_reused(ssl_c), 0);
1051+
1052+
wolfSSL_SESSION_free(sess);
1053+
wolfSSL_free(ssl_c);
1054+
wolfSSL_free(ssl_s);
1055+
wolfSSL_CTX_free(ctx_c);
1056+
wolfSSL_CTX_free(ctx_s);
1057+
#endif
1058+
return EXPECT_RESULT();
1059+
}
1060+
9901061
int test_tls_set_curves_list_ecc_fallback(void)
9911062
{
9921063
EXPECT_DECLS;

tests/api/test_tls.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ int test_tls12_no_null_compression(void);
3434
int test_tls12_etm_failed_resumption(void);
3535
int test_tls_set_session_min_downgrade(void);
3636
int test_tls12_session_id_resumption_sni_mismatch(void);
37+
int test_tls13_session_resumption_sni_mismatch(void);
3738
int test_tls_set_curves_list_ecc_fallback(void);
3839
int test_tls12_corrupted_finished(void);
3940
int test_tls12_peerauth_failsafe(void);
@@ -51,6 +52,7 @@ int test_tls12_peerauth_failsafe(void);
5152
TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \
5253
TEST_DECL_GROUP("tls", test_tls_set_session_min_downgrade), \
5354
TEST_DECL_GROUP("tls", test_tls12_session_id_resumption_sni_mismatch), \
55+
TEST_DECL_GROUP("tls", test_tls13_session_resumption_sni_mismatch), \
5456
TEST_DECL_GROUP("tls", test_tls_set_curves_list_ecc_fallback), \
5557
TEST_DECL_GROUP("tls", test_tls12_corrupted_finished), \
5658
TEST_DECL_GROUP("tls", test_tls12_peerauth_failsafe)

0 commit comments

Comments
 (0)