From 191ec409b0152e27b43f7297949d5140049ef495 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Wed, 21 Oct 2026 07:19:30 +0900 Subject: [PATCH 1/9] F-1447 - Do not cache unsigned chain CAs after a verification override A chain certificate that fails verification with either a missing signer or a self-signed chain is not rooted in a trusted certificate. If the application's verification callback overrides the error, the certificate is still added to the context's certificate manager as a chain CA. Since these entries act as trust anchors for all subsequent connections using the same context, and the verification callback is only invoked on errors, the application is never asked to verify that certificate again. Record the verification result before invoking the callback and skip adding the certificate for these two error codes. This prevents caching through both the AddCA path and the pending CA path for Certificate Status Request v2. Other errors remain unchanged because the certificate is rooted in a trusted anchor, and the rest of the chain is verified against the cached entry. --- src/internal.c | 12 ++++++ tests/api/test_ssl_cert.c | 90 +++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 5 ++- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index e4585216985..59e75260d21 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19259,6 +19259,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif /* WOLFSSL_TRUST_PEER_CERT */ ) { int skipAddCA = 0; + int preCbRet = 0; #if defined(HAVE_OCSP) && defined(HAVE_CRL) int ocspNoUrl = 0; int ocspStapleDeferred = 0; @@ -19570,6 +19571,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif /* defined(__APPLE__) && defined(WOLFSSL_SYS_CA_CERTS) */ /* Do verify callback */ + preCbRet = ret; ret = DoVerifyCallback(SSL_CM(ssl), ssl, ret, args); if (ssl->options.verifyNone && (ret == WC_NO_ERR_TRACE(CRL_MISSING) || @@ -19585,6 +19587,16 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, skipAddCA = 1; } #endif + + /* An unsigned chain cert the verify callback waved through + * stays out of the cache: cached CAs are trust anchors for + * every later connection and the callback only runs on an + * error. Other errors still chain to a trusted anchor, and + * the rest of the chain needs the cached entry. */ + if (preCbRet == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E) || + preCbRet == WC_NO_ERR_TRACE(ASN_SELF_SIGNED_E)) { + skipAddCA = 1; + } #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) if (ret == 0 && addToPendingCAs && !alreadySigner && !ssl->options.verifyNone && !skipAddCA) { diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 68dcedca73a..2317fa9e461 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -5262,3 +5262,93 @@ int test_wolfSSL_dtls_api_more_guards(void) #endif return EXPECT_RESULT(); } + +/* Accept every certificate the way an application that makes its own trust + * decision about the peer does. */ +static int test_chain_ca_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* s) +{ + (void)preverify; + (void)s; + return 1; +} + +/* Test that a chain certificate which failed verification is not cached as a + * CA when the application's verify callback overrides the error. + * + * The client trusts the test root but not the intermediate that issued the + * server's chain certificate, so that certificate fails with ASN_NO_SIGNER_E. + * A callback returning 1 carries this handshake past the failure, which is + * what the application asked for. It must not also leave the certificate in + * the context's certificate manager: entries there are trust anchors for every + * later connection out of the same context, and the verify callback only runs + * on an error, so nothing would ask the application about it again. + * + * @return TEST_SUCCESS on success. + */ +int test_wolfSSL_chain_ca_not_cached_on_override(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WOLFSSL_PEM_TO_DER) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + struct test_memio_ctx test_ctx; + /* The leaf and the intermediate that signed it. The issuer of that + * intermediate is a second intermediate the client does not hold, so the + * chain certificate has no signer. */ + const char* svrChain = "./certs/intermediate/server-chain-short.pem"; + const char* svrKey = "./certs/server-key.pem"; + const char* leafCert = "./certs/intermediate/server-int-cert.pem"; + const char* rootCert = "./certs/ca-cert.pem"; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + /* Build both contexts here so that test_memio_setup() only creates the + * objects: the client gets the root alone and the server presents the + * chain rather than the default certificate. */ + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_2_server_method())); + if ((ctx_c != NULL) && (ctx_s != NULL)) { + wolfSSL_SetIORecv(ctx_c, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_c, test_memio_write_cb); + wolfSSL_SetIORecv(ctx_s, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_s, test_memio_write_cb); + } + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_c, rootCert, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(ctx_s, svrChain), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_s, svrKey, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + + ExpectNotNull(cm = wolfSSL_CTX_GetCertManager(ctx_c)); + /* The leaf does not chain to anything the client holds yet, so the check + * after the handshake is reporting a change and not a constant. */ + ExpectIntNE(wolfSSL_CertManagerVerify(cm, leafCert, WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, test_chain_ca_override_cb); + + /* The override carries the handshake past the unverifiable chain cert. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* That certificate must not have become a trust anchor: a leaf it issued + * still has no signer in the client context's certificate manager. */ + ExpectIntNE(wolfSSL_CertManagerVerify(cm, leafCert, WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index bb6978ea123..f6d7dc17b3a 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -32,6 +32,7 @@ int test_wolfSSL_load_pathological_files(void); int test_wolfSSL_load_from_fifo(void); int test_wolfSSL_dtls_api_more_guards(void); int test_wolfSSL_alloc_failure_sweep(void); +int test_wolfSSL_chain_ca_not_cached_on_override(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -141,6 +142,8 @@ int test_wolfSSL_SESSION_get0_peer_full_chain(void); TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_pathological_files), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_from_fifo), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_more_guards), \ - TEST_DECL_GROUP("ssl_cert", test_wolfSSL_alloc_failure_sweep) + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_alloc_failure_sweep), \ + TEST_DECL_GROUP("ssl_cert", \ + test_wolfSSL_chain_ca_not_cached_on_override) #endif /* TESTS_API_SSL_CERT_H */ From ffe28d5a4aa8c435b6a2bba4d325651a4e25273b Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Wed, 21 Oct 2026 09:29:18 +0900 Subject: [PATCH 2/9] F-14457 - Guard test callback with the test build condition The verify callback for the chain CA override test sat outside the build condition that guards the test body. A configuration that compiles the test out, such as --enable-leantls, which defines WOLFSSL_NO_TLS12, left the callback defined but unused and the build failed on -Werror=unused-function. Name the condition once and guard both the callback and the test body with it so the two cannot drift apart. --- tests/api/test_ssl_cert.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 2317fa9e461..18b2b3b2abc 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -5263,6 +5263,14 @@ int test_wolfSSL_dtls_api_more_guards(void) return EXPECT_RESULT(); } +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WOLFSSL_PEM_TO_DER) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + #define TEST_CHAIN_CA_OVERRIDE +#endif + +#ifdef TEST_CHAIN_CA_OVERRIDE /* Accept every certificate the way an application that makes its own trust * decision about the peer does. */ static int test_chain_ca_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* s) @@ -5271,6 +5279,7 @@ static int test_chain_ca_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* s) (void)s; return 1; } +#endif /* Test that a chain certificate which failed verification is not cached as a * CA when the application's verify callback overrides the error. @@ -5288,10 +5297,7 @@ static int test_chain_ca_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* s) int test_wolfSSL_chain_ca_not_cached_on_override(void) { EXPECT_DECLS; -#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ - !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) && \ - !defined(NO_FILESYSTEM) && defined(WOLFSSL_PEM_TO_DER) && \ - !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +#ifdef TEST_CHAIN_CA_OVERRIDE WOLFSSL_CTX* ctx_c = NULL; WOLFSSL_CTX* ctx_s = NULL; WOLFSSL* ssl_c = NULL; From 38c4cb412ea7d213e94d59029c6eb33e69b199ec Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Wed, 21 Oct 2026 14:05:34 +0900 Subject: [PATCH 3/9] F-14457 - Do not cache chain CAs whose signature did not verify A chain certificate can reach the verify callback with its signature unconfirmed in more ways than a missing signer. A forged certificate that names a loaded root as its issuer is looked up successfully and then fails ConfirmSignature, and a signature algorithm that does not match the issuer key fails before the signature is checked at all. Neither certificate has been authenticated, so neither may be cached when the callback clears the error. Add both error codes to the set that skips the add, and correct the comment: the question is whether the signature verified against a trusted anchor, not whether an issuer was found. The errors left out are raised after the signature verified, so the key is authentic and the rest of the chain needs the cached entry. Add a regression test that presents an intermediate carrying the test root's issuer name and authority key identifier but signed with a key the root does not hold. --- src/internal.c | 16 +++-- tests/api/test_ssl_cert.c | 119 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 5 +- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/src/internal.c b/src/internal.c index 59e75260d21..1313ca43201 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19588,13 +19588,17 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif - /* An unsigned chain cert the verify callback waved through - * stays out of the cache: cached CAs are trust anchors for - * every later connection and the callback only runs on an - * error. Other errors still chain to a trusted anchor, and + /* A chain cert whose signature was never confirmed against + * a trusted anchor stays out of the cache even when the + * verify callback waved it through: cached CAs are trust + * anchors for every later connection and the callback only + * runs on an error. The errors left out here were raised + * after the signature verified, so the key is authentic and * the rest of the chain needs the cached entry. */ - if (preCbRet == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E) || - preCbRet == WC_NO_ERR_TRACE(ASN_SELF_SIGNED_E)) { + if (preCbRet == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E) || + preCbRet == WC_NO_ERR_TRACE(ASN_SELF_SIGNED_E) || + preCbRet == WC_NO_ERR_TRACE(ASN_SIG_CONFIRM_E) || + preCbRet == WC_NO_ERR_TRACE(ASN_SIG_OID_E)) { skipAddCA = 1; } #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 18b2b3b2abc..0572f033ccb 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3303,6 +3303,125 @@ int test_wolfSSL_small_cert_verify_sig_error(void) return EXPECT_RESULT(); } +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ + !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) + #define TEST_FORGED_CHAIN_CA +#endif + +/* Test that a chain CA whose signature never verified is not cached as a trust + * anchor when the application's verify callback overrides the error. + * + * The intermediate names the test root as its issuer and carries the root's + * authority key identifier, so the signer lookup finds the root, but it is + * signed with a key the root does not hold and fails with ASN_SIG_CONFIRM_E. + * Nothing about that certificate has been authenticated. Overriding the error + * decides this handshake and nothing else. Caching the key would hand every + * later connection from the same context a trust anchor the root never signed, + * and the verify callback only runs on an error, so nothing would ask the + * application about it again. + * + * @return TEST_SUCCESS on success. + */ +int test_wolfSSL_forged_chain_ca_not_cached(void) +{ + EXPECT_DECLS; +#ifdef TEST_FORGED_CHAIN_CA + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + struct test_memio_ctx test_ctx; + byte* caDer = NULL; + byte* leafDer = NULL; + byte* chainDer = NULL; + RsaKey attackerKey; + RsaKey leafKey; + WC_RNG rng; + int attackerInit = 0; + int leafInit = 0; + int rngInit = 0; + int caSz = 0; + int leafSz = 0; + int chainSz = 0; + word32 idx = 0; + + ExpectNotNull(caDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectNotNull(leafDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectNotNull(chainDer = (byte*)XMALLOC(2 * TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngInit = 1; + /* Any key the root does not hold serves as the attacker's. */ + ExpectIntEQ(wc_InitRsaKey(&attackerKey, NULL), 0); + if (EXPECT_SUCCESS()) attackerInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(server_key_der_2048, &idx, &attackerKey, + (word32)sizeof_server_key_der_2048), 0); + ExpectIntEQ(wc_InitRsaKey(&leafKey, NULL), 0); + if (EXPECT_SUCCESS()) leafInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(client_key_der_2048, &idx, &leafKey, + (word32)sizeof_client_key_der_2048), 0); + + /* The intermediate claims the root as its issuer but is signed with the + * attacker's key, so the signer lookup succeeds and the signature check + * does not. */ + ExpectIntGT((caSz = test_eku_gen_ca(caDer, TEST_EKU_CERT_BUF_SZ, + &attackerKey, ca_cert_der_2048, (int)sizeof_ca_cert_der_2048, + &attackerKey, &rng, "serverAuth,clientAuth", "Forged Intermediate", 0, + NULL)), 0); + ExpectIntGT((leafSz = test_eku_gen_leaf(leafDer, TEST_EKU_CERT_BUF_SZ, + &leafKey, caDer, caSz, &attackerKey, &rng)), 0); + ExpectIntLE(leafSz + caSz, 2 * TEST_EKU_CERT_BUF_SZ); + + if (EXPECT_SUCCESS()) { + /* The chain buffer holds the leaf first, then its issuer. */ + XMEMCPY(chainDer, leafDer, (size_t)leafSz); + XMEMCPY(chainDer + leafSz, caDer, (size_t)caSz); + chainSz = leafSz + caSz; + } + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup_ex(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfSSLv23_server_method, + (byte*)ca_cert_der_2048, (int)sizeof_ca_cert_der_2048, + chainDer, chainSz, + (byte*)client_key_der_2048, (int)sizeof_client_key_der_2048), 0); + + ExpectNotNull(cm = wolfSSL_CTX_GetCertManager(ctx_c)); + /* The leaf chains to nothing the client holds yet, so the same check after + * the handshake reports a change and not a constant. */ + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, leafDer, (long)leafSz, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, test_eku_override_cb); + /* The override carries the handshake past the forged intermediate. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* The forged key must not have become a trust anchor. */ + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, leafDer, (long)leafSz, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + if (rngInit) wc_FreeRng(&rng); + if (attackerInit) wc_FreeRsaKey(&attackerKey); + if (leafInit) wc_FreeRsaKey(&leafKey); + XFREE(caDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(leafDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(chainDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} + #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index f6d7dc17b3a..e0ded9b2d56 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -33,6 +33,7 @@ int test_wolfSSL_load_from_fifo(void); int test_wolfSSL_dtls_api_more_guards(void); int test_wolfSSL_alloc_failure_sweep(void); int test_wolfSSL_chain_ca_not_cached_on_override(void); +int test_wolfSSL_forged_chain_ca_not_cached(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -144,6 +145,8 @@ int test_wolfSSL_SESSION_get0_peer_full_chain(void); TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_more_guards), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_alloc_failure_sweep), \ TEST_DECL_GROUP("ssl_cert", \ - test_wolfSSL_chain_ca_not_cached_on_override) + test_wolfSSL_chain_ca_not_cached_on_override), \ + TEST_DECL_GROUP("ssl_cert", \ + test_wolfSSL_forged_chain_ca_not_cached) #endif /* TESTS_API_SSL_CERT_H */ From c6abcc591552c5454555a2e657be6e2797379550 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Wed, 21 Oct 2026 14:27:00 +0900 Subject: [PATCH 4/9] F-14457 - Spell out the test build conditions in place check-api-guards.py matches the enclosing #if chain as text, so naming the conditions in a macro hid NO_FILESYSTEM and the other guards from it. --- tests/api/test_ssl_cert.c | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 0572f033ccb..33c523edb42 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3303,13 +3303,6 @@ int test_wolfSSL_small_cert_verify_sig_error(void) return EXPECT_RESULT(); } -#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ - !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ - defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ - defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) - #define TEST_FORGED_CHAIN_CA -#endif - /* Test that a chain CA whose signature never verified is not cached as a trust * anchor when the application's verify callback overrides the error. * @@ -3327,7 +3320,11 @@ int test_wolfSSL_small_cert_verify_sig_error(void) int test_wolfSSL_forged_chain_ca_not_cached(void) { EXPECT_DECLS; -#ifdef TEST_FORGED_CHAIN_CA +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ + !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) WOLFSSL_CTX* ctx_c = NULL; WOLFSSL_CTX* ctx_s = NULL; WOLFSSL* ssl_c = NULL; @@ -5386,10 +5383,6 @@ int test_wolfSSL_dtls_api_more_guards(void) !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) && \ !defined(NO_FILESYSTEM) && defined(WOLFSSL_PEM_TO_DER) && \ !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) - #define TEST_CHAIN_CA_OVERRIDE -#endif - -#ifdef TEST_CHAIN_CA_OVERRIDE /* Accept every certificate the way an application that makes its own trust * decision about the peer does. */ static int test_chain_ca_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* s) @@ -5416,7 +5409,10 @@ static int test_chain_ca_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* s) int test_wolfSSL_chain_ca_not_cached_on_override(void) { EXPECT_DECLS; -#ifdef TEST_CHAIN_CA_OVERRIDE +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WOLFSSL_PEM_TO_DER) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) WOLFSSL_CTX* ctx_c = NULL; WOLFSSL_CTX* ctx_s = NULL; WOLFSSL* ssl_c = NULL; From ea457ad8775778a68d717052d2b96b9739ad0626 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Wed, 21 Oct 2026 17:28:11 +0900 Subject: [PATCH 5/9] F-14457 - Document and test the handshake scope of an override Accepting a chain CA that is not in the trust store now decides that one certificate rather than the rest of the chain, because the CA is no longer cached. Pin that: a callback accepting only the chain CA ends in ASN_NO_SIGNER_E on the leaf, the same chain completes when the callback accepts the leaf as well, and the CA is not a trust anchor afterwards either way. Record the scope of an override in the verify callback documentation, which described the mode flags only. --- doc/dox_comments/header_files/ssl.h | 18 ++++ tests/api/test_ssl_cert.c | 153 ++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 5 +- 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 4328cbe7411..d29c83ed487 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3200,6 +3200,15 @@ int wolfSSL_GetSessionAtIndex(int index, WOLFSSL_SESSION* session); wolfSSL_CTX_get_verify_mode() will not report it back. Including it in mode has no effect on any of the other flags. + A non-zero return from verify_callback overrides the error for that one + certificate, and applies to the handshake in progress only. A chain + certificate whose signature was never confirmed against a trusted anchor + is not cached, so a certificate it signed has no signer of its own and is + reported to verify_callback in its turn rather than verified against the + accepted key. Accepting each certificate in the chain completes the + handshake. Nothing accepted this way becomes a trust anchor for later + connections made from the same context. + \return none No return. \param ctx pointer to the SSL context, created with wolfSSL_CTX_new(). @@ -3252,6 +3261,15 @@ void wolfSSL_CTX_set_verify(WOLFSSL_CTX* ctx, int mode, wolfSSL_CTX_get_verify_mode() will not report it back. Including it in mode has no effect on any of the other flags. + A non-zero return from verify_callback overrides the error for that one + certificate, and applies to the handshake in progress only. A chain + certificate whose signature was never confirmed against a trusted anchor + is not cached, so a certificate it signed has no signer of its own and is + reported to verify_callback in its turn rather than verified against the + accepted key. Accepting each certificate in the chain completes the + handshake. Nothing accepted this way becomes a trust anchor for later + connections made from the same session's context. + \return none No return. \param ssl pointer to the SSL session, created with wolfSSL_new(). diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 33c523edb42..d8d2881dfb9 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3419,6 +3419,159 @@ int test_wolfSSL_forged_chain_ca_not_cached(void) return EXPECT_RESULT(); } +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ + !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* Accept the chain CA and leave the leaf to the library. */ +static int test_chain_ca_only_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) +{ + if (store->error_depth > 0) + return 1; + return preverify; +} + +/* Run one handshake against the generated chain. Reports the handshake result + * and whether the chain CA ended up usable as a trust anchor afterwards. */ +static int test_chain_ca_scope_run(const byte* chainDer, int chainSz, + const byte* leafDer, int leafSz, VerifyCallback cb, int* hsRet, + int* leafVerifies) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup_ex(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfSSLv23_server_method, + (byte*)ca_cert_der_2048, (int)sizeof_ca_cert_der_2048, + (byte*)chainDer, chainSz, + (byte*)client_key_der_2048, (int)sizeof_client_key_der_2048), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, cb); + if (EXPECT_SUCCESS()) { + *hsRet = test_memio_do_handshake(ssl_c, ssl_s, 10, NULL); + if (*hsRet != 0) { + *hsRet = wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + } + } + + ExpectNotNull(cm = wolfSSL_CTX_GetCertManager(ctx_c)); + if (EXPECT_SUCCESS()) { + *leafVerifies = (wolfSSL_CertManagerVerifyBuffer(cm, leafDer, + (long)leafSz, WOLFSSL_FILETYPE_ASN1) == WOLFSSL_SUCCESS); + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + + return EXPECT_RESULT(); +} +#endif + +/* Test that accepting an unverifiable chain CA in the verify callback decides + * that handshake alone. + * + * The chain CA is not cached, so the certificate it signed has no signer and + * is reported to the application in its own turn rather than being verified + * against a key the callback merely waved through. An application that means + * to accept the chain says so for each certificate, and either way the CA does + * not become a trust anchor for later connections from the same context. + * + * @return TEST_SUCCESS on success. + */ +int test_wolfSSL_chain_ca_override_is_handshake_scoped(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ + !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + byte* caDer = NULL; + byte* leafDer = NULL; + byte* chainDer = NULL; + RsaKey untrustedKey; + RsaKey leafKey; + WC_RNG rng; + int untrustedInit = 0; + int leafInit = 0; + int rngInit = 0; + int caSz = 0; + int leafSz = 0; + int chainSz = 0; + int hsRet = 0; + int leafVerifies = 1; + word32 idx = 0; + + ExpectNotNull(caDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectNotNull(leafDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectNotNull(chainDer = (byte*)XMALLOC(2 * TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngInit = 1; + /* Any key the test root does not hold stands in for the untrusted CA. */ + ExpectIntEQ(wc_InitRsaKey(&untrustedKey, NULL), 0); + if (EXPECT_SUCCESS()) untrustedInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(server_key_der_2048, &idx, + &untrustedKey, (word32)sizeof_server_key_der_2048), 0); + ExpectIntEQ(wc_InitRsaKey(&leafKey, NULL), 0); + if (EXPECT_SUCCESS()) leafInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(client_key_der_2048, &idx, &leafKey, + (word32)sizeof_client_key_der_2048), 0); + + /* A self-signed CA the client does not trust, and a leaf it did sign. */ + ExpectIntGT((caSz = test_eku_gen_ca(caDer, TEST_EKU_CERT_BUF_SZ, + &untrustedKey, NULL, 0, &untrustedKey, &rng, "serverAuth,clientAuth", + "Untrusted Root", 0, NULL)), 0); + ExpectIntGT((leafSz = test_eku_gen_leaf(leafDer, TEST_EKU_CERT_BUF_SZ, + &leafKey, caDer, caSz, &untrustedKey, &rng)), 0); + ExpectIntLE(leafSz + caSz, 2 * TEST_EKU_CERT_BUF_SZ); + + if (EXPECT_SUCCESS()) { + XMEMCPY(chainDer, leafDer, (size_t)leafSz); + XMEMCPY(chainDer + leafSz, caDer, (size_t)caSz); + chainSz = leafSz + caSz; + } + + /* Accepting the CA alone is not enough: the leaf has no signer and the + * application is asked about it rather than told it verified. */ + ExpectIntEQ(test_chain_ca_scope_run(chainDer, chainSz, leafDer, leafSz, + test_chain_ca_only_cb, &hsRet, &leafVerifies), TEST_SUCCESS); + ExpectIntEQ(hsRet, WC_NO_ERR_TRACE(ASN_NO_SIGNER_E)); + ExpectIntEQ(leafVerifies, 0); + + /* Accepting the leaf in its turn completes the handshake, and the CA still + * does not become a trust anchor. */ + leafVerifies = 1; + ExpectIntEQ(test_chain_ca_scope_run(chainDer, chainSz, leafDer, leafSz, + test_eku_override_cb, &hsRet, &leafVerifies), TEST_SUCCESS); + ExpectIntEQ(hsRet, 0); + ExpectIntEQ(leafVerifies, 0); + + if (rngInit) wc_FreeRng(&rng); + if (untrustedInit) wc_FreeRsaKey(&untrustedKey); + if (leafInit) wc_FreeRsaKey(&leafKey); + XFREE(caDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(leafDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(chainDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} + #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index e0ded9b2d56..e1774abff55 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -34,6 +34,7 @@ int test_wolfSSL_dtls_api_more_guards(void); int test_wolfSSL_alloc_failure_sweep(void); int test_wolfSSL_chain_ca_not_cached_on_override(void); int test_wolfSSL_forged_chain_ca_not_cached(void); +int test_wolfSSL_chain_ca_override_is_handshake_scoped(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -147,6 +148,8 @@ int test_wolfSSL_SESSION_get0_peer_full_chain(void); TEST_DECL_GROUP("ssl_cert", \ test_wolfSSL_chain_ca_not_cached_on_override), \ TEST_DECL_GROUP("ssl_cert", \ - test_wolfSSL_forged_chain_ca_not_cached) + test_wolfSSL_forged_chain_ca_not_cached), \ + TEST_DECL_GROUP("ssl_cert", \ + test_wolfSSL_chain_ca_override_is_handshake_scoped) #endif /* TESTS_API_SSL_CERT_H */ From aa240a8a873a512f3c44633645ed1eb39bed3b0b Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Thu, 22 Oct 2026 14:24:26 +0900 Subject: [PATCH 6/9] F-14457 - Decide caching on the signature result, not error codes Enumerating the error codes that mean a chain certificate's signature was never confirmed left gaps. ASN_PATHLEN_INV_E is one: the path length check returns it before ConfirmSignature runs, so an override cached a certificate nothing had authenticated. Record the outcome instead. ParseCertRelative sets a flag once every signature the certificate needed has been confirmed against the issuer it found, and a chain CA is cached only when that flag is set. Errors raised after the signature verified still cache, so a certificate waived for its OCSP status, extended key usage, dates or revocation behaves as before. Under WOLFSSL_SMALL_CERT_VERIFY the signature is checked separately by wc_CheckCertSignature, so the flag is taken from that result there. The flag joins the existing bitfield run, leaving sizeof(DecodedCert) unchanged. --- src/internal.c | 16 +++++++++------- wolfcrypt/src/asn.c | 4 ++++ wolfssl/wolfcrypt/asn.h | 1 + 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/internal.c b/src/internal.c index 1313ca43201..9a4e5ea16fe 100644 --- a/src/internal.c +++ b/src/internal.c @@ -17560,6 +17560,11 @@ PRAGMA_GCC_DIAG_POP if (ret == 0 || (sigRet != 0 && IsPostSigParseErr(ret))) { ret = sigRet; } + /* ParseCertRelative() ran with VERIFY_NAME and left the signature to + * wc_CheckCertSignature() above, so it could not record the result. */ + if (args->dCert != NULL) { + args->dCert->sigVerified = (verify == VERIFY_NAME) && (sigRet == 0); + } #endif if (pSubjectHash) @@ -19592,13 +19597,10 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, * a trusted anchor stays out of the cache even when the * verify callback waved it through: cached CAs are trust * anchors for every later connection and the callback only - * runs on an error. The errors left out here were raised - * after the signature verified, so the key is authentic and - * the rest of the chain needs the cached entry. */ - if (preCbRet == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E) || - preCbRet == WC_NO_ERR_TRACE(ASN_SELF_SIGNED_E) || - preCbRet == WC_NO_ERR_TRACE(ASN_SIG_CONFIRM_E) || - preCbRet == WC_NO_ERR_TRACE(ASN_SIG_OID_E)) { + * runs on an error. A failure raised after the signature + * verified leaves the key authentic, and the rest of the + * chain needs the cached entry. */ + if (preCbRet != 0 && !args->dCert->sigVerified) { skipAddCA = 1; } #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index afec636c635..1c192914228 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -26568,6 +26568,10 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, } } #endif /* WOLFSSL_DUAL_ALG_CERTS */ + + /* Every signature this certificate needed has been confirmed + * against the issuer found in the certificate manager. */ + cert->sigVerified = 1; } } /* cert->ca */ #ifdef WOLFSSL_CERT_REQ diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 81870bba23d..b6859a45f12 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -2341,6 +2341,7 @@ struct DecodedCert { WOLFSSL_AIA_ENTRY extAuthInfoList[WOLFSSL_MAX_AIA_ENTRIES]; WC_BITFIELD extAuthInfoListSz:7; WC_BITFIELD extAuthInfoListOverflow:1; + WC_BITFIELD sigVerified:1; /* Issuer's key verified the signature */ /* Appended to preserve the offsets of existing members. */ word32 extExtKeyUsageOidCnt; /* Number of EKU KeyPurposeIds seen, * recognized or not */ From fafdd09e10fb5afe4bfce67bf364e7b3e1397e55 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 23 Oct 2026 12:08:56 +0900 Subject: [PATCH 7/9] F-14457 - Record the stapling exception in the verify callback docs Accepting a chain certificate whose signature was never confirmed does not complete a handshake that requested OCSP stapling. The staple is verified against the certificate manager, which does not hold the accepted certificate, so the responder's own certificate cannot be verified and the handshake ends with a fatal alert the verify callback never sees. A live OCSP lookup fails for the same reason but its error does reach the callback. Name the one hook that can accept the stapled case. The text also described the override without saying what happens without one, so say that first. --- doc/dox_comments/header_files/ssl.h | 58 +++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index d29c83ed487..cb4a915e35a 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3200,14 +3200,27 @@ int wolfSSL_GetSessionAtIndex(int index, WOLFSSL_SESSION* session); wolfSSL_CTX_get_verify_mode() will not report it back. Including it in mode has no effect on any of the other flags. - A non-zero return from verify_callback overrides the error for that one - certificate, and applies to the handshake in progress only. A chain - certificate whose signature was never confirmed against a trusted anchor - is not cached, so a certificate it signed has no signer of its own and is - reported to verify_callback in its turn rather than verified against the - accepted key. Accepting each certificate in the chain completes the - handshake. Nothing accepted this way becomes a trust anchor for later - connections made from the same context. + A non-zero return from verify_callback accepts a certificate the library + rejected, for the handshake in progress only. Without such a callback, or + when it returns preverify_ok, the rejection stands and the handshake ends + with a fatal alert as before. + + An accepted certificate whose signature was never confirmed against a + trusted anchor is not added to the certificate manager, so it does not + become a trust anchor for later connections made from the same context, + and it is not available as the signer of the next certificate in the + chain. That next certificate is reported to verify_callback in its turn + rather than verified against the accepted key, and accepting each one in + turn completes the handshake. + + OCSP is the exception. A stapled response is verified against the + certificate manager, which does not hold the accepted certificate, so the + responder's own certificate cannot be verified; the handshake ends with + BAD_CERTIFICATE_STATUS_ERROR and a fatal alert without consulting + verify_callback, and only a callback set with + wolfSSL_CTX_SetOCSP_StatusVerifyCb() can accept that result. A live OCSP + lookup fails for the same reason, but its error does reach + verify_callback and can be accepted there. \return none No return. @@ -3261,14 +3274,27 @@ void wolfSSL_CTX_set_verify(WOLFSSL_CTX* ctx, int mode, wolfSSL_CTX_get_verify_mode() will not report it back. Including it in mode has no effect on any of the other flags. - A non-zero return from verify_callback overrides the error for that one - certificate, and applies to the handshake in progress only. A chain - certificate whose signature was never confirmed against a trusted anchor - is not cached, so a certificate it signed has no signer of its own and is - reported to verify_callback in its turn rather than verified against the - accepted key. Accepting each certificate in the chain completes the - handshake. Nothing accepted this way becomes a trust anchor for later - connections made from the same session's context. + A non-zero return from verify_callback accepts a certificate the library + rejected, for the handshake in progress only. Without such a callback, or + when it returns preverify_ok, the rejection stands and the handshake ends + with a fatal alert as before. + + An accepted certificate whose signature was never confirmed against a + trusted anchor is not added to the certificate manager, so it does not + become a trust anchor for later connections made from the same session's + context, and it is not available as the signer of the next certificate in + the chain. That next certificate is reported to verify_callback in its + turn rather than verified against the accepted key, and accepting each one + in turn completes the handshake. + + OCSP is the exception. A stapled response is verified against the + certificate manager, which does not hold the accepted certificate, so the + responder's own certificate cannot be verified; the handshake ends with + BAD_CERTIFICATE_STATUS_ERROR and a fatal alert without consulting + verify_callback, and only a callback set with + wolfSSL_CTX_SetOCSP_StatusVerifyCb() can accept that result. A live OCSP + lookup fails for the same reason, but its error does reach + verify_callback and can be accepted there. \return none No return. From 7974c4af6db221b8992f20e23e90b5bce1c6b7bc Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 23 Oct 2026 15:07:17 +0900 Subject: [PATCH 8/9] F-14457 - Say how far accepting a rejected certificate reaches The text said an acceptance applied to the handshake in progress only. That holds for a certificate whose signature was never confirmed, which is not cached, but not for one whose signature was confirmed and which failed a later check: the date, revocation, OCSP and key usage checks all run after the signature verified, so an expired or revoked intermediate waved through still becomes a trust anchor for later connections from the same context. Say which case is which instead. --- doc/dox_comments/header_files/ssl.h | 50 ++++++++++++++++------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index cb4a915e35a..4daf20a0334 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -3201,17 +3201,20 @@ int wolfSSL_GetSessionAtIndex(int index, WOLFSSL_SESSION* session); mode has no effect on any of the other flags. A non-zero return from verify_callback accepts a certificate the library - rejected, for the handshake in progress only. Without such a callback, or - when it returns preverify_ok, the rejection stands and the handshake ends - with a fatal alert as before. - - An accepted certificate whose signature was never confirmed against a - trusted anchor is not added to the certificate manager, so it does not - become a trust anchor for later connections made from the same context, - and it is not available as the signer of the next certificate in the - chain. That next certificate is reported to verify_callback in its turn - rather than verified against the accepted key, and accepting each one in - turn completes the handshake. + rejected. Without such a callback, or when it returns preverify_ok, the + rejection stands and the handshake ends with a fatal alert as before. + + How far the acceptance reaches depends on why the certificate was + rejected. A chain certificate whose signature was confirmed against a + trusted anchor, and which failed a later check such as its validity + dates, its revocation status or its key usage, is still added to the + certificate manager: accepting it also makes it a trust anchor for later + connections from the same context, and verify_callback is not consulted + about it again. A certificate whose signature was never confirmed is not + added, so that acceptance applies to this handshake alone. It is then not + available as the signer of the next certificate in the chain, which is + reported to verify_callback in its turn, and accepting each one in turn + completes the handshake. OCSP is the exception. A stapled response is verified against the certificate manager, which does not hold the accepted certificate, so the @@ -3275,17 +3278,20 @@ void wolfSSL_CTX_set_verify(WOLFSSL_CTX* ctx, int mode, mode has no effect on any of the other flags. A non-zero return from verify_callback accepts a certificate the library - rejected, for the handshake in progress only. Without such a callback, or - when it returns preverify_ok, the rejection stands and the handshake ends - with a fatal alert as before. - - An accepted certificate whose signature was never confirmed against a - trusted anchor is not added to the certificate manager, so it does not - become a trust anchor for later connections made from the same session's - context, and it is not available as the signer of the next certificate in - the chain. That next certificate is reported to verify_callback in its - turn rather than verified against the accepted key, and accepting each one - in turn completes the handshake. + rejected. Without such a callback, or when it returns preverify_ok, the + rejection stands and the handshake ends with a fatal alert as before. + + How far the acceptance reaches depends on why the certificate was + rejected. A chain certificate whose signature was confirmed against a + trusted anchor, and which failed a later check such as its validity + dates, its revocation status or its key usage, is still added to the + certificate manager: accepting it also makes it a trust anchor for later + connections from the same session's context, and verify_callback is not + consulted about it again. A certificate whose signature was never + confirmed is not added, so that acceptance applies to this handshake + alone. It is then not available as the signer of the next certificate in + the chain, which is reported to verify_callback in its turn, and + accepting each one in turn completes the handshake. OCSP is the exception. A stapled response is verified against the certificate manager, which does not hold the accepted certificate, so the From bfe44e81b071d11f60b63206a8b893b67d69bcec Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Wed, 28 Oct 2026 17:31:37 +0900 Subject: [PATCH 9/9] F-14457 - Do not mark dual-algorithm certs verified on the small path wc_CheckCertSignature() confirms the primary signature only, so leave sigVerified clear when the certificate carries an alternative signature. --- src/internal.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/internal.c b/src/internal.c index 9a4e5ea16fe..92cfaf20951 100644 --- a/src/internal.c +++ b/src/internal.c @@ -17564,6 +17564,12 @@ PRAGMA_GCC_DIAG_POP * wc_CheckCertSignature() above, so it could not record the result. */ if (args->dCert != NULL) { args->dCert->sigVerified = (verify == VERIFY_NAME) && (sigRet == 0); + #ifdef WOLFSSL_DUAL_ALG_CERTS + /* wc_CheckCertSignature() checks the primary signature only. */ + if (args->dCert->extAltSigAlgSet && args->dCert->extAltSigValSet) { + args->dCert->sigVerified = 0; + } + #endif } #endif