diff --git a/src/internal.c b/src/internal.c index 6dc0cbe2d1f..5b6a7223da0 100644 --- a/src/internal.c +++ b/src/internal.c @@ -25169,9 +25169,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; } @@ -26028,9 +26028,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; } @@ -26058,9 +26060,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; } @@ -39618,11 +39621,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"); @@ -39636,7 +39655,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; } diff --git a/src/ocsp.c b/src/ocsp.c index 8c779c62476..f7e75ecb232 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; } diff --git a/src/ssl.c b/src/ssl.c index e5ca3ca9c26..2002f1061be 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -15754,6 +15754,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) diff --git a/src/ssl_sess.c b/src/ssl_sess.c index ec790575057..6559b24d8a7 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -1606,6 +1606,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/src/tls.c b/src/tls.c index 7f7e0b0d1bf..aff86f2c638 100644 --- a/src/tls.c +++ b/src/tls.c @@ -3658,9 +3658,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; } diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index c6e8f7cc286..9fcca98f491 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -2909,3 +2909,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 3a1e083a60e..e10ff55ad10 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -54,6 +54,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), \ @@ -87,5 +88,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 aedae4f7031..2de15544b0b 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 c0f74f21505..3ca34689e3e 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) diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index ba12b7e3689..9ac35d943ee 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -4815,6 +4815,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 c8b42fc56c6..cf6f6177c76 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -47,6 +47,7 @@ int test_tls13_derive_keys_no_key(void); int test_tls13_pqc_hybrid_truncated_keyshare(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_corrupted_finished(void); int test_tls13_peerauth_failsafe(void); @@ -84,6 +85,7 @@ int test_tls13_cert_with_extern_psk_sh_confirms_resumption(void); TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_truncated_keyshare), \ 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_unknown_ext_rejected), \ TEST_DECL_GROUP("tls13", test_tls13_corrupted_finished), \