diff --git a/src/internal.c b/src/internal.c index 8b34e20c37..664c0045b5 100644 --- a/src/internal.c +++ b/src/internal.c @@ -38086,6 +38086,30 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) ssl->options.resuming = 0; return ret; } +#if defined(HAVE_SESSION_TICKET) && \ + (defined(HAVE_SNI) || defined(HAVE_ALPN)) + /* Do not resume session if sniHash/alpnHash do not match. */ + if (!ssl->options.useTicket) { + byte curHash[TICKET_BINDING_HASH_SZ]; +#ifdef HAVE_SNI + if (TicketSniHash(ssl, curHash) != 0 || + XMEMCMP(curHash, session->sniHash, + TICKET_BINDING_HASH_SZ) != 0) { + WOLFSSL_MSG("Resumed session SNI mismatch, full handshake"); + ssl->options.resuming = 0; + } +#endif +#ifdef HAVE_ALPN + if (ssl->options.resuming && + (TicketAlpnHash(ssl, curHash) != 0 || + XMEMCMP(curHash, session->alpnHash, + TICKET_BINDING_HASH_SZ) != 0)) { + WOLFSSL_MSG("Resumed session ALPN mismatch, full handshake"); + ssl->options.resuming = 0; + } +#endif + } +#endif /* HAVE_SESSION_TICKET && (HAVE_SNI || HAVE_ALPN) */ #if !defined(WOLFSSL_NO_TICKET_EXPIRE) && !defined(NO_ASN_TIME) /* check if the ticket is valid */ if (LowResTimer() > session->bornOn + ssl->timeout) { @@ -38667,8 +38691,22 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #endif #if defined(HAVE_SESSION_TICKET) && \ (defined(HAVE_SNI) || defined(HAVE_ALPN)) - if((ret=VerifyTicketBinding(ssl))) - goto out; + /* Only verify here for TLS 1.2 ticket-based resumption. + * For stateful (session-ID) resumption ssl->session is + * not loaded until HandleTlsResumption runs below, which + * performs its own binding check against the cached + * session. On mismatch decline the resumption (RFC 6066 + * Section 3) but proceed with a full handshake; leave + * useTicket set so the server still issues a fresh + * ticket to the client. */ + if (ssl->options.useTicket && + VerifyTicketBinding(ssl) != 0) { + WOLFSSL_MSG("Ticket binding mismatch, " + "declining resumption and falling back " + "to full handshake"); + ssl->options.resuming = 0; + ssl->options.peerAuthGood = 0; + } #endif i += totalExtSz; @@ -39450,7 +39488,7 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #ifdef HAVE_SNI /* Hash server-selected SNI; zeros dst when none. */ - static int TicketSniHash(WOLFSSL* ssl, byte* dst) + int TicketSniHash(WOLFSSL* ssl, byte* dst) { char* name = NULL; word16 nameLen; @@ -39470,16 +39508,23 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #ifdef HAVE_ALPN /* Hash negotiated ALPN; zeros dst when none. */ - static int TicketAlpnHash(WOLFSSL* ssl, byte* dst) + int TicketAlpnHash(WOLFSSL* ssl, byte* dst) { - char* proto = NULL; - word16 protoLen = 0; + TLSX* extension; + ALPN* alpn; - if (TLSX_ALPN_GetRequest(ssl->extensions, (void**)&proto, - &protoLen) == WOLFSSL_SUCCESS && - proto != NULL && protoLen > 0) { - return wc_Hash(TICKET_BINDING_HASH_TYPE, (const byte*)proto, - protoLen, dst, TICKET_BINDING_HASH_SZ); + extension = TLSX_Find(ssl->extensions, TLSX_APPLICATION_LAYER_PROTOCOL); + if (extension != NULL) { + alpn = (ALPN*)extension->data; + if (alpn != NULL && alpn->negotiated == 1 && + alpn->protocol_name != NULL) { + word32 protoLen = (word32)XSTRLEN(alpn->protocol_name); + if (protoLen > 0) { + return wc_Hash(TICKET_BINDING_HASH_TYPE, + (const byte*)alpn->protocol_name, + protoLen, dst, TICKET_BINDING_HASH_SZ); + } + } } XMEMSET(dst, 0, TICKET_BINDING_HASH_SZ); @@ -39488,15 +39533,30 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #endif #if defined(HAVE_SNI) || defined(HAVE_ALPN) - /* Server-side: verify the SNI/ALPN bindings carried on a resumed - * session match what was negotiated for the current connection. - * Must be called after extension parsing and ALPN_Select. - * Returns 0 on match, WOLFSSL_FATAL_ERROR on mismatch. */ + /* Server-side TLS 1.2 ticket-resumption binding check. Confirms the + * SNI/ALPN bound to the resumed session matches what was negotiated + * for the current connection. Must be called after extension + * parsing and ALPN_Select so the negotiated values are available, + * and only once DoClientTicketFinalize has populated + * ssl->session->sniHash/alpnHash from the decrypted ticket. + * + * Other resumption paths handle the same check themselves and do + * not use this function: + * - TLS 1.2 session-ID (stateful): HandleTlsResumption compares + * against the cached session at lookup time. + * - TLS 1.3 PSK: DoPreSharedKeys compares against each candidate + * ticket's bound hashes before committing, allowing the server + * to skip mismatching PSKs and pick the next one. + * + * Returns 0 on match, WOLFSSL_FATAL_ERROR on mismatch. The caller + * is responsible for the policy on mismatch -- RFC 6066 Section 3 + * mandates declining the resumption and proceeding with a full + * handshake rather than aborting. */ int VerifyTicketBinding(WOLFSSL* ssl) { byte curHash[TICKET_BINDING_HASH_SZ]; - if (!ssl->options.resuming || !ssl->options.useTicket) + if (!ssl->options.resuming) return 0; #ifdef HAVE_SNI @@ -40005,8 +40065,9 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) ssl->sessionCtxSz) != 0)) return WOLFSSL_FATAL_ERROR; #endif - /* SNI/ALPN binding is verified after ALPN_Select via - * VerifyTicketBinding(). */ + /* SNI/ALPN binding is checked by the per-PSK loop in + * DoPreSharedKeys, not here, so that mismatching PSKs can be + * skipped in favor of the next candidate. */ return 0; } #endif /* WOLFSSL_SLT13 */ @@ -40102,8 +40163,13 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) } } #endif - /* Carry the ticket bindings on the session for the deferred - * VerifyTicketBinding() check. */ + /* Carry the ticket bindings on the session. TLS 1.2 uses these + * for the deferred VerifyTicketBinding() check in DoClientHello + * (SNI/ALPN aren't known when DoClientTicket runs during + * extension parsing). TLS 1.3 checks bindings per-PSK before + * reaching this point, but still copies them so a subsequent + * SetupSession on a resumed session preserves them in the cache + * for future resumptions. */ #ifdef HAVE_SNI XMEMCPY(ssl->session->sniHash, it->sniHash, TICKET_BINDING_HASH_SZ); #endif @@ -40469,8 +40535,9 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) goto cleanup; } - /* SNI/ALPN binding is verified after ALPN_Select via - * VerifyTicketBinding(). */ + /* SNI/ALPN binding is verified later in DoClientHello via + * VerifyTicketBinding(), once extension parsing and ALPN_Select + * have run and the negotiated values are available. */ DoClientTicketFinalize(ssl, it, NULL); cleanup: diff --git a/src/ssl_sess.c b/src/ssl_sess.c index 647851ac17..cfc5eb4813 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -2715,6 +2715,16 @@ int wolfSSL_i2d_SSL_SESSION(WOLFSSL_SESSION* sess, unsigned char** p) #ifdef HAVE_SESSION_TICKET /* ticket len | ticket */ size += OPAQUE16_LEN + sess->ticketLen; +#if !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) +#ifdef HAVE_SNI + /* sniHash */ + size += TICKET_BINDING_HASH_SZ; +#endif +#ifdef HAVE_ALPN + /* alpnHash */ + size += TICKET_BINDING_HASH_SZ; +#endif +#endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif if (p != NULL) { @@ -2800,6 +2810,16 @@ int wolfSSL_i2d_SSL_SESSION(WOLFSSL_SESSION* sess, unsigned char** p) c16toa(sess->ticketLen, data + idx); idx += OPAQUE16_LEN; XMEMCPY(data + idx, sess->ticket, sess->ticketLen); idx += sess->ticketLen; +#if !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) +#ifdef HAVE_SNI + XMEMCPY(data + idx, sess->sniHash, TICKET_BINDING_HASH_SZ); + idx += TICKET_BINDING_HASH_SZ; +#endif +#ifdef HAVE_ALPN + XMEMCPY(data + idx, sess->alpnHash, TICKET_BINDING_HASH_SZ); + idx += TICKET_BINDING_HASH_SZ; +#endif +#endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif } #endif @@ -3086,6 +3106,26 @@ WOLFSSL_SESSION* wolfSSL_d2i_SSL_SESSION(WOLFSSL_SESSION** sess, goto end; } XMEMCPY(s->ticket, data + idx, s->ticketLen); idx += s->ticketLen; +#if !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) +#ifdef HAVE_SNI + /* sniHash - SNI binding for stateful resumption (RFC 6066 section 3) */ + if (i - idx < TICKET_BINDING_HASH_SZ) { + ret = BUFFER_ERROR; + goto end; + } + XMEMCPY(s->sniHash, data + idx, TICKET_BINDING_HASH_SZ); + idx += TICKET_BINDING_HASH_SZ; +#endif +#ifdef HAVE_ALPN + /* alpnHash - ALPN binding for stateful resumption */ + if (i - idx < TICKET_BINDING_HASH_SZ) { + ret = BUFFER_ERROR; + goto end; + } + XMEMCPY(s->alpnHash, data + idx, TICKET_BINDING_HASH_SZ); + idx += TICKET_BINDING_HASH_SZ; +#endif +#endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif (void)idx; @@ -3664,6 +3704,16 @@ void SetupSession(WOLFSSL* ssl) session->sessionCtxSz = ssl->sessionCtxSz; } #endif +#if defined(HAVE_SESSION_TICKET) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) + /* Bind the current SNI/ALPN to the session to verify on later resumption */ +#ifdef HAVE_SNI + (void)TicketSniHash(ssl, session->sniHash); +#endif +#ifdef HAVE_ALPN + (void)TicketAlpnHash(ssl, session->alpnHash); +#endif +#endif /* HAVE_SESSION_TICKET && !NO_WOLFSSL_SERVER && !NO_TLS */ session->timeout = ssl->timeout; #ifndef NO_ASN_TIME session->bornOn = LowResTimer(); diff --git a/src/tls13.c b/src/tls13.c index 5128f6097f..880882f691 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6429,6 +6429,37 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz, } #endif ret = DoClientTicketCheck(ssl, current, ssl->timeout, suite); + #if defined(HAVE_SNI) || defined(HAVE_ALPN) + if (ret == 0) { + /* Decline this PSK if the SNI/ALPN bound to the ticket + * does not match the current connection. RFC 6066 Sect. + * 3 mandates this for SNI; wolfSSL applies the same + * policy to ALPN as defense in depth. Skipping the PSK + * (rather than aborting) lets the server try the next + * candidate or fall back to a full handshake naturally + * without unwinding committed PSK state. ALPN_Select + * has already run earlier in DoTls13ClientHello so the + * negotiated ALPN is available to TicketAlpnHash. */ + byte curHash[TICKET_BINDING_HASH_SZ]; + #ifdef HAVE_SNI + if (TicketSniHash(ssl, curHash) != 0 || + XMEMCMP(curHash, current->it->sniHash, + TICKET_BINDING_HASH_SZ) != 0) { + WOLFSSL_MSG("Ticket SNI mismatch, skipping PSK"); + ret = WOLFSSL_FATAL_ERROR; + } + #endif + #ifdef HAVE_ALPN + if (ret == 0 && + (TicketAlpnHash(ssl, curHash) != 0 || + XMEMCMP(curHash, current->it->alpnHash, + TICKET_BINDING_HASH_SZ) != 0)) { + WOLFSSL_MSG("Ticket ALPN mismatch, skipping PSK"); + ret = WOLFSSL_FATAL_ERROR; + } + #endif + } + #endif if (ret == 0) DoClientTicketFinalize(ssl, current->it, current->sess); if (current->sess_free_cb != NULL) { @@ -6592,11 +6623,11 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz, return ret; } - /* Extensions pushed on stack/list and PSK must be last. */ - if (ssl->extensions != ext) { - WOLFSSL_ERROR_VERBOSE(PSK_KEY_ERROR); - return PSK_KEY_ERROR; - } + /* Wire-order check that PSK was the last extension in ClientHello is + * performed in DoTls13ClientHello immediately after TLSX_Parse, since + * post-parse code (e.g. ALPN_Select via TLSX_SetALPN) may legitimately + * prepend new entries to ssl->extensions before this point and would + * otherwise trip a head-of-list check here. */ /* Assume we are going to resume with a pre-shared key. */ ssl->options.resuming = 1; @@ -7562,6 +7593,25 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, goto exit_dch; } +#if (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && \ + defined(HAVE_TLS_EXTENSIONS) + /* RFC 8446 Section 4.2.11: the pre_shared_key extension MUST be the + * last extension in the ClientHello. wolfSSL stores extensions in + * reverse wire order (TLSX_Push prepends), so a well-formed + * ClientHello with PSK leaves PSK at the head of ssl->extensions + * here, before any post-parse code (e.g. ALPN_Select) modifies the + * list. */ + { + TLSX* pskExt = TLSX_Find(ssl->extensions, TLSX_PRE_SHARED_KEY); + if (pskExt != NULL && ssl->extensions != pskExt) { + WOLFSSL_MSG("pre_shared_key extension was not last in " + "ClientHello"); + WOLFSSL_ERROR_VERBOSE(PSK_KEY_ERROR); + ERROR_OUT(PSK_KEY_ERROR, exit_dch); + } + } +#endif + #if defined(HAVE_ECH) if (!ssl->options.echProcessingInner && echX != NULL && ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { @@ -7671,6 +7721,15 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, } #endif +#ifdef HAVE_ALPN + /* Select the ALPN protocol before PSK selection so that the + * selected value is available to the per-PSK SNI/ALPN binding check + * inside CheckPreSharedKeys/DoPreSharedKeys. ALPN_Select itself + * only inspects ssl->extensions and the app callback; it does not + * depend on any state set during PSK validation. */ + if ((ret = ALPN_Select(ssl)) != 0) + goto exit_dch; +#endif #if (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && \ defined(HAVE_TLS_EXTENSIONS) ret = CheckPreSharedKeys(ssl, input + args->begin, helloSz, ssl->clSuites, @@ -7712,16 +7771,6 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #endif } -#ifdef HAVE_ALPN - /* With PSK and all other things validated, it's time to - * select the ALPN protocol, if so requested */ - if ((ret = ALPN_Select(ssl)) != 0) - goto exit_dch; -#endif -#if defined(HAVE_SESSION_TICKET) && (defined(HAVE_SNI) || defined(HAVE_ALPN)) - if ((ret = VerifyTicketBinding(ssl)) != 0) - goto exit_dch; -#endif } /* case TLS_ASYNC_BEGIN */ FALL_THROUGH; diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index ee2e112bfa..4abf2b6206 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -906,6 +906,228 @@ int test_tls_set_session_min_downgrade(void) return EXPECT_RESULT(); } +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + (!defined(WOLFSSL_NO_TLS12) || defined(WOLFSSL_TLS13)) && \ + defined(HAVE_SNI) && defined(HAVE_SESSION_TICKET) && \ + !defined(NO_SESSION_CACHE) +/* Accept-all SNI callback. */ +static int accept_any_sni_cb(WOLFSSL* ssl, int* ret, void* arg) +{ + (void)ssl; (void)ret; (void)arg; + return 0; /* accept */ +} +#endif + +/* TLS resumption must proceed with full handshake to establish new session if + * SNI/ALPN does not match previously established session. */ +int test_tls12_session_id_resumption_sni_mismatch(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SNI) && \ + 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* sniA = "public.example"; + const char* sniB = "admin.example"; + + /* Step 1: full TLS 1.2 handshake under SNI=public.example, with the + * session ticket path disabled so resumption can only happen via the + * server's session-ID cache. The server-side SNI callback ensures + * ssl->extensions retains the client's SNI in builds that don't + * compile in WOLFSSL_ALWAYS_KEEP_SNI. */ + 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); + wolfSSL_CTX_set_servername_callback(ctx_s, accept_any_sni_cb); + ExpectIntEQ(wolfSSL_NoTicketTLSv12(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_NoTicketTLSv12(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, + sniA, (word16)XSTRLEN(sniA)), 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 advertises a *different* SNI. The server's + * cache lookup will match by session ID, but per RFC 6066 Section 3 the + * server MUST NOT resume because the SNI differs from the original. */ + 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_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, + sniB, (word16)XSTRLEN(sniB)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Post-fix expected behavior: server falls back to a full handshake + * because the SNI in the ClientHello does not match the SNI bound to + * the cached session. Pre-fix, the server silently resumes - which is + * the bug. 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 SNI in + * the resumed ClientHello does not match the SNI bound to the original + * session (RFC 6066 Section 3 / RFC 8446 Section 4.6.1). */ +int test_tls13_session_resumption_sni_mismatch(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_SNI) && 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* sniA = "public.example"; + const char* sniB = "admin.example"; + byte readBuf[16]; + + /* Step 1: full TLS 1.3 handshake under SNI=public.example to obtain a + * session ticket. The server-side SNI callback ensures ssl->extensions + * retains the client's SNI in builds that don't compile in + * WOLFSSL_ALWAYS_KEEP_SNI. */ + 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); + wolfSSL_CTX_set_servername_callback(ctx_s, accept_any_sni_cb); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, + sniA, (word16)XSTRLEN(sniA)), 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 + * advertises a *different* SNI. The server MUST NOT resume because the + * SNI differs from the original. */ + 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_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, + sniB, (word16)XSTRLEN(sniB)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Desired behavior: server falls back to a full handshake because the + * SNI in the ClientHello does not match the SNI 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(); +} + +/* Regression test for the post-ALPN_Select PSK-head check. + * When ALPN_Select runs before CheckPreSharedKeys (so the per-PSK + * binding check has the negotiated ALPN available), TLSX_SetALPN + * prepends a new ALPN entry to ssl->extensions, displacing the PSK + * extension from the head of the list. The "PSK was last in + * ClientHello" check therefore must run right after TLSX_Parse, + * not inside CheckPreSharedKeys. This test exercises that path + * (TLS 1.3 PSK resumption with ALPN, no SNI callback -- the grpc + * server scenario). */ +int test_tls13_resumption_with_alpn(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_SNI) && 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* sni = "foo.test.google.fr"; + const char alpn[] = "h2"; + byte readBuf[16]; + + 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_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, + sni, (word16)XSTRLEN(sni)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_c, (char*)alpn, (word32)XSTRLEN(alpn), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_s, (char*)alpn, (word32)XSTRLEN(alpn), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + 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; + + 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_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, + sni, (word16)XSTRLEN(sni)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_c, (char*)alpn, (word32)XSTRLEN(alpn), + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl_s, (char*)alpn, (word32)XSTRLEN(alpn), + 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); + + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 1); + ExpectIntEQ(wolfSSL_session_reused(ssl_c), 1); + + 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 fb796244d7..4b4f5385c3 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -33,6 +33,9 @@ int test_tls12_bad_cv_sig_alg(void); int test_tls12_no_null_compression(void); int test_tls12_etm_failed_resumption(void); 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_tls_set_curves_list_ecc_fallback(void); int test_tls12_corrupted_finished(void); int test_tls12_peerauth_failsafe(void); @@ -51,6 +54,9 @@ int test_wolfSSL_alert_desc_string(void); TEST_DECL_GROUP("tls", test_tls12_no_null_compression), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls_set_session_min_downgrade), \ + 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_tls_set_curves_list_ecc_fallback), \ TEST_DECL_GROUP("tls", test_tls12_corrupted_finished), \ TEST_DECL_GROUP("tls", test_tls12_peerauth_failsafe), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index c09647c1ba..c2ba7a1966 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -6824,9 +6824,21 @@ WOLFSSL_LOCAL int DoClientTicket_ex(const WOLFSSL* ssl, PreSharedKey* psk, #endif WOLFSSL_LOCAL int DoClientTicket(WOLFSSL* ssl, const byte* input, word32 len); +/* TicketSniHash, TicketAlpnHash, and VerifyTicketBinding are defined in + * internal.c only when !NO_WOLFSSL_SERVER && !NO_TLS - gate the + * declarations to match so client-only or no-TLS builds don't compile in + * call sites that would fail to link. */ +#if !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) +#ifdef HAVE_SNI +WOLFSSL_LOCAL int TicketSniHash(WOLFSSL* ssl, byte* dst); +#endif +#ifdef HAVE_ALPN +WOLFSSL_LOCAL int TicketAlpnHash(WOLFSSL* ssl, byte* dst); +#endif #if defined(HAVE_SNI) || defined(HAVE_ALPN) WOLFSSL_LOCAL int VerifyTicketBinding(WOLFSSL* ssl); #endif +#endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif /* HAVE_SESSION_TICKET */ WOLFSSL_LOCAL int SendData(WOLFSSL* ssl, const void* data, size_t sz); #ifdef WOLFSSL_THREADED_CRYPT