From 09fbfdff8851a836b051d5a456c6b93221764064 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Thu, 16 Jul 2026 16:41:38 -0600 Subject: [PATCH] F-6307: reject presented but untrusted client cert under wantClientAuth --- .../jsse/WolfSSLInternalVerifyCb.java | 17 ++- .../provider/jsse/test/WolfSSLEngineTest.java | 15 ++- .../provider/jsse/test/WolfSSLSocketTest.java | 110 ++++++++++++++++++ 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/src/java/com/wolfssl/provider/jsse/WolfSSLInternalVerifyCb.java b/src/java/com/wolfssl/provider/jsse/WolfSSLInternalVerifyCb.java index 1e97882..3cc2441 100644 --- a/src/java/com/wolfssl/provider/jsse/WolfSSLInternalVerifyCb.java +++ b/src/java/com/wolfssl/provider/jsse/WolfSSLInternalVerifyCb.java @@ -638,18 +638,17 @@ public class WolfSSLInternalVerifyCb implements WolfSSLVerifyCallback { else if ((!this.clientMode) && (this.params != null) && this.params.getWantClientAuth() && (!this.params.getNeedClientAuth())) { - /* wantClientAuth is set and client sent a certificate. - * Try to verify via TrustManager, but don't fail the - * handshake if verification fails — matches SunJSSE - * behavior where wantClientAuth is non-fatal. */ - if (VerifyCertChainWithTrustManager(x509certs, authType)) { - WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, - () -> "wantClientAuth: client cert verified successfully"); - } else { + /* wantClientAuth only makes an absent client cert non-fatal. + * A cert that is presented must still validate, matching SunJSSE, + * so abort the handshake if the TrustManager rejects it. */ + if (VerifyCertChainWithTrustManager(x509certs, authType) == false) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, () -> "wantClientAuth: client cert verification failed, " + - "continuing handshake (non-fatal)"); + "aborting handshake"); + return 0; } + WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, + () -> "wantClientAuth: client cert verified successfully"); } else { /* Poll X509TrustManager / X509ExtendedTrustManager for certificate diff --git a/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java b/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java index 492da86..9266052 100644 --- a/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java +++ b/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java @@ -1006,22 +1006,27 @@ public class WolfSSLEngineTest { }; /* All combinations with external X509ExtendedTrustManager registered - * which will trust NO client certs and ALL server certs */ + * which will trust NO client certs and ALL server certs. The client + * presents a cert, so when the server requests one (want or need) it + * fails validation and the handshake aborts. wantClientAuth only makes + * an absent client cert non-fatal, matching SunJSSE, so serverWant with + * a presented-but-rejected cert is expected to fail. Only the cases + * where the server requests no client cert succeed. */ PeerAuthConfig[] configsDefaultManagers = new PeerAuthConfig[] { new PeerAuthConfig(true, true, true, true, false), - new PeerAuthConfig(true, true, true, false, true), + new PeerAuthConfig(true, true, true, false, false), new PeerAuthConfig(true, true, false, true, false), new PeerAuthConfig(true, true, false, false, true), new PeerAuthConfig(true, false, true, true, false), - new PeerAuthConfig(true, false, true, false, true), + new PeerAuthConfig(true, false, true, false, false), new PeerAuthConfig(true, false, false, true, false), new PeerAuthConfig(true, false, false, false, true), new PeerAuthConfig(false, true, true, true, false), - new PeerAuthConfig(false, true, true, false, true), + new PeerAuthConfig(false, true, true, false, false), new PeerAuthConfig(false, true, false, true, false), new PeerAuthConfig(false, true, false, false, true), new PeerAuthConfig(false, false, true, true, false), - new PeerAuthConfig(false, false, true, false, true), + new PeerAuthConfig(false, false, true, false, false), new PeerAuthConfig(false, false, false, true, false), new PeerAuthConfig(false, false, false, false, true) }; diff --git a/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java b/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java index 30b4ba3..1c8a8af 100644 --- a/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java +++ b/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java @@ -2173,6 +2173,116 @@ public class WolfSSLSocketTest { } + /** + * With wantClientAuth(true) and needClientAuth(false), a client that + * presents a certificate the server cannot validate must abort the + * handshake, matching SunJSSE. wantClientAuth only makes an absent + * client cert non-fatal, not an invalid one. + */ + @Test + public void testWantClientAuthRejectsUntrustedClientCert() + throws Exception { + + /* Negative case: server does not trust the presented client cert. + * Server truststore (caServerJKS) does not contain the CA that signed + * the client cert, so a presented client cert cannot be validated. */ + SSLContext srvCtx = tf.createSSLContext("TLSv1.2", ctxProvider, + tf.createTrustManager("SunX509", tf.caServerJKS, ctxProvider), + tf.createKeyManager("SunX509", tf.serverJKS, ctxProvider)); + + /* Client trusts the server and has a client cert (clientJKS) to + * present when the server sends a CertificateRequest. */ + SSLContext cliCtx = tf.createSSLContext("TLSv1.2", ctxProvider, + tf.createTrustManager("SunX509", tf.caServerJKS, ctxProvider), + tf.createKeyManager("SunX509", tf.clientJKS, ctxProvider)); + + SSLServerSocket ss = (SSLServerSocket)srvCtx.getServerSocketFactory() + .createServerSocket(0); + SSLSocket cs = (SSLSocket)cliCtx.getSocketFactory().createSocket(); + cs.connect(new InetSocketAddress(ss.getLocalPort())); + + final SSLSocket server = (SSLSocket)ss.accept(); + server.setWantClientAuth(true); + server.setNeedClientAuth(false); + + ExecutorService es = Executors.newSingleThreadExecutor(); + Future serverFuture = es.submit(new Callable() { + @Override + public Void call() throws Exception { + try { + server.startHandshake(); + fail("Server accepted an untrusted client cert under " + + "wantClientAuth"); + + } catch (SSLException e) { + /* expected: presented client cert failed validation */ + } + server.close(); + return null; + } + }); + + try { + cs.startHandshake(); + fail("Client handshake succeeded despite server rejecting cert"); + + } catch (SSLException e) { + /* expected: server sent a fatal alert */ + } + cs.close(); + + es.shutdown(); + serverFuture.get(); + ss.close(); + + /* Server truststore (caClientJKS) trusts the client cert, so a valid + * presented client cert must let the handshake complete. */ + SSLContext srvCtx2 = tf.createSSLContext("TLSv1.2", ctxProvider, + tf.createTrustManager("SunX509", tf.caClientJKS, ctxProvider), + tf.createKeyManager("SunX509", tf.serverJKS, ctxProvider)); + + SSLContext cliCtx2 = tf.createSSLContext("TLSv1.2", ctxProvider, + tf.createTrustManager("SunX509", tf.caServerJKS, ctxProvider), + tf.createKeyManager("SunX509", tf.clientJKS, ctxProvider)); + + ss = (SSLServerSocket)srvCtx2.getServerSocketFactory() + .createServerSocket(0); + cs = (SSLSocket)cliCtx2.getSocketFactory().createSocket(); + cs.connect(new InetSocketAddress(ss.getLocalPort())); + + final SSLSocket server2 = (SSLSocket)ss.accept(); + server2.setWantClientAuth(true); + server2.setNeedClientAuth(false); + + es = Executors.newSingleThreadExecutor(); + serverFuture = es.submit(new Callable() { + @Override + public Void call() throws Exception { + try { + server2.startHandshake(); + + } catch (SSLException e) { + fail("Server rejected a valid client cert under " + + "wantClientAuth"); + } + server2.close(); + return null; + } + }); + + try { + cs.startHandshake(); + + } catch (SSLException e) { + fail("Client handshake failed with a valid client cert"); + } + cs.close(); + + es.shutdown(); + serverFuture.get(); + ss.close(); + } + @Test public void testProtocolTLSv10() throws Exception {