From f23544f09476d420becdfabd781eec0dd1a9c597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 16 Jun 2026 15:59:54 +0200 Subject: [PATCH] TLS 1.3: fix for post-handshake authentication Only exempt the missing-certificate check during the initial handshake; once a post-handshake CertificateRequest is outstanding the server again requires the client certificate (and its CertificateVerify). Adds a post-handshake auth test. --- src/tls13.c | 19 ++++++++++-- tests/api/test_tls13.c | 68 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 2 ++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/tls13.c b/src/tls13.c index 0314766310..ecfd5946dd 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -11835,7 +11835,10 @@ int DoTls13Finished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #endif if ( #ifdef WOLFSSL_POST_HANDSHAKE_AUTH - !ssl->options.verifyPostHandshake && + /* Exempt only the initial handshake; a pending post-handshake + * CertificateRequest (certReqCtx != NULL) still requires a peer + * certificate and a valid CertificateVerify. */ + (!ssl->options.verifyPostHandshake || ssl->certReqCtx != NULL) && #endif (!ssl->options.havePeerCert || !ssl->options.havePeerVerify)) { ret = NO_PEER_CERT; /* NO_PEER_VERIFY */ @@ -13507,7 +13510,19 @@ static int SanityCheckTls13MsgReceived(WOLFSSL* ssl, byte type) */ if (ssl->options.verifyPeer && #ifdef WOLFSSL_POST_HANDSHAKE_AUTH - !ssl->options.verifyPostHandshake && + /* The post-handshake-auth exemption is only valid during + * the initial handshake. On the server, once a + * post-handshake CertificateRequest is outstanding + * (certReqCtx != NULL), a Certificate is required again. + * Scoped to the server: certReqCtx means something + * different on the client (a received request) and the + * client does not process an inbound Finished in that + * state. Whether an empty Certificate is then accepted + * follows the verify mode (FAIL_IF_NO_PEER_CERT), exactly + * as for first-handshake client authentication. */ + (!ssl->options.verifyPostHandshake || + (ssl->options.side == WOLFSSL_SERVER_END && + ssl->certReqCtx != NULL)) && #endif !ssl->msgsReceived.got_certificate) { WOLFSSL_MSG("Finished received out of order - " diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index d820c55de5..e4d5c3821d 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -2820,6 +2820,74 @@ int test_tls13_rpk_handshake_no_negotiation(void) return EXPECT_RESULT(); } +/* Post-handshake authentication (PHA) positive guard. A server configured with + * WOLFSSL_VERIFY_POST_HANDSHAKE must complete the initial handshake WITHOUT a + * client certificate (verification is deferred) and then be able to request one + * post-handshake. This guards the property the auth-bypass fix must preserve: + * the missing-certificate exemption stays in force while no post-handshake + * request is outstanding (certReqCtx == NULL). If the fix wrongly treated the + * initial handshake as a pending PHA request, the handshake below would fail. + * + * Scope: this is a positive guard only. + * - The bypass itself is NOT reproduced here: the + * SanityCheckTls13MsgReceived / DoTls13Finished branches the fix re-enables + * only fire when a client sends a Finished with NO Certificate message at all + * (got_certificate == 0). A conformant wolfSSL client always sends at least + * an empty Certificate, so reproducing the bypass needs a non-conformant + * client; the report used a state-machine model. The negative direction + * rests on code review. + * - Driving the full post-handshake certificate exchange to completion is + * intentionally not asserted: whether the peer chain ends up populated + * depends on build options unrelated to this fix (it differs between + * --enable-all and the user_settings_all header build), so it is not a + * portable assertion. */ +int test_tls13_pha(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ + defined(WOLFSSL_POST_HANDSHAKE_AUTH) && defined(SESSION_CERTS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_RSA) && !defined(NO_CERTS) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + WOLFSSL_X509_CHAIN* chain = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Server: trust the (self-signed) client certificate and defer + * verification to post-handshake. */ + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_POST_HANDSHAKE, NULL); + + /* Client: load a cert/key and advertise post-handshake auth. */ + 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); + ExpectIntEQ(wolfSSL_allow_post_handshake_auth(ssl_c), 0); + + /* Initial handshake must complete even though no client cert was sent - + * the verifyPostHandshake exemption (certReqCtx == NULL) is intact. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(chain = wolfSSL_get_peer_chain(ssl_s)); + ExpectIntEQ(wolfSSL_get_chain_count(chain), 0); + + /* And the server can issue a post-handshake certificate request. */ + ExpectIntEQ(wolfSSL_request_certificate(ssl_s), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + #if defined(HAVE_IO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) && \ defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) && \ diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 6e392f46d5..e16c97e165 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -29,6 +29,7 @@ int test_tls13_cipher_suites(void); int test_tls13_bad_psk_binder(void); int test_tls13_rpk_handshake(void); int test_tls13_rpk_handshake_no_negotiation(void); +int test_tls13_pha(void); int test_tls13_pq_groups(void); int test_tls13_multi_pqc_key_share(void); int test_tls13_early_data(void); @@ -90,6 +91,7 @@ int test_tls13_AEAD_limit_KU_aes128_ccm_8_sha256(void); TEST_DECL_GROUP("tls13", test_tls13_bad_psk_binder), \ TEST_DECL_GROUP("tls13", test_tls13_rpk_handshake), \ TEST_DECL_GROUP("tls13", test_tls13_rpk_handshake_no_negotiation), \ + TEST_DECL_GROUP("tls13", test_tls13_pha), \ TEST_DECL_GROUP("tls13", test_tls13_pq_groups), \ TEST_DECL_GROUP("tls13", test_tls13_multi_pqc_key_share), \ TEST_DECL_GROUP("tls13", test_tls13_early_data), \