Repository: eclipse-vertx/vert.x

Problem statement:
Missing peer host and port info in SSLEngine for server SslHandler
### Version

4.4.9

### Context

We have a customized key manager that extends `X509ExtendedKeyManager` that want to override the `public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)` method to choose the server alias partly depending on the peer host address.
However, the `engine.getPeerHost()` always returns `null`.

After read related code, I find that netty `SslContext.newHandler` do support passing in an advisory peer information of peer host and port.
However, in vert.x, when creating `SslHandler` for server in `SslChannelProvider`, the peer host and port info is not passed to `SslContext.newHandler`, result in `null` for `engine.getPeerHost()` in `X509ExtendedKeyManager.chooseEngineServerAlias`.
(The `SslChannelProvider` do provide peer host and port info when creating client `SslHandler`)

I tried to pass the peer host and port info from `HttpServerWorker` to `SslChannelProvider.createServerHandler` and find the peer host and port are available in the `SSLEngine` in `X509ExtendedKeyManager.chooseEngineServerAlias`:
```diff
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 cf37c4e8b..82402c858 100644
--- a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java
@@ -35,6 +35,8 @@ import io.vertx.core.impl.VertxInternal;
import io.vertx.core.net.impl.*;
import io.vertx.core.spi.metrics.HttpServerMetrics;

+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.BiConsumer;
@@ -131,7 +133,12 @@ public class HttpServerWorker implements BiConsumer<Channel, SslChannelProvider>
   private void configurePipeline(Channel ch, SslChannelProvider sslChannelProvider) {
     ChannelPipeline pipeline = ch.pipeline();
     if (options.isSsl()) {
-      pipeline.addLast("ssl", sslChannelProvider.createServerHandler());
+      SocketAddress remoteAddress = ch.remoteAddress();
+      if (remoteAddress instanceof InetSocketAddress) {
+        pipeline.addLast("ssl", sslChannelProvider.createServerHandler(((InetSocketAddress) remoteAddress).getHostString(), ((InetSocketAddress) remoteAddress).getPort()));
+      } else {
+        pipeline.addLast("ssl", sslChannelProvider.createServerHandler());
+      }
       ChannelPromise p = ch.newPromise();
       pipeline.addLast("handshaker", new SslHandshakeCompletionHandler(p));
       p.addListener(future -> {
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 290bf8c23..cb5aba5d1 100644
--- a/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java
+++ b/src/main/java/io/vertx/core/net/impl/SslChannelProvider.java
@@ -144,17 +144,21 @@ public class SslChannelProvider {
   }

   public ChannelHandler createServerHandler() {
+    return createServerHandler(null, -1);
+  }
+
+  public ChannelHandler createServerHandler(String peerHost, int peerPort) {
     if (sni) {
       return createSniHandler();
     } else {
-      return createServerSslHandler(useAlpn);
+      return createServerSslHandler(useAlpn, peerHost, peerPort);
     }
   }

-  private SslHandler createServerSslHandler(boolean useAlpn) {
+  private SslHandler createServerSslHandler(boolean useAlpn, String peerHost, int peerPort) {
     SslContext sslContext = sslServerContext(useAlpn);
     Executor delegatedTaskExec = useWorkerPool ? workerPool : ImmediateExecutor.INSTANCE;
-    SslHandler sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, delegatedTaskExec);
+    SslHandler sslHandler = sslContext.newHandler(ByteBufAllocator.DEFAULT, peerHost, peerPort, delegatedTaskExec);
     sslHandler.setHandshakeTimeout(sslHandshakeTimeout, sslHandshakeTimeoutUnit);
     return sslHandler;
   }

```

There are a few other places calling `SslChannelProvider.createServerHandler` so although this change works in my use case, a more complete fix may be needed.



