From 032de2b42f43f6a917ac3172bec418bb1a1616c0 Mon Sep 17 00:00:00 2001 From: ben1222 Date: Tue, 22 Oct 2024 22:10:58 -0400 Subject: [PATCH] Add peer host and port info for server SslHandler --- .../core/http/impl/HttpServerWorker.java | 2 +- .../io/vertx/core/http/impl/HttpUtils.java | 18 ++ .../io/vertx/core/net/impl/NetServerImpl.java | 5 +- .../io/vertx/core/net/impl/NetSocketImpl.java | 3 +- .../core/net/impl/SslChannelProvider.java | 20 +- .../vertx/core/net/impl/VertxSniHandler.java | 13 +- .../java/io/vertx/core/http/HttpTLSTest.java | 192 +++++++++++++++++- src/test/java/io/vertx/core/net/NetTest.java | 51 +++++ 8 files changed, 289 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java index baac3693afb..13f6d7ee78b 100644 --- a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java +++ b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java @@ -129,7 +129,7 @@ public void accept(Channel ch, SslChannelProvider sslChannelProvider) { private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) { ChannelPipeline pipeline = ch.pipeline(); if (options.isSsl()) { - pipeline.addLast("ssl", sslChannelProvider.createServerHandler()); + pipeline.addLast("ssl", sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(ch.remoteAddress()))); ChannelPromise p = ch.newPromise(); pipeline.addLast("handshaker", new SslHandshakeCompletionHandler(p)); p.addListener(future -> { diff --git a/src/main/java/io/vertx/core/http/impl/HttpUtils.java b/src/main/java/io/vertx/core/http/impl/HttpUtils.java index 02971b3e4e6..238341a1dcc 100644 --- a/src/main/java/io/vertx/core/http/impl/HttpUtils.java +++ b/src/main/java/io/vertx/core/http/impl/HttpUtils.java @@ -40,6 +40,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; @@ -1078,4 +1080,20 @@ public static boolean canUpgradeToWebSocket(HttpServerRequest req) { } return false; } + + /** + * Convert a {@link SocketAddress} to a {@link HostAndPort}. + * If the socket address is an {@link InetSocketAddress}, the hostString and port are used. + * Otherwise {@code null} is returned. + * + * @param socketAddress The socket address to convert + * @return The converted instance or {@code null} if not applicable. + */ + public static HostAndPort socketAddressToHostAndPort(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + return new HostAndPortImpl(inetSocketAddress.getHostString(), inetSocketAddress.getPort()); + } + return null; + } } diff --git a/src/main/java/io/vertx/core/net/impl/NetServerImpl.java b/src/main/java/io/vertx/core/net/impl/NetServerImpl.java index b77d2bc7633..9f8c3ca4949 100644 --- a/src/main/java/io/vertx/core/net/impl/NetServerImpl.java +++ b/src/main/java/io/vertx/core/net/impl/NetServerImpl.java @@ -14,7 +14,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; -import io.netty.channel.EventLoopGroup; import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.stream.ChunkedWriteHandler; @@ -26,6 +25,7 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Promise; +import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.logging.Logger; @@ -34,7 +34,6 @@ import io.vertx.core.net.NetServerOptions; import io.vertx.core.net.NetSocket; import io.vertx.core.net.SocketAddress; -import io.vertx.core.net.TrafficShapingOptions; import io.vertx.core.spi.metrics.MetricsProvider; import io.vertx.core.spi.metrics.TCPMetrics; import io.vertx.core.spi.metrics.VertxMetrics; @@ -223,7 +222,7 @@ public void accept(Channel ch, SslChannelProvider sslChannelProvider) { private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) { if (options.isSsl()) { - ch.pipeline().addLast("ssl", sslChannelProvider.createServerHandler()); + ch.pipeline().addLast("ssl", sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(ch.remoteAddress()))); ChannelPromise p = ch.newPromise(); ch.pipeline().addLast("handshaker", new SslHandshakeCompletionHandler(p)); p.addListener(future -> { diff --git a/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java b/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java index 0dd073392c0..883b653591f 100644 --- a/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java +++ b/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java @@ -26,6 +26,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.future.PromiseInternal; import io.vertx.core.impl.logging.Logger; @@ -337,7 +338,7 @@ public Future upgradeToSsl(String serverName) { if (remoteAddress != null) { sslHandler = sslChannelProvider.createClientSslHandler(remoteAddress, serverName, false); } else { - sslHandler = sslChannelProvider.createServerHandler(); + sslHandler = sslChannelProvider.createServerHandler(HttpUtils.socketAddressToHostAndPort(chctx.channel().remoteAddress())); } chctx.pipeline().addFirst("ssl", sslHandler); } else { diff --git a/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java b/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java index 290bf8c23f7..4c54eb412b9 100644 --- a/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java +++ b/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java @@ -18,6 +18,7 @@ import io.netty.util.AsyncMapping; import io.netty.util.concurrent.ImmediateExecutor; import io.vertx.core.VertxException; +import io.vertx.core.net.HostAndPort; import io.vertx.core.net.SocketAddress; import javax.net.ssl.KeyManagerFactory; @@ -143,25 +144,30 @@ public SslHandler createClientSslHandler(SocketAddress remoteAddress, String ser return sslHandler; } - public ChannelHandler createServerHandler() { + public ChannelHandler createServerHandler(HostAndPort remoteAddress) { if (sni) { - return createSniHandler(); + return createSniHandler(remoteAddress); } else { - return createServerSslHandler(useAlpn); + return createServerSslHandler(useAlpn, remoteAddress); } } - private SslHandler createServerSslHandler(boolean useAlpn) { + private SslHandler createServerSslHandler(boolean useAlpn, HostAndPort remoteAddress) { SslContext sslContext = sslServerContext(useAlpn); Executor delegatedTaskExec = useWorkerPool ? workerPool : ImmediateExecutor.INSTANCE; - SslHandler sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec); + SslHandler sslHandler; + if (remoteAddress != null) { + sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, remoteAddress.host(), remoteAddress.port(), delegatedTaskExec); + } else { + sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec); + } sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit); return sslHandler; } - private SniHandler createSniHandler() { + private SniHandler createSniHandler(HostAndPort remoteAddress) { Executor delegatedTaskExec = useWorkerPool ? workerPool : ImmediateExecutor.INSTANCE; - return new VertxSniHandler(serverNameMapping(), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec); + return new VertxSniHandler(serverNameMapping(), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec, remoteAddress); } private static int idx(boolean useAlpn) { diff --git a/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java b/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java index 90d5a571c57..07142c6d1c4 100644 --- a/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java +++ b/src/main/java/io/vertx/core/net/impl/VertxSniHandler.java @@ -15,6 +15,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.util.AsyncMapping; +import io.vertx.core.net.HostAndPort; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -27,16 +28,24 @@ class VertxSniHandler extends SniHandler { private final Executor delegatedTaskExec; + private final HostAndPort remoteAddress; - public VertxSniHandler(AsyncMapping mapping, long handshakeTimeoutMillis, Executor delegatedTaskExec) { + public VertxSniHandler(AsyncMapping mapping, long handshakeTimeoutMillis, Executor delegatedTaskExec, + HostAndPort remoteAddress) { super(mapping, handshakeTimeoutMillis); this.delegatedTaskExec = delegatedTaskExec; + this.remoteAddress = remoteAddress; } @Override protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) { - SslHandler sslHandler = context.newHandler(allocator, delegatedTaskExec); + SslHandler sslHandler; + if (remoteAddress != null) { + sslHandler = context.newHandler(allocator, remoteAddress.host(), remoteAddress.port(), delegatedTaskExec); + } else { + sslHandler = context.newHandler(allocator, delegatedTaskExec); + } sslHandler.setHandshakeTimeout(handshakeTimeoutMillis, TimeUnit.MILLISECONDS); return sslHandler; } diff --git a/src/test/java/io/vertx/core/http/HttpTLSTest.java b/src/test/java/io/vertx/core/http/HttpTLSTest.java index 6a78dad41d0..4ca45faceb3 100755 --- a/src/test/java/io/vertx/core/http/HttpTLSTest.java +++ b/src/test/java/io/vertx/core/http/HttpTLSTest.java @@ -27,8 +27,10 @@ import java.security.interfaces.RSAPrivateKey; import java.util.*; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -36,7 +38,6 @@ import io.vertx.core.*; import io.vertx.core.impl.VertxThread; -import io.vertx.core.net.SSLOptions; import io.vertx.core.net.impl.KeyStoreHelper; import org.junit.Assume; import org.junit.Rule; @@ -2105,4 +2106,193 @@ public PrivateKey getPrivateKey(String alias) { // It is fine using worker threads in this case } } + + /** + * Test that for HttpServer, the peer host and port info is available in the SSLEngine + * when the X509ExtendedKeyManager.chooseEngineServerAlias is called. + * + * @throws Exception if an error occurs + */ + @Test + public void testTLSServerSSLEnginePeerHost() throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + testTLS(Cert.NONE, Trust.SERVER_JKS, testPeerHostServerCert(Cert.SERVER_JKS, called), Trust.NONE).pass(); + assertTrue("X509ExtendedKeyManager.chooseEngineServerAlias is not called", called.get()); + } + + /** + * Test that for HttpServer with SNI, the peer host and port info is available in the SSLEngine + * when the X509ExtendedKeyManager.chooseEngineServerAlias is called. + * + * @throws Exception if an error occurs + */ + @Test + public void testSNIServerSSLEnginePeerHost() throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + TLSTest test = testTLS(Cert.NONE, Trust.SNI_JKS_HOST2, testPeerHostServerCert(Cert.SNI_JKS, called), Trust.NONE) + .serverSni() + .requestOptions(new RequestOptions().setSsl(true).setPort(DEFAULT_HTTPS_PORT).setHost("host2.com")) + .pass(); + assertEquals("host2.com", TestUtils.cnOf(test.clientPeerCert())); + assertEquals("host2.com", test.indicatedServerName); + assertTrue("X509ExtendedKeyManager.chooseEngineServerAlias is not called", called.get()); + } + + /** + * Create a {@link Cert} that will verify the peer host is not null and port is not -1 in the {@link SSLEngine} + * when the {@link X509ExtendedKeyManager#chooseEngineServerAlias(String, Principal[], SSLEngine)} + * is called. + * + * @param delegate The delegated Cert + * @param chooseEngineServerAliasCalled Will be set to true when the + * X509ExtendedKeyManager.chooseEngineServerAlias is called + * @return The {@link Cert} + */ + public static Cert testPeerHostServerCert(Cert delegate, AtomicBoolean chooseEngineServerAliasCalled) { + return testPeerHostServerCert(delegate, (peerHost, peerPort) -> { + chooseEngineServerAliasCalled.set(true); + if (peerHost == null || peerPort == -1) { + throw new RuntimeException("Missing peer host/port"); + } + }); + } + + /** + * Create a {@link Cert} that will verify the peer host and port in the {@link SSLEngine} + * when the {@link X509ExtendedKeyManager#chooseEngineServerAlias(String, Principal[], SSLEngine)} + * is called. + * + * @param delegate The delegated Cert + * @param peerHostVerifier The consumer to verify the peer host and port when the + * X509ExtendedKeyManager.chooseEngineServerAlias is called + * @return The {@link Cert} + */ + public static Cert testPeerHostServerCert(Cert delegate, BiConsumer peerHostVerifier) { + return () -> new VerifyServerPeerHostKeyCertOptions(delegate.get(), peerHostVerifier); + } + + private static class VerifyServerPeerHostKeyCertOptions implements KeyCertOptions { + private final KeyCertOptions delegate; + private final BiConsumer peerHostVerifier; + + VerifyServerPeerHostKeyCertOptions(KeyCertOptions delegate, BiConsumer peerHostVerifier) { + this.delegate = delegate; + this.peerHostVerifier = peerHostVerifier; + } + + @Override + public KeyCertOptions copy() { + return new VerifyServerPeerHostKeyCertOptions(delegate.copy(), peerHostVerifier); + } + + @Override + public KeyManagerFactory getKeyManagerFactory(Vertx vertx) throws Exception { + return new VerifyServerPeerHostKeyManagerFactory(delegate.getKeyManagerFactory(vertx), peerHostVerifier); + } + + @Override + public Function keyManagerFactoryMapper(Vertx vertx) throws Exception { + Function mapper = delegate.keyManagerFactoryMapper(vertx); + return serverName -> new VerifyServerPeerHostKeyManagerFactory(mapper.apply(serverName), peerHostVerifier); + } + } + + private static class VerifyServerPeerHostKeyManagerFactory extends KeyManagerFactory { + VerifyServerPeerHostKeyManagerFactory(KeyManagerFactory delegate, BiConsumer peerHostVerifier) { + super(new KeyManagerFactorySpiWrapper(delegate, peerHostVerifier), delegate.getProvider(), delegate.getAlgorithm()); + } + + private static class KeyManagerFactorySpiWrapper extends KeyManagerFactorySpi { + private final KeyManagerFactory delegate; + private final BiConsumer peerHostVerifier; + + KeyManagerFactorySpiWrapper(KeyManagerFactory delegate, BiConsumer peerHostVerifier) { + super(); + this.delegate = delegate; + this.peerHostVerifier = peerHostVerifier; + } + + @Override + protected void engineInit(KeyStore keyStore, char[] chars) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + delegate.init(keyStore, chars); + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException { + delegate.init(managerFactoryParameters); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + KeyManager[] keyManagers = delegate.getKeyManagers().clone(); + for (int i = 0; i < keyManagers.length; ++i) { + KeyManager km = keyManagers[i]; + if (km instanceof X509KeyManager) { + keyManagers[i] = new VerifyServerPeerHostKeyManager((X509KeyManager) km, peerHostVerifier); + } + } + + return keyManagers; + } + } + } + + private static class VerifyServerPeerHostKeyManager extends X509ExtendedKeyManager { + private final X509KeyManager delegate; + private final BiConsumer peerHostVerifier; + + VerifyServerPeerHostKeyManager(X509KeyManager delegate, BiConsumer peerHostVerifier) { + this.delegate = delegate; + this.peerHostVerifier = peerHostVerifier; + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + if (delegate instanceof X509ExtendedKeyManager) { + return ((X509ExtendedKeyManager) delegate).chooseEngineClientAlias(keyType, issuers, engine); + } else { + return delegate.chooseClientAlias(keyType, issuers, null); + } + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + peerHostVerifier.accept(engine.getPeerHost(), engine.getPeerPort()); + if (delegate instanceof X509ExtendedKeyManager) { + return ((X509ExtendedKeyManager) delegate).chooseEngineServerAlias(keyType, issuers, engine); + } else { + return delegate.chooseServerAlias(keyType, issuers, null); + } + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return delegate.chooseClientAlias(keyType, issuers, socket); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return delegate.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public String[] getClientAliases(String s, Principal[] principals) { + return delegate.getClientAliases(s, principals); + } + + @Override + public String[] getServerAliases(String s, Principal[] principals) { + return delegate.getServerAliases(s, principals); + } + + @Override + public X509Certificate[] getCertificateChain(String s) { + return delegate.getCertificateChain(s); + } + + @Override + public PrivateKey getPrivateKey(String s) { + return delegate.getPrivateKey(s); + } + } } diff --git a/src/test/java/io/vertx/core/net/NetTest.java b/src/test/java/io/vertx/core/net/NetTest.java index 78852d4dc96..60ea6220978 100755 --- a/src/test/java/io/vertx/core/net/NetTest.java +++ b/src/test/java/io/vertx/core/net/NetTest.java @@ -106,6 +106,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import static io.vertx.core.http.HttpTLSTest.testPeerHostServerCert; import static io.vertx.core.http.HttpTestBase.DEFAULT_HTTPS_HOST; import static io.vertx.core.http.HttpTestBase.DEFAULT_HTTPS_PORT; import static io.vertx.test.core.TestUtils.*; @@ -4598,4 +4599,54 @@ public void testInvalidPort() { } catch (IllegalArgumentException ignore) { } } + + /** + * Test that for NetServer, the peer host and port info is available in the SSLEngine + * when the X509ExtendedKeyManager.chooseEngineServerAlias is called. + * + * @throws Exception if an error occurs + */ + @Test + public void testTLSServerSSLEnginePeerHost() throws Exception { + testTLSServerSSLEnginePeerHostImpl(false); + } + + /** + * Test that for NetServer with start TLS, the peer host and port info is available + * in the SSLEngine when the X509ExtendedKeyManager.chooseEngineServerAlias is called. + * + * @throws Exception if an error occurs + */ + @Test + public void testStartTLSServerSSLEnginePeerHost() throws Exception { + testTLSServerSSLEnginePeerHostImpl(true); + } + + private void testTLSServerSSLEnginePeerHostImpl(boolean startTLS) throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + testTLS(Cert.NONE, Trust.SERVER_JKS, testPeerHostServerCert(Cert.SERVER_JKS, called), Trust.NONE, + false, false, true, startTLS); + assertTrue("X509ExtendedKeyManager.chooseEngineServerAlias is not called", called.get()); + } + + /** + * Test that for NetServer with SNI, the peer host and port info is available + * in the SSLEngine when the X509ExtendedKeyManager.chooseEngineServerAlias is called. + * + * @throws Exception if an error occurs + */ + @Test + public void testSNIServerSSLEnginePeerHost() throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + TLSTest test = new TLSTest() + .clientTrust(Trust.SNI_JKS_HOST2) + .address(SocketAddress.inetSocketAddress(DEFAULT_HTTPS_PORT, "host2.com")) + .serverCert(testPeerHostServerCert(Cert.SNI_JKS, called)) + .sni(true); + test.run(true); + await(); + assertEquals("host2.com", cnOf(test.clientPeerCert())); + assertEquals("host2.com", test.indicatedServerName); + assertTrue("X509ExtendedKeyManager.chooseEngineServerAlias is not called", called.get()); + } }