From 9dcc3ae9a650d7b02331daed9933bbdfb346ef91 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 3 Sep 2026 17:33:47 +0000 Subject: [PATCH 1/9] Add WOLFSSL_CHAIN_VERIFY_CB to replace peer chain verification Adds an opt-in callback that takes over the peer certificate trust decision. wolfSSL builds no chain, verifies no signature and checks no date, revocation status, key usage or host name; the verify callback is not called either. The callback gets only the DER certificates from the Certificate message as WOLFSSL_BUFFER_INFO entries, certs[0] first. It is consulted even under WOLFSSL_VERIFY_NONE, and its rejection fails the handshake either way. wolfSSL still decodes every certificate with ParseCert(NO_VERIFY) before the callback runs, so malformed DER fails the handshake and the callback is never handed it. Content wolfSSL does not understand - an unknown critical extension, an unsupported key or signature algorithm, a signature algorithm mismatch - is not a decoding failure and is left for the callback to judge. The check runs once, not again on re-entry after a deferred verdict. The callback may return CHAIN_VERIFY_WANT_E to suspend the handshake and be asked again with the same certificates when the application re-enters wolfSSL_connect(), wolfSSL_accept(), wolfSSL_read() or wolfSSL_write(). A certificate received after the handshake (TLS 1.3 post-handshake authentication) arrives inside wolfSSL_read() and is resumed by reading again: ReceiveData() re-enters on any handshake-suspend error, and the ticket-sent accept state leaves a suspended message alone. Rejection fails with CHAIN_VERIFY_CB_E and one bad_certificate alert, and sets peerVerifyRet so wolfSSL_get_verify_result() does not report X509_V_OK. Still applied: an empty Certificate message keeps the existing mutual-auth policy (the callback is not called for it), and the minimum peer key sizes still gate the peer's own key, which the handshake uses directly. DTLS, raw public keys and OCSP stapling are not supported with the callback. wolfSSL_CTX_SetChainVerifyCb() and wolfSSL_SetChainVerifyCb() refuse with CHAIN_VERIFY_UNSUPPORTED_E when the context or object is already configured for one of them, and a connection that uses one of them anyway fails with the same error and an internal_error alert before the callback is called. The DTLS paths are otherwise untouched. The callback and its user context can be set on the context or on the SSL object, the object's taking precedence. The feature is part of --enable-all, has a CMake option and Doxygen entries. The suspend machinery is shared with WOLFSSL_NONBLOCK_OCSP and WOLFSSL_ASYNC_CRYPT. The open-coded pairs of comparisons against WC_PENDING_E and OCSP_WANT_READ are replaced by IsHsSuspendErr(), and the matching build guards by WOLFSSL_HAVE_HS_SUSPEND. That also closes three holes which affected WOLFSSL_NONBLOCK_OCSP builds: - DoHandShakeMsg() freed ssl->pendingMsg on a non-WC_PENDING_E suspend while ProcPeerCertArgs still pointed into it. - wolfSSL_connect()/wolfSSL_accept() and their TLS 1.3 counterparts called FreeAsyncCtx() when buffered output flushed, discarding the saved certificate state. - ProcessPeerCerts() XFREE'd args in WOLFSSL_SMALL_STACK builds without WOLFSSL_ASYNC_CRYPT, where args points inside ssl->async. --- .github/configs/os-check-linux.json | 9 + CMakeLists.txt | 9 + cmake/options.h.in | 3 + configure.ac | 11 + doc/dox_comments/header_files/ssl.h | 150 +++++++ examples/configs/user_settings_all.h | 1 + src/internal.c | 463 ++++++++++++++++++---- src/ssl_api_cert.c | 114 ++++++ src/ssl_api_hs.c | 16 +- src/tls13.c | 55 +-- tests/api.c | 10 + tests/api/test_tls.c | 570 +++++++++++++++++++++++++++ tests/api/test_tls.h | 22 +- tests/api/test_tls13.c | 87 ++++ tests/api/test_tls13.h | 4 +- tests/utils.c | 10 + wolfssl/error-ssl.h | 9 +- wolfssl/internal.h | 31 ++ wolfssl/ssl.h | 63 +++ wolfssl/wolfcrypt/settings.h | 3 +- 20 files changed, 1533 insertions(+), 107 deletions(-) diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index 4432aa4e697..c8102748171 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -81,6 +81,15 @@ {"name": "dtls13-ocspstapling-cert-cb", "minutes": 3.1, "configure": ["--enable-dtls", "--enable-dtls13", "--enable-ocspstapling", "--enable-ocspstapling2", "--enable-cert-setup-cb", "--enable-sessioncerts"]}, +{"name": "chain-verify-cb", "minutes": 3.1, + "comment": "postauth, stapling and rpk exercise the callback's resume-from-read and fail-hard paths.", + "configure": ["--enable-chain-verify-cb", "--enable-opensslextra", + "--enable-sessioncerts", "--enable-smallstack", "--enable-postauth", + "--enable-ocspstapling", "--enable-rpk"]}, +{"name": "chain-verify-cb-nonblock-ocsp", "minutes": 3.1, + "comment": "chain verify callback alongside the other handshake-suspend feature, and DTLS.", + "configure": ["--enable-chain-verify-cb", "--enable-ocsp", "--enable-crl", + "--enable-dtls", "CPPFLAGS=-DWOLFSSL_NONBLOCK_OCSP"]}, {"name": "tsp-verifier", "minutes": 3, "comment": "Time-Stamp Protocol Verifier", "configure": ["--enable-tsp", "--enable-opensslall", diff --git a/CMakeLists.txt b/CMakeLists.txt index a35e365e017..114db300b8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1654,6 +1654,11 @@ add_option("WOLFSSL_CERTGENCACHE" "Enable decoded cert caching (default: disabled)" "no" "yes;no") +# Chain verify callback +add_option("WOLFSSL_CHAIN_VERIFY_CB" + "Enable replacing peer certificate verification with a user callback (default: disabled)" + "no" "yes;no") + # HKDF add_option("WOLFSSL_HKDF" "Enable HKDF (HMAC-KDF) support (default: disabled)" @@ -3118,6 +3123,10 @@ if(WOLFSSL_CERTGENCACHE) list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_CERT_GEN_CACHE") endif() +if(WOLFSSL_CHAIN_VERIFY_CB) + list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_CHAIN_VERIFY_CB") +endif() + if(WOLFSSL_CRYPTOCB) list(APPEND WOLFSSL_DEFINITIONS "-DWOLF_CRYPTO_CB") endif() diff --git a/cmake/options.h.in b/cmake/options.h.in index 78f4ce3c55b..1cf1f27ca91 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -775,6 +775,9 @@ extern "C" { #cmakedefine WOLFSSL_ALWAYS_VERIFY_CB #undef WOLFSSL_CERT_SETUP_CB #cmakedefine WOLFSSL_CERT_SETUP_CB + +#undef WOLFSSL_CHAIN_VERIFY_CB +#cmakedefine WOLFSSL_CHAIN_VERIFY_CB #undef WOLFSSL_CIPHER_INTERNALNAME #cmakedefine WOLFSSL_CIPHER_INTERNALNAME #undef WOLFSSL_DER_LOAD diff --git a/configure.ac b/configure.ac index ea998c287ea..4d17483d880 100644 --- a/configure.ac +++ b/configure.ac @@ -1413,6 +1413,7 @@ then test "$enable_savesession" = "" && enable_savesession=yes test "$enable_savecert" = "" && enable_savecert=yes test "$enable_postauth" = "" && enable_postauth=yes + test "$enable_chain_verify_cb" = "" && enable_chain_verify_cb=yes test "$enable_hrrcookie" = "" && enable_hrrcookie=yes test "$enable_crl_monitor" = "" && enable_crl_monitor=yes test "$enable_sni" = "" && enable_sni=yes @@ -12206,6 +12207,13 @@ AC_ARG_ENABLE([cert-setup-cb], [ ENABLED_CERT_SETUP_CB=no ] ) +# Replaces peer certificate chain verification with an application callback +AC_ARG_ENABLE([chain-verify-cb], + [AS_HELP_STRING([--enable-chain-verify-cb],[Enable replacing peer certificate verification with a user callback (default: disabled)])], + [ ENABLED_CHAIN_VERIFY_CB=$enableval ], + [ ENABLED_CHAIN_VERIFY_CB=no ] + ) + # check if should run the trusted peer certs test # (for now checking both C_FLAGS and C_EXTRA_FLAGS) AS_CASE(["$CFLAGS $CPPFLAGS"],[*'WOLFSSL_TRUST_PEER_CERT'*],[ENABLED_TRUSTED_PEER_CERT=yes]) @@ -12715,6 +12723,9 @@ AS_IF([test "x$ENABLED_RPK" = "xyes"], AS_IF([test "x$ENABLED_CERT_SETUP_CB" = "xyes"], [AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_CERT_SETUP_CB"]) +AS_IF([test "x$ENABLED_CHAIN_VERIFY_CB" = "xyes"], + [AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_CHAIN_VERIFY_CB"]) + AS_IF([test "x$ENABLED_ALTNAMES" = "xyes"], [AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_ALT_NAMES"]) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index a58c6b4f202..a16727f6bc4 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3187,6 +3187,156 @@ void wolfSSL_SetCertCbCtx(WOLFSSL* ssl, void* ctx); */ void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); +/*! + \ingroup CertsKeys + + \brief Replaces wolfSSL's verification of the peer's certificate chain + with an application callback, for every SSL/TLS object created from the + context. When a callback is set, wolfSSL decodes the certificates from the + Certificate message and hands them to the callback as raw DER, the peer's + own certificate first. It builds no chain, verifies no signature and + checks no date, revocation status, key usage or host name, and the verify + callback set with wolfSSL_CTX_set_verify() is not called; the callback is + consulted even under WOLFSSL_VERIFY_NONE. Malformed DER still fails the + handshake before the callback is called. The callback returns 0 to accept, + CHAIN_VERIFY_WANT_E to suspend the handshake until the application + re-enters wolfSSL_connect(), wolfSSL_accept(), wolfSSL_read() or + wolfSSL_write(), or any other value to reject with CHAIN_VERIFY_CB_E and a + fatal bad_certificate alert. DTLS, raw public keys and OCSP stapling are + not supported with the callback: setting it on a context configured for + one of them fails, and so does the handshake of a connection using one + of them, with CHAIN_VERIFY_UNSUPPORTED_E. Requires + WOLFSSL_CHAIN_VERIFY_CB (--enable-chain-verify-cb). + + \return WOLFSSL_SUCCESS on success. + \return BAD_FUNC_ARG when ctx is NULL. + \return CHAIN_VERIFY_UNSUPPORTED_E when the context uses a DTLS method, + raw public keys or OCSP stapling. + + \param ctx pointer to the SSL context, created with wolfSSL_CTX_new(). + \param cb the callback, or NULL to clear it. + + _Example_ + \code + static int myChainVerify(WOLFSSL* ssl, const WOLFSSL_BUFFER_INFO* certs, + int certsSz, void* ctx) + { + // certs[0] is the peer's certificate, the rest is the chain it sent + if (!hsmVerifyStarted(certs, certsSz)) + return CHAIN_VERIFY_WANT_E; // ask again later + return hsmVerifyPassed() ? 0 : -1; + } + ... + WOLFSSL_CTX* ctx = wolfSSL_CTX_new(method); + if (wolfSSL_CTX_SetChainVerifyCb(ctx, myChainVerify) != WOLFSSL_SUCCESS) { + // context uses DTLS, raw public keys or OCSP stapling + } + \endcode + + \sa wolfSSL_SetChainVerifyCb + \sa wolfSSL_CTX_SetChainVerifyCtx + \sa wolfSSL_SetChainVerifyCtx + \sa wolfSSL_GetChainVerifyCtx +*/ +int wolfSSL_CTX_SetChainVerifyCb(WOLFSSL_CTX* ctx, ChainVerifyCb cb); + +/*! + \ingroup CertsKeys + + \brief Sets the chain verification callback for one SSL/TLS object. It + takes precedence over the callback set on the context with + wolfSSL_CTX_SetChainVerifyCb(), which documents the callback's contract. + + \return WOLFSSL_SUCCESS on success. + \return BAD_FUNC_ARG when ssl is NULL. + \return CHAIN_VERIFY_UNSUPPORTED_E when the object uses DTLS, raw public + keys or OCSP stapling. + + \param ssl pointer to the SSL session, created with wolfSSL_new(). + \param cb the callback, or NULL to fall back to the context's. + + _Example_ + \code + WOLFSSL* ssl = wolfSSL_new(ctx); + if (wolfSSL_SetChainVerifyCb(ssl, myChainVerify) != WOLFSSL_SUCCESS) { + // object uses DTLS, raw public keys or OCSP stapling + } + \endcode + + \sa wolfSSL_CTX_SetChainVerifyCb + \sa wolfSSL_SetChainVerifyCtx +*/ +int wolfSSL_SetChainVerifyCb(WOLFSSL* ssl, ChainVerifyCb cb); + +/*! + \ingroup CertsKeys + + \brief Sets the user context passed to the chain verification callback + for every SSL/TLS object created from the context. + + \return none No return. + + \param ctx pointer to the SSL context, created with wolfSSL_CTX_new(). + \param userCtx the value the callback receives as its ctx argument. + + _Example_ + \code + WOLFSSL_CTX* ctx = wolfSSL_CTX_new(method); + wolfSSL_CTX_SetChainVerifyCb(ctx, myChainVerify); + wolfSSL_CTX_SetChainVerifyCtx(ctx, &myHsm); + \endcode + + \sa wolfSSL_CTX_SetChainVerifyCb + \sa wolfSSL_SetChainVerifyCtx + \sa wolfSSL_GetChainVerifyCtx +*/ +void wolfSSL_CTX_SetChainVerifyCtx(WOLFSSL_CTX* ctx, void* userCtx); + +/*! + \ingroup CertsKeys + + \brief Sets the user context passed to the chain verification callback + for one SSL/TLS object. It takes precedence over the value set on the + context with wolfSSL_CTX_SetChainVerifyCtx(). + + \return none No return. + + \param ssl pointer to the SSL session, created with wolfSSL_new(). + \param ctx the value the callback receives as its ctx argument, or NULL + to fall back to the context's. + + _Example_ + \code + WOLFSSL* ssl = wolfSSL_new(ctx); + wolfSSL_SetChainVerifyCtx(ssl, &myConnectionState); + \endcode + + \sa wolfSSL_CTX_SetChainVerifyCtx + \sa wolfSSL_GetChainVerifyCtx +*/ +void wolfSSL_SetChainVerifyCtx(WOLFSSL* ssl, void* ctx); + +/*! + \ingroup CertsKeys + + \brief Returns the user context the chain verification callback is + called with for this SSL/TLS object: the value set with + wolfSSL_SetChainVerifyCtx() when there is one, otherwise the context's. + + \return void* the user context, or NULL when none is set or ssl is NULL. + + \param ssl pointer to the SSL session, created with wolfSSL_new(). + + _Example_ + \code + struct myState* st = (struct myState*)wolfSSL_GetChainVerifyCtx(ssl); + \endcode + + \sa wolfSSL_SetChainVerifyCtx + \sa wolfSSL_CTX_SetChainVerifyCtx +*/ +void* wolfSSL_GetChainVerifyCtx(const WOLFSSL* ssl); + /*! \ingroup IO diff --git a/examples/configs/user_settings_all.h b/examples/configs/user_settings_all.h index a0574f80882..0c040571175 100644 --- a/examples/configs/user_settings_all.h +++ b/examples/configs/user_settings_all.h @@ -126,6 +126,7 @@ extern "C" { #define HAVE_OID_ENCODING #define WOLFSSL_ASN_TEMPLATE #define WOLFSSL_ALT_NAMES /* Support subject alternative names extension */ +#define WOLFSSL_CHAIN_VERIFY_CB /* Replace peer chain verification with an application callback */ /* Certificate Revocation */ #define HAVE_OCSP diff --git a/src/internal.c b/src/internal.c index c78193cd183..f472d4ad3f6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -645,6 +645,24 @@ int IsTLS_ex(const ProtocolVersion pv) } +int IsHsSuspendErr(int err) +{ +#ifdef WOLFSSL_ASYNC_CRYPT + if (err == WC_NO_ERR_TRACE(WC_PENDING_E)) + return 1; +#endif +#ifdef WOLFSSL_NONBLOCK_OCSP + if (err == WC_NO_ERR_TRACE(OCSP_WANT_READ)) + return 1; +#endif +#ifdef WOLFSSL_CHAIN_VERIFY_CB + if (err == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) + return 1; +#endif + (void)err; + return 0; +} + int IsAtLeastTLSv1_2(const WOLFSSL* ssl) { if (ssl->version.major == SSLv3_MAJOR && ssl->version.minor >=TLSv1_2_MINOR) @@ -15985,6 +16003,12 @@ void DoCertFatalAlert(WOLFSSL* ssl, int ret) alertWhy = unsupported_certificate; } #endif /* HAVE_RPK */ +#ifdef WOLFSSL_CHAIN_VERIFY_CB + else if (ret == WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)) { + /* a local configuration problem, not the peer's certificate */ + alertWhy = internal_error; + } +#endif else if (ret == WC_NO_ERR_TRACE(NO_PEER_CERT)) { #ifdef WOLFSSL_TLS13 if (ssl->options.tls1_3) { @@ -17899,11 +17923,231 @@ static int RpkIsTrusted(WOLFSSL* ssl, const byte* spki, word32 spkiSz) } #endif /* HAVE_RPK */ +#ifdef WOLFSSL_CHAIN_VERIFY_CB +/* The SSL object's callback and user context take precedence over the + * context's. */ +static ChainVerifyCb GetChainVerifyCb(const WOLFSSL* ssl) +{ + if (ssl->chainVerifyCb != NULL) + return ssl->chainVerifyCb; + return ssl->ctx->chainVerifyCb; +} + +static void* GetChainVerifyCtx(const WOLFSSL* ssl) +{ + if (ssl->chainVerifyCtx != NULL) + return ssl->chainVerifyCtx; + return ssl->ctx->chainVerifyCtx; +} + +/* Non-zero when the application has replaced chain verification. */ +#define UsingChainVerifyCb(ssl) (GetChainVerifyCb(ssl) != NULL) + +/* DTLS, raw public keys and OCSP stapling are not supported with the chain + * verify callback. Checked when the callback is set, against what the context + * or object is configured for, and again when the peer's certificates arrive, + * against what was negotiated, so that using them fails as early as + * possible. */ +static int ChainVerifyCbStaplingRequested(TLSX* extensions) +{ +#if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) + if (TLSX_Find(extensions, TLSX_STATUS_REQUEST) != NULL) + return 1; +#endif +#if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) + if (TLSX_Find(extensions, TLSX_STATUS_REQUEST_V2) != NULL) + return 1; +#endif + (void)extensions; + return 0; +} + +#ifdef HAVE_RPK +static int ChainVerifyCbRpkConfigured(const RpkConfig* cfg) +{ + int i; + + for (i = 0; i < cfg->preferred_ClientCertTypeCnt; i++) { + if (cfg->preferred_ClientCertTypes[i] == WOLFSSL_CERT_TYPE_RPK) + return 1; + } + for (i = 0; i < cfg->preferred_ServerCertTypeCnt; i++) { + if (cfg->preferred_ServerCertTypes[i] == WOLFSSL_CERT_TYPE_RPK) + return 1; + } + return 0; +} +#endif + +int ChainVerifyCbCheckCtx(const WOLFSSL_CTX* ctx) +{ +#ifdef WOLFSSL_DTLS + if (ctx->method->version.major == DTLS_MAJOR) { + WOLFSSL_MSG("DTLS not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#ifdef HAVE_RPK + if (ChainVerifyCbRpkConfigured(&ctx->rpkConfig)) { + WOLFSSL_MSG("Raw public key not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) + if ((ctx->method->side != WOLFSSL_SERVER_END) && + (((ctx->cm != NULL) && ctx->cm->ocspMustStaple) || + ChainVerifyCbStaplingRequested(ctx->extensions))) { + WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif + (void)ctx; + return 0; +} + +int ChainVerifyCbCheckSsl(const WOLFSSL* ssl) +{ +#ifdef WOLFSSL_DTLS + if (ssl->options.dtls) { + WOLFSSL_MSG("DTLS not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#ifdef HAVE_RPK + if (ChainVerifyCbRpkConfigured(&ssl->options.rpkConfig) || + ((ssl->options.side == WOLFSSL_CLIENT_END) && + (ssl->options.rpkState.received_ServerCertTypeCnt == 1) && + (ssl->options.rpkState.received_ServerCertTypes[0] == + WOLFSSL_CERT_TYPE_RPK)) || + ((ssl->options.side == WOLFSSL_SERVER_END) && + (ssl->options.rpkState.sending_ClientCertTypeCnt == 1) && + (ssl->options.rpkState.sending_ClientCertTypes[0] == + WOLFSSL_CERT_TYPE_RPK))) { + WOLFSSL_MSG("Raw public key not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) + if ((ssl->options.side != WOLFSSL_SERVER_END) && + (SSL_CM(ssl)->ocspMustStaple || + ChainVerifyCbStaplingRequested(ssl->extensions) || + ChainVerifyCbStaplingRequested(ssl->ctx->extensions))) { + WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif + (void)ssl; + return 0; +} + +/* Errors from ParseCert(NO_VERIFY) that report content wolfSSL does not + * understand rather than malformed DER: an unknown critical extension, an + * unsupported key or signature algorithm, or mismatched signature algorithm + * identifiers. The certificate decoded; judging its content is the chain + * verify callback's job. */ +static int IsCertContentErr(int err) +{ + return (err == WC_NO_ERR_TRACE(ASN_CRIT_EXT_E)) || + (err == WC_NO_ERR_TRACE(ASN_SIG_OID_E)) || + (err == WC_NO_ERR_TRACE(ASN_UNKNOWN_OID_E)); +} + +/* Decode every certificate the peer sent, verifying nothing. The chain verify + * callback replaces wolfSSL's trust decision, not its parsing, so DER that + * wolfSSL cannot decode fails the handshake before the callback is consulted. + * NO_VERIFY skips the signature, issuer and date checks but still walks the + * whole structure. */ +static int CheckPeerCertsDecode(WOLFSSL* ssl, ProcPeerCertArgs* args) +{ + WC_DECLARE_VAR(cert, DecodedCert, 1, ssl->heap); + int ret = 0; + int i; + + WC_ALLOC_VAR_EX(cert, DecodedCert, 1, ssl->heap, DYNAMIC_TYPE_DCERT, + return MEMORY_E); + + for (i = 0; i < args->totalCerts; i++) { + InitDecodedCert(cert, args->certs[i].buffer, args->certs[i].length, + ssl->heap); + ret = ParseCert(cert, CERT_TYPE, NO_VERIFY, NULL); + if (IsCertContentErr(ret)) + ret = 0; + #ifdef HAVE_RPK + if ((ret == 0) && cert->isRPK) { + WOLFSSL_MSG("Raw public key not supported with chain verify " + "callback"); + ret = CHAIN_VERIFY_UNSUPPORTED_E; + } + #endif + FreeDecodedCert(cert); + + if (ret != 0) { + WOLFSSL_MSG_EX("Peer certificate %d failed to decode", i); + WOLFSSL_ERROR_VERBOSE(ret); + break; + } + } + + WC_FREE_VAR_EX(cert, ssl->heap, DYNAMIC_TYPE_DCERT); + + return ret; +} + +/* Hand the peer's DER certificates to the application, which has taken over + * chain verification completely. Called once per Certificate message, and + * again on every re-entry while the callback defers its verdict. + * + * Returns 0 when accepted, CHAIN_VERIFY_WANT_E to suspend the handshake, + * CHAIN_VERIFY_CB_E when rejected, or MEMORY_E. */ +static int DoChainVerifyCb(WOLFSSL* ssl, ProcPeerCertArgs* args) +{ + WOLFSSL_BUFFER_INFO* certs = NULL; + int ret; + int i; + + if (args->totalCerts > 0) { + certs = (WOLFSSL_BUFFER_INFO*)XMALLOC( + sizeof(WOLFSSL_BUFFER_INFO) * (size_t)args->totalCerts, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (certs == NULL) + return MEMORY_E; + + for (i = 0; i < args->totalCerts; i++) { + certs[i].buffer = args->certs[i].buffer; + certs[i].length = args->certs[i].length; + } + } + + WOLFSSL_MSG("Calling user chain verify callback"); + ret = GetChainVerifyCb(ssl)(ssl, certs, args->totalCerts, + GetChainVerifyCtx(ssl)); + + XFREE(certs, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + + if (ret == 0) { + WOLFSSL_MSG("\tuser chain verify callback accepted"); + return 0; + } + if (ret == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + WOLFSSL_MSG("\tuser chain verify callback has no verdict yet"); + return CHAIN_VERIFY_WANT_E; + } + + WOLFSSL_MSG("\tuser chain verify callback rejected"); + WOLFSSL_ERROR_VERBOSE(CHAIN_VERIFY_CB_E); + return CHAIN_VERIFY_CB_E; +} +#else +#define UsingChainVerifyCb(ssl) 0 +#endif /* WOLFSSL_CHAIN_VERIFY_CB */ + int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, word32 totalSz) { int ret = 0; -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) +#ifdef WOLFSSL_HAVE_HS_SUSPEND ProcPeerCertArgs* args = NULL; WOLFSSL_ASSERT_SIZEOF_GE(ssl->async->args, *args); #elif defined(WOLFSSL_SMALL_STACK) @@ -17921,7 +18165,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif WOLFSSL_ENTER("ProcessPeerCerts"); -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) +#ifdef WOLFSSL_HAVE_HS_SUSPEND if (ssl->async == NULL) { ssl->async = (struct WOLFSSL_ASYNC*) XMALLOC(sizeof(struct WOLFSSL_ASYNC), ssl->heap, @@ -17961,13 +18205,25 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } else #endif /* WOLFSSL_NONBLOCK_OCSP */ +#ifdef WOLFSSL_CHAIN_VERIFY_CB + if (ssl->error == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + /* Re-entry after the chain verify callback deferred its verdict. Keep + * the saved state so the same certificates are handed back to it. */ + #ifdef WOLFSSL_ASYNC_CRYPT + /* if async operations not pending, reset error code */ + if (ret == WC_NO_ERR_TRACE(WC_NO_PENDING_E)) + ret = 0; + #endif + } + else +#endif /* WOLFSSL_CHAIN_VERIFY_CB */ #elif defined(WOLFSSL_SMALL_STACK) args = (ProcPeerCertArgs*)XMALLOC( sizeof(ProcPeerCertArgs), ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); if (args == NULL) { ERROR_OUT(MEMORY_E, exit_ppc); } -#endif /* WOLFSSL_ASYNC_CRYPT || WOLFSSL_NONBLOCK_OCSP */ +#endif /* WOLFSSL_HAVE_HS_SUSPEND */ { /* Reset state */ ret = 0; @@ -17975,7 +18231,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, XMEMSET(args, 0, sizeof(ProcPeerCertArgs)); args->idx = *inOutIdx; args->begin = *inOutIdx; - #if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) + #ifdef WOLFSSL_HAVE_HS_SUSPEND ssl->async->freeArgs = FreeProcPeerCertArgs; #endif } @@ -18177,6 +18433,47 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, case TLS_ASYNC_BUILD: { + #ifdef WOLFSSL_CHAIN_VERIFY_CB + /* The application verifies the chain instead of wolfSSL. No chain + * is built, so no intermediate is added to the CM either, and + * everything wolfSSL would check about the certificates is + * skipped below; only the parsing the handshake needs is kept. + * Only consult the callback if the Certificate message itself + * parsed cleanly - an error here (an empty message, or a chain + * past MAX_CHAIN_DEPTH) means it would be handed an incomplete + * list. That error is kept and reported by the check below. */ + if (UsingChainVerifyCb(ssl)) { + /* Check once, not again on every re-entry. */ + if (ret == 0 && args->count > 0 && !args->chainDecoded) { + ret = ChainVerifyCbCheckSsl(ssl); + if (ret == 0) + ret = CheckPeerCertsDecode(ssl, args); + if (ret != 0) { + args->fatal = 1; + DoCertFatalAlert(ssl, ret); + goto exit_ppc; + } + args->chainDecoded = 1; + } + if (ret == 0 && args->count > 0) { + ret = DoChainVerifyCb(ssl, args); + if (ret == WC_NO_ERR_TRACE(CHAIN_VERIFY_CB_E)) { + args->fatal = 1; + #if defined(OPENSSL_EXTRA) || \ + defined(OPENSSL_EXTRA_X509_SMALL) + if (ssl->peerVerifyRet == 0) { + ssl->peerVerifyRet = + WOLFSSL_X509_V_ERR_CERT_REJECTED; + } + #endif + DoCertFatalAlert(ssl, ret); + } + if (ret != 0) + goto exit_ppc; + } + } + else + #endif /* WOLFSSL_CHAIN_VERIFY_CB */ if (args->count > 0) { /* check for trusted peer and get untrustedDepth */ @@ -18624,8 +18921,11 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* select peer cert (first one) */ args->certIdx = 0; + /* With a chain verify callback the leaf is only parsed, never + * verified - the parse is needed for the peer's public key. */ ret = ProcessPeerCertParse(ssl, args, CERT_TYPE, - !ssl->options.verifyNone ? VERIFY : NO_VERIFY, + (!ssl->options.verifyNone && !UsingChainVerifyCb(ssl)) ? + VERIFY : NO_VERIFY, &subjectHash, &alreadySigner); #if defined(OPENSSL_ALL) && defined(WOLFSSL_CERT_GEN) && \ (defined(WOLFSSL_CERT_REQ) || defined(WOLFSSL_CERT_EXT)) && \ @@ -18643,7 +18943,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, args->dCertInit = 0; /* once again */ ret = ProcessPeerCertParse(ssl, args, CERT_TYPE, - !ssl->options.verifyNone ? VERIFY : NO_VERIFY, + (!ssl->options.verifyNone && + !UsingChainVerifyCb(ssl)) ? VERIFY : NO_VERIFY, &subjectHash, &alreadySigner); } else { @@ -18671,7 +18972,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, * pinned out of band (expected RPK). Applies to both peers * - client checking the server's RPK and server checking * the client's RPK. */ - if (args->dCert->isRPK) { + if (args->dCert->isRPK && !UsingChainVerifyCb(ssl)) { int rpkTrusted = RpkIsTrusted(ssl, args->certs[args->certIdx].buffer, args->certs[args->certIdx].length); @@ -18737,7 +19038,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, * different version has been negotiated using RFC 7250. * OpenSSL doesn't appear to be performing this check. * For TLS 1.3 see RFC8446 Section 4.4.2.3 */ - if (ssl->options.side == WOLFSSL_SERVER_END) { + if (ssl->options.side == WOLFSSL_SERVER_END && + !UsingChainVerifyCb(ssl)) { #if defined(HAVE_RPK) if (args->dCert->isRPK) { /* RPK certs carry no X.509 version; the RPK trust @@ -18821,15 +19123,28 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif /* defined(__APPLE__) && defined(WOLFSSL_SYS_CA_CERTS) */ - /* Do verify callback. */ - args->leafVerifyErr = ret = - DoVerifyCallback(SSL_CM(ssl), ssl, ret, args); - - if (ret != 0) { + #ifdef WOLFSSL_CHAIN_VERIFY_CB + /* The chain verify callback already accepted these + * certificates, so anything left here is a decode failure + * and there is no verify callback to override it. */ + if (UsingChainVerifyCb(ssl)) { WOLFSSL_MSG("\tfatal cert error"); args->fatal = 1; DoCertFatalAlert(ssl, ret); } + else + #endif + { + /* Do verify callback. */ + args->leafVerifyErr = ret = + DoVerifyCallback(SSL_CM(ssl), ssl, ret, args); + + if (ret != 0) { + WOLFSSL_MSG("\tfatal cert error"); + args->fatal = 1; + DoCertFatalAlert(ssl, ret); + } + } } #ifdef HAVE_SECURE_RENEGOTIATION @@ -18893,7 +19208,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #if defined(HAVE_OCSP) || defined(HAVE_CRL) /* only attempt to check OCSP or CRL if not previous error such * as ASN_BEFORE_DATE_E or ASN_AFTER_DATE_E */ - if (args->fatal == 0 && ret == 0) { + if (args->fatal == 0 && ret == 0 && !UsingChainVerifyCb(ssl)) { if (ProcessPeerCertLeafRevocation(ssl, args, &ret)) goto exit_ppc; } @@ -18929,7 +19244,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } else #endif - if (args->dCert->extKeyUsageSet) { + if (args->dCert->extKeyUsageSet && !UsingChainVerifyCb(ssl)) { if ((ssl->specs.kea == rsa_kea) && (ssl->options.side == WOLFSSL_CLIENT_END) && (args->dCert->extKeyUsage & KEYUSE_KEY_ENCIPHER) == 0) { @@ -18962,7 +19277,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } else #endif - if (args->dCert->extExtKeyUsageSet) { + if (args->dCert->extExtKeyUsageSet && !UsingChainVerifyCb(ssl)) { if (ssl->options.side == WOLFSSL_CLIENT_END) { if ((args->dCert->extExtKeyUsage & (EXTKEYUSE_ANY | EXTKEYUSE_SERVER_AUTH)) == 0) { @@ -19027,7 +19342,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif - if (!ssl->options.verifyNone && domainName) { + if (!ssl->options.verifyNone && !UsingChainVerifyCb(ssl) && + domainName) { #ifndef WOLFSSL_ALLOW_NO_CN_IN_SAN /* Per RFC 5280 section 4.2.1.6, "Whenever such identities * are to be bound into a certificate, the subject @@ -19085,7 +19401,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #ifndef OPENSSL_EXTRA - if (!ssl->options.verifyNone && ssl->buffers.ipasc.buffer) { + if (!ssl->options.verifyNone && !UsingChainVerifyCb(ssl) && + ssl->buffers.ipasc.buffer) { if (CheckIPAddr(args->dCert, (const char*)ssl->buffers.ipasc.buffer, (size_t)ssl->buffers.ipasc.length) != 0) { @@ -19134,8 +19451,10 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif - /* Do leaf verify callback when it wasn't called yet */ - if (ret == 0 || ret != args->leafVerifyErr) + /* Do leaf verify callback when it wasn't called yet. Skipped when + * the chain verify callback owns the verdict. */ + if (!UsingChainVerifyCb(ssl) && + (ret == 0 || ret != args->leafVerifyErr)) ret = DoVerifyCallback(SSL_CM(ssl), ssl, ret, args); if (ssl->options.verifyNone && @@ -19179,17 +19498,16 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, WOLFSSL_LEAVE("ProcessPeerCerts", ret); -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) - if (ret == WC_NO_ERR_TRACE(WC_PENDING_E) || - ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) { +#ifdef WOLFSSL_HAVE_HS_SUSPEND + if (IsHsSuspendErr(ret)) { /* Mark message as not received so it can process again */ ssl->msgsReceived.got_certificate = 0; return ret; } -#endif /* WOLFSSL_ASYNC_CRYPT || WOLFSSL_NONBLOCK_OCSP */ +#endif /* WOLFSSL_HAVE_HS_SUSPEND */ -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) +#ifdef WOLFSSL_HAVE_HS_SUSPEND /* Cleanup async */ FreeAsyncCtx(ssl, 0); #elif defined(WOLFSSL_SMALL_STACK) @@ -19199,9 +19517,11 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #else FreeProcPeerCertArgs(ssl, args); -#endif /* WOLFSSL_ASYNC_CRYPT || WOLFSSL_NONBLOCK_OCSP || WOLFSSL_SMALL_STACK */ +#endif /* WOLFSSL_HAVE_HS_SUSPEND || WOLFSSL_SMALL_STACK */ -#if !defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_SMALL_STACK) +/* args points into ssl->async when the handshake can suspend, so it is only a + * separate allocation to free otherwise. */ +#if !defined(WOLFSSL_HAVE_HS_SUSPEND) && defined(WOLFSSL_SMALL_STACK) XFREE(args, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); #endif @@ -19226,9 +19546,8 @@ static int DoCertificate(WOLFSSL* ssl, byte* input, word32* inOutIdx, #ifdef SESSION_CERTS /* Reset the session cert chain count in case the session resume failed, * do not reset if we are resuming after an async wait */ -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) - if (ssl->error != WC_NO_ERR_TRACE(OCSP_WANT_READ) && - ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E)) +#ifdef WOLFSSL_HAVE_HS_SUSPEND + if (!IsHsSuspendErr(ssl->error)) #endif { ssl->session->chain.count = 0; @@ -19241,7 +19560,11 @@ static int DoCertificate(WOLFSSL* ssl, byte* input, word32* inOutIdx, ret = ProcessPeerCerts(ssl, input, inOutIdx, size); #ifdef OPENSSL_EXTRA - ssl->options.serverState = SERVER_CERT_COMPLETE; + /* the certificate is not processed yet if it only suspended */ +#ifdef WOLFSSL_HAVE_HS_SUSPEND + if (!IsHsSuspendErr(ret)) +#endif + ssl->options.serverState = SERVER_CERT_COMPLETE; #endif WOLFSSL_LEAVE("DoCertificate", ret); @@ -20177,13 +20500,12 @@ static int SanityCheckMsgReceived(WOLFSSL* ssl, byte type) ((defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3)) || \ (defined(HAVE_ED25519) && !defined(NO_ED25519_CLIENT_AUTH)) || \ (defined(HAVE_ED448) && !defined(NO_ED448_CLIENT_AUTH))) -/* Free the cached handshake messages used for sign/verify, unless an async or - * non-blocking OCSP operation is still pending. */ +/* Free the cached handshake messages used for sign/verify, unless the + * handshake is suspended part way through a message. */ static void FreeCachedHandshakeMessages(WOLFSSL* ssl, int ret) { -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) - if (ret != WC_NO_ERR_TRACE(WC_PENDING_E) && - ret != WC_NO_ERR_TRACE(OCSP_WANT_READ)) +#ifdef WOLFSSL_HAVE_HS_SUSPEND + if (!IsHsSuspendErr(ret)) #endif { ssl->options.cacheMessages = 0; @@ -20334,11 +20656,8 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* above checks handshake state */ /* hello_request not hashed */ if (type != hello_request - #ifdef WOLFSSL_ASYNC_CRYPT - && ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E) - #endif - #ifdef WOLFSSL_NONBLOCK_OCSP - && ssl->error != WC_NO_ERR_TRACE(OCSP_WANT_READ) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + && !IsHsSuspendErr(ssl->error) #endif ) { ret = HashInput(ssl, input + *inOutIdx, (int)size); @@ -20522,10 +20841,9 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, WOLFSSL_ERROR_VERBOSE(ret); } -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) - /* if async, offset index so this msg will be processed again */ - if ((ret == WC_NO_ERR_TRACE(WC_PENDING_E) || - ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) && *inOutIdx > 0) { +#ifdef WOLFSSL_HAVE_HS_SUSPEND + /* if suspended, offset index so this msg will be processed again */ + if (IsHsSuspendErr(ret) && *inOutIdx > 0) { *inOutIdx -= HANDSHAKE_HEADER_SZ; #ifdef WOLFSSL_DTLS if (ssl->options.dtls) { @@ -20534,12 +20852,11 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif } - /* make sure async error is cleared */ - if (ret == 0 && (ssl->error == WC_NO_ERR_TRACE(WC_PENDING_E) || - ssl->error == WC_NO_ERR_TRACE(OCSP_WANT_READ))) { + /* make sure suspend error is cleared */ + if (ret == 0 && IsHsSuspendErr(ssl->error)) { ssl->error = 0; } -#endif /* WOLFSSL_ASYNC_CRYPT || WOLFSSL_NONBLOCK_OCSP */ +#endif /* WOLFSSL_HAVE_HS_SUSPEND */ #ifdef WOLFSSL_DTLS if (ret == 0) { @@ -20661,12 +20978,13 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, return ret; } - #ifdef WOLFSSL_ASYNC_CRYPT - if (ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E)) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + if (!IsHsSuspendErr(ssl->error)) #endif { - /* for async this copy was already done, do not replace, since - * contents may have been changed for inline operations */ + /* when resuming a suspended message this copy was already done, do + * not replace, since contents may have been changed for inline + * operations */ XMEMCPY(ssl->pendingMsg + ssl->pendingMsgOffset, input + *inOutIdx, inputLength); } @@ -20681,8 +20999,10 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, &idx, ssl->pendingMsgType, ssl->pendingMsgSz - idx, ssl->pendingMsgSz); - #ifdef WOLFSSL_ASYNC_CRYPT - if (ret == WC_NO_ERR_TRACE(WC_PENDING_E)) { + #ifdef WOLFSSL_HAVE_HS_SUSPEND + /* ProcPeerCertArgs keeps pointers into pendingMsg across a + * suspend, so it must outlive this call. */ + if (IsHsSuspendErr(ret)) { /* setup to process fragment again */ ssl->pendingMsgOffset -= inputLength; *inOutIdx -= inputLength; @@ -20721,6 +21041,9 @@ int SendFatalAlertOnly(WOLFSSL *ssl, int error) #endif #ifdef WOLFSSL_ASYNC_CRYPT case WC_NO_ERR_TRACE(WC_PENDING_E): +#endif +#ifdef WOLFSSL_CHAIN_VERIFY_CB + case WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E): #endif return 0; @@ -25075,11 +25398,8 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) #if defined(HAVE_SECURE_RENEGOTIATION) || defined(WOLFSSL_DTLS13) && ssl->error != WC_NO_ERR_TRACE(APP_DATA_READY) #endif - #ifdef WOLFSSL_ASYNC_CRYPT - && ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E) - #endif - #ifdef WOLFSSL_NONBLOCK_OCSP - && ssl->error != WC_NO_ERR_TRACE(OCSP_WANT_READ) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + && !IsHsSuspendErr(ssl->error) #endif && (allowSocketErr != 1 || ssl->error != WC_NO_ERR_TRACE(SOCKET_ERROR_E)) @@ -29241,8 +29561,9 @@ int ReceiveData(WOLFSSL* ssl, byte* output, size_t sz, int peek) #endif /* WOLFSSL_DTLS */ if (error != 0 && error != WC_NO_ERR_TRACE(WANT_WRITE) -#ifdef WOLFSSL_ASYNC_CRYPT - && error != WC_NO_ERR_TRACE(WC_PENDING_E) +#ifdef WOLFSSL_HAVE_HS_SUSPEND + /* a suspended handshake message is resumed by reading again */ + && !IsHsSuspendErr(error) #endif #if defined(HAVE_SECURE_RENEGOTIATION) || defined(WOLFSSL_DTLS13) && error != WC_NO_ERR_TRACE(APP_DATA_READY) @@ -30319,6 +30640,15 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e) case RPK_UNTRUSTED_E: return "RFC 7250 Raw Public Key not trusted"; + + case CHAIN_VERIFY_WANT_E: + return "Chain verify callback has no verdict yet"; + + case CHAIN_VERIFY_CB_E: + return "Chain verify callback rejected peer certificates"; + + case CHAIN_VERIFY_UNSUPPORTED_E: + return "Chain verify callback used with unsupported feature"; } return "unknown error number"; @@ -46424,13 +46754,8 @@ int wolfSSL_TestAppleNativeCertValidation_AppendCA(WOLFSSL_CTX* ctx, void wolfssl_local_MaybeCheckAlertOnErr(WOLFSSL* ssl, int err) { #if defined(WOLFSSL_CHECK_ALERT_ON_ERR) -#if defined(WOLFSSL_ASYNC_CRYPT) - if (err == WC_NO_ERR_TRACE(WC_PENDING_E)) { - return; - } -#endif -#if defined(WOLFSSL_NONBLOCK_OCSP) - if (err == WC_NO_ERR_TRACE(OCSP_WANT_READ)) { +#if defined(WOLFSSL_HAVE_HS_SUSPEND) + if (IsHsSuspendErr(err)) { return; } #endif diff --git a/src/ssl_api_cert.c b/src/ssl_api_cert.c index 9c036fac882..90bad6f15ed 100644 --- a/src/ssl_api_cert.c +++ b/src/ssl_api_cert.c @@ -639,6 +639,120 @@ void wolfSSL_CTX_set_cert_verify_callback(WOLFSSL_CTX* ctx, } #endif +#ifdef WOLFSSL_CHAIN_VERIFY_CB +/* Set the callback that replaces peer certificate chain verification for + * every SSL/TLS object created from the context. + * + * Setting a callback turns off all of wolfSSL's own checking of the peer's + * certificates. See ChainVerifyCb in ssl.h. + * + * @param [in, out] ctx SSL/TLS context object. + * @param [in] cb Chain verification callback. NULL to clear. + * @return WOLFSSL_SUCCESS on success. + * @return BAD_FUNC_ARG when ctx is NULL. + * @return CHAIN_VERIFY_UNSUPPORTED_E when the context uses DTLS, raw public + * keys or OCSP stapling. + */ +int wolfSSL_CTX_SetChainVerifyCb(WOLFSSL_CTX* ctx, ChainVerifyCb cb) +{ + int ret; + + WOLFSSL_ENTER("wolfSSL_CTX_SetChainVerifyCb"); + + if (ctx == NULL) + return BAD_FUNC_ARG; + + if (cb != NULL) { + ret = ChainVerifyCbCheckCtx(ctx); + if (ret != 0) + return ret; + } + ctx->chainVerifyCb = cb; + + return WOLFSSL_SUCCESS; +} + +/* Set the callback that replaces peer certificate chain verification for one + * SSL/TLS object. Takes precedence over the context's callback. + * + * @param [in, out] ssl SSL/TLS object. + * @param [in] cb Chain verification callback. NULL to fall back to the + * context's. + * @return WOLFSSL_SUCCESS on success. + * @return BAD_FUNC_ARG when ssl is NULL. + * @return CHAIN_VERIFY_UNSUPPORTED_E when the object uses DTLS, raw public + * keys or OCSP stapling. + */ +int wolfSSL_SetChainVerifyCb(WOLFSSL* ssl, ChainVerifyCb cb) +{ + int ret; + + WOLFSSL_ENTER("wolfSSL_SetChainVerifyCb"); + + if (ssl == NULL) + return BAD_FUNC_ARG; + + if (cb != NULL) { + ret = ChainVerifyCbCheckSsl(ssl); + if (ret != 0) + return ret; + } + ssl->chainVerifyCb = cb; + + return WOLFSSL_SUCCESS; +} + +/* Set the user context passed to the chain verification callback for every + * SSL/TLS object created from the context. + * + * @param [in, out] ctx SSL/TLS context object. + * @param [in] userCtx User context. + */ +void wolfSSL_CTX_SetChainVerifyCtx(WOLFSSL_CTX* ctx, void* userCtx) +{ + WOLFSSL_ENTER("wolfSSL_CTX_SetChainVerifyCtx"); + + if (ctx != NULL) { + ctx->chainVerifyCtx = userCtx; + } +} + +/* Set the user context passed to the chain verification callback for one + * SSL/TLS object. Takes precedence over the context's. + * + * @param [in, out] ssl SSL/TLS object. + * @param [in] ctx User context. NULL to fall back to the context's. + */ +void wolfSSL_SetChainVerifyCtx(WOLFSSL* ssl, void* ctx) +{ + WOLFSSL_ENTER("wolfSSL_SetChainVerifyCtx"); + + if (ssl != NULL) { + ssl->chainVerifyCtx = ctx; + } +} + +/* Get the user context the chain verification callback is called with: the + * SSL/TLS object's when set, otherwise the context's. + * + * @param [in] ssl SSL/TLS object. + * @return User context, or NULL when none set or ssl is NULL. + */ +void* wolfSSL_GetChainVerifyCtx(const WOLFSSL* ssl) +{ + WOLFSSL_ENTER("wolfSSL_GetChainVerifyCtx"); + + if (ssl != NULL) { + if (ssl->chainVerifyCtx != NULL) { + return ssl->chainVerifyCtx; + } + return ssl->ctx->chainVerifyCtx; + } + + return NULL; +} +#endif /* WOLFSSL_CHAIN_VERIFY_CB */ + /* Set the verification options against the SSL/TLS object. * * @param [in, out] ssl SSL/TLS object. diff --git a/src/ssl_api_hs.c b/src/ssl_api_hs.c index fe7be6441a0..40e1fe26b74 100644 --- a/src/ssl_api_hs.c +++ b/src/ssl_api_hs.c @@ -131,10 +131,10 @@ static int wolfssl_connect_flush(WOLFSSL* ssl) #endif /* WOLFSSL_DTLS13 */ if ((ssl->buffers.outputBuffer.length > 0) - #ifdef WOLFSSL_ASYNC_CRYPT - /* do not send buffered or advance state if last error was an - async pending operation */ - && (ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E)) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + /* do not send buffered or advance state if the last error + suspended the handshake - advancing frees the saved state */ + && !IsHsSuspendErr(ssl->error) #endif ) { ret = SendBuffered(ssl); @@ -201,10 +201,10 @@ static int wolfssl_accept_flush(WOLFSSL* ssl) int ret = 0; if ((ssl->buffers.outputBuffer.length > 0) - #ifdef WOLFSSL_ASYNC_CRYPT - /* do not send buffered or advance state if last error was an - async pending operation */ - && (ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E)) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + /* do not send buffered or advance state if the last error + suspended the handshake - advancing frees the saved state */ + && !IsHsSuspendErr(ssl->error) #endif ) { ret = SendBuffered(ssl); diff --git a/src/tls13.c b/src/tls13.c index 1322d76df55..30db75955e5 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -15129,9 +15129,8 @@ int DoTls13HandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, && (!ssl->options.dtls) #endif ) { - #if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) - if (ret != WC_NO_ERR_TRACE(WC_PENDING_E) && - ret != WC_NO_ERR_TRACE(OCSP_WANT_READ)) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + if (!IsHsSuspendErr(ret)) #endif { ssl->options.cacheMessages = 0; @@ -15246,21 +15245,19 @@ int DoTls13HandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, break; } -#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_ASYNC_IO) - /* if async, offset index so this msg will be processed again */ +#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_ASYNC_IO) || \ + defined(WOLFSSL_HAVE_HS_SUSPEND) + /* if suspended, offset index so this msg will be processed again */ /* NOTE: check this now before other calls can overwrite ret */ - if ((ret == WC_NO_ERR_TRACE(WC_PENDING_E) || - ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) && *inOutIdx > 0) { + if (IsHsSuspendErr(ret) && *inOutIdx > 0) { /* DTLS always stores a message in a buffer when async is enable, so we * don't need to adjust for the extra bytes here (*inOutIdx is always * == 0) */ *inOutIdx -= HANDSHAKE_HEADER_SZ; } - /* make sure async error is cleared */ - if (ret == 0 && - (ssl->error == WC_NO_ERR_TRACE(WC_PENDING_E) || - ssl->error == WC_NO_ERR_TRACE(OCSP_WANT_READ))) { + /* make sure suspend error is cleared */ + if (ret == 0 && IsHsSuspendErr(ssl->error)) { ssl->error = 0; } #endif @@ -15582,9 +15579,8 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, &idx, ssl->pendingMsgType, ssl->pendingMsgSz - HANDSHAKE_HEADER_SZ, ssl->pendingMsgSz); - #if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) - if (ret == WC_NO_ERR_TRACE(WC_PENDING_E) || - ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) { + #ifdef WOLFSSL_HAVE_HS_SUSPEND + if (IsHsSuspendErr(ret)) { /* setup to process fragment again */ ssl->pendingMsgOffset -= inputLength; *inOutIdx -= inputLength; @@ -15679,10 +15675,10 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) #endif /* WOLFSSL_DTLS13 */ if (ssl->buffers.outputBuffer.length > 0 - #ifdef WOLFSSL_ASYNC_CRYPT - /* do not send buffered or advance state if last error was an - async pending operation */ - && ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + /* do not send buffered or advance state if the last error suspended + the handshake - advancing frees the saved state */ + && !IsHsSuspendErr(ssl->error) #endif ) { if ((ret = SendBuffered(ssl)) == 0) { @@ -17000,10 +16996,10 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) #endif /* NO_CERTS */ if (ssl->buffers.outputBuffer.length > 0 - #ifdef WOLFSSL_ASYNC_CRYPT - /* do not send buffered or advance state if last error was an - async pending operation */ - && ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + /* do not send buffered or advance state if the last error suspended + the handshake - advancing frees the saved state */ + && !IsHsSuspendErr(ssl->error) #endif ) { @@ -17379,12 +17375,19 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) FreeHandshakeResources(ssl); } +#ifdef WOLFSSL_HAVE_HS_SUSPEND + /* A post-handshake Certificate may be suspended in ssl->async. + * Keep it and the error so wolfSSL_read() can resume it. */ + if (!IsHsSuspendErr(ssl->error)) +#endif + { #if defined(WOLFSSL_ASYNC_IO) && !defined(WOLFSSL_ASYNC_CRYPT) - /* Free the remaining async context if not using it for crypto */ - FreeAsyncCtx(ssl, 1); + /* Free the remaining async context if not using it for + * crypto */ + FreeAsyncCtx(ssl, 1); #endif - - ssl->error = 0; /* clear the error */ + ssl->error = 0; /* clear the error */ + } WOLFSSL_LEAVE("wolfSSL_accept", WOLFSSL_SUCCESS); return WOLFSSL_SUCCESS; diff --git a/tests/api.c b/tests/api.c index 88999c0cfb7..3be65432775 100644 --- a/tests/api.c +++ b/tests/api.c @@ -6574,6 +6574,11 @@ int test_ssl_memio_do_handshake(test_ssl_memio_ctx* ctx, int max_rounds, if (err == WC_NO_ERR_TRACE(MP_WOULDBLOCK)) { /* retry non-blocking math */ } + #ifdef WOLFSSL_CHAIN_VERIFY_CB + else if (err == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + /* application has not reached a verdict yet; retry */ + } + #endif else if (err != WOLFSSL_ERROR_WANT_READ && err != WOLFSSL_ERROR_WANT_WRITE && err != WC_NO_ERR_TRACE(OCSP_WANT_READ)) { @@ -6600,6 +6605,11 @@ int test_ssl_memio_do_handshake(test_ssl_memio_ctx* ctx, int max_rounds, if (err == WC_NO_ERR_TRACE(MP_WOULDBLOCK)) { /* retry non-blocking math */ } + #ifdef WOLFSSL_CHAIN_VERIFY_CB + else if (err == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + /* application has not reached a verdict yet; retry */ + } + #endif else if (err != WOLFSSL_ERROR_WANT_READ && err != WOLFSSL_ERROR_WANT_WRITE && err != WC_NO_ERR_TRACE(OCSP_WANT_READ)) { diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index c9c0c8a44b6..d818615dbd4 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3378,3 +3378,573 @@ int test_record_size_cache_invalidated_on_renegotiation(void) #endif return EXPECT_RESULT(); } + +#if defined(WOLFSSL_CHAIN_VERIFY_CB) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +#define HAVE_CHAIN_VERIFY_CB_TESTS +#endif + +#ifdef HAVE_CHAIN_VERIFY_CB_TESTS + +/* State shared with the chain verify callbacks below. */ +typedef struct test_chain_verify_cb_ctx { + int calls; /* how many times the callback ran */ + int deferrals; /* how many times to answer CHAIN_VERIFY_WANT_E first */ + int reject; /* non-zero to reject the chain */ + int certsSeen; /* certsSz of the last call */ + int leafMatched; /* certs[0] matched the expected leaf DER */ + /* A copy of the expected leaf: the peer may unload its own certificate + * buffer once its side of the handshake completes, which in TLS 1.3 is + * before the server has processed it. */ + unsigned char leafDer[2048]; + unsigned int leafDerSz; +} test_chain_verify_cb_ctx; + +/* Remember the certificate the peer will present as certs[0]. */ +static int test_chain_verify_cb_set_leaf(test_chain_verify_cb_ctx* cbCtx, + const DerBuffer* cert) +{ + if ((cert == NULL) || (cert->length > sizeof(cbCtx->leafDer))) + return -1; + XMEMCPY(cbCtx->leafDer, cert->buffer, cert->length); + cbCtx->leafDerSz = cert->length; + return 0; +} + +static int test_chain_verify_cb(WOLFSSL* ssl, const WOLFSSL_BUFFER_INFO* certs, + int certsSz, void* ctx) +{ + test_chain_verify_cb_ctx* cbCtx = (test_chain_verify_cb_ctx*)ctx; + + (void)ssl; + + cbCtx->calls++; + cbCtx->certsSeen = certsSz; + + if ((certsSz > 0) && (cbCtx->leafDerSz > 0) && + (certs[0].length == cbCtx->leafDerSz) && + (XMEMCMP(certs[0].buffer, cbCtx->leafDer, cbCtx->leafDerSz) == 0)) { + cbCtx->leafMatched = 1; + } + + if (cbCtx->calls <= cbCtx->deferrals) + return WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E); + if (cbCtx->reject) + return -1; + + return 0; +} + +/* Which end of the connection verifies with the callback. */ +#define TEST_CVC_CLIENT 0 +#define TEST_CVC_SERVER 1 + +/* Set up a handshake that the verifying end cannot complete on its own. + * Client side: the client's CA store is emptied, so verifying the server's + * certificate fails with ASN_NO_SIGNER_E. Server side: the client presents a + * certificate the server has no CA for, and the server insists on one. + * Installing the chain verify callback on that end is then the only thing + * that can make the handshake succeed. Leaves the objects allocated so the + * caller can inspect them. Pass cbCtx as NULL to install no callback + * (negative control). */ +static int test_chain_verify_cb_run(test_chain_verify_cb_ctx* cbCtx, int side, + method_provider client_method, method_provider server_method, + WOLFSSL_CTX** ctx_c, WOLFSSL_CTX** ctx_s, WOLFSSL** ssl_c, WOLFSSL** ssl_s, + struct test_memio_ctx* test_ctx) +{ + EXPECT_DECLS; + WOLFSSL* verifier = NULL; + WOLFSSL* peer = NULL; + + XMEMSET(test_ctx, 0, sizeof(*test_ctx)); + ExpectIntEQ(test_memio_setup(test_ctx, ctx_c, ctx_s, ssl_c, ssl_s, + client_method, server_method), 0); + + if (side == TEST_CVC_CLIENT) { + ExpectIntEQ(wolfSSL_CTX_UnloadCAs(*ctx_c), WOLFSSL_SUCCESS); + /* explicit, since OPENSSL_COMPATIBLE_DEFAULTS turns verification off + * on clients by default */ + wolfSSL_set_verify(*ssl_c, WOLFSSL_VERIFY_PEER, NULL); + verifier = *ssl_c; + peer = *ssl_s; + } + else { + ExpectIntEQ(wolfSSL_use_certificate_file(*ssl_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(*ssl_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + wolfSSL_set_verify(*ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + verifier = *ssl_s; + peer = *ssl_c; + } + + if (EXPECT_SUCCESS() && (cbCtx != NULL)) { + if (side == TEST_CVC_CLIENT) { + /* context-level callback, object-level user context */ + ExpectIntEQ(wolfSSL_CTX_SetChainVerifyCb(*ctx_c, + test_chain_verify_cb), WOLFSSL_SUCCESS); + wolfSSL_SetChainVerifyCtx(verifier, cbCtx); + } + else { + /* object-level callback, context-level user context */ + ExpectIntEQ(wolfSSL_SetChainVerifyCb(verifier, + test_chain_verify_cb), WOLFSSL_SUCCESS); + wolfSSL_CTX_SetChainVerifyCtx(*ctx_s, cbCtx); + } + ExpectPtrEq(wolfSSL_GetChainVerifyCtx(verifier), cbCtx); + + /* certs[0] must be the peer's own certificate. */ + ExpectIntEQ(test_chain_verify_cb_set_leaf(cbCtx, + peer->buffers.certificate), 0); + } + + return EXPECT_RESULT(); +} + +static void test_chain_verify_cb_free(WOLFSSL_CTX** ctx_c, WOLFSSL_CTX** ctx_s, + WOLFSSL** ssl_c, WOLFSSL** ssl_s) +{ + wolfSSL_free(*ssl_c); + wolfSSL_free(*ssl_s); + wolfSSL_CTX_free(*ctx_c); + wolfSSL_CTX_free(*ctx_s); + *ssl_c = NULL; + *ssl_s = NULL; + *ctx_c = NULL; + *ctx_s = NULL; +} + +/* Negative control first, then the callback accepting after the given number + * of deferrals. */ +static int test_chain_verify_cb_accept(int side, int deferrals, + method_provider client_method, method_provider server_method) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + int err; + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + cbCtx.deferrals = deferrals; + + /* Negative control: without the callback this same setup must fail, so the + * success below can only come from the callback replacing verification. */ + ExpectIntEQ(test_chain_verify_cb_run(NULL, side, client_method, + server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), + TEST_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + /* The client certificate is self-signed, which some builds report as + * such rather than as having no signer. */ + err = wolfSSL_get_error((side == TEST_CVC_CLIENT) ? ssl_c : ssl_s, + WOLFSSL_FATAL_ERROR); + ExpectTrue((err == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E)) || + (err == WC_NO_ERR_TRACE(ASN_SELF_SIGNED_E))); + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); + + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, side, client_method, + server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), + TEST_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Deferred the requested number of times, then accepted, every call + * seeing the same chain. */ + ExpectIntEQ(cbCtx.calls, deferrals + 1); + ExpectIntGT(cbCtx.certsSeen, 0); + ExpectIntEQ(cbCtx.leafMatched, 1); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); + return EXPECT_RESULT(); +} + +#if !defined(WOLFSSL_NO_TLS12) || defined(HAVE_CERTIFICATE_STATUS_REQUEST) +/* The verifying end fails before the callback is consulted, with the given + * error, and the callback is never called. */ +static int test_chain_verify_cb_expect_fail(test_chain_verify_cb_ctx* cbCtx, + WOLFSSL* verifier, WOLFSSL* ssl_c, WOLFSSL* ssl_s, int err) +{ + EXPECT_DECLS; + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + ExpectIntEQ(wolfSSL_get_error(verifier, WOLFSSL_FATAL_ERROR), err); + ExpectIntEQ(cbCtx->calls, 0); + + return EXPECT_RESULT(); +} +#endif + +#endif /* HAVE_CHAIN_VERIFY_CB_TESTS */ + +int test_tls12_chain_verify_cb(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(WOLFSSL_NO_TLS12) + ExpectIntEQ(test_chain_verify_cb_accept(TEST_CVC_CLIENT, 0, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +int test_tls12_chain_verify_cb_server(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(WOLFSSL_NO_TLS12) + /* Client authentication: the server verifies, deferring twice. */ + ExpectIntEQ(test_chain_verify_cb_accept(TEST_CVC_SERVER, 2, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +int test_tls13_chain_verify_cb_async(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_TLS13) + ExpectIntEQ(test_chain_verify_cb_accept(TEST_CVC_CLIENT, 3, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +int test_tls13_chain_verify_cb_server(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_TLS13) + ExpectIntEQ(test_chain_verify_cb_accept(TEST_CVC_SERVER, 2, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +int test_tls13_chain_verify_cb_reject(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_TLS13) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + WOLFSSL_ALERT_HISTORY h; + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + XMEMSET(&h, 0, sizeof(h)); + cbCtx.deferrals = 1; + cbCtx.reject = 1; + + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, + &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), TEST_SUCCESS); + + /* Handshake must fail, and only after the deferral was honoured. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + ExpectIntEQ(cbCtx.calls, 2); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(CHAIN_VERIFY_CB_E)); + + /* A single generic certificate alert is sent - the callback's reason is + * never leaked to the peer. */ + ExpectIntEQ(wolfSSL_get_alert_history(ssl_c, &h), WOLFSSL_SUCCESS); + ExpectIntEQ(h.last_tx.level, alert_fatal); + ExpectIntEQ(h.last_tx.code, bad_certificate); +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + ExpectIntEQ(wolfSSL_get_verify_result(ssl_c), + WOLFSSL_X509_V_ERR_CERT_REJECTED); +#endif + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + +int test_tls12_chain_verify_cb_bad_der(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method, + &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), TEST_SUCCESS); + + /* Break the length of the leaf's outer SEQUENCE. The Certificate message + * still frames correctly, but the certificate no longer decodes. */ + ExpectNotNull(ssl_s->buffers.certificate); + ExpectIntGT(ssl_s->buffers.certificate->length, 4); + if (EXPECT_SUCCESS()) { + ssl_s->buffers.certificate->buffer[2] = 0xFF; + ssl_s->buffers.certificate->buffer[3] = 0xFF; + } + + /* wolfSSL rejects it on its own - the callback is never consulted. */ + ExpectIntEQ(test_chain_verify_cb_expect_fail(&cbCtx, ssl_c, ssl_c, ssl_s, + WC_NO_ERR_TRACE(ASN_PARSE_E)), TEST_SUCCESS); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + +int test_tls12_chain_verify_cb_bad_chain(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + + /* First with the chain intact: the callback sees both the server's + * certificate and the issuer it sends along. */ + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method, + &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(ssl_s, svrCertFile), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_chain_verify_cb_set_leaf(&cbCtx, + ssl_s->buffers.certificate), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(cbCtx.calls, 1); + ExpectIntEQ(cbCtx.certsSeen, 2); + ExpectIntEQ(cbCtx.leafMatched, 1); + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); + + /* Now break the issuer's outer SEQUENCE length. A chain entry is a 3-byte + * length followed by the DER, so the message still frames. */ + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method, + &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(ssl_s, svrCertFile), + WOLFSSL_SUCCESS); + ExpectNotNull(ssl_s->buffers.certChain); + ExpectIntGT(ssl_s->buffers.certChain->length, OPAQUE24_LEN + 4); + if (EXPECT_SUCCESS()) { + ssl_s->buffers.certChain->buffer[OPAQUE24_LEN + 2] = 0xFF; + ssl_s->buffers.certChain->buffer[OPAQUE24_LEN + 3] = 0xFF; + } + + ExpectIntEQ(test_chain_verify_cb_expect_fail(&cbCtx, ssl_c, ssl_c, ssl_s, + WC_NO_ERR_TRACE(ASN_PARSE_E)), TEST_SUCCESS); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + +int test_tls13_chain_verify_cb_postauth(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_TLS13) && \ + defined(WOLFSSL_POST_HANDSHAKE_AUTH) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + char buf[8]; + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + cbCtx.deferrals = 2; + + /* Client: has a certificate and allows post-handshake authentication. + * The certificate is loaded on the context: without OPENSSL_EXTRA a + * certificate loaded on the SSL object is unloaded when the handshake + * completes, and the post-handshake Certificate would be empty. */ + 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(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(wolfSSL_allow_post_handshake_auth(ssl_c), 0); + + /* Server: no CA for the client, the callback verifies instead, and only + * after the handshake. Message grouping (on by default with + * OPENSSL_COMPATIBLE_DEFAULTS) would hold the CertificateRequest back in + * the output buffer. */ + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_POST_HANDSHAKE, NULL); + ExpectIntEQ(wolfSSL_clear_group_messages(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_s, test_chain_verify_cb), + WOLFSSL_SUCCESS); + wolfSSL_SetChainVerifyCtx(ssl_s, &cbCtx); + ExpectIntEQ(test_chain_verify_cb_set_leaf(&cbCtx, + ssl_c->buffers.certificate), 0); + + /* No certificate during the handshake, so no callback yet. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(cbCtx.calls, 0); + + /* The server asks; the client answers with its certificate from inside + * wolfSSL_read(). */ + ExpectIntEQ(wolfSSL_request_certificate(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + + /* The certificate arrives inside wolfSSL_read(); the callback defers. */ + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), + WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)); + ExpectIntEQ(cbCtx.calls, 1); + + /* wolfSSL_accept() meanwhile must leave the suspended message alone. */ + ExpectIntEQ(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + + /* Reading again asks the callback again: still deferred. */ + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), + WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)); + ExpectIntEQ(cbCtx.calls, 2); + + /* Accepted; CertificateVerify and Finished follow, then nothing to read. */ + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(cbCtx.calls, 3); + ExpectIntEQ(cbCtx.certsSeen, 1); + ExpectIntEQ(cbCtx.leafMatched, 1); + + /* The connection is intact: application data flows. */ + ExpectIntEQ(wolfSSL_write(ssl_c, "hi", 2), 2); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), 2); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + +int test_chain_verify_cb_dtls(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_DTLS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + 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(wolfSSL_CTX_UnloadCAs(ctx_c), WOLFSSL_SUCCESS); + + /* DTLS is not supported with the callback: the setters refuse it. */ + ExpectIntEQ(wolfSSL_CTX_SetChainVerifyCb(ctx_c, test_chain_verify_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_c, test_chain_verify_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectNull(wolfSSL_GetChainVerifyCtx(ssl_c)); + + /* A callback that got in regardless fails the handshake before it is + * consulted. */ + if (EXPECT_SUCCESS()) { + ssl_c->chainVerifyCb = test_chain_verify_cb; + ssl_c->chainVerifyCtx = &cbCtx; + } + ExpectIntEQ(test_chain_verify_cb_expect_fail(&cbCtx, ssl_c, ssl_c, ssl_s, + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)), TEST_SUCCESS); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + +int test_chain_verify_cb_stapling(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + (!defined(WOLFSSL_NO_TLS12) || defined(WOLFSSL_TLS13)) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; +#ifndef WOLFSSL_NO_TLS12 + method_provider client_method = wolfTLSv1_2_client_method; + method_provider server_method = wolfTLSv1_2_server_method; +#else + method_provider client_method = wolfTLSv1_3_client_method; + method_provider server_method = wolfTLSv1_3_server_method; +#endif + + /* Stapling requested before the callback: the setters refuse it, on the + * object and on the context. The server side is not affected, since a + * server's stapling is about its own certificate. */ + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(NULL, TEST_CVC_CLIENT, + client_method, server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + &test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl_c, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_c, test_chain_verify_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectIntEQ(wolfSSL_CTX_UseOCSPStapling(ctx_c, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetChainVerifyCb(ctx_c, test_chain_verify_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectIntEQ(wolfSSL_CTX_SetChainVerifyCb(ctx_s, test_chain_verify_cb), + WOLFSSL_SUCCESS); + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); + + /* Stapling requested after the callback: the handshake fails before the + * callback is consulted, whether or not the server staples. */ + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + client_method, server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + &test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl_c, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_chain_verify_cb_expect_fail(&cbCtx, ssl_c, ssl_c, ssl_s, + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)), TEST_SUCCESS); + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); + +#ifdef HAVE_OCSP + /* So is must-staple on its own, both ways round. */ + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(NULL, TEST_CVC_CLIENT, + client_method, server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + &test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPMustStaple(ctx_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetChainVerifyCb(ctx_c, test_chain_verify_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_c, test_chain_verify_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + client_method, server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + &test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPMustStaple(ctx_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_chain_verify_cb_expect_fail(&cbCtx, ssl_c, ssl_c, ssl_s, + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)), TEST_SUCCESS); + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 6c3fda89962..98082b40c04 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -62,6 +62,16 @@ int test_tls12_ecdhe_ecdsa_rsa_client_cert(void); int test_tls12_ecdhe_rsa_ecdsa_client_cert(void); int test_wolfSSL_alert_type_string(void); int test_wolfSSL_alert_desc_string(void); +int test_tls12_chain_verify_cb(void); +int test_tls12_chain_verify_cb_server(void); +int test_tls13_chain_verify_cb_async(void); +int test_tls13_chain_verify_cb_server(void); +int test_tls13_chain_verify_cb_reject(void); +int test_tls12_chain_verify_cb_bad_der(void); +int test_tls12_chain_verify_cb_bad_chain(void); +int test_tls13_chain_verify_cb_postauth(void); +int test_chain_verify_cb_dtls(void); +int test_chain_verify_cb_stapling(void); int test_record_size_matches_build_message(void); int test_record_size_preserves_build_msg_state(void); int test_record_size_cache_invalidated_on_renegotiation(void); @@ -114,6 +124,16 @@ int test_wolfSSL_get_shared_ciphers(void); test_record_size_preserves_build_msg_state), \ TEST_DECL_GROUP("tls", \ test_record_size_cache_invalidated_on_renegotiation), \ - TEST_DECL_GROUP("tls", test_wolfSSL_get_shared_ciphers) + TEST_DECL_GROUP("tls", test_wolfSSL_get_shared_ciphers), \ + TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb), \ + TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_server), \ + TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_async), \ + TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_server), \ + TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_reject), \ + TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_bad_der), \ + TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_bad_chain), \ + TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_postauth), \ + TEST_DECL_GROUP("tls", test_chain_verify_cb_dtls), \ + TEST_DECL_GROUP("tls", test_chain_verify_cb_stapling) #endif /* TESTS_API_TEST_TLS_H */ diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 449514541cd..20db8795730 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -3245,6 +3245,93 @@ static WC_INLINE int test_rpk_memio_setup( #endif /* HAVE_RPK && !NO_TLS && !NO_WOLFSSL_CLIENT && !NO_WOLFSSL_SERVER */ +#if defined(HAVE_RPK) && defined(WOLFSSL_CHAIN_VERIFY_CB) && \ + defined(WOLFSSL_TLS13) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +static int test_chain_verify_cb_rpk_cb(WOLFSSL* ssl, + const WOLFSSL_BUFFER_INFO* certs, int certsSz, void* ctx) +{ + (void)ssl; + (void)certs; + (void)certsSz; + (*(int*)ctx)++; + return 0; +} +#endif + +/* A raw public key is not supported with the chain verify callback. */ +int test_tls13_chain_verify_cb_rpk(void) +{ + EXPECT_DECLS; +#if defined(HAVE_RPK) && defined(WOLFSSL_CHAIN_VERIFY_CB) && \ + defined(WOLFSSL_TLS13) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + char certType[MAX_CLIENT_CERT_TYPE_CNT]; + int calls = 0; + int i; + + certType[0] = WOLFSSL_CERT_TYPE_RPK; + + /* First with raw public keys configured before the callback: the setters + * refuse it. Then with the callback set first: the handshake fails before + * the callback is consulted. */ + for (i = 0; i < 2; i++) { + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ( + test_rpk_memio_setup( + &test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, + clntRpkCertFile, WOLFSSL_FILETYPE_ASN1, + svrRpkCertFile, WOLFSSL_FILETYPE_ASN1, + cliKeyFile, CERT_FILETYPE, + svrKeyFile, CERT_FILETYPE ) + , 0); + + if (i == 1) { + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_c, + test_chain_verify_cb_rpk_cb), WOLFSSL_SUCCESS); + wolfSSL_SetChainVerifyCtx(ssl_c, &calls); + } + + /* Both ends negotiate raw public keys only. */ + ExpectIntEQ(wolfSSL_set_client_cert_type(ssl_c, certType, 1), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_server_cert_type(ssl_c, certType, 1), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_client_cert_type(ssl_s, certType, 1), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_server_cert_type(ssl_s, certType, 1), + WOLFSSL_SUCCESS); + + if (i == 0) { + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_c, + test_chain_verify_cb_rpk_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectIntEQ(wolfSSL_SetChainVerifyCb(ssl_s, + test_chain_verify_cb_rpk_cb), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + } + else { + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(CHAIN_VERIFY_UNSUPPORTED_E)); + ExpectIntEQ(calls, 0); + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + ssl_c = ssl_s = NULL; + ctx_c = ctx_s = NULL; + } +#endif + return EXPECT_RESULT(); +} + int test_tls13_rpk_handshake(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 7877fdc695c..b66b6ab3293 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -120,6 +120,7 @@ int test_tls13_KeyUpdate_sender_limit(void); int test_tls13_pqc_hybrid_async_server(void); int test_tls13_pha_status_request(void); int test_tls13_x25519_keyshare_masks_reserved_bit(void); +int test_tls13_chain_verify_cb_rpk(void); #define TEST_TLS13_DECLS \ TEST_DECL_GROUP("tls13", test_tls13_apis), \ @@ -217,6 +218,7 @@ int test_tls13_x25519_keyshare_masks_reserved_bit(void); TEST_DECL_GROUP("tls13", test_tls13_KeyUpdate_sender_limit), \ TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_async_server), \ TEST_DECL_GROUP("tls13", test_tls13_pha_status_request), \ - TEST_DECL_GROUP("tls13", test_tls13_x25519_keyshare_masks_reserved_bit) + TEST_DECL_GROUP("tls13", test_tls13_x25519_keyshare_masks_reserved_bit), \ + TEST_DECL_GROUP("tls13", test_tls13_chain_verify_cb_rpk) #endif /* WOLFCRYPT_TEST_TLS13_H */ diff --git a/tests/utils.c b/tests/utils.c index a8566adc86e..ca5328bfcb5 100644 --- a/tests/utils.c +++ b/tests/utils.c @@ -192,6 +192,11 @@ int test_memio_do_handshake(WOLFSSL *ssl_c, WOLFSSL *ssl_s, if (ret < 0) return -1; } + #endif + #ifdef WOLFSSL_CHAIN_VERIFY_CB + else if (err == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + /* application has not reached a verdict yet; retry */ + } #endif else if (err != WOLFSSL_ERROR_WANT_READ && err != WOLFSSL_ERROR_WANT_WRITE) { @@ -220,6 +225,11 @@ int test_memio_do_handshake(WOLFSSL *ssl_c, WOLFSSL *ssl_s, if (ret < 0) return -1; } + #endif + #ifdef WOLFSSL_CHAIN_VERIFY_CB + else if (err == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + /* application has not reached a verdict yet; retry */ + } #endif else if (err != WOLFSSL_ERROR_WANT_READ && err != WOLFSSL_ERROR_WANT_WRITE) { diff --git a/wolfssl/error-ssl.h b/wolfssl/error-ssl.h index 4c6893fbeb3..ceac7267355 100644 --- a/wolfssl/error-ssl.h +++ b/wolfssl/error-ssl.h @@ -253,7 +253,14 @@ enum wolfSSL_ErrorCodes { OCSP_NO_URL = -522, /* Cert advertises no OCSP responder * and no override URL is set */ - WOLFSSL_LAST_E = -522 + CHAIN_VERIFY_WANT_E = -523, /* Chain verify callback has not + * reached a verdict yet */ + CHAIN_VERIFY_CB_E = -524, /* Chain verify callback rejected the + * peer's certificates */ + CHAIN_VERIFY_UNSUPPORTED_E = -525, /* Chain verify callback used with + * DTLS, RPK or OCSP stapling */ + + WOLFSSL_LAST_E = -525 /* codes -1000 to -1999 are reserved for wolfCrypt. */ }; diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 426894de5b0..96ef1f09b30 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2880,6 +2880,9 @@ typedef struct ProcPeerCertArgs { word16 fatal:1; word16 verifyErr:1; word16 dCertInit:1; +#ifdef WOLFSSL_CHAIN_VERIFY_CB + word16 chainDecoded:1; /* peer certs already decode-checked */ +#endif #ifdef WOLFSSL_TRUST_PEER_CERT word16 haveTrustPeer:1; /* was cert verified by loaded trusted peer cert */ #endif @@ -4376,6 +4379,10 @@ struct WOLFSSL_CTX { CertVerifyCallback verifyCertCb; void* verifyCertCbArg; #endif /* OPENSSL_ALL */ +#ifdef WOLFSSL_CHAIN_VERIFY_CB + ChainVerifyCb chainVerifyCb; /* replaces peer chain verification */ + void* chainVerifyCtx; /* chain verify callback user ctx */ +#endif #ifdef OPENSSL_EXTRA SSL_Msg_Cb protoMsgCb; /* inspect protocol message callback */ void* protoMsgCtx; /* user set context with msg callback */ @@ -5385,6 +5392,26 @@ enum asyncState { TLS_ASYNC_END }; +/* Handshake message processing can suspend part way through and resume when + * the application re-enters wolfSSL_connect()/wolfSSL_accept(). State for the + * message in flight is held in ssl->async. */ +#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) || \ + defined(WOLFSSL_CHAIN_VERIFY_CB) + #undef WOLFSSL_HAVE_HS_SUSPEND + #define WOLFSSL_HAVE_HS_SUSPEND +#endif + +/* True for error codes that suspend handshake processing instead of failing + * it. Only the codes whose feature is compiled in are recognized. */ +WOLFSSL_LOCAL int IsHsSuspendErr(int err); + +#ifdef WOLFSSL_CHAIN_VERIFY_CB +/* Non-zero when the context or object is configured for something the chain + * verify callback does not support. */ +WOLFSSL_LOCAL int ChainVerifyCbCheckCtx(const WOLFSSL_CTX* ctx); +WOLFSSL_LOCAL int ChainVerifyCbCheckSsl(const WOLFSSL* ssl); +#endif + /* sub-states for build message */ enum buildMsgState { BUILD_MSG_BEGIN = 0, @@ -6477,6 +6504,10 @@ struct WOLFSSL { WC_RNG* rng; void* verifyCbCtx; /* cert verify callback user ctx*/ VerifyCallback verifyCallback; /* cert verification callback */ +#ifdef WOLFSSL_CHAIN_VERIFY_CB + ChainVerifyCb chainVerifyCb; /* overrides the ctx's when set */ + void* chainVerifyCtx; /* chain verify callback user ctx */ +#endif void* heap; /* for user overrides */ #ifdef HAVE_WRITE_DUP WriteDup* dupWrite; /* valid pointer indicates ON */ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index dd35a5f9291..23ed3bb907d 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1745,6 +1745,69 @@ WOLFSSL_API int wolfSSL_set_post_handshake_auth(WOLFSSL* ssl, int val); WOLFSSL_API void wolfSSL_SetCertCbCtx(WOLFSSL* ssl, void* ctx); WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); +#ifdef WOLFSSL_CHAIN_VERIFY_CB +/* Replaces wolfSSL's peer certificate verification in its entirety. When set, + * wolfSSL builds no chain, verifies no signature, checks no date, revocation + * status, key usage or host name, and the verify callback set by + * wolfSSL_CTX_set_verify() is not called. The callback owns all of it. It is + * consulted even when verification was turned off with WOLFSSL_VERIFY_NONE, + * and a rejection fails the handshake either way. + * + * wolfSSL still owns the parsing. Every certificate is decoded before the + * callback runs, and malformed DER fails the handshake without the callback + * ever seeing it. Content wolfSSL does not understand - an unknown critical + * extension, an unsupported key or signature algorithm - is not a decoding + * failure; it is passed through for the callback to judge. + * + * Two more things still apply. An empty Certificate message is handled by + * wolfSSL itself (see wolfSSL_CTX_set_verify() and the mutual-auth options) + * and the callback is not called for it, so certsSz is always at least 1. The + * minimum peer key sizes set by wolfSSL_CTX_SetMinRsaKey_Sz() and friends are + * still enforced on the peer's own certificate, because the handshake uses + * that key directly. + * + * Not supported with the callback: DTLS, raw public keys (RFC 7250) and OCSP + * stapling. Setting the callback on a context or object already configured + * for one of them fails with CHAIN_VERIFY_UNSUPPORTED_E, and so does the + * handshake of a connection that uses one of them, before the callback is + * called. + * + * certs DER certificates in the order the peer sent them, one + * WOLFSSL_BUFFER_INFO each: certs[0] is the peer's own certificate, + * the rest are the chain it supplied. The buffers are wolfSSL's own + * receive buffer: treat them as read-only, and only for the duration + * of the call. + * certsSz Number of entries in certs, always 1 or more. + * ctx Value set with wolfSSL_SetChainVerifyCtx(), or failing that with + * wolfSSL_CTX_SetChainVerifyCtx(). + * + * Return 0 to accept, CHAIN_VERIFY_WANT_E to suspend the handshake, or any + * other value to reject. Rejection fails the handshake with + * CHAIN_VERIFY_CB_E and sends one fatal bad_certificate alert; the returned + * value is not reported to the peer. + * + * After CHAIN_VERIFY_WANT_E the callback is called again with the same + * certificates when the application re-enters wolfSSL_connect(), + * wolfSSL_accept(), wolfSSL_read() or wolfSSL_write(). A certificate received + * after the handshake (TLS 1.3 post-handshake authentication) arrives inside + * wolfSSL_read() and is resumed by calling wolfSSL_read() again. + */ +typedef int (*ChainVerifyCb)(WOLFSSL* ssl, const WOLFSSL_BUFFER_INFO* certs, + int certsSz, void* ctx); + +/* The callback and user context set on an SSL/TLS object take precedence over + * those set on its context. The setters return WOLFSSL_SUCCESS, BAD_FUNC_ARG, + * or CHAIN_VERIFY_UNSUPPORTED_E when the context or object is configured for + * DTLS, raw public keys or OCSP stapling. */ +WOLFSSL_API int wolfSSL_CTX_SetChainVerifyCb(WOLFSSL_CTX* ctx, + ChainVerifyCb cb); +WOLFSSL_API int wolfSSL_SetChainVerifyCb(WOLFSSL* ssl, ChainVerifyCb cb); +WOLFSSL_API void wolfSSL_CTX_SetChainVerifyCtx(WOLFSSL_CTX* ctx, + void* userCtx); +WOLFSSL_API void wolfSSL_SetChainVerifyCtx(WOLFSSL* ssl, void* ctx); +WOLFSSL_API void* wolfSSL_GetChainVerifyCtx(const WOLFSSL* ssl); +#endif /* WOLFSSL_CHAIN_VERIFY_CB */ + WOLFSSL_ABI WOLFSSL_API int wolfSSL_pending(WOLFSSL* ssl); WOLFSSL_API int wolfSSL_has_pending(const WOLFSSL* ssl); diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h index b66ae2cac39..4d621e78bc1 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -5491,11 +5491,12 @@ blinding by defining WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS." #endif #if !defined(WOLFSSL_NO_ASYNC_IO) || defined(WOLFSSL_ASYNC_CRYPT) || \ - defined(WOLFSSL_NONBLOCK_OCSP) + defined(WOLFSSL_NONBLOCK_OCSP) || defined(WOLFSSL_CHAIN_VERIFY_CB) /* Enable asynchronous support in TLS functions to support one or more of * the following: * - re-entry after a network blocking return * - re-entry after OCSP blocking return + * - re-entry after a chain verify callback deferred its verdict * - asynchronous cryptography */ #undef WOLFSSL_ASYNC_IO #define WOLFSSL_ASYNC_IO From 2cf34883a213f4fca9363fb560d8fb23b5764ccc Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 3 Sep 2026 17:56:31 +0000 Subject: [PATCH 2/9] Fix chain verify callback build without stapling and test without client auth ChainVerifyCbStaplingRequested() was only called under the stapling macros, so a build with the callback and no stapling failed on the unused function. Guard its definition the same way. With WOLFSSL_NO_CLIENT_AUTH a TLS 1.2 server neither requests nor gets a client certificate, so the server-side TLS 1.2 test's negative control cannot fail. Skip that test there; the TLS 1.3 tests are unaffected. --- src/internal.c | 3 +++ tests/api/test_tls.c | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index f472d4ad3f6..613c5f5a40e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -17948,6 +17948,8 @@ static void* GetChainVerifyCtx(const WOLFSSL* ssl) * or object is configured for, and again when the peer's certificates arrive, * against what was negotiated, so that using them fails as early as * possible. */ +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) static int ChainVerifyCbStaplingRequested(TLSX* extensions) { #if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) @@ -17961,6 +17963,7 @@ static int ChainVerifyCbStaplingRequested(TLSX* extensions) (void)extensions; return 0; } +#endif #ifdef HAVE_RPK static int ChainVerifyCbRpkConfigured(const RpkConfig* cfg) diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index d818615dbd4..6319d1a4e5b 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3593,7 +3593,8 @@ int test_tls12_chain_verify_cb(void) int test_tls12_chain_verify_cb_server(void) { EXPECT_DECLS; -#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(WOLFSSL_NO_TLS12) +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(WOLFSSL_NO_CLIENT_AUTH) /* Client authentication: the server verifies, deferring twice. */ ExpectIntEQ(test_chain_verify_cb_accept(TEST_CVC_SERVER, 2, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), TEST_SUCCESS); From e006697755154ce56ecbdc1d1c79bb818669bf5b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 3 Sep 2026 18:30:04 +0000 Subject: [PATCH 3/9] Address chain verify callback review and fix the no-client build The setter-side checks lived inside the peer-certificate region, which is compiled out with NO_WOLFSSL_CLIENT and WOLFSSL_NO_CLIENT_AUTH, so the setters failed to link there. Move them out. Review findings: - The minimum peer key sizes were skipped under WOLFSSL_VERIFY_NONE even with the callback set, contrary to the documentation. They now apply whenever the callback is in use. - ParseCert(NO_VERIFY) still rejects keyCertSign on a non-CA with KEYUSAGE_E. That is content for the callback to judge, so the decode check passes it through like the other content errors. - The verify depth no longer limits the chain handed to the callback; only MAX_CHAIN_DEPTH does. - The leaf's second parse could still fail on the content errors the decode check passes through; it now treats them the same way. Add a test that an undersized peer key fails under WOLFSSL_VERIFY_NONE with the callback accepting. --- src/internal.c | 289 +++++++++++++++++++++++-------------------- tests/api/test_tls.c | 38 ++++++ tests/api/test_tls.h | 2 + wolfssl/ssl.h | 13 +- 4 files changed, 202 insertions(+), 140 deletions(-) diff --git a/src/internal.c b/src/internal.c index 613c5f5a40e..9075fdf8888 100644 --- a/src/internal.c +++ b/src/internal.c @@ -663,6 +663,110 @@ int IsHsSuspendErr(int err) return 0; } +#ifdef WOLFSSL_CHAIN_VERIFY_CB +/* DTLS, raw public keys and OCSP stapling are not supported with the chain + * verify callback. Checked when the callback is set, against what the context + * or object is configured for, and again when the peer's certificates arrive, + * against what was negotiated, so that using them fails as early as + * possible. */ +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) +static int ChainVerifyCbStaplingRequested(TLSX* extensions) +{ +#if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) + if (TLSX_Find(extensions, TLSX_STATUS_REQUEST) != NULL) + return 1; +#endif +#if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) + if (TLSX_Find(extensions, TLSX_STATUS_REQUEST_V2) != NULL) + return 1; +#endif + (void)extensions; + return 0; +} +#endif + +#ifdef HAVE_RPK +static int ChainVerifyCbRpkConfigured(const RpkConfig* cfg) +{ + int i; + + for (i = 0; i < cfg->preferred_ClientCertTypeCnt; i++) { + if (cfg->preferred_ClientCertTypes[i] == WOLFSSL_CERT_TYPE_RPK) + return 1; + } + for (i = 0; i < cfg->preferred_ServerCertTypeCnt; i++) { + if (cfg->preferred_ServerCertTypes[i] == WOLFSSL_CERT_TYPE_RPK) + return 1; + } + return 0; +} +#endif + +int ChainVerifyCbCheckCtx(const WOLFSSL_CTX* ctx) +{ +#ifdef WOLFSSL_DTLS + if (ctx->method->version.major == DTLS_MAJOR) { + WOLFSSL_MSG("DTLS not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#ifdef HAVE_RPK + if (ChainVerifyCbRpkConfigured(&ctx->rpkConfig)) { + WOLFSSL_MSG("Raw public key not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) + if ((ctx->method->side != WOLFSSL_SERVER_END) && + (((ctx->cm != NULL) && ctx->cm->ocspMustStaple) || + ChainVerifyCbStaplingRequested(ctx->extensions))) { + WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif + (void)ctx; + return 0; +} + +int ChainVerifyCbCheckSsl(const WOLFSSL* ssl) +{ +#ifdef WOLFSSL_DTLS + if (ssl->options.dtls) { + WOLFSSL_MSG("DTLS not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#ifdef HAVE_RPK + if (ChainVerifyCbRpkConfigured(&ssl->options.rpkConfig) || + ((ssl->options.side == WOLFSSL_CLIENT_END) && + (ssl->options.rpkState.received_ServerCertTypeCnt == 1) && + (ssl->options.rpkState.received_ServerCertTypes[0] == + WOLFSSL_CERT_TYPE_RPK)) || + ((ssl->options.side == WOLFSSL_SERVER_END) && + (ssl->options.rpkState.sending_ClientCertTypeCnt == 1) && + (ssl->options.rpkState.sending_ClientCertTypes[0] == + WOLFSSL_CERT_TYPE_RPK))) { + WOLFSSL_MSG("Raw public key not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) + if ((ssl->options.side != WOLFSSL_SERVER_END) && + (SSL_CM(ssl)->ocspMustStaple || + ChainVerifyCbStaplingRequested(ssl->extensions) || + ChainVerifyCbStaplingRequested(ssl->ctx->extensions))) { + WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } +#endif + (void)ssl; + return 0; +} +#endif /* WOLFSSL_CHAIN_VERIFY_CB */ + int IsAtLeastTLSv1_2(const WOLFSSL* ssl) { if (ssl->version.major == SSLv3_MAJOR && ssl->version.minor >=TLSv1_2_MINOR) @@ -15973,6 +16077,29 @@ int InitSigPkCb(WOLFSSL* ssl, SignatureCtx* sigCtx) #endif /* HAVE_PK_CALLBACKS */ #if !defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH) + +#ifdef WOLFSSL_CHAIN_VERIFY_CB +/* The SSL object's callback and user context take precedence over the + * context's. */ +static ChainVerifyCb GetChainVerifyCb(const WOLFSSL* ssl) +{ + if (ssl->chainVerifyCb != NULL) + return ssl->chainVerifyCb; + return ssl->ctx->chainVerifyCb; +} + +static void* GetChainVerifyCtx(const WOLFSSL* ssl) +{ + if (ssl->chainVerifyCtx != NULL) + return ssl->chainVerifyCtx; + return ssl->ctx->chainVerifyCtx; +} + +/* Non-zero when the application has replaced chain verification. */ +#define UsingChainVerifyCb(ssl) (GetChainVerifyCb(ssl) != NULL) +#else +#define UsingChainVerifyCb(ssl) 0 +#endif /* WOLFSSL_CHAIN_VERIFY_CB */ void DoCertFatalAlert(WOLFSSL* ssl, int ret) { int alertWhy; @@ -17493,7 +17620,8 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args, /* check size of peer RSA key */ if (ret == 0 && ssl->peerRsaKeyPresent && - !ssl->options.verifyNone && + (!ssl->options.verifyNone || + UsingChainVerifyCb(ssl)) && wc_RsaEncryptSize(ssl->peerRsaKey) < ssl->options.minRsaKeySz) { ret = RSA_KEY_SIZE_E; @@ -17572,7 +17700,8 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args, /* check size of peer ECC key */ if (ret == 0 && ssl->peerEccDsaKeyPresent && - !ssl->options.verifyNone && + (!ssl->options.verifyNone || + UsingChainVerifyCb(ssl)) && wc_ecc_size(ssl->peerEccDsaKey) < ssl->options.minEccKeySz) { ret = ECC_KEY_SIZE_E; @@ -17632,7 +17761,7 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args, /* check size of peer ECC key */ if (ret == 0 && ssl->peerEd25519KeyPresent && - !ssl->options.verifyNone && + (!ssl->options.verifyNone || UsingChainVerifyCb(ssl)) && ED25519_KEY_SIZE < ssl->options.minEccKeySz) { ret = ECC_KEY_SIZE_E; WOLFSSL_ERROR_VERBOSE(ret); @@ -17690,7 +17819,7 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args, /* check size of peer ECC key */ if (ret == 0 && ssl->peerEd448KeyPresent && - !ssl->options.verifyNone && + (!ssl->options.verifyNone || UsingChainVerifyCb(ssl)) && ED448_KEY_SIZE < ssl->options.minEccKeySz) { ret = ECC_KEY_SIZE_E; WOLFSSL_ERROR_VERBOSE(ret); @@ -17743,7 +17872,8 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args, /* check size of peer Falcon key */ if (ret == 0 && ssl->peerFalconKeyPresent && - !ssl->options.verifyNone && + (!ssl->options.verifyNone || + UsingChainVerifyCb(ssl)) && FALCON_MAX_KEY_SIZE < ssl->options.minFalconKeySz) { ret = FALCON_KEY_SIZE_E; @@ -17817,7 +17947,8 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args, /* check size of peer Dilithium key */ if (ret == 0 && ssl->peerMlDsaKeyPresent && - !ssl->options.verifyNone && + (!ssl->options.verifyNone || + UsingChainVerifyCb(ssl)) && MLDSA_MAX_KEY_SIZE < ssl->options.minMlDsaKeySz) { ret = MLDSA_KEY_SIZE_E; @@ -17924,137 +18055,17 @@ static int RpkIsTrusted(WOLFSSL* ssl, const byte* spki, word32 spkiSz) #endif /* HAVE_RPK */ #ifdef WOLFSSL_CHAIN_VERIFY_CB -/* The SSL object's callback and user context take precedence over the - * context's. */ -static ChainVerifyCb GetChainVerifyCb(const WOLFSSL* ssl) -{ - if (ssl->chainVerifyCb != NULL) - return ssl->chainVerifyCb; - return ssl->ctx->chainVerifyCb; -} - -static void* GetChainVerifyCtx(const WOLFSSL* ssl) -{ - if (ssl->chainVerifyCtx != NULL) - return ssl->chainVerifyCtx; - return ssl->ctx->chainVerifyCtx; -} - -/* Non-zero when the application has replaced chain verification. */ -#define UsingChainVerifyCb(ssl) (GetChainVerifyCb(ssl) != NULL) - -/* DTLS, raw public keys and OCSP stapling are not supported with the chain - * verify callback. Checked when the callback is set, against what the context - * or object is configured for, and again when the peer's certificates arrive, - * against what was negotiated, so that using them fails as early as - * possible. */ -#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ - defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) -static int ChainVerifyCbStaplingRequested(TLSX* extensions) -{ -#if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) - if (TLSX_Find(extensions, TLSX_STATUS_REQUEST) != NULL) - return 1; -#endif -#if !defined(NO_TLS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) - if (TLSX_Find(extensions, TLSX_STATUS_REQUEST_V2) != NULL) - return 1; -#endif - (void)extensions; - return 0; -} -#endif - -#ifdef HAVE_RPK -static int ChainVerifyCbRpkConfigured(const RpkConfig* cfg) -{ - int i; - - for (i = 0; i < cfg->preferred_ClientCertTypeCnt; i++) { - if (cfg->preferred_ClientCertTypes[i] == WOLFSSL_CERT_TYPE_RPK) - return 1; - } - for (i = 0; i < cfg->preferred_ServerCertTypeCnt; i++) { - if (cfg->preferred_ServerCertTypes[i] == WOLFSSL_CERT_TYPE_RPK) - return 1; - } - return 0; -} -#endif - -int ChainVerifyCbCheckCtx(const WOLFSSL_CTX* ctx) -{ -#ifdef WOLFSSL_DTLS - if (ctx->method->version.major == DTLS_MAJOR) { - WOLFSSL_MSG("DTLS not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; - } -#endif -#ifdef HAVE_RPK - if (ChainVerifyCbRpkConfigured(&ctx->rpkConfig)) { - WOLFSSL_MSG("Raw public key not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; - } -#endif -#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ - defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) - if ((ctx->method->side != WOLFSSL_SERVER_END) && - (((ctx->cm != NULL) && ctx->cm->ocspMustStaple) || - ChainVerifyCbStaplingRequested(ctx->extensions))) { - WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; - } -#endif - (void)ctx; - return 0; -} - -int ChainVerifyCbCheckSsl(const WOLFSSL* ssl) -{ -#ifdef WOLFSSL_DTLS - if (ssl->options.dtls) { - WOLFSSL_MSG("DTLS not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; - } -#endif -#ifdef HAVE_RPK - if (ChainVerifyCbRpkConfigured(&ssl->options.rpkConfig) || - ((ssl->options.side == WOLFSSL_CLIENT_END) && - (ssl->options.rpkState.received_ServerCertTypeCnt == 1) && - (ssl->options.rpkState.received_ServerCertTypes[0] == - WOLFSSL_CERT_TYPE_RPK)) || - ((ssl->options.side == WOLFSSL_SERVER_END) && - (ssl->options.rpkState.sending_ClientCertTypeCnt == 1) && - (ssl->options.rpkState.sending_ClientCertTypes[0] == - WOLFSSL_CERT_TYPE_RPK))) { - WOLFSSL_MSG("Raw public key not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; - } -#endif -#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ - defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) - if ((ssl->options.side != WOLFSSL_SERVER_END) && - (SSL_CM(ssl)->ocspMustStaple || - ChainVerifyCbStaplingRequested(ssl->extensions) || - ChainVerifyCbStaplingRequested(ssl->ctx->extensions))) { - WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; - } -#endif - (void)ssl; - return 0; -} - /* Errors from ParseCert(NO_VERIFY) that report content wolfSSL does not - * understand rather than malformed DER: an unknown critical extension, an - * unsupported key or signature algorithm, or mismatched signature algorithm - * identifiers. The certificate decoded; judging its content is the chain - * verify callback's job. */ + * understand or agree with rather than malformed DER: an unknown critical + * extension, an unsupported key or signature algorithm, mismatched signature + * algorithm identifiers, or keyCertSign asserted by a non-CA. The certificate + * decoded; judging its content is the chain verify callback's job. */ static int IsCertContentErr(int err) { return (err == WC_NO_ERR_TRACE(ASN_CRIT_EXT_E)) || (err == WC_NO_ERR_TRACE(ASN_SIG_OID_E)) || - (err == WC_NO_ERR_TRACE(ASN_UNKNOWN_OID_E)); + (err == WC_NO_ERR_TRACE(ASN_UNKNOWN_OID_E)) || + (err == WC_NO_ERR_TRACE(KEYUSAGE_E)); } /* Decode every certificate the peer sent, verifying nothing. The chain verify @@ -18142,8 +18153,6 @@ static int DoChainVerifyCb(WOLFSSL* ssl, ProcPeerCertArgs* args) WOLFSSL_ERROR_VERBOSE(CHAIN_VERIFY_CB_E); return CHAIN_VERIFY_CB_E; } -#else -#define UsingChainVerifyCb(ssl) 0 #endif /* WOLFSSL_CHAIN_VERIFY_CB */ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, @@ -18326,7 +18335,10 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, * can hold */ } #else - if (args->totalCerts >= ssl->verifyDepth || + /* The verify depth is chain-building policy, which the chain + * verify callback owns; MAX_CHAIN_DEPTH is the buffer. */ + if ((!UsingChainVerifyCb(ssl) && + args->totalCerts >= ssl->verifyDepth) || args->totalCerts >= MAX_CHAIN_DEPTH) { WOLFSSL_ERROR_VERBOSE(MAX_CHAIN_ERROR); ERROR_OUT(MAX_CHAIN_ERROR, exit_ppc); @@ -18956,6 +18968,13 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } } #endif + #ifdef WOLFSSL_CHAIN_VERIFY_CB + /* The callback accepted this certificate, content wolfSSL + * would object to included. Only what the handshake needs + * from it - the key - can still fail below. */ + if (UsingChainVerifyCb(ssl) && IsCertContentErr(ret)) + ret = 0; + #endif #ifdef WOLFSSL_ASYNC_CRYPT if (ret == WC_NO_ERR_TRACE(WC_PENDING_E)) goto exit_ppc; diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 6319d1a4e5b..0fd79cbfa9d 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3834,6 +3834,44 @@ int test_tls13_chain_verify_cb_postauth(void) return EXPECT_RESULT(); } +int test_chain_verify_cb_min_key(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && !defined(NO_RSA) && \ + (!defined(WOLFSSL_NO_TLS12) || defined(WOLFSSL_TLS13)) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; +#ifndef WOLFSSL_NO_TLS12 + method_provider client_method = wolfTLSv1_2_client_method; + method_provider server_method = wolfTLSv1_2_server_method; +#else + method_provider client_method = wolfTLSv1_3_client_method; + method_provider server_method = wolfTLSv1_3_server_method; +#endif + + /* The callback accepts, but the handshake uses the server's key directly, + * so the minimum key size still applies - even with verification turned + * off, which is not the callback's concern. */ + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + client_method, server_method, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + &test_ctx), TEST_SUCCESS); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + ExpectIntEQ(wolfSSL_SetMinRsaKey_Sz(ssl_c, 4096), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(RSA_KEY_SIZE_E)); + ExpectIntEQ(cbCtx.calls, 1); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + int test_chain_verify_cb_dtls(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 98082b40c04..61523172602 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -70,6 +70,7 @@ int test_tls13_chain_verify_cb_reject(void); int test_tls12_chain_verify_cb_bad_der(void); int test_tls12_chain_verify_cb_bad_chain(void); int test_tls13_chain_verify_cb_postauth(void); +int test_chain_verify_cb_min_key(void); int test_chain_verify_cb_dtls(void); int test_chain_verify_cb_stapling(void); int test_record_size_matches_build_message(void); @@ -133,6 +134,7 @@ int test_wolfSSL_get_shared_ciphers(void); TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_bad_der), \ TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_bad_chain), \ TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_postauth), \ + TEST_DECL_GROUP("tls", test_chain_verify_cb_min_key), \ TEST_DECL_GROUP("tls", test_chain_verify_cb_dtls), \ TEST_DECL_GROUP("tls", test_chain_verify_cb_stapling) diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 23ed3bb907d..c066773bc73 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1755,16 +1755,19 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * * wolfSSL still owns the parsing. Every certificate is decoded before the * callback runs, and malformed DER fails the handshake without the callback - * ever seeing it. Content wolfSSL does not understand - an unknown critical - * extension, an unsupported key or signature algorithm - is not a decoding - * failure; it is passed through for the callback to judge. + * ever seeing it. Content wolfSSL does not understand or agree with - an + * unknown critical extension, an unsupported key or signature algorithm, a + * key usage inconsistent with the basic constraints - is not a decoding + * failure; it is passed through for the callback to judge. The only limit on + * the chain is the compile-time MAX_CHAIN_DEPTH; the verify depth does not + * apply. * * Two more things still apply. An empty Certificate message is handled by * wolfSSL itself (see wolfSSL_CTX_set_verify() and the mutual-auth options) * and the callback is not called for it, so certsSz is always at least 1. The * minimum peer key sizes set by wolfSSL_CTX_SetMinRsaKey_Sz() and friends are - * still enforced on the peer's own certificate, because the handshake uses - * that key directly. + * still enforced on the peer's own certificate, even under + * WOLFSSL_VERIFY_NONE, because the handshake uses that key directly. * * Not supported with the callback: DTLS, raw public keys (RFC 7250) and OCSP * stapling. Setting the callback on a context or object already configured From 137e15e70c5e742a364eac3d734e9ec5b7670ffa Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 3 Sep 2026 18:36:18 +0000 Subject: [PATCH 4/9] Guard clear-chain-certs test helper like its only caller The helper is defined without the NO_WOLFSSL_CLIENT condition its caller has, so a build with the client compiled out fails on the unused function. Give the definition the same guard. --- tests/api.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/api.c b/tests/api.c index 3be65432775..42be86603e6 100644 --- a/tests/api.c +++ b/tests/api.c @@ -4626,7 +4626,8 @@ static int test_wolfSSL_clear_chain_certs(void) #if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && defined(OPENSSL_EXTRA) && \ defined(KEEP_OUR_CERT) && !defined(NO_RSA) && !defined(NO_TLS) && \ - !defined(NO_WOLFSSL_SERVER) && !defined(OPENSSL_COEXIST) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(OPENSSL_COEXIST) && \ (defined(OPENSSL_ALL) || defined(WOLFSSL_ASIO) || \ defined(WOLFSSL_HAPROXY) || defined(WOLFSSL_NGINX)) /* Server-side ssl_ready hook: add chain certs then clear them, so the From 537ceaa066634797cd455f4044edbda21b63c0b3 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 4 Sep 2026 05:45:56 +0000 Subject: [PATCH 5/9] Wrap two lines over 80 columns --- examples/configs/user_settings_all.h | 3 ++- src/internal.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/configs/user_settings_all.h b/examples/configs/user_settings_all.h index 0c040571175..71ccab5f1a0 100644 --- a/examples/configs/user_settings_all.h +++ b/examples/configs/user_settings_all.h @@ -126,7 +126,8 @@ extern "C" { #define HAVE_OID_ENCODING #define WOLFSSL_ASN_TEMPLATE #define WOLFSSL_ALT_NAMES /* Support subject alternative names extension */ -#define WOLFSSL_CHAIN_VERIFY_CB /* Replace peer chain verification with an application callback */ +/* Replace peer chain verification with an application callback */ +#define WOLFSSL_CHAIN_VERIFY_CB /* Certificate Revocation */ #define HAVE_OCSP diff --git a/src/internal.c b/src/internal.c index 9075fdf8888..bb89bda189a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19299,7 +19299,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } else #endif - if (args->dCert->extExtKeyUsageSet && !UsingChainVerifyCb(ssl)) { + if (args->dCert->extExtKeyUsageSet && + !UsingChainVerifyCb(ssl)) { if (ssl->options.side == WOLFSSL_CLIENT_END) { if ((args->dCert->extExtKeyUsage & (EXTKEYUSE_ANY | EXTKEYUSE_SERVER_AUTH)) == 0) { From 215cf4a5abe0c2f90c33f2664592d553341ae93c Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 4 Sep 2026 09:29:33 +0000 Subject: [PATCH 6/9] Hand the chain verify callback the certificate array directly args->certs is already an array of WOLFSSL_BUFFER_INFO, so the copy made for every call, including every re-entry after CHAIN_VERIFY_WANT_E, was an allocation and a MEMORY_E path for nothing. Check the unsupported-feature configuration before any certificate entry or its extensions is parsed. Checking after left a malformed status_request in a TLS 1.3 Certificate entry failing with the parser's error instead of the documented CHAIN_VERIFY_UNSUPPORTED_E. Document that a certificate the parser refuses regardless of the verify mode, such as one with a zero serial number, fails before the callback like malformed DER does. --- doc/dox_comments/header_files/ssl.h | 19 ++++++++-------- src/internal.c | 35 +++++++++++------------------ wolfssl/ssl.h | 18 ++++++++------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index a16727f6bc4..29ab74a33a3 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3197,15 +3197,16 @@ void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); own certificate first. It builds no chain, verifies no signature and checks no date, revocation status, key usage or host name, and the verify callback set with wolfSSL_CTX_set_verify() is not called; the callback is - consulted even under WOLFSSL_VERIFY_NONE. Malformed DER still fails the - handshake before the callback is called. The callback returns 0 to accept, - CHAIN_VERIFY_WANT_E to suspend the handshake until the application - re-enters wolfSSL_connect(), wolfSSL_accept(), wolfSSL_read() or - wolfSSL_write(), or any other value to reject with CHAIN_VERIFY_CB_E and a - fatal bad_certificate alert. DTLS, raw public keys and OCSP stapling are - not supported with the callback: setting it on a context configured for - one of them fails, and so does the handshake of a connection using one - of them, with CHAIN_VERIFY_UNSUPPORTED_E. Requires + consulted even under WOLFSSL_VERIFY_NONE. Malformed DER, or a certificate + the parser refuses regardless of the verify mode such as one with a zero + serial number, still fails the handshake before the callback is called. The + callback returns 0 to accept, CHAIN_VERIFY_WANT_E to suspend the handshake + until the application re-enters wolfSSL_connect(), wolfSSL_accept(), + wolfSSL_read() or wolfSSL_write(), or any other value to reject with + CHAIN_VERIFY_CB_E and a fatal bad_certificate alert. DTLS, raw public keys + and OCSP stapling are not supported with the callback: setting it on a + context configured for one of them fails, and so does the handshake of a + connection using one of them, with CHAIN_VERIFY_UNSUPPORTED_E. Requires WOLFSSL_CHAIN_VERIFY_CB (--enable-chain-verify-cb). \return WOLFSSL_SUCCESS on success. diff --git a/src/internal.c b/src/internal.c index bb89bda189a..63b5a06b2d8 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18117,29 +18117,11 @@ static int CheckPeerCertsDecode(WOLFSSL* ssl, ProcPeerCertArgs* args) * CHAIN_VERIFY_CB_E when rejected, or MEMORY_E. */ static int DoChainVerifyCb(WOLFSSL* ssl, ProcPeerCertArgs* args) { - WOLFSSL_BUFFER_INFO* certs = NULL; int ret; - int i; - - if (args->totalCerts > 0) { - certs = (WOLFSSL_BUFFER_INFO*)XMALLOC( - sizeof(WOLFSSL_BUFFER_INFO) * (size_t)args->totalCerts, ssl->heap, - DYNAMIC_TYPE_TMP_BUFFER); - if (certs == NULL) - return MEMORY_E; - - for (i = 0; i < args->totalCerts; i++) { - certs[i].buffer = args->certs[i].buffer; - certs[i].length = args->certs[i].length; - } - } WOLFSSL_MSG("Calling user chain verify callback"); - ret = GetChainVerifyCb(ssl)(ssl, certs, args->totalCerts, + ret = GetChainVerifyCb(ssl)(ssl, args->certs, args->totalCerts, GetChainVerifyCtx(ssl)); - - XFREE(certs, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - if (ret == 0) { WOLFSSL_MSG("\tuser chain verify callback accepted"); return 0; @@ -18277,6 +18259,17 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif + #ifdef WOLFSSL_CHAIN_VERIFY_CB + /* Before any certificate entry or its extensions is parsed. */ + if (UsingChainVerifyCb(ssl)) { + ret = ChainVerifyCbCheckSsl(ssl); + if (ret != 0) { + DoCertFatalAlert(ssl, ret); + goto exit_ppc; + } + } + #endif + /* allocate buffer for certs */ args->certs = (buffer*)XMALLOC(sizeof(buffer) * MAX_CHAIN_DEPTH, ssl->heap, DYNAMIC_TYPE_DER); @@ -18460,9 +18453,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, if (UsingChainVerifyCb(ssl)) { /* Check once, not again on every re-entry. */ if (ret == 0 && args->count > 0 && !args->chainDecoded) { - ret = ChainVerifyCbCheckSsl(ssl); - if (ret == 0) - ret = CheckPeerCertsDecode(ssl, args); + ret = CheckPeerCertsDecode(ssl, args); if (ret != 0) { args->fatal = 1; DoCertFatalAlert(ssl, ret); diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index c066773bc73..fc1c4ca835a 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1755,12 +1755,14 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * * wolfSSL still owns the parsing. Every certificate is decoded before the * callback runs, and malformed DER fails the handshake without the callback - * ever seeing it. Content wolfSSL does not understand or agree with - an - * unknown critical extension, an unsupported key or signature algorithm, a - * key usage inconsistent with the basic constraints - is not a decoding - * failure; it is passed through for the callback to judge. The only limit on - * the chain is the compile-time MAX_CHAIN_DEPTH; the verify depth does not - * apply. + * ever seeing it. So does a certificate the parser refuses regardless of the + * verify mode, such as one with a zero serial number (unless + * WOLFSSL_ASN_ALLOW_0_SERIAL is defined). Content wolfSSL does not understand + * or agree with - an unknown critical extension, an unsupported key or + * signature algorithm, a key usage inconsistent with the basic constraints - + * is not a decoding failure; it is passed through for the callback to judge. + * The only limit on the chain is the compile-time MAX_CHAIN_DEPTH; the verify + * depth does not apply. * * Two more things still apply. An empty Certificate message is handled by * wolfSSL itself (see wolfSSL_CTX_set_verify() and the mutual-auth options) @@ -1772,8 +1774,8 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * Not supported with the callback: DTLS, raw public keys (RFC 7250) and OCSP * stapling. Setting the callback on a context or object already configured * for one of them fails with CHAIN_VERIFY_UNSUPPORTED_E, and so does the - * handshake of a connection that uses one of them, before the callback is - * called. + * handshake of a connection that uses one of them, before its Certificate + * message is parsed. * * certs DER certificates in the order the peer sent them, one * WOLFSSL_BUFFER_INFO each: certs[0] is the peer's own certificate, From 4dcb2510a16c4b3e170dbe41a4d246ad759f144f Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 4 Sep 2026 11:10:58 +0000 Subject: [PATCH 7/9] Keep a suspended Certificate intact across wolfSSL_write() Sending frees the saved state of a Certificate the chain verify callback suspended: under async crypto SendData() frees ssl->async after building the record. Now wolfSSL_write() resumes the message through wolfSSL_negotiate() while the handshake is in progress, and fails with CHAIN_VERIFY_WANT_E after it, where only wolfSSL_read() resumes a post-handshake Certificate. A write duplicate treated the deferral as a fatal read error and stayed failed for good. Use IsHsSuspendErr() in that gate so no suspend result is handed over. --- doc/dox_comments/header_files/ssl.h | 22 ++++++++++++---------- src/internal.c | 23 +++++++++++++++++++++++ src/ssl_api_rw.c | 4 ++-- tests/api/test_tls.c | 23 +++++++++++++++++++++++ wolfssl/ssl.h | 3 ++- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 29ab74a33a3..4470c482209 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3190,19 +3190,21 @@ void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); /*! \ingroup CertsKeys - \brief Replaces wolfSSL's verification of the peer's certificate chain - with an application callback, for every SSL/TLS object created from the - context. When a callback is set, wolfSSL decodes the certificates from the + \brief Replaces wolfSSL's verification of the peer's certificate chain with + an application callback, for every SSL/TLS object created from the context. + When a callback is set, wolfSSL decodes the certificates from the Certificate message and hands them to the callback as raw DER, the peer's - own certificate first. It builds no chain, verifies no signature and - checks no date, revocation status, key usage or host name, and the verify - callback set with wolfSSL_CTX_set_verify() is not called; the callback is - consulted even under WOLFSSL_VERIFY_NONE. Malformed DER, or a certificate - the parser refuses regardless of the verify mode such as one with a zero - serial number, still fails the handshake before the callback is called. The + own certificate first. It builds no chain, verifies no signature and checks + no date, revocation status, key usage or host name, and the verify callback + set with wolfSSL_CTX_set_verify() is not called; the callback is consulted + even under WOLFSSL_VERIFY_NONE. Malformed DER, or a certificate the parser + refuses regardless of the verify mode such as one with a zero serial + number, still fails the handshake before the callback is called. The callback returns 0 to accept, CHAIN_VERIFY_WANT_E to suspend the handshake until the application re-enters wolfSSL_connect(), wolfSSL_accept(), - wolfSSL_read() or wolfSSL_write(), or any other value to reject with + wolfSSL_read() or wolfSSL_write() (a certificate received after the + handshake is resumed by wolfSSL_read() only, and wolfSSL_write() fails with + CHAIN_VERIFY_WANT_E until then), or any other value to reject with CHAIN_VERIFY_CB_E and a fatal bad_certificate alert. DTLS, raw public keys and OCSP stapling are not supported with the callback: setting it on a context configured for one of them fails, and so does the handshake of a diff --git a/src/internal.c b/src/internal.c index 63b5a06b2d8..f68452b9f89 100644 --- a/src/internal.c +++ b/src/internal.c @@ -29175,6 +29175,29 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz) } } +#ifdef WOLFSSL_CHAIN_VERIFY_CB + /* A Certificate suspended by the chain verify callback is resumed before + * anything is sent; sending would free its saved state. wolfSSL_negotiate() + * resumes it during the handshake, only wolfSSL_read() after it. */ + if (error == WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)) { + int err; + if (ssl->options.handShakeState == HANDSHAKE_DONE) { + WOLFSSL_MSG("Chain verify callback pending, use wolfSSL_read"); + return error; + } + WOLFSSL_MSG("Chain verify callback pending, trying to finish"); + if ((err = wolfSSL_negotiate(ssl)) != WOLFSSL_SUCCESS) { + #ifdef WOLFSSL_ASYNC_CRYPT + /* if async would block return WANT_WRITE */ + if (ssl->error == WC_NO_ERR_TRACE(WC_PENDING_E)) { + return WOLFSSL_CBIO_ERR_WANT_WRITE; + } + #endif + return err; + } + } +#endif + #ifdef WOLFSSL_EARLY_DATA if (ssl->options.side == WOLFSSL_CLIENT_END && ssl->earlyData != no_early_data && diff --git a/src/ssl_api_rw.c b/src/ssl_api_rw.c index 589c5950575..89626ed0b3d 100644 --- a/src/ssl_api_rw.c +++ b/src/ssl_api_rw.c @@ -592,8 +592,8 @@ static int wolfSSL_read_internal(WOLFSSL* ssl, void* data, size_t sz, int peek) if (ssl->dupWrite != NULL) { if ((ssl->error != 0) && (ssl->error != WC_NO_ERR_TRACE(WANT_READ)) - #ifdef WOLFSSL_ASYNC_CRYPT - && (ssl->error != WC_NO_ERR_TRACE(WC_PENDING_E)) + #ifdef WOLFSSL_HAVE_HS_SUSPEND + && !IsHsSuspendErr(ssl->error) #endif ) { int notifyErr; diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 0fd79cbfa9d..cdeacf8cc14 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3761,6 +3761,9 @@ int test_tls13_chain_verify_cb_postauth(void) struct test_memio_ctx test_ctx; test_chain_verify_cb_ctx cbCtx; char buf[8]; +#ifdef HAVE_WRITE_DUP + WOLFSSL* ssl_w = NULL; +#endif XMEMSET(&cbCtx, 0, sizeof(cbCtx)); cbCtx.deferrals = 2; @@ -3809,15 +3812,32 @@ int test_tls13_chain_verify_cb_postauth(void) WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)); ExpectIntEQ(cbCtx.calls, 1); + /* Only wolfSSL_read() resumes it: a write is refused, and the callback + * is not asked. */ + ExpectIntEQ(wolfSSL_write(ssl_s, "hi", 2), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), + WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)); + ExpectIntEQ(cbCtx.calls, 1); + /* wolfSSL_accept() meanwhile must leave the suspended message alone. */ ExpectIntEQ(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); +#ifdef HAVE_WRITE_DUP + ExpectNotNull(ssl_w = wolfSSL_write_dup(ssl_s)); +#endif + /* Reading again asks the callback again: still deferred. */ ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), -1); ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E)); ExpectIntEQ(cbCtx.calls, 2); +#ifdef HAVE_WRITE_DUP + /* The deferral is not a read error handed to the write duplicate. */ + ExpectIntEQ(wolfSSL_write(ssl_w, "hi", 2), 2); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), 2); +#endif + /* Accepted; CertificateVerify and Finished follow, then nothing to read. */ ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), -1); ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); @@ -3829,6 +3849,9 @@ int test_tls13_chain_verify_cb_postauth(void) ExpectIntEQ(wolfSSL_write(ssl_c, "hi", 2), 2); ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), 2); +#ifdef HAVE_WRITE_DUP + wolfSSL_free(ssl_w); +#endif test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); #endif return EXPECT_RESULT(); diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index fc1c4ca835a..a1836b38569 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1795,7 +1795,8 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * certificates when the application re-enters wolfSSL_connect(), * wolfSSL_accept(), wolfSSL_read() or wolfSSL_write(). A certificate received * after the handshake (TLS 1.3 post-handshake authentication) arrives inside - * wolfSSL_read() and is resumed by calling wolfSSL_read() again. + * wolfSSL_read() and is resumed by calling wolfSSL_read() again; until then + * wolfSSL_write() fails with CHAIN_VERIFY_WANT_E as well. */ typedef int (*ChainVerifyCb)(WOLFSSL* ssl, const WOLFSSL_BUFFER_INFO* certs, int certsSz, void* ctx); From dd5fd3a905c105e8a19904578c68ea47d77b763e Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 7 Sep 2026 10:12:47 +0000 Subject: [PATCH 8/9] Document what accepting means and re-check the callback's restrictions Documentation: - wolfSSL_get_verify_result() reports WOLFSSL_X509_V_OK once the callback accepts. wolfSSL verified nothing, so say that this records the callback's verdict rather than a check wolfSSL made. - The OCSP stapling restriction is client-side. Both guards test side != WOLFSSL_SERVER_END: a server staples its own status, which is not part of verifying the peer. A server that asks the client to staple is not refused, and no stapled response is checked for it either, because ProcessPeerCertLeafRevocation is skipped with the callback. - The list of what still applies was neither "two" nor complete. Add PEER_KEY_ERROR and SCR_DIFFERENT_CERT_E, note MAX_CERTIFICATE_SZ next to MAX_CHAIN_DEPTH, and state that 0 is the only accepting value, so returning WOLFSSL_SUCCESS rejects. - DoChainVerifyCb no longer allocates; drop MEMORY_E from its comment. Code: - NULL-check the certificate manager in ChainVerifyCbCheckSsl, as ChainVerifyCbCheckCtx already does for ctx->cm. - Re-run ChainVerifyCbCheckSsl in TLS_ASYNC_BUILD. A deferred verdict resumes there, so TLS_ASYNC_BEGIN ran on the first pass only. Tests: - Pin both halves of the headline contract on the accept path: the verify callback set with wolfSSL_set_verify() is not called, and wolfSSL_get_verify_result() reports WOLFSSL_X509_V_OK. - Cover a callback returning WOLFSSL_SUCCESS, which rejects. - Rename test_tls13_chain_verify_cb_async to _defer. It exercises deferral over TLS 1.3, not async crypto. CI: - Add chain-verify-cb-async, the first build combining the callback with WOLFSSL_ASYNC_CRYPT, and the only one with async crypt and CRL but without WOLFSSL_NONBLOCK_OCSP. Three behaviour changes to existing configurations, introduced earlier in this series and not called out until now: - IsHsSuspendErr() recognizes OCSP_WANT_READ only under WOLFSSL_NONBLOCK_OCSP, where the sites it replaced tested WOLFSSL_ASYNC_CRYPT || WOLFSSL_NONBLOCK_OCSP. The keep-state re-entry branch it replaced was itself under WOLFSSL_NONBLOCK_OCSP, so an async-only build rewound into reset args and a freed pendingMsg. It now fails cleanly instead. - DoCertificate no longer advances serverState to SERVER_CERT_COMPLETE when ProcessPeerCerts suspended. - ReceiveData admits a suspended handshake error rather than refusing the read, which under WOLFSSL_NONBLOCK_OCSP now includes OCSP_WANT_READ. --- .github/configs/os-check-linux.json | 5 +++ doc/dox_comments/header_files/ssl.h | 22 +++++---- src/internal.c | 48 ++++++++++++++------ tests/api/test_tls.c | 70 +++++++++++++++++++++++++++-- tests/api/test_tls.h | 6 ++- wolfssl/ssl.h | 43 ++++++++++++------ 6 files changed, 153 insertions(+), 41 deletions(-) diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index c8102748171..903fa29be18 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -90,6 +90,11 @@ "comment": "chain verify callback alongside the other handshake-suspend feature, and DTLS.", "configure": ["--enable-chain-verify-cb", "--enable-ocsp", "--enable-crl", "--enable-dtls", "CPPFLAGS=-DWOLFSSL_NONBLOCK_OCSP"]}, +{"name": "chain-verify-cb-async", "minutes": 3.5, + "comment": "chain verify callback with async crypt, the third handshake-suspend feature. Also the only leg with async crypt and CRL but without WOLFSSL_NONBLOCK_OCSP.", + "configure": ["--enable-chain-verify-cb", "--enable-asynccrypt", + "--enable-asynccrypt-sw", "--enable-ocsp", "--enable-crl", + "--enable-opensslextra", "--enable-sessioncerts"]}, {"name": "tsp-verifier", "minutes": 3, "comment": "Time-Stamp Protocol Verifier", "configure": ["--enable-tsp", "--enable-opensslall", diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 4470c482209..1fb6cc5cdd2 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3205,16 +3205,22 @@ void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); wolfSSL_read() or wolfSSL_write() (a certificate received after the handshake is resumed by wolfSSL_read() only, and wolfSSL_write() fails with CHAIN_VERIFY_WANT_E until then), or any other value to reject with - CHAIN_VERIFY_CB_E and a fatal bad_certificate alert. DTLS, raw public keys - and OCSP stapling are not supported with the callback: setting it on a - context configured for one of them fails, and so does the handshake of a - connection using one of them, with CHAIN_VERIFY_UNSUPPORTED_E. Requires + CHAIN_VERIFY_CB_E and a fatal bad_certificate alert; 0 is the only + accepting value, so returning WOLFSSL_SUCCESS rejects. Because wolfSSL + checks nothing itself, wolfSSL_get_verify_result() reports + WOLFSSL_X509_V_OK once the callback accepts, recording its verdict rather + than a verification wolfSSL performed. DTLS, raw public keys and verifying + a stapled OCSP response are not supported with the callback: setting it on + a context configured for one of them fails, and so does the handshake of a + connection using one of them, with CHAIN_VERIFY_UNSUPPORTED_E. The + stapling check covers the client, the side that verifies a stapled + response; a server may still staple its own status. Requires WOLFSSL_CHAIN_VERIFY_CB (--enable-chain-verify-cb). \return WOLFSSL_SUCCESS on success. \return BAD_FUNC_ARG when ctx is NULL. \return CHAIN_VERIFY_UNSUPPORTED_E when the context uses a DTLS method, - raw public keys or OCSP stapling. + raw public keys or client-side OCSP stapling. \param ctx pointer to the SSL context, created with wolfSSL_CTX_new(). \param cb the callback, or NULL to clear it. @@ -3232,7 +3238,7 @@ void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); ... WOLFSSL_CTX* ctx = wolfSSL_CTX_new(method); if (wolfSSL_CTX_SetChainVerifyCb(ctx, myChainVerify) != WOLFSSL_SUCCESS) { - // context uses DTLS, raw public keys or OCSP stapling + // context uses DTLS, raw public keys or client-side stapling } \endcode @@ -3253,7 +3259,7 @@ int wolfSSL_CTX_SetChainVerifyCb(WOLFSSL_CTX* ctx, ChainVerifyCb cb); \return WOLFSSL_SUCCESS on success. \return BAD_FUNC_ARG when ssl is NULL. \return CHAIN_VERIFY_UNSUPPORTED_E when the object uses DTLS, raw public - keys or OCSP stapling. + keys or client-side OCSP stapling. \param ssl pointer to the SSL session, created with wolfSSL_new(). \param cb the callback, or NULL to fall back to the context's. @@ -3262,7 +3268,7 @@ int wolfSSL_CTX_SetChainVerifyCb(WOLFSSL_CTX* ctx, ChainVerifyCb cb); \code WOLFSSL* ssl = wolfSSL_new(ctx); if (wolfSSL_SetChainVerifyCb(ssl, myChainVerify) != WOLFSSL_SUCCESS) { - // object uses DTLS, raw public keys or OCSP stapling + // object uses DTLS, raw public keys or client-side stapling } \endcode diff --git a/src/internal.c b/src/internal.c index f68452b9f89..db5c3efb767 100644 --- a/src/internal.c +++ b/src/internal.c @@ -664,11 +664,12 @@ int IsHsSuspendErr(int err) } #ifdef WOLFSSL_CHAIN_VERIFY_CB -/* DTLS, raw public keys and OCSP stapling are not supported with the chain - * verify callback. Checked when the callback is set, against what the context - * or object is configured for, and again when the peer's certificates arrive, - * against what was negotiated, so that using them fails as early as - * possible. */ +/* DTLS, raw public keys and verifying a stapled OCSP response are not + * supported with the chain verify callback. Checked when the callback is set, + * against what the context or object is configured for, and again when the + * peer's certificates arrive, against what was negotiated, so that using them + * fails as early as possible. The stapling check is client-side: a server + * staples its own status, which is not part of verifying the peer. */ #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) static int ChainVerifyCbStaplingRequested(TLSX* extensions) @@ -754,12 +755,16 @@ int ChainVerifyCbCheckSsl(const WOLFSSL* ssl) #endif #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) - if ((ssl->options.side != WOLFSSL_SERVER_END) && - (SSL_CM(ssl)->ocspMustStaple || - ChainVerifyCbStaplingRequested(ssl->extensions) || - ChainVerifyCbStaplingRequested(ssl->ctx->extensions))) { - WOLFSSL_MSG("OCSP stapling not supported with chain verify callback"); - return CHAIN_VERIFY_UNSUPPORTED_E; + if (ssl->options.side != WOLFSSL_SERVER_END) { + const WOLFSSL_CERT_MANAGER* cm = SSL_CM(ssl); + + if (((cm != NULL) && cm->ocspMustStaple) || + ChainVerifyCbStaplingRequested(ssl->extensions) || + ChainVerifyCbStaplingRequested(ssl->ctx->extensions)) { + WOLFSSL_MSG("OCSP stapling not supported with chain verify " + "callback"); + return CHAIN_VERIFY_UNSUPPORTED_E; + } } #endif (void)ssl; @@ -18113,8 +18118,9 @@ static int CheckPeerCertsDecode(WOLFSSL* ssl, ProcPeerCertArgs* args) * chain verification completely. Called once per Certificate message, and * again on every re-entry while the callback defers its verdict. * - * Returns 0 when accepted, CHAIN_VERIFY_WANT_E to suspend the handshake, - * CHAIN_VERIFY_CB_E when rejected, or MEMORY_E. */ + * Returns 0 when accepted, CHAIN_VERIFY_WANT_E to suspend the handshake, or + * CHAIN_VERIFY_CB_E when rejected. Only 0 accepts; every other value the + * callback returns, WOLFSSL_SUCCESS included, rejects. */ static int DoChainVerifyCb(WOLFSSL* ssl, ProcPeerCertArgs* args) { int ret; @@ -18260,7 +18266,9 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif #ifdef WOLFSSL_CHAIN_VERIFY_CB - /* Before any certificate entry or its extensions is parsed. */ + /* Before any certificate entry or its extensions is parsed. + * TLS_ASYNC_BUILD checks again, for the passes that skip this + * state. */ if (UsingChainVerifyCb(ssl)) { ret = ChainVerifyCbCheckSsl(ssl); if (ret != 0) { @@ -18451,6 +18459,18 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, * past MAX_CHAIN_DEPTH) means it would be handed an incomplete * list. That error is kept and reported by the check below. */ if (UsingChainVerifyCb(ssl)) { + /* A deferred verdict resumes at this state, so TLS_ASYNC_BEGIN + * ran only on the first pass. Re-check what the connection is + * configured for; the callback runs application code that can + * change it. */ + if (ret == 0) { + ret = ChainVerifyCbCheckSsl(ssl); + if (ret != 0) { + args->fatal = 1; + DoCertFatalAlert(ssl, ret); + goto exit_ppc; + } + } /* Check once, not again on every re-entry. */ if (ret == 0 && args->count > 0 && !args->chainDecoded) { ret = CheckPeerCertsDecode(ssl, args); diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index cdeacf8cc14..672d78affcb 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3392,6 +3392,7 @@ typedef struct test_chain_verify_cb_ctx { int calls; /* how many times the callback ran */ int deferrals; /* how many times to answer CHAIN_VERIFY_WANT_E first */ int reject; /* non-zero to reject the chain */ + int rejectRet; /* value to reject with, 0 for the default -1 */ int certsSeen; /* certsSz of the last call */ int leafMatched; /* certs[0] matched the expected leaf DER */ /* A copy of the expected leaf: the peer may unload its own certificate @@ -3431,11 +3432,25 @@ static int test_chain_verify_cb(WOLFSSL* ssl, const WOLFSSL_BUFFER_INFO* certs, if (cbCtx->calls <= cbCtx->deferrals) return WC_NO_ERR_TRACE(CHAIN_VERIFY_WANT_E); if (cbCtx->reject) - return -1; + return (cbCtx->rejectRet != 0) ? cbCtx->rejectRet : -1; return 0; } +/* F5: the ordinary verify callback must not run while the chain verify + * callback owns the verdict. This one rejects, so being called at all fails + * the handshake as well as showing up in the count. */ +static int test_chain_verify_cb_verify_calls; + +static int test_chain_verify_cb_verify(int preverify, + WOLFSSL_X509_STORE_CTX* store) +{ + (void)preverify; + (void)store; + test_chain_verify_cb_verify_calls++; + return 0; +} + /* Which end of the connection verifies with the callback. */ #define TEST_CVC_CLIENT 0 #define TEST_CVC_SERVER 1 @@ -3456,7 +3471,12 @@ static int test_chain_verify_cb_run(test_chain_verify_cb_ctx* cbCtx, int side, EXPECT_DECLS; WOLFSSL* verifier = NULL; WOLFSSL* peer = NULL; + /* Only when the chain verify callback is installed: the negative control + * relies on wolfSSL's own verification failing. */ + VerifyCallback verifyCb = (cbCtx != NULL) ? test_chain_verify_cb_verify + : NULL; + test_chain_verify_cb_verify_calls = 0; XMEMSET(test_ctx, 0, sizeof(*test_ctx)); ExpectIntEQ(test_memio_setup(test_ctx, ctx_c, ctx_s, ssl_c, ssl_s, client_method, server_method), 0); @@ -3465,7 +3485,7 @@ static int test_chain_verify_cb_run(test_chain_verify_cb_ctx* cbCtx, int side, ExpectIntEQ(wolfSSL_CTX_UnloadCAs(*ctx_c), WOLFSSL_SUCCESS); /* explicit, since OPENSSL_COMPATIBLE_DEFAULTS turns verification off * on clients by default */ - wolfSSL_set_verify(*ssl_c, WOLFSSL_VERIFY_PEER, NULL); + wolfSSL_set_verify(*ssl_c, WOLFSSL_VERIFY_PEER, verifyCb); verifier = *ssl_c; peer = *ssl_s; } @@ -3475,7 +3495,8 @@ static int test_chain_verify_cb_run(test_chain_verify_cb_ctx* cbCtx, int side, ExpectIntEQ(wolfSSL_use_PrivateKey_file(*ssl_c, cliKeyFile, WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); wolfSSL_set_verify(*ssl_s, - WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verifyCb); verifier = *ssl_s; peer = *ssl_c; } @@ -3558,6 +3579,16 @@ static int test_chain_verify_cb_accept(int side, int deferrals, ExpectIntGT(cbCtx.certsSeen, 0); ExpectIntEQ(cbCtx.leafMatched, 1); + /* The callback replaces wolfSSL's decision entirely, so the verify + * callback set with wolfSSL_set_verify() is never consulted. */ + ExpectIntEQ(test_chain_verify_cb_verify_calls, 0); +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + /* Accepting reports success even though wolfSSL verified nothing: this + * records the callback's verdict, not a check wolfSSL made. */ + ExpectIntEQ(wolfSSL_get_verify_result( + (side == TEST_CVC_CLIENT) ? ssl_c : ssl_s), WOLFSSL_X509_V_OK); +#endif + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); return EXPECT_RESULT(); } @@ -3602,7 +3633,7 @@ int test_tls12_chain_verify_cb_server(void) return EXPECT_RESULT(); } -int test_tls13_chain_verify_cb_async(void) +int test_tls13_chain_verify_cb_defer(void) { EXPECT_DECLS; #if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_TLS13) @@ -3664,6 +3695,37 @@ int test_tls13_chain_verify_cb_reject(void) return EXPECT_RESULT(); } +int test_tls13_chain_verify_cb_success_rejects(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHAIN_VERIFY_CB_TESTS) && defined(WOLFSSL_TLS13) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + test_chain_verify_cb_ctx cbCtx; + + XMEMSET(&cbCtx, 0, sizeof(cbCtx)); + cbCtx.reject = 1; + /* Every other wolfSSL verify callback accepts with WOLFSSL_SUCCESS. This + * one accepts on 0 only, so 1 has to reject. */ + cbCtx.rejectRet = WOLFSSL_SUCCESS; + + ExpectIntEQ(test_chain_verify_cb_run(&cbCtx, TEST_CVC_CLIENT, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, + &ctx_c, &ctx_s, &ssl_c, &ssl_s, &test_ctx), TEST_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + ExpectIntEQ(cbCtx.calls, 1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(CHAIN_VERIFY_CB_E)); + + test_chain_verify_cb_free(&ctx_c, &ctx_s, &ssl_c, &ssl_s); +#endif + return EXPECT_RESULT(); +} + int test_tls12_chain_verify_cb_bad_der(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index 61523172602..7c942c7f4de 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -64,9 +64,10 @@ int test_wolfSSL_alert_type_string(void); int test_wolfSSL_alert_desc_string(void); int test_tls12_chain_verify_cb(void); int test_tls12_chain_verify_cb_server(void); -int test_tls13_chain_verify_cb_async(void); +int test_tls13_chain_verify_cb_defer(void); int test_tls13_chain_verify_cb_server(void); int test_tls13_chain_verify_cb_reject(void); +int test_tls13_chain_verify_cb_success_rejects(void); int test_tls12_chain_verify_cb_bad_der(void); int test_tls12_chain_verify_cb_bad_chain(void); int test_tls13_chain_verify_cb_postauth(void); @@ -128,9 +129,10 @@ int test_wolfSSL_get_shared_ciphers(void); TEST_DECL_GROUP("tls", test_wolfSSL_get_shared_ciphers), \ TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb), \ TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_server), \ - TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_async), \ + TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_defer), \ TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_server), \ TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_reject), \ + TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_success_rejects), \ TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_bad_der), \ TEST_DECL_GROUP("tls", test_tls12_chain_verify_cb_bad_chain), \ TEST_DECL_GROUP("tls", test_tls13_chain_verify_cb_postauth), \ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index a1836b38569..59e2b049564 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1753,6 +1753,11 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * consulted even when verification was turned off with WOLFSSL_VERIFY_NONE, * and a rejection fails the handshake either way. * + * Because wolfSSL checks nothing itself, wolfSSL_get_verify_result() reports + * WOLFSSL_X509_V_OK once the callback accepts. That records the callback's + * verdict, not a verification wolfSSL performed. A rejection leaves + * WOLFSSL_X509_V_ERR_CERT_REJECTED. + * * wolfSSL still owns the parsing. Every certificate is decoded before the * callback runs, and malformed DER fails the handshake without the callback * ever seeing it. So does a certificate the parser refuses regardless of the @@ -1761,21 +1766,32 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * or agree with - an unknown critical extension, an unsupported key or * signature algorithm, a key usage inconsistent with the basic constraints - * is not a decoding failure; it is passed through for the callback to judge. - * The only limit on the chain is the compile-time MAX_CHAIN_DEPTH; the verify + * The chain is bounded by the compile-time MAX_CHAIN_DEPTH and by + * MAX_CERTIFICATE_SZ, the largest Certificate message accepted; the verify * depth does not apply. * - * Two more things still apply. An empty Certificate message is handled by - * wolfSSL itself (see wolfSSL_CTX_set_verify() and the mutual-auth options) - * and the callback is not called for it, so certsSz is always at least 1. The - * minimum peer key sizes set by wolfSSL_CTX_SetMinRsaKey_Sz() and friends are - * still enforced on the peer's own certificate, even under - * WOLFSSL_VERIFY_NONE, because the handshake uses that key directly. + * Some things still apply. An empty Certificate message is handled by wolfSSL + * itself (see wolfSSL_CTX_set_verify() and the mutual-auth options) and the + * callback is not called for it, so certsSz is always at least 1. The minimum + * peer key sizes set by wolfSSL_CTX_SetMinRsaKey_Sz() and friends are still + * enforced on the peer's own certificate, even under WOLFSSL_VERIFY_NONE, + * because the handshake uses that key directly, and so is that key being + * usable at all: one wolfSSL cannot decode fails with PEER_KEY_ERROR after the + * callback accepted. Under secure renegotiation, a peer that renegotiates with + * a different certificate still fails with SCR_DIFFERENT_CERT_E. + * + * Not supported with the callback: DTLS, raw public keys (RFC 7250) and + * verifying a stapled OCSP response. Setting the callback on a context or + * object already configured for one of them fails with + * CHAIN_VERIFY_UNSUPPORTED_E, and so does the handshake of a connection that + * uses one of them, before its Certificate message is parsed. * - * Not supported with the callback: DTLS, raw public keys (RFC 7250) and OCSP - * stapling. Setting the callback on a context or object already configured - * for one of them fails with CHAIN_VERIFY_UNSUPPORTED_E, and so does the - * handshake of a connection that uses one of them, before its Certificate - * message is parsed. + * That stapling check covers the side which verifies a stapled response, the + * client. A server may still staple its own status, which is not part of + * verifying the peer. A server that asks the client to staple instead (a + * status_request in its CertificateRequest, under TLS 1.3 post-handshake + * authentication) is not refused, but no stapled response is checked for it + * either; the callback owns the trust decision. * * certs DER certificates in the order the peer sent them, one * WOLFSSL_BUFFER_INFO each: certs[0] is the peer's own certificate, @@ -1787,7 +1803,8 @@ WOLFSSL_API void wolfSSL_CTX_SetCertCbCtx(WOLFSSL_CTX* ctx, void* userCtx); * wolfSSL_CTX_SetChainVerifyCtx(). * * Return 0 to accept, CHAIN_VERIFY_WANT_E to suspend the handshake, or any - * other value to reject. Rejection fails the handshake with + * other value to reject; 0 is the only accepting value, so returning + * WOLFSSL_SUCCESS (1) rejects. Rejection fails the handshake with * CHAIN_VERIFY_CB_E and sends one fatal bad_certificate alert; the returned * value is not reported to the peer. * From 4f77e9da12472834dee37fde9bbb29b318d79ca2 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 7 Sep 2026 13:59:51 +0000 Subject: [PATCH 9/9] Let an accepted chain clear an earlier verify result wolfSSL_get_verify_result() only recorded WOLFSSL_X509_V_OK when nothing had been recorded yet, so a result already on the object - set with wolfSSL_set_verify_result(), or left by an earlier certificate - survived the callback accepting and was reported instead of its verdict. Rejecting kept the same stale value for the same reason. The callback judges the chain as a whole, so let its verdict replace what is there rather than keeping the first error. The accept and reject tests now seed a stale result first, which is what catches this. --- src/internal.c | 13 +++++++++---- tests/api/test_tls.c | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/internal.c b/src/internal.c index db5c3efb767..29c9c15b3b0 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18487,13 +18487,18 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, args->fatal = 1; #if defined(OPENSSL_EXTRA) || \ defined(OPENSSL_EXTRA_X509_SMALL) - if (ssl->peerVerifyRet == 0) { - ssl->peerVerifyRet = - WOLFSSL_X509_V_ERR_CERT_REJECTED; - } + /* The callback judges the chain as a whole, so its + * verdict replaces any result recorded earlier + * instead of keeping the first error. */ + ssl->peerVerifyRet = WOLFSSL_X509_V_ERR_CERT_REJECTED; #endif DoCertFatalAlert(ssl, ret); } + #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + else if (ret == 0) { + ssl->peerVerifyRet = WOLFSSL_X509_V_OK; + } + #endif if (ret != 0) goto exit_ppc; } diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 672d78affcb..93fdb4a9c14 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -3516,6 +3516,13 @@ static int test_chain_verify_cb_run(test_chain_verify_cb_ctx* cbCtx, int side, } ExpectPtrEq(wolfSSL_GetChainVerifyCtx(verifier), cbCtx); +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + /* Seed a stale result, so the callback's verdict has to replace it + * rather than being reported as this earlier failure. */ + wolfSSL_set_verify_result(verifier, + WOLFSSL_X509_V_ERR_CERT_HAS_EXPIRED); +#endif + /* certs[0] must be the peer's own certificate. */ ExpectIntEQ(test_chain_verify_cb_set_leaf(cbCtx, peer->buffers.certificate), 0);