diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java index 81eb1d63f8b..26d0760f43e 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java +++ b/vertx-core/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; @@ -819,4 +821,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/vertx-core/src/main/java/io/vertx/core/internal/net/SslChannelProvider.java b/vertx-core/src/main/java/io/vertx/core/internal/net/SslChannelProvider.java index 0885ebd2b6e..9067fa12f7b 100644 --- a/vertx-core/src/main/java/io/vertx/core/internal/net/SslChannelProvider.java +++ b/vertx-core/src/main/java/io/vertx/core/internal/net/SslChannelProvider.java @@ -18,8 +18,10 @@ import io.netty.util.concurrent.ImmediateExecutor; import io.vertx.core.internal.VertxInternal; import io.vertx.core.internal.tls.SslContextProvider; +import io.vertx.core.net.HostAndPort; import io.vertx.core.net.SocketAddress; +import java.net.InetSocketAddress; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -59,25 +61,30 @@ public SslHandler createClientSslHandler(SocketAddress peerAddress, String serve return sslHandler; } - public ChannelHandler createServerHandler(boolean useAlpn, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit) { + public ChannelHandler createServerHandler(boolean useAlpn, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit, HostAndPort remoteAddress) { if (sni) { - return createSniHandler(useAlpn, sslHandshakeTimeout, sslHandshakeTimeoutUnit); + return createSniHandler(useAlpn, sslHandshakeTimeout, sslHandshakeTimeoutUnit, remoteAddress); } else { - return createServerSslHandler(useAlpn, sslHandshakeTimeout, sslHandshakeTimeoutUnit); + return createServerSslHandler(useAlpn, sslHandshakeTimeout, sslHandshakeTimeoutUnit, remoteAddress); } } - private SslHandler createServerSslHandler(boolean useAlpn, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit) { + private SslHandler createServerSslHandler(boolean useAlpn, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit, HostAndPort remoteAddress) { SslContext sslContext = sslContextProvider.sslServerContext(useAlpn); Executor delegatedTaskExec = sslContextProvider.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(boolean useAlpn, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit) { + private SniHandler createSniHandler(boolean useAlpn, long sslHandshakeTimeout, TimeUnit sslHandshakeTimeoutUnit, HostAndPort remoteAddress) { Executor delegatedTaskExec = sslContextProvider.useWorkerPool() ? workerPool : ImmediateExecutor.INSTANCE; - return new VertxSniHandler(sslContextProvider.serverNameMapping(delegatedTaskExec, useAlpn), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec); + return new VertxSniHandler(sslContextProvider.serverNameMapping(delegatedTaskExec, useAlpn), sslHandshakeTimeoutUnit.toMillis(sslHandshakeTimeout), delegatedTaskExec, remoteAddress); } } diff --git a/vertx-core/src/main/java/io/vertx/core/internal/net/VertxSniHandler.java b/vertx-core/src/main/java/io/vertx/core/internal/net/VertxSniHandler.java index 36e43815858..cbbceaa763e 100644 --- a/vertx-core/src/main/java/io/vertx/core/internal/net/VertxSniHandler.java +++ b/vertx-core/src/main/java/io/vertx/core/internal/net/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/vertx-core/src/main/java/io/vertx/core/net/impl/NetServerImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/NetServerImpl.java index e196a7e357a..a3ca2bc1cc0 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/NetServerImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/NetServerImpl.java @@ -26,6 +26,7 @@ import io.vertx.core.buffer.impl.PartialPooledByteBufAllocator; import io.vertx.core.http.ClientAuth; import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.internal.CloseSequence; import io.vertx.core.impl.HostnameResolver; import io.vertx.core.internal.ContextInternal; @@ -221,7 +222,8 @@ public void accept(Channel ch, SslContextProvider sslChannelProvider, SslContext private void configurePipeline(Channel ch, SslContextProvider sslContextProvider, SslContextManager sslContextManager, ServerSSLOptions sslOptions) { if (options.isSsl()) { SslChannelProvider sslChannelProvider = new SslChannelProvider(vertx, sslContextProvider, sslOptions.isSni()); - ch.pipeline().addLast("ssl", sslChannelProvider.createServerHandler(options.isUseAlpn(), options.getSslHandshakeTimeout(), options.getSslHandshakeTimeoutUnit())); + ch.pipeline().addLast("ssl", sslChannelProvider.createServerHandler(options.isUseAlpn(), options.getSslHandshakeTimeout(), + options.getSslHandshakeTimeoutUnit(), HttpUtils.socketAddressToHostAndPort(ch.remoteAddress()))); ChannelPromise p = ch.newPromise(); ch.pipeline().addLast("handshaker", new SslHandshakeCompletionHandler(p)); p.addListener(future -> { diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java index 68b8948fe9d..bf6779d861e 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java @@ -27,6 +27,7 @@ import io.vertx.core.eventbus.Message; import io.vertx.core.eventbus.MessageConsumer; import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.internal.ContextInternal; import io.vertx.core.internal.PromiseInternal; import io.vertx.core.internal.concurrent.InboundMessageQueue; @@ -323,7 +324,8 @@ private Future sslUpgrade(String serverName, SSLOptions sslOptions, ByteBu ClientSSLOptions clientSSLOptions = (ClientSSLOptions) sslOptions; sslHandler = provider.createClientSslHandler(remoteAddress, serverName, sslOptions.isUseAlpn(), clientSSLOptions.getSslHandshakeTimeout(), clientSSLOptions.getSslHandshakeTimeoutUnit()); } else { - sslHandler = provider.createServerHandler(sslOptions.isUseAlpn(), sslOptions.getSslHandshakeTimeout(), sslOptions.getSslHandshakeTimeoutUnit()); + sslHandler = provider.createServerHandler(sslOptions.isUseAlpn(), sslOptions.getSslHandshakeTimeout(), + sslOptions.getSslHandshakeTimeoutUnit(), HttpUtils.socketAddressToHostAndPort(chctx.channel().remoteAddress())); } chctx.pipeline().addFirst("ssl", sslHandler); channelPromise.addListener(p); diff --git a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java index fd077d58cd6..8a25381377d 100755 --- a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java @@ -91,6 +91,7 @@ import static io.vertx.test.http.HttpTestBase.DEFAULT_HTTPS_HOST; import static io.vertx.test.http.HttpTestBase.DEFAULT_HTTPS_PORT; import static io.vertx.test.core.TestUtils.*; +import static io.vertx.tests.tls.HttpTLSTest.testPeerHostServerCert; import static org.hamcrest.CoreMatchers.*; /** @@ -4602,4 +4603,54 @@ public void testConnectToServerShutdown() throws Exception { assertWaitUntil(closed::get); fut.await(); } + + /** + * 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()); + } } diff --git a/vertx-core/src/test/java/io/vertx/tests/tls/HttpTLSTest.java b/vertx-core/src/test/java/io/vertx/tests/tls/HttpTLSTest.java index 5e3e7cf9835..4524635b812 100755 --- a/vertx-core/src/test/java/io/vertx/tests/tls/HttpTLSTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/tls/HttpTLSTest.java @@ -28,8 +28,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; @@ -2118,4 +2120,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); + } + } }