From ae0bac50c99df4ddec26cb5add9f8c3040ec36c7 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 16 Jul 2026 17:42:31 -0700 Subject: [PATCH] OCSP: opt-in fail-closed on missing responder WOLFSSL_OCSP_CHECKALL is a scope flag ("always leaf, but all ?"), selecting which certs are checked, not how hard to fail when one advertises no OCSP responder in its AIA. Treating a missing responder as a hard failure breaks any chain whose CA publishes no OCSP URI (e.g. Google GTS WR2), a configuration that only grows as CAs drop responders. Strictness belongs in its own opt-in flag. - Keep CHECKALL scope-only; a cert with no responder soft-fails by default. - Return distinct OCSP_NO_URL for "no responder", separate from a failed lookup; stapling response handling stays best-effort (mapped to CERT_GOOD). - Add WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED (via OcspNoUrlPolicy) to refuse such certs; independent of CHECKALL. - Add test_ocsp_no_url_policy covering both halves. --- src/internal.c | 30 +++++++++++++++---- src/ocsp.c | 38 +++++++++++++++++------- src/ssl_certman.c | 14 +++++++-- src/tls.c | 26 ++++++++++++++--- tests/api.c | 1 + tests/api/test_ocsp.c | 68 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_ocsp.h | 1 + wolfssl/error-ssl.h | 5 +++- wolfssl/internal.h | 2 ++ wolfssl/ocsp.h | 1 + wolfssl/ssl.h | 6 ++++ 11 files changed, 169 insertions(+), 23 deletions(-) diff --git a/src/internal.c b/src/internal.c index 0ea1b04f2b7..2b4b33832aa 100644 --- a/src/internal.c +++ b/src/internal.c @@ -16499,6 +16499,8 @@ static int ProcessPeerCertLeafRevocation(WOLFSSL* ssl, ProcPeerCertArgs* args, return 1; } #endif + if (ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) + ret = OcspNoUrlPolicy(SSL_CM(ssl)); doLookup = (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN)); if (ret != 0) { WOLFSSL_MSG("\tOCSP Lookup not ok"); @@ -17485,6 +17487,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, goto exit_ppc; } #endif + if (ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) + ret = OcspNoUrlPolicy(SSL_CM(ssl)); if (ret != 0) { WOLFSSL_ERROR_VERBOSE(ret); WOLFSSL_MSG("\tOCSP Lookup not ok"); @@ -26232,9 +26236,12 @@ int CreateOcspResponse(WOLFSSL* ssl, OcspRequest** ocspRequest, ssl->heap); /* Suppressing soft-fail responder errors. OCSP_CERT_REVOKED is an - * explicit positive assertion of revocation and must not be ignored. */ + * explicit positive assertion of revocation and must not be ignored. + * OCSP_NO_URL just means there is no responder to staple from; + * stapling stays best-effort. */ if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || - ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { + ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL) || + ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) { ret = 0; } } @@ -27221,9 +27228,12 @@ int SendCertificateStatus(WOLFSSL* ssl) /* Suppressing soft-fail responder errors. * OCSP_CERT_REVOKED is an explicit positive * assertion of revocation and must not be - * ignored. */ + * ignored. OCSP_NO_URL just means there is no + * responder to staple from; stapling stays + * best-effort. */ if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || - ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { + ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL) || + ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) { ret = 0; } @@ -27252,9 +27262,12 @@ int SendCertificateStatus(WOLFSSL* ssl) /* Suppressing soft-fail responder errors. * OCSP_CERT_REVOKED is an explicit positive assertion of - * revocation and must not be ignored. */ + * revocation and must not be ignored. OCSP_NO_URL just + * means there is no responder to staple from; stapling + * stays best-effort. */ if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || - ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { + ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL) || + ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) { ret = 0; } } @@ -28690,6 +28703,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e) case OCSP_NEED_URL: return "OCSP need URL"; + case OCSP_NO_URL: + return "Cert has no OCSP responder URL"; + case OCSP_CERT_UNKNOWN: return "OCSP Cert unknown"; @@ -41452,6 +41468,8 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) * Run OCSP if the CertManager has it enabled. */ if (ret == 0 && SSL_CM(ssl)->ocspEnabled) { ret = CheckCertOCSP_ex(SSL_CM(ssl)->ocsp, dCert, ssl); + if (ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) + ret = OcspNoUrlPolicy(SSL_CM(ssl)); } #endif #ifdef HAVE_CRL diff --git a/src/ocsp.c b/src/ocsp.c index 4048e7628ea..15b9b3443b5 100644 --- a/src/ocsp.c +++ b/src/ocsp.c @@ -535,16 +535,12 @@ int CheckOcspRequest(WOLFSSL_OCSP* ocsp, OcspRequest* ocspRequest, urlSz = ocspRequest->urlSz; } else { - /* 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; + /* Cert advertises no OCSP responder and no override URL is set, so + * OCSP has no opinion on this cert. Report that distinctly from a + * failed lookup; the caller owns the policy decision. Callers wanting + * the historical soft-fail run this through OcspNoUrlPolicy(). */ + WOLFSSL_MSG("Cert has no OCSP URL"); + return OCSP_NO_URL; } request = (byte*)XMALLOC((size_t)requestSz, ocsp->cm->heap, DYNAMIC_TYPE_OCSP); @@ -581,6 +577,28 @@ int CheckOcspRequest(WOLFSSL_OCSP* ocsp, OcspRequest* ocspRequest, return ret; } +/* Map OCSP_NO_URL - "this cert advertises no OCSP responder" - to a + * verification result. + * + * WOLFSSL_OCSP_CHECKALL selects which certificates get checked, not how hard + * to fail when one cannot be checked, so it deliberately has no say here. + * Refuse the cert only when the user explicitly asked for that with + * WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED. + * + * Returns OCSP_NEED_URL to refuse the cert, or 0 for the historical + * soft-fail. */ +int OcspNoUrlPolicy(WOLFSSL_CERT_MANAGER* cm) +{ + if (cm != NULL && cm->ocspFailIfNotSupported) { + WOLFSSL_MSG("Cert has no OCSP URL and OCSP is required for every cert"); + WOLFSSL_ERROR_VERBOSE(OCSP_NEED_URL); + return OCSP_NEED_URL; + } + + WOLFSSL_MSG("Cert has no OCSP URL, assuming CERT_GOOD"); + return 0; +} + /* Enforce https://www.rfc-editor.org/rfc/rfc6960#section-4.2.2.2. Both halves * of CertID (issuerNameHash and issuerKeyHash) must match; name-only matching * would authorize a same-DN / different-key CA. issuerKeyHash may be NULL when diff --git a/src/ssl_certman.c b/src/ssl_certman.c index 094051d2908..d712e3b4592 100644 --- a/src/ssl_certman.c +++ b/src/ssl_certman.c @@ -2137,7 +2137,8 @@ int wolfSSL_CertManagerLoadCRLFile(WOLFSSL_CERT_MANAGER* cm, const char* file, * @param [in] cm Certificate manager. * @param [in] options Options for using OCSP. Valid flags: * WOLFSSL_OCSP_URL_OVERRIDE, WOLFSSL_OCSP_NO_NONCE, - * WOLFSSL_OCSP_CHECKALL. + * WOLFSSL_OCSP_CHECKALL, + * WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED. * @return WOLFSSL_SUCCESS on success. * @return 0 when initializing the OCSP object fails. * @return BAD_FUNC_ARG when cm is NULL. @@ -2201,6 +2202,10 @@ int wolfSSL_CertManagerEnableOCSP(WOLFSSL_CERT_MANAGER* cm, int options) if (options & WOLFSSL_OCSP_CHECKALL) { cm->ocspCheckAll = 1; } + /* Fail closed on certs that advertise no OCSP responder if requested. */ + if (options & WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED) { + cm->ocspFailIfNotSupported = 1; + } #ifndef WOLFSSL_USER_IO /* Set built-in OCSP lookup. */ cm->ocspIOCb = EmbedOcspLookup; @@ -2446,7 +2451,12 @@ int wolfSSL_CertManagerCheckOCSP(WOLFSSL_CERT_MANAGER* cm, } /* Do OCSP checks with decoded certificate. */ else if ((ret = CheckCertOCSP(cm->ocsp, cert)) != 0) { - WOLFSSL_MSG("CheckCertOCSP failed"); + /* Apply the caller's policy for a cert that advertises no + * responder rather than reporting a lookup failure. */ + if (ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) + ret = OcspNoUrlPolicy(cm); + if (ret != 0) + WOLFSSL_MSG("CheckCertOCSP failed"); } /* Dispose of dynamically allocated memory. */ diff --git a/src/tls.c b/src/tls.c index 69c2e0a710b..85fc4ea73c6 100644 --- a/src/tls.c +++ b/src/tls.c @@ -3730,9 +3730,11 @@ int ProcessChainOCSPRequest(WOLFSSL* ssl) request, &csr->responses[i], ssl->heap); /* Suppressing soft-fail responder errors. OCSP_CERT_REVOKED * is an explicit positive assertion of revocation and must - * not be ignored. */ + * not be ignored. OCSP_NO_URL just means there is no + * responder to staple from; stapling stays best-effort. */ if (ret == WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN) || - ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)) { + ret == WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL) || + ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) { ret = 0; } i++; @@ -4007,9 +4009,17 @@ int TLSX_CSR_ForceRequest(WOLFSSL* ssl) switch (csr->status_type) { case WOLFSSL_CSR_OCSP: if (SSL_CM(ssl)->ocspEnabled) { + int ret; csr->request.ocsp[0].ssl = ssl; - return CheckOcspRequest(SSL_CM(ssl)->ocsp, + ret = CheckOcspRequest(SSL_CM(ssl)->ocsp, &csr->request.ocsp[0], NULL, NULL); + /* This is the client's fallback leaf lookup on the + * verification instance, so honor the no-responder policy + * just like the non-stapling leaf path. Default stays + * best-effort; FAIL_IF_NOT_SUPPORTED makes it fail closed. */ + if (ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) + ret = OcspNoUrlPolicy(SSL_CM(ssl)); + return ret; } else { WOLFSSL_ERROR_VERBOSE(OCSP_LOOKUP_FAIL); @@ -4542,9 +4552,17 @@ int TLSX_CSR2_ForceRequest(WOLFSSL* ssl) case WOLFSSL_CSR2_OCSP_MULTI: if (SSL_CM(ssl)->ocspEnabled && csr2->requests >= 1) { + int ret; csr2->request.ocsp[csr2->requests-1].ssl = ssl; - return CheckOcspRequest(SSL_CM(ssl)->ocsp, + ret = CheckOcspRequest(SSL_CM(ssl)->ocsp, &csr2->request.ocsp[csr2->requests-1], NULL, NULL); + /* This is the client's fallback leaf lookup on the + * verification instance, so honor the no-responder policy + * just like the non-stapling leaf path. Default stays + * best-effort; FAIL_IF_NOT_SUPPORTED makes it fail closed. */ + if (ret == WC_NO_ERR_TRACE(OCSP_NO_URL)) + ret = OcspNoUrlPolicy(SSL_CM(ssl)); + return ret; } else { WOLFSSL_ERROR_VERBOSE(OCSP_LOOKUP_FAIL); diff --git a/tests/api.c b/tests/api.c index f37573e6d5f..47b4f2c6181 100644 --- a/tests/api.c +++ b/tests/api.c @@ -37734,6 +37734,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_ocsp_status_request_v2_multi_revoked_single), TEST_DECL(test_ocsp_cert_unknown_crl_fallback), TEST_DECL(test_ocsp_cert_unknown_crl_fallback_nonleaf), + TEST_DECL(test_ocsp_no_url_policy), TEST_DECL(test_tls13_nonblock_ocsp_low_mfl), TEST_DECL(test_ocsp_responder), TEST_DECL(test_wolfIO_DecodeUrl_crlf_reject), diff --git a/tests/api/test_ocsp.c b/tests/api/test_ocsp.c index 37d405563c1..dca2e1c5a8a 100644 --- a/tests/api/test_ocsp.c +++ b/tests/api/test_ocsp.c @@ -1685,6 +1685,74 @@ int test_ocsp_cert_unknown_crl_fallback_nonleaf(void) } #endif /* HAVE_OCSP && HAVE_CRL && HAVE_SSL_MEMIO_TESTS_DEPENDENCIES */ +#if defined(HAVE_OCSP) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) + +/* The default memio chain (certs/server-cert.pem under certs/ca-cert.pem) + * carries no AIA extension, so no cert in it advertises an OCSP responder + * and no override URL is configured. */ + +static int test_ocsp_no_url_checkall_ctx_ready(WOLFSSL_CTX* ctx) +{ + EXPECT_DECLS; + + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL); + /* CHECKALL selects scope only, so it must not fail a chain that + * advertises no responder. */ + ExpectIntEQ(wolfSSL_CTX_EnableOCSP(ctx, WOLFSSL_OCSP_CHECKALL), + WOLFSSL_SUCCESS); + + return EXPECT_RESULT(); +} + +static int test_ocsp_no_url_fail_closed_ctx_ready(WOLFSSL_CTX* ctx) +{ + EXPECT_DECLS; + + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL); + ExpectIntEQ(wolfSSL_CTX_EnableOCSP(ctx, WOLFSSL_OCSP_CHECKALL | + WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED), + WOLFSSL_SUCCESS); + + return EXPECT_RESULT(); +} + +int test_ocsp_no_url_policy(void) +{ + EXPECT_DECLS; + struct test_ssl_memio_ctx test_ctx; + + /* WOLFSSL_OCSP_CHECKALL on its own soft-fails a cert that advertises no + * responder, as it has since the flag was introduced. Regression test: + * failing closed here breaks any chain whose CA publishes no OCSP URI, + * which is common on the public web. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_cb.ctx_ready = test_ocsp_no_url_checkall_ctx_ready; + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + test_ssl_memio_cleanup(&test_ctx); + + /* Opting in with WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED refuses the same + * chain with OCSP_NEED_URL. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_cb.ctx_ready = test_ocsp_no_url_fail_closed_ctx_ready; + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(OCSP_NEED_URL)); + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +#else +int test_ocsp_no_url_policy(void) +{ + return TEST_SKIPPED; +} +#endif /* HAVE_OCSP && HAVE_SSL_MEMIO_TESTS_DEPENDENCIES */ + #if defined(HAVE_OCSP) && defined(WOLFSSL_TLS13) && \ defined(WOLFSSL_NONBLOCK_OCSP) && defined(HAVE_MAX_FRAGMENT) && \ defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && \ diff --git a/tests/api/test_ocsp.h b/tests/api/test_ocsp.h index 361d5a712ff..23b26551d15 100644 --- a/tests/api/test_ocsp.h +++ b/tests/api/test_ocsp.h @@ -33,6 +33,7 @@ int test_ocsp_tls_cert_cb(void); int test_ocsp_status_request_v2_multi_revoked_single(void); int test_ocsp_cert_unknown_crl_fallback(void); int test_ocsp_cert_unknown_crl_fallback_nonleaf(void); +int test_ocsp_no_url_policy(void); int test_tls13_nonblock_ocsp_low_mfl(void); int test_ocsp_responder(void); int test_ocsp_ancestor_responder_rejected(void); diff --git a/wolfssl/error-ssl.h b/wolfssl/error-ssl.h index 2783f00813b..c1c9aa7d35a 100644 --- a/wolfssl/error-ssl.h +++ b/wolfssl/error-ssl.h @@ -250,7 +250,10 @@ enum wolfSSL_ErrorCodes { RPK_UNTRUSTED_E = -521, /* RFC 7250 Raw Public Key not trusted * out of band */ - WOLFSSL_LAST_E = -521 + OCSP_NO_URL = -522, /* Cert advertises no OCSP responder + * and no override URL is set */ + + WOLFSSL_LAST_E = -522 /* codes -1000 to -1999 are reserved for wolfCrypt. */ }; diff --git a/wolfssl/internal.h b/wolfssl/internal.h index d54496e0f9c..e8fe90a1954 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2668,6 +2668,8 @@ struct WOLFSSL_CERT_MANAGER { byte crlCheckAll:1; /* always leaf, but all ? */ byte ocspEnabled:1; /* is OCSP on ? */ byte ocspCheckAll:1; /* always leaf, but all ? */ + byte ocspFailIfNotSupported:1; /* refuse a cert that advertises + * no OCSP responder ? */ byte ocspSendNonce:1; /* send the OCSP nonce ? */ byte ocspUseOverrideURL:1; /* ignore cert responder, override */ byte ocspStaplingEnabled:1; /* is OCSP Stapling on ? */ diff --git a/wolfssl/ocsp.h b/wolfssl/ocsp.h index b0ec5045bf7..054eb7103a7 100644 --- a/wolfssl/ocsp.h +++ b/wolfssl/ocsp.h @@ -69,6 +69,7 @@ WOLFSSL_LOCAL int CheckCertOCSP_ex(WOLFSSL_OCSP* ocsp, DecodedCert* cert, WOLFSSL_LOCAL int CheckOcspRequest(WOLFSSL_OCSP* ocsp, OcspRequest* ocspRequest, WOLFSSL_BUFFER_INFO* responseBuffer, void* heap); +WOLFSSL_LOCAL int OcspNoUrlPolicy(WOLFSSL_CERT_MANAGER* cm); WOLFSSL_LOCAL int CheckOcspResponse(WOLFSSL_OCSP *ocsp, byte *response, int responseSz, WOLFSSL_BUFFER_INFO *responseBuffer, CertStatus *status, OcspEntry *entry, OcspRequest *ocspRequest, diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index b5a0fb4a07a..b85b99e6bd9 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -2948,7 +2948,13 @@ enum { enum { WOLFSSL_OCSP_URL_OVERRIDE = 1, WOLFSSL_OCSP_NO_NONCE = 2, + /* Check every cert in the chain, not just the leaf. Selects scope only; + * see WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED for failure policy. */ WOLFSSL_OCSP_CHECKALL = 4, + /* Refuse a cert that advertises no OCSP responder, instead of the default + * soft-fail. Independent of WOLFSSL_OCSP_CHECKALL, which decides which + * certs are checked rather than how hard to fail. */ + WOLFSSL_OCSP_FAIL_IF_NOT_SUPPORTED = 8, WOLFSSL_CRL_CHECKALL = 1, WOLFSSL_CRL_CHECK = 2