diff --git a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java index c1a00326620..5922be7e4f9 100644 --- a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java +++ b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.alpn.client; -import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -34,11 +33,10 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.NegotiatingClientConnectionFactory; import org.eclipse.jetty.io.ssl.ALPNProcessor.Client; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; -import org.eclipse.jetty.io.ssl.SslHandshakeListener; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -public class ALPNClientConnectionFactory extends NegotiatingClientConnectionFactory implements SslHandshakeListener +public class ALPNClientConnectionFactory extends NegotiatingClientConnectionFactory { private static final Logger LOG = Log.getLogger(ALPNClientConnectionFactory.class); @@ -96,7 +94,7 @@ public class ALPNClientConnectionFactory extends NegotiatingClientConnectionFact } @Override - public Connection newConnection(EndPoint endPoint, Map context) throws IOException + public Connection newConnection(EndPoint endPoint, Map context) { SSLEngine engine = (SSLEngine)context.get(SslClientConnectionFactory.SSL_ENGINE_CONTEXT_KEY); for (Client processor : processors) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 3d86f7ded42..f683ec3268c 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -32,6 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import org.eclipse.jetty.client.api.ContentResponse; @@ -325,7 +326,6 @@ public class HttpClientTLSTest @Test public void testHandshakeSucceededWithSessionResumption() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); @@ -405,7 +405,6 @@ public class HttpClientTLSTest @Test public void testClientRawCloseDoesNotInvalidateSession() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); @@ -527,4 +526,30 @@ public class HttpClientTLSTest assertTrue(latch.await(5, TimeUnit.SECONDS)); } } + + @Test + public void testHostNameVerificationFailure() throws Exception + { + SslContextFactory serverTLSFactory = createSslContextFactory(); + startServer(serverTLSFactory, new EmptyServerHandler()); + + SslContextFactory clientTLSFactory = createSslContextFactory(); + // Make sure the host name is not verified at the TLS level. + clientTLSFactory.setEndpointIdentificationAlgorithm(null); + // Add host name verification after the TLS handshake. + clientTLSFactory.setHostnameVerifier((host, session) -> false); + startClient(clientTLSFactory); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .send(result -> + { + Throwable failure = result.getFailure(); + if (failure instanceof SSLPeerUnverifiedException) + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java index a86fcb9f3c7..31d99197984 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java @@ -23,7 +23,10 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ClientConnectionFactory; @@ -100,6 +103,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory EndPoint appEndPoint = sslConnection.getDecryptedEndPoint(); appEndPoint.setConnection(connectionFactory.newConnection(appEndPoint, context)); + sslConnection.addHandshakeListener(new HTTPSHandshakeListener(context)); customize(sslConnection, context); return sslConnection; @@ -124,4 +128,37 @@ public class SslClientConnectionFactory implements ClientConnectionFactory } return ClientConnectionFactory.super.customize(connection, context); } + + private class HTTPSHandshakeListener implements SslHandshakeListener + { + private final Map context; + + private HTTPSHandshakeListener(Map context) + { + this.context = context; + } + + @Override + public void handshakeSucceeded(Event event) throws SSLException + { + HostnameVerifier verifier = sslContextFactory.getHostnameVerifier(); + if (verifier != null) + { + String host = (String)context.get(SSL_PEER_HOST_CONTEXT_KEY); + try + { + if (!verifier.verify(host, event.getSSLEngine().getSession())) + throw new SSLPeerUnverifiedException("Host name verification failed for host: " + host); + } + catch (SSLException x) + { + throw x; + } + catch (Throwable x) + { + throw (SSLException)new SSLPeerUnverifiedException("Host name verification failed for host: " + host).initCause(x); + } + } + } + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java index 53467589e95..bc2431d8ecc 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -759,7 +759,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } } - private void handshakeSucceeded() + private void handshakeSucceeded() throws SSLException { if (_handshake.compareAndSet(Handshake.INITIAL, Handshake.SUCCEEDED)) { @@ -1182,7 +1182,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } } - private void notifyHandshakeSucceeded(SSLEngine sslEngine) + private void notifyHandshakeSucceeded(SSLEngine sslEngine) throws SSLException { SslHandshakeListener.Event event = null; for (SslHandshakeListener listener : handshakeListeners) @@ -1193,6 +1193,10 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr { listener.handshakeSucceeded(event); } + catch (SSLException x) + { + throw x; + } catch (Throwable x) { LOG.info("Exception while notifying listener " + listener, x); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java index 5ccff435164..edba6925749 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java @@ -22,6 +22,7 @@ import java.util.EventListener; import java.util.EventObject; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; /** *

Implementations of this interface are notified of TLS handshake events.

@@ -36,7 +37,7 @@ public interface SslHandshakeListener extends EventListener * * @param event the event object carrying information about the TLS handshake event */ - default void handshakeSucceeded(Event event) + default void handshakeSucceeded(Event event) throws SSLException { } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index fde565083f1..71a06604501 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -51,6 +51,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.CertPathTrustManagerParameters; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SNIHostName; @@ -194,6 +195,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable private int _renegotiationLimit = 5; private Factory _factory; private PKIXCertPathChecker _pkixCertPathChecker; + private HostnameVerifier _hostnameVerifier; /** * Construct an instance of SslContextFactory @@ -1596,6 +1598,31 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable _sslSessionTimeout = sslSessionTimeout; } + /** + * @return the HostnameVerifier used by a client to verify host names in the server certificate + */ + public HostnameVerifier getHostnameVerifier() + { + return _hostnameVerifier; + } + + /** + *

Sets a {@code HostnameVerifier} used by a client to verify host names in the server certificate.

+ *

The {@code HostnameVerifier} works in conjunction with {@link #setEndpointIdentificationAlgorithm(String)}.

+ *

When {@code endpointIdentificationAlgorithm=="HTTPS"} (the default) the JDK TLS implementation + * checks that the host name indication set by the client matches the host names in the server certificate. + * If this check passes successfully, the {@code HostnameVerifier} is invoked and the application + * can perform additional checks and allow/deny the connection to the server.

+ *

When {@code endpointIdentificationAlgorithm==null} the JDK TLS implementation will not check + * the host names, and any check is therefore performed only by the {@code HostnameVerifier.}

+ * + * @param hostnameVerifier the HostnameVerifier used by a client to verify host names in the server certificate + */ + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) + { + _hostnameVerifier = hostnameVerifier; + } + /** * Returns the password object for the given realm. *