Hints:
@ben1222 can you provide a reproducer for this using the vertx tests so we are covered, that would help
@vietj I tried to create a unit test under `Http1xTLSTest`:
```java
public class Http1xTLSTest extends HttpTLSTest {
  private static final Logger LOG = LogManager.getLogger(Http1xTLSTest.class);

  @Test
  public void testTLSServerSSLEnginePeerHost() throws Exception {
    testTLS(Cert.NONE, Trust.SERVER_JKS, () -> {
      try {
        return KeyCertOptions.wrap(new MyKeyManager((X509KeyManager) Cert.SERVER_JKS.get().getKeyManagerFactory(vertx).getKeyManagers()[0]));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }, Trust.NONE).pass();
  }


  private static class MyKeyManager extends X509ExtendedKeyManager {
    private final X509KeyManager wrapped;

    MyKeyManager(X509KeyManager wrapped) {
      this.wrapped = wrapped;
    }

    @Override
    public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
      throw new UnsupportedOperationException();
    }

    @Override
    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
      LOG.info("In chooseEngineServerAlias, keyType: {}, issuers: {}, peer host: {}, peer port: {}",
        keyType, issuers, engine.getPeerHost(), engine.getPeerPort());
      if (engine.getPeerHost() == null || engine.getPeerPort() == -1) {
        throw new RuntimeException("Missing peer host/port");
      }
      return wrapped.chooseServerAlias(keyType, issuers, null);
    }

    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
      throw new UnsupportedOperationException();
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
      throw new UnsupportedOperationException();
    }

    @Override
    public String[] getClientAliases(String s, Principal[] principals) {
      throw new UnsupportedOperationException();
    }

    @Override
    public String[] getServerAliases(String s, Principal[] principals) {
      return wrapped.getServerAliases(s, principals);
    }

    @Override
    public X509Certificate[] getCertificateChain(String s) {
      LOG.info("In getCertificateChain, s: {}", s);
      return wrapped.getCertificateChain(s);
    }

    @Override
    public PrivateKey getPrivateKey(String s) {
      LOG.info("In getPrivateKey, s: {}", s);
      return wrapped.getPrivateKey(s);
    }
  }

//...
}
```
Currently it will fail with:
```
Starting test: Http1xTLSTest#testTLSServerSSLEnginePeerHost 
2024-08-30 00:02:25,606 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.chooseEngineServerAlias:296 null - In chooseEngineServerAlias, keyType: EC, issuers: null, peer host: null, peer port: -1
java.lang.RuntimeException: Missing peer host/port
	at io.vertx.core.http.Http1xTLSTest$MyKeyManager.chooseEngineServerAlias(Http1xTLSTest.java:299)
	at java.base/sun.security.ssl.X509Authentication$X509PossessionGenerator.createServerPossession(X509Authentication.java:293)
	at java.base/sun.security.ssl.X509Authentication$X509PossessionGenerator.createPossession(X509Authentication.java:214)
	at java.base/sun.security.ssl.X509Authentication.createPossession(X509Authentication.java:90)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateProducer.choosePossession(CertificateMessage.java:1081)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateProducer.onProduceCertificate(CertificateMessage.java:970)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateProducer.produce(CertificateMessage.java:961)
	at java.base/sun.security.ssl.SSLHandshake.produce(SSLHandshake.java:436)
...
java.lang.AssertionError: Should not fail Failed to create SSL connection
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertFalse(Assert.java:65)
	at io.vertx.test.core.AsyncTestBase.assertFalse(AsyncTestBase.java:259)
	at io.vertx.core.http.HttpTLSTest.access$300(HttpTLSTest.java:68)
	at io.vertx.core.http.HttpTLSTest$TLSTest.lambda$run$10(HttpTLSTest.java:1312)
	at io.vertx.core.impl.future.FutureImpl$2.onFailure(FutureImpl.java:117)
...
```
With the changes in `HttpServerWorker` and `SslChannelProvider`, it succeeds:
```
Starting test: Http1xTLSTest#testTLSServerSSLEnginePeerHost 
2024-08-30 00:10:35,108 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.chooseEngineServerAlias:294 null - In chooseEngineServerAlias, keyType: EC, issuers: null, peer host: 127.0.0.1, peer port: 48470
2024-08-30 00:10:35,111 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.chooseEngineServerAlias:294 null - In chooseEngineServerAlias, keyType: EC, issuers: null, peer host: 127.0.0.1, peer port: 48470
2024-08-30 00:10:35,112 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.chooseEngineServerAlias:294 null - In chooseEngineServerAlias, keyType: EC, issuers: null, peer host: 127.0.0.1, peer port: 48470
2024-08-30 00:10:35,112 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.chooseEngineServerAlias:294 null - In chooseEngineServerAlias, keyType: RSA, issuers: null, peer host: 127.0.0.1, peer port: 48470
2024-08-30 00:10:35,112 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.getPrivateKey:330 null - In getPrivateKey, s: test-store
2024-08-30 00:10:35,113 [vert.x-eventloop-thread-2] INFO io.vertx.core.http.Http1xTLSTest Http1xTLSTest$MyKeyManager.getCertificateChain:324 null - In getCertificateChain, s: test-store

```
do you mind contributing a pull request to the 4.x branch and master branch ? 
@vietj I can have a try. Do I need to go through some process before sending the pull request? I see the contributing guideline mentioned about signing ECA?
Since there are a few other places (the `NetServerImpl`, `NetSocketImpl`) calling `SslChannelProvider.createServerHandler`, I'll try to also update them to pass the peer host info to `SslChannelProvider.createServerHandler`, is that ok?
you should sign the Eclipse Agreement indeed

