From b542760c4b3b59f4eacbaa41f6a2cd63c242912c Mon Sep 17 00:00:00 2001 From: Kareem Date: Tue, 29 Sep 2026 16:50:13 -0700 Subject: [PATCH 1/9] Clear session ticket resumption state in wolfSSL_clear. Thanks to ByteRay Ltd for the report. --- src/internal.c | 7 ++++ src/ssl.c | 14 ++++++++ tests/api/test_tls.c | 84 ++++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 2 ++ 4 files changed, 107 insertions(+) diff --git a/src/internal.c b/src/internal.c index f3657517e65..4a9f378c4c4 100644 --- a/src/internal.c +++ b/src/internal.c @@ -41956,6 +41956,13 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) /* Reset to sane value for SCR */ ssl->options.resuming = 0; ssl->arrays->sessionIDSz = 0; +#ifdef HAVE_SESSION_TICKET + /* Set again below by TLSX_SessionTicket_Parse() when this ClientHello + * carries a ticket. HandleTlsResumption() takes the retained session + * instead of the presented session id when it is set, so a value left + * over from an earlier handshake on this object must not reach it. */ + ssl->options.useTicket = 0; +#endif /* protocol version, random and session id length check */ if (OPAQUE16_LEN + RAN_LEN + OPAQUE8_LEN > helloSz) diff --git a/src/ssl.c b/src/ssl.c index 2443fbd7829..caea080d30c 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5933,7 +5933,21 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, #endif #endif ssl->options.rejectTicket = 0; + /* Records that the handshake in progress took a ticket, and selects + * the retained session over a session id lookup. Only the current + * ClientHello may set it. */ + ssl->options.useTicket = 0; + ssl->options.createTicket = 0; #endif + /* A server keeps its session across the reset, and the cache lookup + * prefers this id over the one the ClientHello carries. The next + * client has not asked for that session, so the id goes; a client + * keeps it, being the side that resumes what it held. */ + if ((ssl->options.side == WOLFSSL_SERVER_END) && + (ssl->session != NULL)) { + ssl->session->haveAltSessionID = 0; + ForceZero(ssl->session->altSessionID, ID_LEN); + } #ifdef WOLFSSL_EARLY_DATA ssl->earlyData = no_early_data; ssl->earlyDataSz = 0; diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index a3d343d88ee..d68d3c8d9e5 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3098,6 +3098,90 @@ int test_tls12_resume_ticket_wrong_suite(void) return EXPECT_RESULT(); } +/* options.useTicket says the handshake in progress accepted a session ticket, + * and HandleTlsResumption() reads it to take ssl->session instead of looking + * up the session id the client presented. It is per-handshake state, so an + * object reused with wolfSSL_clear() must not carry it into the next + * ClientHello: the retained session would be resumed for a client that never + * held it, keyed from the master secret wolfSSL_clear() wiped. + * + * The first handshake takes a ticket, so the flag is set. A second client then + * offers a session id that has been dropped from the cache, so nothing can + * legitimately resolve it: only the stale flag could still produce a + * resumption, and it must not. */ +int test_tls12_reuse_clears_use_ticket(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_SESSION_CACHE) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL, *ctx_s2 = NULL; + WOLFSSL *ssl_c = NULL, *ssl_c2 = NULL, *ssl_c3 = NULL; + WOLFSSL *ssl_s = NULL, *ssl_s2 = NULL; + WOLFSSL_SESSION* sess = NULL; + struct test_memio_ctx test_ctx; + int useTicketAfterClear = -1; + + 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_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->options.useTicket, 1); + + /* A session to offer next, taken from a server that issues no tickets so + * the second ClientHello carries a session id and no ticket extension. + * Nothing is modified in place: a session handed out by the cache may + * still be backed by it. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s2, &ssl_c2, &ssl_s2, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c2)); + ExpectIntEQ(sess->ticketLen, 0); + /* Drop it from the cache the two ends share in-process, so no lookup can + * legitimately resolve the id it offers. */ + ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(ctx_s2, sess), 1); + + ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); + /* Read now, assert last: an expectation that fails here would skip the + * behavioural half below. */ + if (ssl_s != NULL) + useTicketAfterClear = ssl_s->options.useTicket; + + /* Existing CTXs are kept; only the third client object is created, and the + * reused server is pointed at the transport it shares with it. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c3, NULL, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + if (ssl_s != NULL) { + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + } + ExpectIntEQ(wolfSSL_set_session(ssl_c3, sess), WOLFSSL_SUCCESS); + /* The handshake itself may fail: what matters is that the server did not + * hand this client the retained session. */ + if ((ssl_c3 != NULL) && (ssl_s != NULL)) + test_memio_do_handshake(ssl_c3, ssl_s, 10, NULL); + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->options.resuming, 0); + ExpectIntEQ(useTicketAfterClear, 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_c2); + wolfSSL_free(ssl_c3); + wolfSSL_free(ssl_s); + wolfSSL_free(ssl_s2); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + wolfSSL_CTX_free(ctx_s2); +#endif + return EXPECT_RESULT(); +} + /* A ticket the server can't honor must fall back to a full handshake (RFC 5077 * 3.4), even under a different suite than the cached ticket session - the * F-5811 suite check must not abort it. The second handshake uses a fresh diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 20e6e17fd15..11a969b1222 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -56,6 +56,7 @@ int test_tls_version_mask_alert_record(void); int test_tls_version_error_alert_mapping(void); int test_tls12_etm_failed_resumption(void); int test_tls12_resume_ticket_wrong_suite(void); +int test_tls12_reuse_clears_use_ticket(void); int test_tls12_resume_ticket_decline_fallback(void); int test_tls12_ticket_dropped_on_bad_finished(void); int test_tls12_ticket_cached_after_finished(void); @@ -115,6 +116,7 @@ int test_tls12_aesgcm_record_nonce_unique(void); TEST_DECL_GROUP("tls", test_tls_version_error_alert_mapping), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_wrong_suite), \ + TEST_DECL_GROUP("tls", test_tls12_reuse_clears_use_ticket), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_decline_fallback), \ TEST_DECL_GROUP("tls", test_tls12_ticket_dropped_on_bad_finished), \ TEST_DECL_GROUP("tls", test_tls12_ticket_cached_after_finished), \ From 356019bb461a5108fb2759841ffaf4193c910cf9 Mon Sep 17 00:00:00 2001 From: Kareem Date: Tue, 29 Sep 2026 17:10:08 -0700 Subject: [PATCH 2/9] Ensure that a resumed session properly handles client auth. Thanks to ByteRay Ltd for the report. --- src/internal.c | 49 ++++++++- src/ssl_sess.c | 19 ++++ src/tls13.c | 9 ++ tests/api.c | 53 ++++++++++ tests/api/test_tls.c | 241 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 6 ++ wolfssl/internal.h | 16 ++- 7 files changed, 388 insertions(+), 5 deletions(-) diff --git a/src/internal.c b/src/internal.c index 4a9f378c4c4..e70b018da93 100644 --- a/src/internal.c +++ b/src/internal.c @@ -41623,7 +41623,14 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) if (!session) { WOLFSSL_MSG("Session lookup for resume failed"); ssl->options.resuming = 0; - } else { + } + else if (ssl->options.verifyPeer && ssl->options.failNoCert && + !session->peerAuthOk) { + WOLFSSL_MSG("Session lacks client auth, do full handshake"); + ssl->options.resuming = 0; + ssl->options.peerAuthGood = 0; + } + else { if (MatchSuite(ssl, &clSuites) < 0) { WOLFSSL_MSG("Unsupported cipher suite, OldClientHello"); return UNSUPPORTED_SUITE; @@ -41707,6 +41714,13 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #endif ) { int secretSz = SECRET_LEN; + if (ssl->options.verifyPeer && ssl->options.failNoCert && + !ssl->session->peerAuthOk) { + WOLFSSL_MSG("Session lacks client auth, do full handshake"); + ssl->options.resuming = 0; + ssl->options.peerAuthGood = 0; + return ret; + } WOLFSSL_MSG("Calling session secret callback"); ret = wc_RNG_GenerateBlock(ssl->rng, ssl->arrays->serverRandom, RAN_LEN); @@ -41754,6 +41768,16 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) ssl->options.resuming = 0; return ret; } + if (ssl->options.verifyPeer && ssl->options.failNoCert && + !session->peerAuthOk) { + /* This connection requires a client certificate and the resumed + * session never presented one. A full handshake sends a + * CertificateRequest instead. */ + WOLFSSL_MSG("Session lacks client auth, do full handshake"); + ssl->options.resuming = 0; + ssl->options.peerAuthGood = 0; + return ret; + } #if defined(HAVE_SESSION_TICKET) && \ (defined(HAVE_SNI) || defined(HAVE_ALPN)) /* Do not resume session if sniHash/alpnHash do not match. */ @@ -43450,6 +43474,17 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) it->suite[0] = ssl->options.cipherSuite0; it->suite[1] = ssl->options.cipherSuite; + /* Both arms below only add to this, so start from a known value: an + * async re-entry may not have re-zeroed the ticket buffer. + * A ticket minted on a resumed connection carries what the session it + * resumed recorded, because no Certificate flows in an abbreviated + * handshake and the option flags describe nothing here. */ + it->flags = 0; + if (ssl->options.resuming ? ssl->session->peerAuthOk : + (ssl->options.havePeerCert && ssl->options.havePeerVerify)) { + it->flags |= WOLFSSL_TICKET_FLAG_PEER_AUTH; + } + #ifdef WOLFSSL_EARLY_DATA c32toa(ssl->options.maxEarlyDataSz, it->maxEarlyDataSz); #endif @@ -43464,7 +43499,8 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #ifndef NO_ASN_TIME c32toa(LowResTimer(), it->timestamp); #endif - it->haveEMS = (byte) ssl->options.haveEMS; + if (ssl->options.haveEMS) + it->flags |= WOLFSSL_TICKET_FLAG_EMS; } else { #ifdef WOLFSSL_TLS13 @@ -44004,6 +44040,9 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #ifdef HAVE_ALPN XMEMCPY(ssl->session->alpnHash, it->alpnHash, TICKET_BINDING_HASH_SZ); #endif + /* After the DupSession above, which would otherwise overwrite it. */ + ssl->session->peerAuthOk = + (it->flags & WOLFSSL_TICKET_FLAG_PEER_AUTH) ? 1 : 0; if (!IsAtLeastTLSv1_3(ssl->version)) { if (ssl->arrays == NULL) @@ -44011,7 +44050,8 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) XMEMCPY(ssl->arrays->masterSecret, it->msecret, SECRET_LEN); /* Copy the haveExtendedMasterSecret property from the ticket to * the saved session, so the property may be checked later. */ - ssl->session->haveEMS = it->haveEMS; + ssl->session->haveEMS = + (it->flags & WOLFSSL_TICKET_FLAG_EMS) ? 1 : 0; ato32((const byte*)&it->timestamp, &ssl->session->bornOn); #ifndef NO_RESUME_SUITE_CHECK ssl->session->cipherSuite0 = it->suite[0]; @@ -44092,7 +44132,8 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) c32toa((word32)(milliBornOn >> 32), it->timestamp); c32toa((word32)milliBornOn , it->timestamp + OPAQUE32_LEN); #endif - it->haveEMS = (byte)sess->haveEMS; + it->flags = (byte)((sess->haveEMS ? WOLFSSL_TICKET_FLAG_EMS : 0) | + (sess->peerAuthOk ? WOLFSSL_TICKET_FLAG_PEER_AUTH : 0)); c32toa(sess->ticketAdd, it->ageAdd); c16toa(sess->namedGroup, it->namedGroup); if (sess->ticketNonce.len <= MAX_TICKET_NONCE_STATIC_SZ) { diff --git a/src/ssl_sess.c b/src/ssl_sess.c index 5217856870d..309a5884084 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -2825,6 +2825,9 @@ int wolfSSL_i2d_SSL_SESSION(WOLFSSL_SESSION* sess, unsigned char** p) #endif #endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif + /* peerAuthOk. Last and unconditional: a reader built before this field + * stops at the end of the fields above and ignores the trailing byte. */ + size += OPAQUE8_LEN; if (p != NULL) { unsigned char *data; @@ -2920,6 +2923,7 @@ int wolfSSL_i2d_SSL_SESSION(WOLFSSL_SESSION* sess, unsigned char** p) #endif #endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif + data[idx++] = sess->peerAuthOk; } #endif @@ -3226,6 +3230,13 @@ WOLFSSL_SESSION* wolfSSL_d2i_SSL_SESSION(WOLFSSL_SESSION** sess, #endif #endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif + /* peerAuthOk, appended after every field above. A blob written before this + * field existed simply ends here, and the session keeps the zero a freshly + * allocated one carries, which declines resumption where a client + * certificate is required. */ + if (i - idx >= OPAQUE8_LEN) { + s->peerAuthOk = data[idx++]; + } (void)idx; if (sess != NULL) { @@ -3795,6 +3806,14 @@ void SetupSession(WOLFSSL* ssl) session->haveEMS = 1; else session->haveEMS = ssl->options.haveEMS; + /* A resumed connection sends no Certificate, so it has nothing of its own + * to record and keeps what it inherited. Only a full handshake decides + * this, and only a verified client certificate counts: the peerAuthGood + * grants made when the connection does not ask for one do not. */ + if (!ssl->options.resuming) { + session->peerAuthOk = (byte)(ssl->options.havePeerCert && + ssl->options.havePeerVerify); + } #ifdef WOLFSSL_SESSION_ID_CTX /* If using compatibility layer then check for and copy over session context * id. */ diff --git a/src/tls13.c b/src/tls13.c index 7ca1e3e0458..ff8ea8ad966 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6890,6 +6890,15 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz, #endif if (ret == 0) DoClientTicketFinalize(ssl, current->it, current->sess); + if (ret == 0 && ssl->options.verifyPeer && + ssl->options.failNoCert && !ssl->session->peerAuthOk) { + /* This connection requires a client certificate and the + * ticket's session never presented one. Skip this PSK, the + * way a binding mismatch does, so the next candidate or a + * full handshake can serve it. */ + WOLFSSL_MSG("Ticket session lacks client auth, skipping PSK"); + ret = WOLFSSL_FATAL_ERROR; + } if (current->sess_free_cb != NULL) { current->sess_free_cb(ssl, current->sess, ¤t->sess_free_cb_ctx); diff --git a/tests/api.c b/tests/api.c index fad41d325ac..d8d90f6b254 100644 --- a/tests/api.c +++ b/tests/api.c @@ -22427,6 +22427,58 @@ static int test_wolfSSL_sigalg_info(void) return EXPECT_RESULT(); } +/* peerAuthOk rides along with a serialized session, so an external cache that + * stores sessions with i2d and reloads them with d2i keeps the client-auth + * outcome and can still resume where a certificate is required. It is the last + * field, and d2i has never required the blob to be consumed exactly, so the + * two compatibility directions both work: a blob written before the field + * existed is short and leaves the zero a new session starts with, and an older + * library reading a new blob ignores the trailing byte. */ +static int test_wolfSSL_i2d_SSL_SESSION_peer_auth(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_EXT_CACHE) && \ + !defined(NO_SESSION_CACHE) + WOLFSSL_SESSION* sess = NULL; + WOLFSSL_SESSION* restored = NULL; + unsigned char* der = NULL; + const unsigned char* ptr = NULL; + unsigned char* pp = NULL; + int sz = 0; + + ExpectNotNull(sess = wolfSSL_SESSION_new()); + if (sess != NULL) { + sess->peerAuthOk = 1; + sess->isSetup = 1; + } + ExpectIntGT((sz = wolfSSL_i2d_SSL_SESSION(sess, NULL)), 0); + ExpectNotNull(der = (unsigned char*)XMALLOC((size_t)sz, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + pp = der; + ExpectIntGT(wolfSSL_i2d_SSL_SESSION(sess, &pp), 0); + + /* Round trip keeps it. */ + ptr = der; + ExpectNotNull(restored = wolfSSL_d2i_SSL_SESSION(NULL, &ptr, (long)sz)); + if (restored != NULL) + ExpectIntEQ(restored->peerAuthOk, 1); + wolfSSL_SESSION_free(restored); + restored = NULL; + + /* A blob written before the field existed is one byte shorter; the import + * must still succeed and must not claim the peer authenticated. */ + ptr = der; + ExpectNotNull(restored = wolfSSL_d2i_SSL_SESSION(NULL, &ptr, (long)sz - 1)); + if (restored != NULL) + ExpectIntEQ(restored->peerAuthOk, 0); + wolfSSL_SESSION_free(restored); + + XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wolfSSL_SESSION_free(sess); +#endif + return EXPECT_RESULT(); +} + static int test_wolfSSL_d2i_SSL_SESSION_bounds_check(void) { EXPECT_DECLS; @@ -43620,6 +43672,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_ciphersuite_auth), TEST_DECL(test_wolfSSL_sigalg_info), /* Can't memory test as tcp_connect aborts. */ + TEST_DECL(test_wolfSSL_i2d_SSL_SESSION_peer_auth), TEST_DECL(test_wolfSSL_d2i_SSL_SESSION_bounds_check), TEST_DECL(test_wolfSSL_sk_GENERAL_NAME), TEST_DECL(test_wolfSSL_GENERAL_NAME_print), diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index d68d3c8d9e5..2509b678d39 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3098,6 +3098,245 @@ int test_tls12_resume_ticket_wrong_suite(void) return EXPECT_RESULT(); } +/* The client-auth outcome has to survive an external session cache, which + * stores sessions with wolfSSL_i2d_SSL_SESSION and reloads them with + * wolfSSL_d2i_SSL_SESSION. The server here keeps no internal cache, so the + * only way the second handshake can resume is through the deserialized + * session, and it requires a client certificate, so it only resumes if the + * recorded outcome came back with it. Session-id resumption, not a ticket: + * a ticket carries the outcome itself and would not exercise the serializer. */ +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(OPENSSL_EXTRA) && \ + defined(HAVE_EXT_CACHE) && !defined(NO_SESSION_CACHE) && \ + !defined(NO_CERTS) && !defined(NO_RSA) +static byte test_extcache_der[2048]; +static int test_extcache_derSz = 0; +static int test_extcache_gets = 0; + +static int test_extcache_new_cb(WOLFSSL* ssl, WOLFSSL_SESSION* sess) +{ + unsigned char* p = test_extcache_der; + int sz; + + (void)ssl; + sz = wolfSSL_i2d_SSL_SESSION(sess, NULL); + if ((sz > 0) && (sz <= (int)sizeof(test_extcache_der))) { + sz = wolfSSL_i2d_SSL_SESSION(sess, &p); + if (sz > 0) + test_extcache_derSz = sz; + } + return 0; +} + +static WOLFSSL_SESSION* test_extcache_get_cb(WOLFSSL* ssl, + const unsigned char* id, int len, int* ref) +{ + const unsigned char* p = test_extcache_der; + + (void)ssl; + (void)id; + (void)len; + test_extcache_gets++; + /* A fresh object each time, so wolfSSL owns it. */ + *ref = 0; + if (test_extcache_derSz <= 0) + return NULL; + return wolfSSL_d2i_SSL_SESSION(NULL, &p, (long)test_extcache_derSz); +} +#endif + +int test_tls12_ext_cache_client_auth_resume(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(OPENSSL_EXTRA) && \ + defined(HAVE_EXT_CACHE) && !defined(NO_SESSION_CACHE) && \ + !defined(NO_CERTS) && !defined(NO_RSA) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL; + WOLFSSL_SESSION* sess = NULL; + struct test_memio_ctx test_ctx; + + test_extcache_derSz = 0; + test_extcache_gets = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* CTXs first: the credentials must be in place before the objects that + * inherit them are created. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + /* Only the external cache answers a lookup. */ + if (ctx_s != NULL) { + wolfSSL_CTX_set_session_cache_mode(ctx_s, + WOLFSSL_SESS_CACHE_SERVER | WOLFSSL_SESS_CACHE_NO_INTERNAL); + wolfSSL_CTX_sess_set_new_cb(ctx_s, test_extcache_new_cb); + wolfSSL_CTX_sess_set_get_cb(ctx_s, test_extcache_get_cb); + } + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 1); + /* The cache callback serialized it. */ + ExpectIntGT(test_extcache_derSz, 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + /* Resume: the server can only get the session back through d2i. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c2, &ssl_s2, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + wolfSSL_set_verify(ssl_s2, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0); + ExpectIntGT(test_extcache_gets, 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_s2), 1); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_free(ssl_c2); + wolfSSL_free(ssl_s2); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* A session ticket is not proof that the client authenticated: the server + * grants options.peerAuthGood on resumption, and the abbreviated handshake + * sends no CertificateRequest, so a ticket minted where no client certificate + * was asked for would satisfy a connection that requires one. Servers sharing + * one ticket key across differing verify policies, or tightening a policy + * without rotating the key, are the exposure. + * + * The ticket here is minted under WOLFSSL_VERIFY_NONE by a client holding no + * certificate, then replayed against a connection set to + * WOLFSSL_VERIFY_PEER|WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT on the same CTX, so + * the ticket key is the same. The server has to decline the resumption; the + * full handshake it falls back to then rejects the client for having no + * certificate. */ +int test_tls12_resume_ticket_client_auth(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL; + WOLFSSL_SESSION* sess = NULL; + struct test_memio_ctx test_ctx; + + /* Mint: no client certificate is loaded and none is asked for. */ + 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_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + ExpectIntGT(sess->ticketLen, 0); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 0); + + /* Replay against a connection that requires a client certificate. Same + * server CTX, so the ticket decrypts. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c2, &ssl_s2, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c2), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s2, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS); + + /* Extra rounds: the declined resumption turns into a full handshake. */ + ExpectIntNE(test_memio_do_handshake(ssl_c2, ssl_s2, 20, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_s2), 0); + if (ssl_s2 != NULL) { + ExpectIntEQ(ssl_s2->options.resuming, 0); + /* Rejected for the missing certificate, not accepted off the ticket. */ + ExpectIntEQ(ssl_s2->error, WC_NO_ERR_TRACE(NO_PEER_CERT)); + } + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_free(ssl_c2); + wolfSSL_free(ssl_s2); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* The same session, resumed by a client that does present its certificate, + * must still abbreviate: the gate above must not cost legitimate mTLS + * resumption its session reuse. */ +int test_tls12_resume_ticket_client_auth_ok(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) && \ + !defined(NO_RSA) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL; + WOLFSSL_SESSION* sess = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* CTXs only: the credentials have to be in place before the objects that + * inherit them are created. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + 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_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 1); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c2, &ssl_s2, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c2), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s2, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_s2), 1); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_free(ssl_c2); + wolfSSL_free(ssl_s2); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + /* options.useTicket says the handshake in progress accepted a session ticket, * and HandleTlsResumption() reads it to take ssl->session instead of looking * up the session id the client presented. It is per-handshake state, so an @@ -3139,6 +3378,8 @@ int test_tls12_reuse_clears_use_ticket(void) wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0); ExpectNotNull(sess = wolfSSL_get1_session(ssl_c2)); + /* Guards the premise rather than the fix: with a ticket the resumption + * would be legitimate and the stale flag would never be consulted. */ ExpectIntEQ(sess->ticketLen, 0); /* Drop it from the cache the two ends share in-process, so no lookup can * legitimately resolve the id it offers. */ diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 11a969b1222..5cd9bf2015e 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -56,6 +56,9 @@ int test_tls_version_mask_alert_record(void); int test_tls_version_error_alert_mapping(void); int test_tls12_etm_failed_resumption(void); int test_tls12_resume_ticket_wrong_suite(void); +int test_tls12_ext_cache_client_auth_resume(void); +int test_tls12_resume_ticket_client_auth(void); +int test_tls12_resume_ticket_client_auth_ok(void); int test_tls12_reuse_clears_use_ticket(void); int test_tls12_resume_ticket_decline_fallback(void); int test_tls12_ticket_dropped_on_bad_finished(void); @@ -116,6 +119,9 @@ int test_tls12_aesgcm_record_nonce_unique(void); TEST_DECL_GROUP("tls", test_tls_version_error_alert_mapping), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_wrong_suite), \ + TEST_DECL_GROUP("tls", test_tls12_ext_cache_client_auth_resume), \ + TEST_DECL_GROUP("tls", test_tls12_resume_ticket_client_auth), \ + TEST_DECL_GROUP("tls", test_tls12_resume_ticket_client_auth_ok), \ TEST_DECL_GROUP("tls", test_tls12_reuse_clears_use_ticket), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_decline_fallback), \ TEST_DECL_GROUP("tls", test_tls12_ticket_dropped_on_bad_finished), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 1ffeac00b5c..ae41bb18f9a 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3829,12 +3829,22 @@ WOLFSSL_LOCAL int SetupClientSecureRenegotiation(WOLFSSL* ssl); /* Our ticket format. All members need to be a byte or array of byte to * avoid alignment issues */ +/* Bits of InternalTicket.flags. Bit 0 is where this byte has always carried + * the extended-master-secret flag, so a ticket minted before the other bits + * were defined reads back with them clear, which is the conservative answer + * for each. Adding a bit here rather than a field keeps sizeof(InternalTicket) + * and every field offset fixed, so such a ticket still decrypts and parses. */ +#define WOLFSSL_TICKET_FLAG_EMS 0x01 +/* Peer presented and passed certificate verification on the session this was + * minted from. */ +#define WOLFSSL_TICKET_FLAG_PEER_AUTH 0x02 + typedef struct InternalTicket { ProtocolVersion pv; /* version when ticket created */ byte suite[SUITE_LEN]; /* cipher suite when created */ byte msecret[SECRET_LEN]; /* master secret */ byte timestamp[TIMESTAMP_LEN]; /* born on */ - byte haveEMS; /* have extended master secret */ + byte flags; /* WOLFSSL_TICKET_FLAG_* */ #ifdef WOLFSSL_TLS13 byte ageAdd[AGEADD_LEN]; /* Obfuscation of age */ byte namedGroup[NAMEDGROUP_LEN]; /* Named group used */ @@ -5148,6 +5158,10 @@ struct WOLFSSL_SESSION { byte masterSecret[SECRET_LEN]; /* stored secret */ word16 haveEMS; /* ext master secret flag */ + /* Server side: the client presented and passed certificate verification + * when this session was established. Placed after heap so + * wolfSSL_DupSession carries it; zero means not established that way. */ + byte peerAuthOk; #if defined(SESSION_CERTS) && defined(OPENSSL_EXTRA) WOLFSSL_X509* peer; /* peer cert */ #endif /* SESSION_CERTS && OPENSSL_EXTRA */ From 647fed4272767a4e1928779b4400992f3f20843c Mon Sep 17 00:00:00 2001 From: Kareem Date: Tue, 29 Sep 2026 17:44:29 -0700 Subject: [PATCH 3/9] Code review feedback: Also clear peerAuthOk in wolfSSL_Clear Bump up WOLFSSL_CACHE_VERSION Add TLS 1.3 tests Add TLS 1.2 test for clearing peerAuthOk --- src/ssl.c | 7 ++ src/ssl_sess.c | 16 +++-- tests/api/test_tls.c | 162 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 6 ++ wolfssl/internal.h | 6 +- wolfssl/ssl_sess.h | 4 +- 6 files changed, 193 insertions(+), 8 deletions(-) diff --git a/src/ssl.c b/src/ssl.c index caea080d30c..7b1ba63a8f4 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5947,6 +5947,13 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, (ssl->session != NULL)) { ssl->session->haveAltSessionID = 0; ForceZero(ssl->session->altSessionID, ID_LEN); + /* The recorded client-auth outcome describes the connection that + * just ended. Whatever resumes next re-establishes it from the + * ticket it presents or the session the lookup finds, so a value + * left here could only be inherited by a session it does not + * belong to - including one a session-secret callback supplies, + * which carries no such record of its own. */ + ssl->session->peerAuthOk = 0; } #ifdef WOLFSSL_EARLY_DATA ssl->earlyData = no_early_data; diff --git a/src/ssl_sess.c b/src/ssl_sess.c index 309a5884084..f87a433c9b0 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -3806,11 +3806,17 @@ void SetupSession(WOLFSSL* ssl) session->haveEMS = 1; else session->haveEMS = ssl->options.haveEMS; - /* A resumed connection sends no Certificate, so it has nothing of its own - * to record and keeps what it inherited. Only a full handshake decides - * this, and only a verified client certificate counts: the peerAuthGood - * grants made when the connection does not ask for one do not. */ - if (!ssl->options.resuming) { + /* Server side only: on a client these same flags describe the server + * certificate the client verified, which is a different statement, and + * nothing reads the field on that side. Leaving a client session at zero + * keeps one meaning for the field, so a session reaching a server through + * a store shared with a client cannot assert an authenticated peer. + * A resumed connection sends no Certificate, so it has nothing of its own + * to record and keeps what it inherited. Only a verified client + * certificate counts: the peerAuthGood grants made when the connection + * does not ask for one do not. */ + if (!ssl->options.resuming && + (ssl->options.side == WOLFSSL_SERVER_END)) { session->peerAuthOk = (byte)(ssl->options.havePeerCert && ssl->options.havePeerVerify); } diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 2509b678d39..680385d3ae5 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3098,6 +3098,168 @@ int test_tls12_resume_ticket_wrong_suite(void) return EXPECT_RESULT(); } +/* The recorded client-auth outcome is per-connection state, so an object put + * back into service with wolfSSL_clear() must not still be holding the + * previous peer's. Whatever resumes next re-establishes it from the ticket it + * presents or the session the lookup finds; a session-secret callback supplies + * no such record, so a value left here would be inherited by a session it does + * not describe. */ +int test_tls12_reuse_clears_peer_auth(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_CERTS) && !defined(NO_RSA) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* CTXs first: the credentials must be in place before the objects that + * inherit them are created. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 1); + /* The client verified the server, but that is a different statement and + * nothing reads the field on that side, so its session stays at zero. */ + if (ssl_c != NULL) + ExpectIntEQ(ssl_c->session->peerAuthOk, 0); + + ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 0); + + 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 counterpart of test_tls12_resume_ticket_client_auth: the PSK the + * server offers itself must not stand in for the client certificate this + * connection requires. DoPreSharedKeys has to skip the identity, leaving a full + * handshake that then rejects the certificate-less client. */ +int test_tls13_resume_psk_client_auth(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(NO_SESSION_CACHE) && !defined(NO_CERTS) + 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; + byte readBuf[16]; + + /* Mint with no client certificate asked for and none held. */ + 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_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Drives the post-handshake NewSessionTicket onto the client session. */ + 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)); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 0); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + /* Replay against a connection that requires a client certificate. */ + 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_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + /* Extra rounds: the skipped PSK turns into a full handshake. */ + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->error, WC_NO_ERR_TRACE(NO_PEER_CERT)); + + 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(); +} + +/* And the PSK of a session that did authenticate the client must still resume, + * so the skip above does not cost legitimate TLS 1.3 mutual auth its + * abbreviated handshake. */ +int test_tls13_resume_psk_client_auth_ok(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(NO_SESSION_CACHE) && !defined(NO_CERTS) && !defined(NO_RSA) + 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; + byte readBuf[16]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* CTXs first: the credentials must be in place before the objects that + * inherit them are created. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 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)); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 1); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + 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_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + 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); + + 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(); +} + /* The client-auth outcome has to survive an external session cache, which * stores sessions with wolfSSL_i2d_SSL_SESSION and reloads them with * wolfSSL_d2i_SSL_SESSION. The server here keeps no internal cache, so the diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 5cd9bf2015e..8a446a73896 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -56,6 +56,9 @@ int test_tls_version_mask_alert_record(void); int test_tls_version_error_alert_mapping(void); int test_tls12_etm_failed_resumption(void); int test_tls12_resume_ticket_wrong_suite(void); +int test_tls12_reuse_clears_peer_auth(void); +int test_tls13_resume_psk_client_auth(void); +int test_tls13_resume_psk_client_auth_ok(void); int test_tls12_ext_cache_client_auth_resume(void); int test_tls12_resume_ticket_client_auth(void); int test_tls12_resume_ticket_client_auth_ok(void); @@ -119,6 +122,9 @@ int test_tls12_aesgcm_record_nonce_unique(void); TEST_DECL_GROUP("tls", test_tls_version_error_alert_mapping), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_wrong_suite), \ + TEST_DECL_GROUP("tls", test_tls12_reuse_clears_peer_auth), \ + TEST_DECL_GROUP("tls", test_tls13_resume_psk_client_auth), \ + TEST_DECL_GROUP("tls", test_tls13_resume_psk_client_auth_ok), \ TEST_DECL_GROUP("tls", test_tls12_ext_cache_client_auth_resume), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_client_auth), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_client_auth_ok), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index ae41bb18f9a..c82832185f9 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -5158,8 +5158,10 @@ struct WOLFSSL_SESSION { byte masterSecret[SECRET_LEN]; /* stored secret */ word16 haveEMS; /* ext master secret flag */ - /* Server side: the client presented and passed certificate verification - * when this session was established. Placed after heap so + /* The client presented and passed certificate verification when this + * session was established. Only ever set on a server session; a client + * leaves it zero, since on that side the same flags describe the server + * certificate and no client-side code reads this. Placed after heap so * wolfSSL_DupSession carries it; zero means not established that way. */ byte peerAuthOk; #if defined(SESSION_CERTS) && defined(OPENSSL_EXTRA) diff --git a/wolfssl/ssl_sess.h b/wolfssl/ssl_sess.h index 7e31681924d..fed99cce1f0 100644 --- a/wolfssl/ssl_sess.h +++ b/wolfssl/ssl_sess.h @@ -163,7 +163,9 @@ #if defined(PERSIST_SESSION_CACHE) && !defined(SESSION_CACHE_DYNAMIC_MEM) /* for persistence, if changes to layout need to increment and modify save_session_cache() and restore_session_cache and memory versions too */ - #define WOLFSSL_CACHE_VERSION 3 + /* 4: WOLFSSL_SESSION gained peerAuthOk. The sessionSz header field does not + * always catch it, since padding can absorb the byte. */ + #define WOLFSSL_CACHE_VERSION 4 /* Session Cache Header information */ typedef struct { From 8fb07da88757c97b0da4ad46f37efd13453249e5 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 30 Sep 2026 11:09:05 -0700 Subject: [PATCH 4/9] Roll peerAuthOk test into useTicket test. --- tests/api/test_tls.c | 91 +++++++++++++++++--------------------------- tests/api/test_tls.h | 2 - 2 files changed, 34 insertions(+), 59 deletions(-) diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 680385d3ae5..bcf5353ffab 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3098,56 +3098,6 @@ int test_tls12_resume_ticket_wrong_suite(void) return EXPECT_RESULT(); } -/* The recorded client-auth outcome is per-connection state, so an object put - * back into service with wolfSSL_clear() must not still be holding the - * previous peer's. Whatever resumes next re-establishes it from the ticket it - * presents or the session the lookup finds; a session-secret callback supplies - * no such record, so a value left here would be inherited by a session it does - * not describe. */ -int test_tls12_reuse_clears_peer_auth(void) -{ - EXPECT_DECLS; -#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ - !defined(WOLFSSL_NO_TLS12) && !defined(NO_CERTS) && !defined(NO_RSA) - WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; - WOLFSSL *ssl_c = NULL, *ssl_s = NULL; - struct test_memio_ctx test_ctx; - - XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - /* CTXs first: the credentials must be in place before the objects that - * inherit them are created. */ - ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, - wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); - ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, - WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); - ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, - WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); - ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), - WOLFSSL_SUCCESS); - ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, - wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); - wolfSSL_set_verify(ssl_s, - WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); - ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); - if (ssl_s != NULL) - ExpectIntEQ(ssl_s->session->peerAuthOk, 1); - /* The client verified the server, but that is a different statement and - * nothing reads the field on that side, so its session stays at zero. */ - if (ssl_c != NULL) - ExpectIntEQ(ssl_c->session->peerAuthOk, 0); - - ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); - if (ssl_s != NULL) - ExpectIntEQ(ssl_s->session->peerAuthOk, 0); - - 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 counterpart of test_tls12_resume_ticket_client_auth: the PSK the * server offers itself must not stand in for the client certificate this * connection requires. DoPreSharedKeys has to skip the identity, leaving a full @@ -3506,30 +3456,54 @@ int test_tls12_resume_ticket_client_auth_ok(void) * ClientHello: the retained session would be resumed for a client that never * held it, keyed from the master secret wolfSSL_clear() wiped. * - * The first handshake takes a ticket, so the flag is set. A second client then - * offers a session id that has been dropped from the cache, so nothing can - * legitimately resolve it: only the stale flag could still produce a - * resumption, and it must not. */ + * The recorded client-auth outcome leaks the same way, and a session-secret + * callback supplies no record of its own, so a value left there would be + * inherited by a session it does not describe. + * + * The first handshake takes a ticket and authenticates the client, so both are + * set. A second client then offers a session id that has been dropped from the + * cache, so nothing can legitimately resolve it: only stale state could still + * produce a resumption, and it must not. */ int test_tls12_reuse_clears_use_ticket(void) { EXPECT_DECLS; #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ - !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_SESSION_CACHE) + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_SESSION_CACHE) && \ + !defined(NO_CERTS) && !defined(NO_RSA) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL, *ctx_s2 = NULL; WOLFSSL *ssl_c = NULL, *ssl_c2 = NULL, *ssl_c3 = NULL; WOLFSSL *ssl_s = NULL, *ssl_s2 = NULL; WOLFSSL_SESSION* sess = NULL; struct test_memio_ctx test_ctx; int useTicketAfterClear = -1; + int peerAuthAfterClear = -1; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* CTXs first: the credentials must be in place before the objects that + * inherit them are created. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); 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_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); - if (ssl_s != NULL) + if (ssl_s != NULL) { ExpectIntEQ(ssl_s->options.useTicket, 1); + ExpectIntEQ(ssl_s->session->peerAuthOk, 1); + } + /* The client verified the server, but that is a different statement and + * nothing reads the field on that side, so its session stays at zero. */ + if (ssl_c != NULL) + ExpectIntEQ(ssl_c->session->peerAuthOk, 0); /* A session to offer next, taken from a server that issues no tickets so * the second ClientHello carries a session id and no ticket extension. @@ -3550,8 +3524,10 @@ int test_tls12_reuse_clears_use_ticket(void) ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); /* Read now, assert last: an expectation that fails here would skip the * behavioural half below. */ - if (ssl_s != NULL) + if (ssl_s != NULL) { useTicketAfterClear = ssl_s->options.useTicket; + peerAuthAfterClear = ssl_s->session->peerAuthOk; + } /* Existing CTXs are kept; only the third client object is created, and the * reused server is pointed at the transport it shares with it. */ @@ -3571,6 +3547,7 @@ int test_tls12_reuse_clears_use_ticket(void) if (ssl_s != NULL) ExpectIntEQ(ssl_s->options.resuming, 0); ExpectIntEQ(useTicketAfterClear, 0); + ExpectIntEQ(peerAuthAfterClear, 0); wolfSSL_SESSION_free(sess); wolfSSL_free(ssl_c); diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 8a446a73896..80287dfd051 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -56,7 +56,6 @@ int test_tls_version_mask_alert_record(void); int test_tls_version_error_alert_mapping(void); int test_tls12_etm_failed_resumption(void); int test_tls12_resume_ticket_wrong_suite(void); -int test_tls12_reuse_clears_peer_auth(void); int test_tls13_resume_psk_client_auth(void); int test_tls13_resume_psk_client_auth_ok(void); int test_tls12_ext_cache_client_auth_resume(void); @@ -122,7 +121,6 @@ int test_tls12_aesgcm_record_nonce_unique(void); TEST_DECL_GROUP("tls", test_tls_version_error_alert_mapping), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_wrong_suite), \ - TEST_DECL_GROUP("tls", test_tls12_reuse_clears_peer_auth), \ TEST_DECL_GROUP("tls", test_tls13_resume_psk_client_auth), \ TEST_DECL_GROUP("tls", test_tls13_resume_psk_client_auth_ok), \ TEST_DECL_GROUP("tls", test_tls12_ext_cache_client_auth_resume), \ From cb7bd953eb00d8be8aa91761b791cfb8fbc8bbdd Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 30 Sep 2026 12:01:24 -0700 Subject: [PATCH 5/9] Refactor client auth conditional into ClientAuthRequired, ensure it checks mutual auth as well. Reset more fields in wolfSSL_Clear to ensure a full reset. Add a test to ensure wolfSSL_Clear fully clears after a connection. Add a test for resuming in TLS 1.2 with mutual auth, and a test for resuming with PHA in TLS 1.3. Minimize comments. --- src/internal.c | 50 +++++--- src/ssl.c | 22 ++-- src/ssl_sess.c | 25 ++-- src/tls13.c | 8 +- tests/api.c | 21 ++-- tests/api/test_tls.c | 289 +++++++++++++++++++++++++++++++------------ tests/api/test_tls.h | 6 + wolfssl/internal.h | 22 ++-- wolfssl/ssl_sess.h | 4 +- 9 files changed, 287 insertions(+), 160 deletions(-) diff --git a/src/internal.c b/src/internal.c index e70b018da93..1f18e7540ac 100644 --- a/src/internal.c +++ b/src/internal.c @@ -41624,8 +41624,7 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) WOLFSSL_MSG("Session lookup for resume failed"); ssl->options.resuming = 0; } - else if (ssl->options.verifyPeer && ssl->options.failNoCert && - !session->peerAuthOk) { + else if (ClientAuthRequired(ssl) && !session->peerAuthOk) { WOLFSSL_MSG("Session lacks client auth, do full handshake"); ssl->options.resuming = 0; ssl->options.peerAuthGood = 0; @@ -41666,6 +41665,25 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #endif /* OLD_HELLO_ALLOWED */ +#if !defined(NO_CERTS) && !defined(WOLFSSL_NO_CLIENT_AUTH) +/* Would a full handshake on this connection require a verified client + * certificate? Mirrors DoClientKeyExchange, the empty-Certificate rule in + * ProcessPeerCerts and DoTls13Finished. */ +int ClientAuthRequired(const WOLFSSL* ssl) +{ + if (ssl->options.side != WOLFSSL_SERVER_END) + return 0; +#ifdef WOLFSSL_POST_HANDSHAKE_AUTH + /* Sends no CertificateRequest in the handshake, so it records no + * outcome to inherit. */ + if (ssl->options.verifyPostHandshake) + return 0; +#endif + return ssl->options.mutualAuth || + (ssl->options.verifyPeer && ssl->options.failNoCert); +} +#endif /* !NO_CERTS && !WOLFSSL_NO_CLIENT_AUTH */ + #ifndef WOLFSSL_NO_TLS12 /** @@ -41714,9 +41732,11 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) #endif ) { int secretSz = SECRET_LEN; - if (ssl->options.verifyPeer && ssl->options.failNoCert && - !ssl->session->peerAuthOk) { - WOLFSSL_MSG("Session lacks client auth, do full handshake"); + /* The secret comes from the callback, and nothing records an auth + * outcome for it. */ + if (ClientAuthRequired(ssl)) { + WOLFSSL_MSG("Session secret callback cannot assert client auth," + " do full handshake"); ssl->options.resuming = 0; ssl->options.peerAuthGood = 0; return ret; @@ -41768,11 +41788,7 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) ssl->options.resuming = 0; return ret; } - if (ssl->options.verifyPeer && ssl->options.failNoCert && - !session->peerAuthOk) { - /* This connection requires a client certificate and the resumed - * session never presented one. A full handshake sends a - * CertificateRequest instead. */ + if (ClientAuthRequired(ssl) && !session->peerAuthOk) { WOLFSSL_MSG("Session lacks client auth, do full handshake"); ssl->options.resuming = 0; ssl->options.peerAuthGood = 0; @@ -41981,11 +41997,9 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) ssl->options.resuming = 0; ssl->arrays->sessionIDSz = 0; #ifdef HAVE_SESSION_TICKET - /* Set again below by TLSX_SessionTicket_Parse() when this ClientHello - * carries a ticket. HandleTlsResumption() takes the retained session - * instead of the presented session id when it is set, so a value left - * over from an earlier handshake on this object must not reach it. */ + /* Only this ClientHello may set them, in TLSX_SessionTicket_Parse(). */ ssl->options.useTicket = 0; + ssl->options.createTicket = 0; #endif /* protocol version, random and session id length check */ @@ -43474,11 +43488,9 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) it->suite[0] = ssl->options.cipherSuite0; it->suite[1] = ssl->options.cipherSuite; - /* Both arms below only add to this, so start from a known value: an - * async re-entry may not have re-zeroed the ticket buffer. - * A ticket minted on a resumed connection carries what the session it - * resumed recorded, because no Certificate flows in an abbreviated - * handshake and the option flags describe nothing here. */ + /* Both arms below only add to this, and an async re-entry may not + * have re-zeroed the buffer. A resumed connection sends no + * Certificate, so it carries what it resumed. */ it->flags = 0; if (ssl->options.resuming ? ssl->session->peerAuthOk : (ssl->options.havePeerCert && ssl->options.havePeerVerify)) { diff --git a/src/ssl.c b/src/ssl.c index 7b1ba63a8f4..d8ff8ac5bfe 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5800,6 +5800,9 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, ssl->options.connReset = 0; ssl->options.sentNotify = 0; ssl->options.closeNotify = 0; + /* Per-connection budgets, not cumulative across a reused object. */ + ssl->options.alertCount = 0; + ssl->options.emptyRecordCount = 0; ssl->options.sendVerify = 0; ssl->options.serverState = NULL_STATE; ssl->options.clientState = NULL_STATE; @@ -5823,8 +5826,9 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, DYNAMIC_TYPE_TMP_BUFFER); ssl->buffers.certVerifyMsg.buffer = NULL; ssl->buffers.certVerifyMsg.length = 0; - ssl->fragOffset = 0; #endif + ssl->fragOffset = 0; + ssl->options.buildingMsg = 0; ssl->options.processReply = 0; /* doProcessInit */ ssl->options.havePeerVerify = 0; ssl->options.havePeerCert = 0; @@ -5933,26 +5937,16 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, #endif #endif ssl->options.rejectTicket = 0; - /* Records that the handshake in progress took a ticket, and selects - * the retained session over a session id lookup. Only the current - * ClientHello may set it. */ ssl->options.useTicket = 0; ssl->options.createTicket = 0; #endif - /* A server keeps its session across the reset, and the cache lookup - * prefers this id over the one the ClientHello carries. The next - * client has not asked for that session, so the id goes; a client - * keeps it, being the side that resumes what it held. */ + /* A server keeps its session across the reset, but the next client + * has not asked for it. A client is the side that resumes what it + * held, so it keeps both. */ if ((ssl->options.side == WOLFSSL_SERVER_END) && (ssl->session != NULL)) { ssl->session->haveAltSessionID = 0; ForceZero(ssl->session->altSessionID, ID_LEN); - /* The recorded client-auth outcome describes the connection that - * just ended. Whatever resumes next re-establishes it from the - * ticket it presents or the session the lookup finds, so a value - * left here could only be inherited by a session it does not - * belong to - including one a session-secret callback supplies, - * which carries no such record of its own. */ ssl->session->peerAuthOk = 0; } #ifdef WOLFSSL_EARLY_DATA diff --git a/src/ssl_sess.c b/src/ssl_sess.c index f87a433c9b0..47cc7a18499 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -2825,8 +2825,7 @@ int wolfSSL_i2d_SSL_SESSION(WOLFSSL_SESSION* sess, unsigned char** p) #endif #endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif - /* peerAuthOk. Last and unconditional: a reader built before this field - * stops at the end of the fields above and ignores the trailing byte. */ + /* peerAuthOk, last so an older reader stops before it. */ size += OPAQUE8_LEN; if (p != NULL) { @@ -3230,12 +3229,10 @@ WOLFSSL_SESSION* wolfSSL_d2i_SSL_SESSION(WOLFSSL_SESSION** sess, #endif #endif /* !NO_WOLFSSL_SERVER && !NO_TLS */ #endif - /* peerAuthOk, appended after every field above. A blob written before this - * field existed simply ends here, and the session keeps the zero a freshly - * allocated one carries, which declines resumption where a client - * certificate is required. */ - if (i - idx >= OPAQUE8_LEN) { - s->peerAuthOk = data[idx++]; + /* Absent from a blob written before the field existed, which leaves the + * zero a new session carries. */ + if (i - idx == OPAQUE8_LEN) { + s->peerAuthOk = (data[idx++] != 0); } (void)idx; @@ -3806,15 +3803,9 @@ void SetupSession(WOLFSSL* ssl) session->haveEMS = 1; else session->haveEMS = ssl->options.haveEMS; - /* Server side only: on a client these same flags describe the server - * certificate the client verified, which is a different statement, and - * nothing reads the field on that side. Leaving a client session at zero - * keeps one meaning for the field, so a session reaching a server through - * a store shared with a client cannot assert an authenticated peer. - * A resumed connection sends no Certificate, so it has nothing of its own - * to record and keeps what it inherited. Only a verified client - * certificate counts: the peerAuthGood grants made when the connection - * does not ask for one do not. */ + /* Server only: on a client these flags describe the server certificate, + * a different statement that nothing reads. A resumed connection sends no + * Certificate, so it keeps what it inherited. */ if (!ssl->options.resuming && (ssl->options.side == WOLFSSL_SERVER_END)) { session->peerAuthOk = (byte)(ssl->options.havePeerCert && diff --git a/src/tls13.c b/src/tls13.c index ff8ea8ad966..ea7fbf5f8ea 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6890,12 +6890,8 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz, #endif if (ret == 0) DoClientTicketFinalize(ssl, current->it, current->sess); - if (ret == 0 && ssl->options.verifyPeer && - ssl->options.failNoCert && !ssl->session->peerAuthOk) { - /* This connection requires a client certificate and the - * ticket's session never presented one. Skip this PSK, the - * way a binding mismatch does, so the next candidate or a - * full handshake can serve it. */ + if (ret == 0 && ClientAuthRequired(ssl) && + !ssl->session->peerAuthOk) { WOLFSSL_MSG("Ticket session lacks client auth, skipping PSK"); ret = WOLFSSL_FATAL_ERROR; } diff --git a/tests/api.c b/tests/api.c index d8d90f6b254..b74f23eb92f 100644 --- a/tests/api.c +++ b/tests/api.c @@ -22427,13 +22427,8 @@ static int test_wolfSSL_sigalg_info(void) return EXPECT_RESULT(); } -/* peerAuthOk rides along with a serialized session, so an external cache that - * stores sessions with i2d and reloads them with d2i keeps the client-auth - * outcome and can still resume where a certificate is required. It is the last - * field, and d2i has never required the blob to be consumed exactly, so the - * two compatibility directions both work: a blob written before the field - * existed is short and leaves the zero a new session starts with, and an older - * library reading a new blob ignores the trailing byte. */ +/* peerAuthOk is the last field, so a blob written before it existed is simply + * short and an older reader ignores the trailing byte. */ static int test_wolfSSL_i2d_SSL_SESSION_peer_auth(void) { EXPECT_DECLS; @@ -22465,13 +22460,21 @@ static int test_wolfSSL_i2d_SSL_SESSION_peer_auth(void) wolfSSL_SESSION_free(restored); restored = NULL; - /* A blob written before the field existed is one byte shorter; the import - * must still succeed and must not claim the peer authenticated. */ + /* One byte shorter, as written before the field existed: must import and + * must not claim the peer authenticated. */ ptr = der; ExpectNotNull(restored = wolfSSL_d2i_SSL_SESSION(NULL, &ptr, (long)sz - 1)); if (restored != NULL) ExpectIntEQ(restored->peerAuthOk, 0); wolfSSL_SESSION_free(restored); + restored = NULL; + + /* A length past the blob must not read whatever follows it. */ + ptr = der; + ExpectNotNull(restored = wolfSSL_d2i_SSL_SESSION(NULL, &ptr, (long)sz + 8)); + if (restored != NULL) + ExpectIntEQ(restored->peerAuthOk, 0); + wolfSSL_SESSION_free(restored); XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); wolfSSL_SESSION_free(sess); diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index bcf5353ffab..7ce196d8744 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3098,10 +3098,185 @@ int test_tls12_resume_ticket_wrong_suite(void) return EXPECT_RESULT(); } -/* TLS 1.3 counterpart of test_tls12_resume_ticket_client_auth: the PSK the - * server offers itself must not stand in for the client certificate this - * connection requires. DoPreSharedKeys has to skip the identity, leaving a full - * handshake that then rejects the certificate-less client. */ +/* Catches per-connection Options state that wolfSSL_clear() forgets, rather + * than one field at a time. Every Options is born inside + * XMEMSET(ssl, 0, sizeof(WOLFSSL)), so padding and bitfield slack are zero in + * both objects and only written fields can differ. */ +int test_tls12_clear_resets_options(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL, *fresh = NULL; + struct test_memio_ctx test_ctx; + char msg[] = "test"; + char reply[sizeof(msg)]; + + 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_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Data both ways and a bidirectional close, so record-layer and shutdown + * state is covered too, not just the handshake. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_write(ssl_s, msg, (int)sizeof(msg)), (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_c, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_shutdown(ssl_c), WOLFSSL_SHUTDOWN_NOT_DONE); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), 0); + ExpectIntEQ(wolfSSL_shutdown(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_shutdown(ssl_c), WOLFSSL_SUCCESS); + /* Same CTX, never handshaked, so its Options is what a reset should + * produce. Created after the handshake so the CTX is fully configured. */ + ExpectNotNull(fresh = wolfSSL_new(ctx_s)); + ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); + + if ((ssl_s != NULL) && (fresh != NULL)) { + /* Negotiated results, kept on purpose for the accessors. */ + ssl_s->options.cipherSuite0 = fresh->options.cipherSuite0; + ssl_s->options.cipherSuite = fresh->options.cipherSuite; + ssl_s->options.hashAlgo = fresh->options.hashAlgo; + ssl_s->options.sigAlgo = fresh->options.sigAlgo; + ssl_s->options.haveDH = fresh->options.haveDH; + /* wolfSSL_get_shutdown() reports a completed bidirectional shutdown + * from this after a clear, by documented intent. */ + ssl_s->options.shutdownDone = fresh->options.shutdownDone; + /* Reset only under WOLFSSL_ASYNC_CRYPT. */ + ssl_s->options.asyncState = fresh->options.asyncState; +#ifndef NO_DH + ssl_s->options.dhDoKeyTest = fresh->options.dhDoKeyTest; + ssl_s->options.dhKeyTested = fresh->options.dhKeyTested; +#endif + ExpectIntEQ(XMEMCMP(&ssl_s->options, &fresh->options, sizeof(Options)), + 0); + if (EXPECT_FAIL()) { + /* Offsets rather than names: Options is mostly bitfields. */ + const byte* a = (const byte*)&ssl_s->options; + const byte* b = (const byte*)&fresh->options; + word32 k; + for (k = 0; k < (word32)sizeof(Options); k++) { + if (a[k] != b[k]) { + fprintf(stderr, "Options differ at byte %u:" + " reused=0x%02x fresh=0x%02x\n", k, a[k], b[k]); + } + } + } + } + + wolfSSL_free(fresh); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* mutual_auth is an alternative to failNoCert in the full handshake, so + * resumption has to agree. */ +int test_tls12_resume_ticket_mutual_auth(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL, *ssl_c2 = NULL, *ssl_s2 = NULL; + WOLFSSL_SESSION* sess = NULL; + struct test_memio_ctx test_ctx; + + 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_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c2, &ssl_s2, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c2), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s2, WOLFSSL_VERIFY_PEER, NULL); + ExpectIntEQ(wolfSSL_mutual_auth(ssl_s2, 1), 0); + ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS); + test_memio_do_handshake(ssl_c2, ssl_s2, 20, NULL); + ExpectIntEQ(wolfSSL_session_reused(ssl_s2), 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_free(ssl_c2); + wolfSSL_free(ssl_s2); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* A post-handshake-auth server records no outcome, so it must still resume. */ +int test_tls13_resume_psk_post_handshake_auth(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(WOLFSSL_POST_HANDSHAKE_AUTH) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_SESSION_CACHE) && \ + !defined(NO_CERTS) && !defined(NO_RSA) + 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; + byte readBuf[16]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT | WOLFSSL_VERIFY_POST_HANDSHAKE, + NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 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)); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->session->peerAuthOk, 0); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + 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_set_verify(ssl_s, WOLFSSL_VERIFY_PEER | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT | WOLFSSL_VERIFY_POST_HANDSHAKE, + NULL); + 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); + + 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: a PSK must not stand in for the required client certificate. */ int test_tls13_resume_psk_client_auth(void) { EXPECT_DECLS; @@ -3114,13 +3289,12 @@ int test_tls13_resume_psk_client_auth(void) struct test_memio_ctx test_ctx; byte readBuf[16]; - /* Mint with no client certificate asked for and none held. */ 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_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); - /* Drives the post-handshake NewSessionTicket onto the client session. */ + /* Drives the NewSessionTicket onto the client session. */ 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)); @@ -3130,14 +3304,13 @@ int test_tls13_resume_psk_client_auth(void) wolfSSL_free(ssl_c); ssl_c = NULL; wolfSSL_free(ssl_s); ssl_s = NULL; - /* Replay against a connection that requires a client certificate. */ 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_set_verify(ssl_s, WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); - /* Extra rounds: the skipped PSK turns into a full handshake. */ + /* The skipped PSK becomes a full handshake. */ ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); if (ssl_s != NULL) @@ -3152,9 +3325,7 @@ int test_tls13_resume_psk_client_auth(void) return EXPECT_RESULT(); } -/* And the PSK of a session that did authenticate the client must still resume, - * so the skip above does not cost legitimate TLS 1.3 mutual auth its - * abbreviated handshake. */ +/* A session that did authenticate the client must still resume. */ int test_tls13_resume_psk_client_auth_ok(void) { EXPECT_DECLS; @@ -3168,8 +3339,7 @@ int test_tls13_resume_psk_client_auth_ok(void) byte readBuf[16]; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - /* CTXs first: the credentials must be in place before the objects that - * inherit them are created. */ + /* CTXs first: credentials must precede the objects inheriting them. */ ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, @@ -3210,13 +3380,8 @@ int test_tls13_resume_psk_client_auth_ok(void) return EXPECT_RESULT(); } -/* The client-auth outcome has to survive an external session cache, which - * stores sessions with wolfSSL_i2d_SSL_SESSION and reloads them with - * wolfSSL_d2i_SSL_SESSION. The server here keeps no internal cache, so the - * only way the second handshake can resume is through the deserialized - * session, and it requires a client certificate, so it only resumes if the - * recorded outcome came back with it. Session-id resumption, not a ticket: - * a ticket carries the outcome itself and would not exercise the serializer. */ +/* The outcome has to survive an i2d/d2i external cache. No internal cache and + * no ticket, so only the deserialized session can carry it. */ #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(OPENSSL_EXTRA) && \ defined(HAVE_EXT_CACHE) && !defined(NO_SESSION_CACHE) && \ @@ -3274,8 +3439,7 @@ int test_tls12_ext_cache_client_auth_resume(void) test_extcache_gets = 0; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - /* CTXs first: the credentials must be in place before the objects that - * inherit them are created. */ + /* CTXs first: credentials must precede the objects inheriting them. */ ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, @@ -3284,7 +3448,7 @@ int test_tls12_ext_cache_client_auth_resume(void) WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), WOLFSSL_SUCCESS); - /* Only the external cache answers a lookup. */ + /* Only the external cache answers. */ if (ctx_s != NULL) { wolfSSL_CTX_set_session_cache_mode(ctx_s, WOLFSSL_SESS_CACHE_SERVER | WOLFSSL_SESS_CACHE_NO_INTERNAL); @@ -3298,11 +3462,10 @@ int test_tls12_ext_cache_client_auth_resume(void) ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); if (ssl_s != NULL) ExpectIntEQ(ssl_s->session->peerAuthOk, 1); - /* The cache callback serialized it. */ ExpectIntGT(test_extcache_derSz, 0); ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); - /* Resume: the server can only get the session back through d2i. */ + /* The server can only get it back through d2i. */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c2, &ssl_s2, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); @@ -3324,19 +3487,8 @@ int test_tls12_ext_cache_client_auth_resume(void) return EXPECT_RESULT(); } -/* A session ticket is not proof that the client authenticated: the server - * grants options.peerAuthGood on resumption, and the abbreviated handshake - * sends no CertificateRequest, so a ticket minted where no client certificate - * was asked for would satisfy a connection that requires one. Servers sharing - * one ticket key across differing verify policies, or tightening a policy - * without rotating the key, are the exposure. - * - * The ticket here is minted under WOLFSSL_VERIFY_NONE by a client holding no - * certificate, then replayed against a connection set to - * WOLFSSL_VERIFY_PEER|WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT on the same CTX, so - * the ticket key is the same. The server has to decline the resumption; the - * full handshake it falls back to then rejects the client for having no - * certificate. */ +/* A ticket minted under VERIFY_NONE, replayed on the same CTX against a + * connection requiring a client certificate, must be declined. */ int test_tls12_resume_ticket_client_auth(void) { EXPECT_DECLS; @@ -3349,7 +3501,6 @@ int test_tls12_resume_ticket_client_auth(void) WOLFSSL_SESSION* sess = NULL; struct test_memio_ctx test_ctx; - /* Mint: no client certificate is loaded and none is asked for. */ 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); @@ -3361,8 +3512,7 @@ int test_tls12_resume_ticket_client_auth(void) if (ssl_s != NULL) ExpectIntEQ(ssl_s->session->peerAuthOk, 0); - /* Replay against a connection that requires a client certificate. Same - * server CTX, so the ticket decrypts. */ + /* Same CTX, so the ticket decrypts. */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c2, &ssl_s2, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); @@ -3371,12 +3521,12 @@ int test_tls12_resume_ticket_client_auth(void) WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS); - /* Extra rounds: the declined resumption turns into a full handshake. */ + /* The decline becomes a full handshake. */ ExpectIntNE(test_memio_do_handshake(ssl_c2, ssl_s2, 20, NULL), 0); ExpectIntEQ(wolfSSL_session_reused(ssl_s2), 0); if (ssl_s2 != NULL) { ExpectIntEQ(ssl_s2->options.resuming, 0); - /* Rejected for the missing certificate, not accepted off the ticket. */ + /* Rejected for the certificate, not accepted off the ticket. */ ExpectIntEQ(ssl_s2->error, WC_NO_ERR_TRACE(NO_PEER_CERT)); } @@ -3391,9 +3541,7 @@ int test_tls12_resume_ticket_client_auth(void) return EXPECT_RESULT(); } -/* The same session, resumed by a client that does present its certificate, - * must still abbreviate: the gate above must not cost legitimate mTLS - * resumption its session reuse. */ +/* A client that does present its certificate must still abbreviate. */ int test_tls12_resume_ticket_client_auth_ok(void) { EXPECT_DECLS; @@ -3408,8 +3556,7 @@ int test_tls12_resume_ticket_client_auth_ok(void) struct test_memio_ctx test_ctx; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - /* CTXs only: the credentials have to be in place before the objects that - * inherit them are created. */ + /* CTXs first: credentials must precede the objects inheriting them. */ ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, @@ -3449,21 +3596,10 @@ int test_tls12_resume_ticket_client_auth_ok(void) return EXPECT_RESULT(); } -/* options.useTicket says the handshake in progress accepted a session ticket, - * and HandleTlsResumption() reads it to take ssl->session instead of looking - * up the session id the client presented. It is per-handshake state, so an - * object reused with wolfSSL_clear() must not carry it into the next - * ClientHello: the retained session would be resumed for a client that never - * held it, keyed from the master secret wolfSSL_clear() wiped. - * - * The recorded client-auth outcome leaks the same way, and a session-secret - * callback supplies no record of its own, so a value left there would be - * inherited by a session it does not describe. - * - * The first handshake takes a ticket and authenticates the client, so both are - * set. A second client then offers a session id that has been dropped from the - * cache, so nothing can legitimately resolve it: only stale state could still - * produce a resumption, and it must not. */ +/* useTicket and peerAuthOk are per-handshake: an object reused with + * wolfSSL_clear() must not carry either into the next ClientHello, or the + * retained session is resumed for a client that never held it. The id offered + * below is dropped from the cache, so only stale state could resume it. */ int test_tls12_reuse_clears_use_ticket(void) { EXPECT_DECLS; @@ -3480,8 +3616,7 @@ int test_tls12_reuse_clears_use_ticket(void) int peerAuthAfterClear = -1; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - /* CTXs first: the credentials must be in place before the objects that - * inherit them are created. */ + /* CTXs first: credentials must precede the objects inheriting them. */ ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, NULL, NULL, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, @@ -3500,37 +3635,30 @@ int test_tls12_reuse_clears_use_ticket(void) ExpectIntEQ(ssl_s->options.useTicket, 1); ExpectIntEQ(ssl_s->session->peerAuthOk, 1); } - /* The client verified the server, but that is a different statement and - * nothing reads the field on that side, so its session stays at zero. */ + /* A different statement on the client, and unread there. */ if (ssl_c != NULL) ExpectIntEQ(ssl_c->session->peerAuthOk, 0); - /* A session to offer next, taken from a server that issues no tickets so - * the second ClientHello carries a session id and no ticket extension. - * Nothing is modified in place: a session handed out by the cache may - * still be backed by it. */ + /* From a server issuing no tickets, so the next ClientHello carries a + * session id only. Not modified in place: it may be cache-backed. */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s2, &ssl_c2, &ssl_s2, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0); ExpectNotNull(sess = wolfSSL_get1_session(ssl_c2)); - /* Guards the premise rather than the fix: with a ticket the resumption - * would be legitimate and the stale flag would never be consulted. */ + /* Premise, not the fix: a ticket would resume legitimately. */ ExpectIntEQ(sess->ticketLen, 0); - /* Drop it from the cache the two ends share in-process, so no lookup can - * legitimately resolve the id it offers. */ + /* The two ends share one cache in-process. */ ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(ctx_s2, sess), 1); ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); - /* Read now, assert last: an expectation that fails here would skip the - * behavioural half below. */ + /* Assert last, or a failure here skips the behavioural half. */ if (ssl_s != NULL) { useTicketAfterClear = ssl_s->options.useTicket; peerAuthAfterClear = ssl_s->session->peerAuthOk; } - /* Existing CTXs are kept; only the third client object is created, and the - * reused server is pointed at the transport it shares with it. */ + /* Point the reused server at the new transport. */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c3, NULL, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); @@ -3539,8 +3667,7 @@ int test_tls12_reuse_clears_use_ticket(void) wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); } ExpectIntEQ(wolfSSL_set_session(ssl_c3, sess), WOLFSSL_SUCCESS); - /* The handshake itself may fail: what matters is that the server did not - * hand this client the retained session. */ + /* May fail; what matters is it did not hand over the retained session. */ if ((ssl_c3 != NULL) && (ssl_s != NULL)) test_memio_do_handshake(ssl_c3, ssl_s, 10, NULL); ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 80287dfd051..779635c0710 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -56,6 +56,9 @@ int test_tls_version_mask_alert_record(void); int test_tls_version_error_alert_mapping(void); int test_tls12_etm_failed_resumption(void); int test_tls12_resume_ticket_wrong_suite(void); +int test_tls12_clear_resets_options(void); +int test_tls12_resume_ticket_mutual_auth(void); +int test_tls13_resume_psk_post_handshake_auth(void); int test_tls13_resume_psk_client_auth(void); int test_tls13_resume_psk_client_auth_ok(void); int test_tls12_ext_cache_client_auth_resume(void); @@ -121,6 +124,9 @@ int test_tls12_aesgcm_record_nonce_unique(void); TEST_DECL_GROUP("tls", test_tls_version_error_alert_mapping), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls12_resume_ticket_wrong_suite), \ + TEST_DECL_GROUP("tls", test_tls12_clear_resets_options), \ + TEST_DECL_GROUP("tls", test_tls12_resume_ticket_mutual_auth), \ + TEST_DECL_GROUP("tls", test_tls13_resume_psk_post_handshake_auth), \ TEST_DECL_GROUP("tls", test_tls13_resume_psk_client_auth), \ TEST_DECL_GROUP("tls", test_tls13_resume_psk_client_auth_ok), \ TEST_DECL_GROUP("tls", test_tls12_ext_cache_client_auth_resume), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index c82832185f9..a787aa8ef40 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2312,6 +2312,9 @@ WOLFSSL_LOCAL int CookiePolicySet(WOLFSSL* ssl, const byte* hrrSecret, WOLFSSL_LOCAL int CookiePolicyEnable(WOLFSSL* ssl); WOLFSSL_LOCAL int CheckCookieState(WOLFSSL* ssl); #endif +#if !defined(NO_CERTS) && !defined(WOLFSSL_NO_CLIENT_AUTH) +WOLFSSL_LOCAL int ClientAuthRequired(const WOLFSSL* ssl); +#endif WOLFSSL_LOCAL int DoClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 helloSz); #ifdef WOLFSSL_TLS13 @@ -3829,14 +3832,12 @@ WOLFSSL_LOCAL int SetupClientSecureRenegotiation(WOLFSSL* ssl); /* Our ticket format. All members need to be a byte or array of byte to * avoid alignment issues */ -/* Bits of InternalTicket.flags. Bit 0 is where this byte has always carried - * the extended-master-secret flag, so a ticket minted before the other bits - * were defined reads back with them clear, which is the conservative answer - * for each. Adding a bit here rather than a field keeps sizeof(InternalTicket) - * and every field offset fixed, so such a ticket still decrypts and parses. */ +/* Bits of InternalTicket.flags. Bit 0 is where this byte has always held + * haveEMS, so sizeof(InternalTicket) and every offset are unchanged and an + * older ticket parses with the newer bits clear. An older peer handed a ticket + * carrying bit 1 copies the whole byte into haveEMS and fails the handshake + * cleanly, so a shared TLS 1.2 ticket key needs matching versions. */ #define WOLFSSL_TICKET_FLAG_EMS 0x01 -/* Peer presented and passed certificate verification on the session this was - * minted from. */ #define WOLFSSL_TICKET_FLAG_PEER_AUTH 0x02 typedef struct InternalTicket { @@ -5158,11 +5159,8 @@ struct WOLFSSL_SESSION { byte masterSecret[SECRET_LEN]; /* stored secret */ word16 haveEMS; /* ext master secret flag */ - /* The client presented and passed certificate verification when this - * session was established. Only ever set on a server session; a client - * leaves it zero, since on that side the same flags describe the server - * certificate and no client-side code reads this. Placed after heap so - * wolfSSL_DupSession carries it; zero means not established that way. */ + /* Client presented a certificate and proved key possession. Server + * sessions only; after heap so wolfSSL_DupSession carries it. */ byte peerAuthOk; #if defined(SESSION_CERTS) && defined(OPENSSL_EXTRA) WOLFSSL_X509* peer; /* peer cert */ diff --git a/wolfssl/ssl_sess.h b/wolfssl/ssl_sess.h index fed99cce1f0..b6d3e6841f2 100644 --- a/wolfssl/ssl_sess.h +++ b/wolfssl/ssl_sess.h @@ -163,8 +163,8 @@ #if defined(PERSIST_SESSION_CACHE) && !defined(SESSION_CACHE_DYNAMIC_MEM) /* for persistence, if changes to layout need to increment and modify save_session_cache() and restore_session_cache and memory versions too */ - /* 4: WOLFSSL_SESSION gained peerAuthOk. The sessionSz header field does not - * always catch it, since padding can absorb the byte. */ + /* 4: WOLFSSL_SESSION gained peerAuthOk; padding can hide it from + * the sessionSz check. */ #define WOLFSSL_CACHE_VERSION 4 /* Session Cache Header information */ From 9dc806f6b9199577ee148c0ccb1e52d2584f950c Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 30 Sep 2026 12:57:12 -0700 Subject: [PATCH 6/9] Don't reset use/createTicket in SCR. --- src/internal.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/internal.c b/src/internal.c index 1f18e7540ac..63d2c155f84 100644 --- a/src/internal.c +++ b/src/internal.c @@ -41997,9 +41997,13 @@ int ClientAuthRequired(const WOLFSSL* ssl) ssl->options.resuming = 0; ssl->arrays->sessionIDSz = 0; #ifdef HAVE_SESSION_TICKET - /* Only this ClientHello may set them, in TLSX_SessionTicket_Parse(). */ - ssl->options.useTicket = 0; - ssl->options.createTicket = 0; + /* Only this ClientHello may set them, in TLSX_SessionTicket_Parse(), + * which ignores the extension during SCR - so an SCR hello inherits + * the connection's. */ + if (!IsSCR(ssl)) { + ssl->options.useTicket = 0; + ssl->options.createTicket = 0; + } #endif /* protocol version, random and session id length check */ From 6ac354d088b76b5a0d3a6795d9e094580e370c61 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 30 Sep 2026 14:03:32 -0700 Subject: [PATCH 7/9] Fix preprocessor guards to handle test failures. --- src/internal.c | 2 +- tests/api/test_tls.c | 26 ++++++++++++++++++-------- wolfssl/internal.h | 3 +++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/internal.c b/src/internal.c index 63d2c155f84..4f270b9c84d 100644 --- a/src/internal.c +++ b/src/internal.c @@ -41673,7 +41673,7 @@ int ClientAuthRequired(const WOLFSSL* ssl) { if (ssl->options.side != WOLFSSL_SERVER_END) return 0; -#ifdef WOLFSSL_POST_HANDSHAKE_AUTH +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_POST_HANDSHAKE_AUTH) /* Sends no CertificateRequest in the handshake, so it records no * outcome to inherit. */ if (ssl->options.verifyPostHandshake) diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 7ce196d8744..7ea5ac2e14e 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3184,7 +3184,8 @@ int test_tls12_resume_ticket_mutual_auth(void) EXPECT_DECLS; #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ - !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL, *ssl_c2 = NULL, *ssl_s2 = NULL; WOLFSSL_SESSION* sess = NULL; @@ -3226,7 +3227,8 @@ int test_tls13_resume_psk_post_handshake_auth(void) #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ defined(WOLFSSL_POST_HANDSHAKE_AUTH) && defined(HAVE_SESSION_TICKET) && \ !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_SESSION_CACHE) && \ - !defined(NO_CERTS) && !defined(NO_RSA) + !defined(NO_CERTS) && !defined(NO_RSA) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; WOLFSSL_SESSION* sess = NULL; @@ -3282,7 +3284,8 @@ int test_tls13_resume_psk_client_auth(void) EXPECT_DECLS; #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ - !defined(NO_SESSION_CACHE) && !defined(NO_CERTS) + !defined(NO_SESSION_CACHE) && !defined(NO_CERTS) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; WOLFSSL_SESSION* sess = NULL; @@ -3331,7 +3334,8 @@ int test_tls13_resume_psk_client_auth_ok(void) EXPECT_DECLS; #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ - !defined(NO_SESSION_CACHE) && !defined(NO_CERTS) && !defined(NO_RSA) + !defined(NO_SESSION_CACHE) && !defined(NO_CERTS) && !defined(NO_RSA) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; WOLFSSL_SESSION* sess = NULL; @@ -3385,7 +3389,8 @@ int test_tls13_resume_psk_client_auth_ok(void) #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(OPENSSL_EXTRA) && \ defined(HAVE_EXT_CACHE) && !defined(NO_SESSION_CACHE) && \ - !defined(NO_CERTS) && !defined(NO_RSA) + !defined(NO_CERTS) && !defined(NO_RSA) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) static byte test_extcache_der[2048]; static int test_extcache_derSz = 0; static int test_extcache_gets = 0; @@ -3428,7 +3433,8 @@ int test_tls12_ext_cache_client_auth_resume(void) #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(OPENSSL_EXTRA) && \ defined(HAVE_EXT_CACHE) && !defined(NO_SESSION_CACHE) && \ - !defined(NO_CERTS) && !defined(NO_RSA) + !defined(NO_CERTS) && !defined(NO_RSA) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL; @@ -3494,7 +3500,8 @@ int test_tls12_resume_ticket_client_auth(void) EXPECT_DECLS; #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ - !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL; @@ -3548,7 +3555,8 @@ int test_tls12_resume_ticket_client_auth_ok(void) #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \ !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_CERTS) && \ - !defined(NO_RSA) + !defined(NO_RSA) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL; @@ -3633,7 +3641,9 @@ int test_tls12_reuse_clears_use_ticket(void) ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); if (ssl_s != NULL) { ExpectIntEQ(ssl_s->options.useTicket, 1); +#ifndef WOLFSSL_NO_CLIENT_AUTH ExpectIntEQ(ssl_s->session->peerAuthOk, 1); +#endif } /* A different statement on the client, and unread there. */ if (ssl_c != NULL) diff --git a/wolfssl/internal.h b/wolfssl/internal.h index a787aa8ef40..ea1cef9a041 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2314,6 +2314,9 @@ WOLFSSL_LOCAL int CheckCookieState(WOLFSSL* ssl); #endif #if !defined(NO_CERTS) && !defined(WOLFSSL_NO_CLIENT_AUTH) WOLFSSL_LOCAL int ClientAuthRequired(const WOLFSSL* ssl); +#else +/* No client certificate can be asked for, so none can be required. */ +#define ClientAuthRequired(ssl) 0 #endif WOLFSSL_LOCAL int DoClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 helloSz); From adfc3cb0031f580d90ed10fcfcf15c15fa54b286 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 30 Sep 2026 15:53:30 -0700 Subject: [PATCH 8/9] Fix conditions for DH options members in added test. --- tests/api/test_tls.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 7ea5ac2e14e..29f12b1a67b 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3148,7 +3148,9 @@ int test_tls12_clear_resets_options(void) ssl_s->options.shutdownDone = fresh->options.shutdownDone; /* Reset only under WOLFSSL_ASYNC_CRYPT. */ ssl_s->options.asyncState = fresh->options.asyncState; -#ifndef NO_DH + /* Same condition the fields are declared under. */ +#if !defined(NO_DH) && !defined(WOLFSSL_OLD_PRIME_CHECK) && \ + !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) ssl_s->options.dhDoKeyTest = fresh->options.dhDoKeyTest; ssl_s->options.dhKeyTested = fresh->options.dhKeyTested; #endif From 6db895a6a6ffee560fec70f2c970ad3bd99a485a Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 2 Oct 2026 12:17:29 -0700 Subject: [PATCH 9/9] Suppress clang-tidy for Options comparison in added test. --- tests/api/test_tls.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 29f12b1a67b..b20ab6c15ff 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3154,6 +3154,7 @@ int test_tls12_clear_resets_options(void) ssl_s->options.dhDoKeyTest = fresh->options.dhDoKeyTest; ssl_s->options.dhKeyTested = fresh->options.dhKeyTested; #endif + /* // NOLINTNEXTLINE(bugprone-suspicious-memory-comparison) */ ExpectIntEQ(XMEMCMP(&ssl_s->options, &fresh->options, sizeof(Options)), 0); if (EXPECT_FAIL()) {