From b4e77e895125ace3740356a8633680c43ffb301b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 7 Sep 2026 10:12:47 +0000 Subject: [PATCH] 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 b93848ef5b..0f3b79a156 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -91,6 +91,11 @@ "CPPFLAGS=-DWOLFSSL_PUBLIC_MP -DWOLFSSL_DEBUG_NONBLOCK"]}, {"name": "all-asn-template", "minutes": 3.5, "configure": ["--enable-all", "--enable-asn=template"]}, +{"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": "tls13-dtls13-session-misc", "minutes": 3.3, "configure": ["--enable-tls13", "--enable-session-ticket", "--enable-dtls", "--enable-dtls13", "--enable-opensslextra", "--enable-sessioncerts", diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index fa2f3b16ea..79ff122d2a 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3348,16 +3348,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. @@ -3375,7 +3381,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 @@ -3396,7 +3402,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. @@ -3405,7 +3411,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 9cf020c1f1..11dd266de2 100644 --- a/src/internal.c +++ b/src/internal.c @@ -670,11 +670,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) @@ -760,12 +761,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; @@ -18712,8 +18717,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; @@ -18859,7 +18865,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) { @@ -19073,6 +19081,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 111f4f0caf..4d57134218 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -5390,6 +5390,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 @@ -5429,11 +5430,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 @@ -5454,7 +5469,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); @@ -5463,7 +5483,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; } @@ -5473,7 +5493,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; } @@ -5556,6 +5577,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(); } @@ -5600,7 +5631,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) @@ -5662,6 +5693,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 108df35725..2d2bfb86af 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -82,9 +82,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); @@ -168,9 +169,10 @@ int test_tls_param_flags_crl_check(void); TEST_DECL_GROUP("tls", test_dhe_psk_max_premaster), \ 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 f887a4c3ba..8da1406cb2 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -1779,6 +1779,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 @@ -1787,21 +1792,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 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. + * 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. + * + * 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, @@ -1813,7 +1829,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. *