diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java index 4c0089a43bb8..a0098276ba72 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java @@ -14,8 +14,12 @@ package org.eclipse.jetty.client.ssl; import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; @@ -243,4 +247,46 @@ public void handshakeSucceeded(Event event) assertEquals(HttpStatus.OK_200, response.getStatus()); assertTrue(handshakeLatch.await(10, TimeUnit.SECONDS)); } + + @Test + public void testTrustManagerWrapperAccessToCertChain() throws Exception + { + // Track certificate chain seen during validation + AtomicReference seenCerts = new AtomicReference<>(); + + SslContextFactory.Server serverSSL = createServerSslContextFactory(); + serverSSL.setNeedClientAuth(true); + + // Wrap TrustManager to capture certificate chain during validation + serverSSL.setTrustManagerWrapper(delegate -> + new SslContextFactory.X509ExtendedTrustManagerWrapper(delegate) + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException + { + // Capture the certificate chain before validation + seenCerts.set(chain); + super.checkClientTrusted(chain, authType, engine); + } + }); + + startServer(serverSSL, new EmptyServerHandler()); + + // Client presents a certificate + SslContextFactory.Client clientSSL = new SslContextFactory.Client(true); + clientSSL.setKeyStorePath("src/test/resources/client_keystore.p12"); + clientSSL.setKeyStorePassword("storepwd"); + startClient(clientSSL); + + ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + + // The wrapper should have captured the client certificate chain + assertNotNull(seenCerts.get()); + assertTrue(seenCerts.get().length > 0); + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index e4ddd4a643a2..658f4ef7db96 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -50,6 +50,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.CertPathTrustManagerParameters; @@ -2247,6 +2248,7 @@ public static class Server extends SslContextFactory implements SniX509ExtendedK private boolean _wantClientAuth; private boolean _sniRequired; private SniX509ExtendedKeyManager.SniSelector _sniSelector; + private UnaryOperator _trustManagerWrapper; public Server() { @@ -2423,6 +2425,54 @@ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyMan { return new SniX509ExtendedKeyManager(keyManager, this); } + + /** + * @return the custom function to wrap trust managers + */ + public UnaryOperator getTrustManagerWrapper() + { + return _trustManagerWrapper; + } + + /** + *

Sets a custom function to wrap trust managers.

+ *

This allows intercepting certificate validation to access + * certificate chains even when validation fails.

+ * + * @param wrapper the wrapper function + */ + public void setTrustManagerWrapper(UnaryOperator wrapper) + { + _trustManagerWrapper = wrapper; + } + + /** + *

Creates a new X509ExtendedTrustManager, possibly wrapping the given one.

+ *

Subclasses may override to provide custom trust manager implementations.

+ * + * @param trustManager the trust manager to wrap + * @return the (possibly wrapped) trust manager + */ + protected X509ExtendedTrustManager newX509ExtendedTrustManager(X509ExtendedTrustManager trustManager) + { + UnaryOperator wrapper = getTrustManagerWrapper(); + return wrapper != null ? wrapper.apply(trustManager) : trustManager; + } + + @Override + protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection crls) throws Exception + { + TrustManager[] managers = super.getTrustManagers(trustStore, crls); + if (managers != null) + { + for (int idx = 0; idx < managers.length; idx++) + { + if (managers[idx] instanceof X509ExtendedTrustManager x509TrustManager) + managers[idx] = newX509ExtendedTrustManager(x509TrustManager); + } + } + return managers; + } } /**