everything should be updated and tested in master and 4.x branches
@vietj I signed ECA and opened pull requests on 4.x branch (#5346) and master branch (#5347), please review.

Patch:
diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java
--- 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
--- 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
--- 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
--- 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<Void> 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
--- 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
--- 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<? super String, ? extends SslContext> mapping, long handshakeTimeoutMillis, Executor delegatedTaskExec) {
+  public VertxSniHandler(AsyncMapping<? super String, ? extends SslContext> 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;
   }


Test patch:
diff --git a/src/test/java/io/vertx/core/http/HttpTLSTest.java b/src/test/java/io/vertx/core/http/HttpTLSTest.java
--- 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<KeyCertOptions> testPeerHostServerCert(Cert<? extends KeyCertOptions> 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<KeyCertOptions> testPeerHostServerCert(Cert<? extends KeyCertOptions> delegate, BiConsumer<String, Integer> peerHostVerifier) {
+    return () -> new VerifyServerPeerHostKeyCertOptions(delegate.get(), peerHostVerifier);
+  }
+
+  private static class VerifyServerPeerHostKeyCertOptions implements KeyCertOptions {
+    private final KeyCertOptions delegate;
+    private final BiConsumer<String, Integer> peerHostVerifier;
+
+    VerifyServerPeerHostKeyCertOptions(KeyCertOptions delegate, BiConsumer<String, Integer> 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<String, KeyManagerFactory> keyManagerFactoryMapper(Vertx vertx) throws Exception {
+      Function<String, KeyManagerFactory> mapper = delegate.keyManagerFactoryMapper(vertx);
+      return serverName -> new VerifyServerPeerHostKeyManagerFactory(mapper.apply(serverName), peerHostVerifier);
+    }
+  }
+
+  private static class VerifyServerPeerHostKeyManagerFactory extends KeyManagerFactory {
+    VerifyServerPeerHostKeyManagerFactory(KeyManagerFactory delegate, BiConsumer<String, Integer> peerHostVerifier) {
+      super(new KeyManagerFactorySpiWrapper(delegate, peerHostVerifier), delegate.getProvider(), delegate.getAlgorithm());
+    }
+
+    private static class KeyManagerFactorySpiWrapper extends KeyManagerFactorySpi {
+      private final KeyManagerFactory delegate;
+      private final BiConsumer<String, Integer> peerHostVerifier;
+
+      KeyManagerFactorySpiWrapper(KeyManagerFactory delegate, BiConsumer<String, Integer> 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<String, Integer> peerHostVerifier;
+
+    VerifyServerPeerHostKeyManager(X509KeyManager delegate, BiConsumer<String, Integer> 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
--- 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());
+  }
 }