From 9d77c217f2b716582fce6666458b161172aa4956 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 30 Apr 2026 22:28:40 +0200 Subject: [PATCH 1/6] Stop suppressing OCSP_CERT_REVOKED in server stapling path Server-side OCSP stapling was unconditionally folding OCSP_CERT_REVOKED, OCSP_CERT_UNKNOWN, and OCSP_LOOKUP_FAIL into a success result so a stapling failure would not break the handshake. OCSP_CERT_REVOKED, however, is an explicit positive assertion of revocation by the responder and must not be ignored: silently suppressing it lets a server keep advertising a revoked certificate to clients that rely on stapling for revocation status. Drop OCSP_CERT_REVOKED from the suppression list in CreateOcspResponse, the CSR2_OCSP_MULTI handler in SendCertificateStatus, and ProcessChainOCSPRequest. Continue suppressing OCSP_CERT_UNKNOWN and OCSP_LOOKUP_FAIL, which are true soft-fail responder conditions where the responder cannot answer. F-1820 --- src/internal.c | 21 ++++++++++++--------- src/tls.c | 7 ++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/internal.c b/src/internal.c index f19d8fa10d..63d81033db 100644 --- a/src/internal.c +++ b/src/internal.c @@ -25409,9 +25409,9 @@ int CreateOcspResponse(WOLFSSL* ssl, OcspRequest** ocspRequest, ret = CheckOcspRequest(SSL_CM(ssl)->ocsp_stapling, request, response, ssl->heap); - /* Suppressing, not critical */ - if (ret == WC_NO_ERR_TRACE(OCSP_CERT_REVOKED) || - ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || + /* Suppressing soft-fail responder errors. OCSP_CERT_REVOKED is an + * explicit positive assertion of revocation and must not be ignored. */ + if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { ret = 0; } @@ -26268,9 +26268,11 @@ int SendCertificateStatus(WOLFSSL* ssl) ret = CheckOcspRequest(SSL_CM(ssl)->ocsp_stapling, request, &responses[i + 1], ssl->heap); - /* Suppressing, not critical */ - if (ret == WC_NO_ERR_TRACE(OCSP_CERT_REVOKED) || - ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || + /* Suppressing soft-fail responder errors. + * OCSP_CERT_REVOKED is an explicit positive + * assertion of revocation and must not be + * ignored. */ + if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { ret = 0; } @@ -26298,9 +26300,10 @@ int SendCertificateStatus(WOLFSSL* ssl) ret = CheckOcspRequest(SSL_CM(ssl)->ocsp_stapling, request, &responses[++i], ssl->heap); - /* Suppressing, not critical */ - if (ret == WC_NO_ERR_TRACE(OCSP_CERT_REVOKED) || - ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || + /* Suppressing soft-fail responder errors. + * OCSP_CERT_REVOKED is an explicit positive assertion of + * revocation and must not be ignored. */ + if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { ret = 0; } diff --git a/src/tls.c b/src/tls.c index e727f655d3..ae3d4c576e 100644 --- a/src/tls.c +++ b/src/tls.c @@ -3666,9 +3666,10 @@ int ProcessChainOCSPRequest(WOLFSSL* ssl) request->ssl = ssl; ret = CheckOcspRequest(SSL_CM(ssl)->ocsp_stapling, request, &csr->responses[i], ssl->heap); - /* Suppressing, not critical */ - if (ret == WC_NO_ERR_TRACE(OCSP_CERT_REVOKED) || - ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || + /* Suppressing soft-fail responder errors. OCSP_CERT_REVOKED + * is an explicit positive assertion of revocation and must + * not be ignored. */ + if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { ret = 0; } From 425d3e9628c1876abaf8883c1a42d350c0b86d56 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 30 Apr 2026 22:30:01 +0200 Subject: [PATCH 2/6] Make DoClientTicketCheckVersion DTLS-aware DTLS minor versions decrease as the protocol version increases (DTLS 1.0=0xFF, DTLS 1.2=0xFD, DTLS 1.3=0xFC), but the ticket version comparisons in DoClientTicketCheckVersion used the TLS direction unconditionally. As a result a DTLS server resuming a session ticket from a different DTLS version could land on the wrong branch: a ticket from a newer DTLS version would be treated as a downgrade instead of being rejected, and a ticket from an older DTLS version would be flagged as 'greater version' and refused outright. The minDowngrade check at the bottom had the same inversion bug. Branch on ssl->options.dtls so the greater-version, lesser-version, and minDowngrade comparisons all use the right direction for the active protocol family. TLS behavior is unchanged. F-1828 --- src/internal.c | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/internal.c b/src/internal.c index 63d81033db..64ff5c2b07 100644 --- a/src/internal.c +++ b/src/internal.c @@ -39952,11 +39952,27 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) static int DoClientTicketCheckVersion(const WOLFSSL* ssl, InternalTicket* it) { - if (ssl->version.minor < it->pv.minor) { + /* DTLS minor versions decrease as the protocol version increases + * (DTLS 1.0=0xFF, DTLS 1.2=0xFD, DTLS 1.3=0xFC), so the version + * comparisons are inverted relative to TLS. */ + byte greaterVersion; + byte lesserVersion; + byte belowMinDowngrade; + + if (ssl->options.dtls) { + greaterVersion = ssl->version.minor > it->pv.minor; + lesserVersion = ssl->version.minor < it->pv.minor; + } + else { + greaterVersion = ssl->version.minor < it->pv.minor; + lesserVersion = ssl->version.minor > it->pv.minor; + } + + if (greaterVersion) { WOLFSSL_MSG("Ticket has greater version"); return VERSION_ERROR; } - else if (ssl->version.minor > it->pv.minor) { + else if (lesserVersion) { if (IsAtLeastTLSv1_3(it->pv) != IsAtLeastTLSv1_3(ssl->version)) { WOLFSSL_MSG("Tickets cannot be shared between " "TLS 1.3 and TLS 1.2 and lower"); @@ -39970,7 +39986,12 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) WOLFSSL_MSG("Downgrading protocol due to ticket"); - if (it->pv.minor < ssl->options.minDowngrade) { + if (ssl->options.dtls) + belowMinDowngrade = it->pv.minor > ssl->options.minDowngrade; + else + belowMinDowngrade = it->pv.minor < ssl->options.minDowngrade; + + if (belowMinDowngrade) { WOLFSSL_MSG("Ticket has lesser version than allowed"); return VERSION_ERROR; } From 130f683d8ca6401857fde3c0388c38496f6d91a0 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 30 Apr 2026 22:31:18 +0200 Subject: [PATCH 3/6] Validate minDowngrade in wolfSSL_SetSession before reusing version When resuming a session wolfSSL_SetSession unconditionally overwrote ssl->version with the version stored in the cached session, even if that version was below the WOLFSSL's configured minDowngrade. The overwritten version then fed straight into SendClientHello, so a client configured to require TLS 1.2 or higher could still emit a ClientHello advertising e.g. TLS 1.0 when resuming an old cached session. The ServerHello path catches the actual downgrade, but the ClientHello version is already a protocol-conformance issue and can confuse middleboxes. Reject the session if its stored minor version is below ssl->options.minDowngrade. The check is DTLS-aware: DTLS minor versions decrease as the protocol version increases, so the direction of the comparison is flipped for DTLS. F-2105 --- src/ssl_sess.c | 17 +++++++++++++++++ tests/api/test_dtls.c | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_dtls.h | 4 +++- tests/api/test_tls.c | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 2 ++ 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/ssl_sess.c b/src/ssl_sess.c index 1a6a6b5f35..647851ac17 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -1610,6 +1610,23 @@ int wolfSSL_SetSession(WOLFSSL* ssl, WOLFSSL_SESSION* session) ssl->options.haveEMS = (ssl->session->haveEMS) ? 1 : 0; if (ssl->session->version.major != 0) { + /* Reject sessions whose protocol version is below the configured + * minimum so a stale cached session cannot make the client send a + * ClientHello advertising a version it isn't allowed to negotiate. + * DTLS minor versions are inverted: a higher minor means an older + * protocol, so the comparison flips. */ + byte belowMinDowngrade; + if (ssl->options.dtls) + belowMinDowngrade = ssl->session->version.minor > + ssl->options.minDowngrade; + else + belowMinDowngrade = ssl->session->version.minor < + ssl->options.minDowngrade; + if (belowMinDowngrade) { + WOLFSSL_MSG("Session version below configured minDowngrade"); + ssl->options.resuming = 0; + return WOLFSSL_FAILURE; + } ssl->version = ssl->session->version; if (IsAtLeastTLSv1_3(ssl->version)) ssl->options.tls1_3 = 1; diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index f3c65fc0d4..7453357ba2 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -3129,3 +3129,45 @@ int test_dtls13_oversized_cert_chain(void) #endif return EXPECT_RESULT(); } + +/* DTLS counterpart to test_tls_set_session_min_downgrade. Exercises the + * inverted DTLS minor-version comparison (DTLS 1.2 minor 0xFD is "below" + * floor 0xFC = DTLS 1.3). */ +int test_dtls_set_session_min_downgrade(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_DTLS) && \ + defined(WOLFSSL_DTLS13) && defined(HAVE_SESSION_TICKET) + 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; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfDTLSv1_2_client_method, wolfDTLSv1_2_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + wolfSSL_CTX_free(ctx_c); ctx_c = NULL; + wolfSSL_CTX_free(ctx_s); ctx_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfDTLS_client_method, wolfDTLS_server_method), 0); + ExpectIntEQ(wolfSSL_SetMinVersion(ssl_c, WOLFSSL_DTLSV1_3), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_FAILURE); + if (ssl_c != NULL) + ExpectIntEQ(ssl_c->options.resuming, 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_dtls.h b/tests/api/test_dtls.h index 062033f572..ee399fbe98 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -56,6 +56,7 @@ int test_dtls_mtu_split_messages(void); int test_dtls13_min_rtx_interval(void); int test_dtls13_no_session_id_echo(void); int test_dtls13_oversized_cert_chain(void); +int test_dtls_set_session_min_downgrade(void); #define TEST_DTLS_DECLS \ TEST_DECL_GROUP("dtls", test_dtls12_basic_connection_id), \ @@ -91,5 +92,6 @@ int test_dtls13_oversized_cert_chain(void); TEST_DECL_GROUP("dtls", test_dtls_memio_wolfio_stateless), \ TEST_DECL_GROUP("dtls", test_dtls13_min_rtx_interval), \ TEST_DECL_GROUP("dtls", test_dtls13_no_session_id_echo), \ - TEST_DECL_GROUP("dtls", test_dtls13_oversized_cert_chain) + TEST_DECL_GROUP("dtls", test_dtls13_oversized_cert_chain), \ + TEST_DECL_GROUP("dtls", test_dtls_set_session_min_downgrade) #endif /* TESTS_API_DTLS_H */ diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index aedae4f703..2de15544b0 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -861,6 +861,48 @@ int test_tls12_etm_failed_resumption(void) return EXPECT_RESULT(); } +/* wolfSSL_set_session() must reject a TLS 1.2 session when minDowngrade is + * set to TLS 1.3. */ +int test_tls_set_session_min_downgrade(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_SESSION_TICKET) + 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; + + 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(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + wolfSSL_CTX_free(ctx_c); ctx_c = NULL; + wolfSSL_CTX_free(ctx_s); ctx_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLS_client_method, wolfTLS_server_method), 0); + ExpectIntEQ(wolfSSL_SetMinVersion(ssl_c, WOLFSSL_TLSV1_3), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_FAILURE); + if (ssl_c != NULL) + ExpectIntEQ(ssl_c->options.resuming, 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + int test_tls_set_curves_list_ecc_fallback(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index c0f74f2150..3ca34689e3 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -32,6 +32,7 @@ int test_tls_certreq_order(void); int test_tls12_bad_cv_sig_alg(void); int test_tls12_no_null_compression(void); int test_tls12_etm_failed_resumption(void); +int test_tls_set_session_min_downgrade(void); int test_tls_set_curves_list_ecc_fallback(void); int test_tls12_corrupted_finished(void); int test_tls12_peerauth_failsafe(void); @@ -47,6 +48,7 @@ int test_tls12_peerauth_failsafe(void); TEST_DECL_GROUP("tls", test_tls12_bad_cv_sig_alg), \ TEST_DECL_GROUP("tls", test_tls12_no_null_compression), \ TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ + TEST_DECL_GROUP("tls", test_tls_set_session_min_downgrade), \ TEST_DECL_GROUP("tls", test_tls_set_curves_list_ecc_fallback), \ TEST_DECL_GROUP("tls", test_tls12_corrupted_finished), \ TEST_DECL_GROUP("tls", test_tls12_peerauth_failsafe) From 3234f66cf3f84b28304c5c7969d8af5963e8864b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 30 Apr 2026 22:37:37 +0200 Subject: [PATCH 4/6] Test TLS 1.3 NewSessionTicket MAX_LIFETIME bound check DoTls13NewSessionTicket rejects a ticket lifetime greater than MAX_LIFETIME (RFC 8446 Section 4.6.1, 7 days), but no test exercised the rejection: every server in the suite stays well within the limit, so a mutation deleting that bound check would go unnoticed. Add a manual memio test that pokes ctx_s->ticketHint to MAX_LIFETIME + 1 (the public setter clamps to 604800), runs a full TLS 1.3 handshake, and reads the post-handshake NewSessionTicket on the client. The test confirms the over-limit lifetime surfaces from wolfSSL_read as SERVER_HINT_ERROR. F-2121 --- tests/api/test_tls13.c | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 2 ++ 2 files changed, 44 insertions(+) diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 4d9abc71c0..59e84e76f7 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -5288,6 +5288,48 @@ int test_tls13_short_session_ticket(void) } +/* RFC 8446 Section 4.6.1: a NewSessionTicket lifetime greater than + * MAX_LIFETIME (604800 seconds, 7 days) must be rejected. The public + * wolfSSL_CTX_set_TicketHint setter clamps the value, so write the + * out-of-range hint directly into the server CTX to force the server to + * encode an over-limit lifetime onto the wire and confirm the client's + * DoTls13NewSessionTicket bound check fires. */ +int test_tls13_new_session_ticket_max_lifetime(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + char buf[64]; + + 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); + + /* Bypass the public-API clamp at 604800. */ + if (EXPECT_SUCCESS()) { + ctx_s->ticketHint = MAX_LIFETIME + 1; + } + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Reading the post-handshake NewSessionTicket should surface the + * over-limit lifetime as SERVER_HINT_ERROR. */ + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(SERVER_HINT_ERROR)); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + /* Test that a corrupted TLS 1.3 Finished verify_data is properly rejected * with VERIFY_FINISHED_ERROR. We run the handshake step-by-step and corrupt * the server's client_write_MAC_secret before it processes the client's diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index ae48d6ebbf..cd201c3673 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -49,6 +49,7 @@ int test_tls13_pqc_hybrid_truncated_keyshare(void); int test_tls13_pqc_hybrid_malformed_ecdh(void); int test_tls13_empty_record_limit(void); int test_tls13_short_session_ticket(void); +int test_tls13_new_session_ticket_max_lifetime(void); int test_tls13_early_data_0rtt_replay(void); int test_tls13_0rtt_default_off(void); int test_tls13_0rtt_stateless_replay(void); @@ -99,6 +100,7 @@ int test_tls13_cipher_fuzz_aes128_ccm_8_sha256(void); TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_malformed_ecdh), \ TEST_DECL_GROUP("tls13", test_tls13_empty_record_limit), \ TEST_DECL_GROUP("tls13", test_tls13_short_session_ticket), \ + TEST_DECL_GROUP("tls13", test_tls13_new_session_ticket_max_lifetime), \ TEST_DECL_GROUP("tls13", test_tls13_early_data_0rtt_replay), \ TEST_DECL_GROUP("tls13", test_tls13_0rtt_default_off), \ TEST_DECL_GROUP("tls13", test_tls13_0rtt_stateless_replay), \ From ed4f4ce8260709ced750a6bcff95d51b9bc8bed4 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 30 Apr 2026 22:38:50 +0200 Subject: [PATCH 5/6] Document SNI per-host policy gap in wolfSSL_set_SSL_CTX wolfSSL_set_SSL_CTX is the OpenSSL-compatible entry point that an SNI callback uses to swap in the per-vhost certificate during the handshake. By design it only copies the certificate chain and private key from the new CTX. Verification settings, the trusted CA store, CRL/OCSP configuration, minimum key-size requirements, and cipher/version policy stay attached to the original CTX. For multi-tenant servers where each virtual host has its own security policy, that means one host's verification rules silently apply to a connection meant for another. Expand the leading comment with an explicit SECURITY WARNING that lists the settings which are NOT inherited and points at the WOLFSSL*-level setters callers must use inside the SNI callback when virtual hosts have different policies. The behavior of the function is unchanged. F-2902 --- src/ssl.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ssl.c b/src/ssl.c index 9b694806e1..a54c0782d6 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -15871,6 +15871,12 @@ WOLFSSL_CTX* wolfSSL_set_SSL_CTX(WOLFSSL* ssl, WOLFSSL_CTX* ctx) * - changing the server certificate(s) * - changing the server id for session handling * and everything else in WOLFSSL* needs to remain untouched. + * + * SECURITY: swapping ssl->ctx switches cm-resolved settings (CA store, + * CRL, OCSP) to the new CTX but leaves ssl-cached ones (verify mode and + * callback, minDowngrade, key-size minimums, suites, version bounds) + * pinned to the original. SNI callbacks must re-apply those ssl-level + * settings explicitly; CRL/OCSP isolation requires an SSL-local store. */ WOLFSSL_ENTER("wolfSSL_set_SSL_CTX"); if (ssl == NULL || ctx == NULL) From fd91f681e59c571a724ec00036fb54a273fc4c7f Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 30 Apr 2026 22:39:55 +0200 Subject: [PATCH 6/6] Fail closed in CheckOcspRequest when ocspCheckAll and no URL CheckOcspRequest used to return CERT_GOOD whenever a certificate lacked an AIA extension and no override URL was configured, with the rationale 'Cert has no OCSP URL, assuming CERT_GOOD'. That is a fail-open soft-fail: an operator who turned on WOLFSSL_OCSP_CHECKALL expecting every certificate in the chain to be revocation-checked would still silently accept a certificate that omits its OCSP responder URL, letting a misconfigured (or attacker-controlled) issuer bypass revocation for non-stapled flows. Gate the fail-open path on cm->ocspCheckAll. When the caller has asked for full-chain OCSP checking, return OCSP_NEED_URL so the chain is refused. The legacy behavior is preserved when ocspCheckAll is not set, keeping the soft-fail default for plain WOLFSSL_OCSP_ENABLE users. F-3227 --- src/ocsp.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ocsp.c b/src/ocsp.c index ded2bbd7ae..b90fcc8af9 100644 --- a/src/ocsp.c +++ b/src/ocsp.c @@ -544,7 +544,14 @@ int CheckOcspRequest(WOLFSSL_OCSP* ocsp, OcspRequest* ocspRequest, urlSz = ocspRequest->urlSz; } else { - /* cert doesn't have extAuthInfo, assuming CERT_GOOD */ + /* No AIA URL and no override. ocspCheckAll asks for strict chain + * checking, so fail closed - but only on the client verification + * instance (cm->ocsp); stapling (cm->ocsp_stapling) shares the cm + * flag and must stay best-effort. */ + if (ocsp->cm->ocspCheckAll && ocsp == ocsp->cm->ocsp) { + WOLFSSL_MSG("Cert has no OCSP URL and ocspCheckAll is set"); + return OCSP_NEED_URL; + } WOLFSSL_MSG("Cert has no OCSP URL, assuming CERT_GOOD"); return 0; }