This is an automated email from the ASF dual-hosted git repository. elecharny pushed a commit to branch 2.2.X in repository https://gitbox.apache.org/repos/asf/mina.git
commit 3937c93c1be2595b93c1c20b5bce8549165aa8cb Author: emmanuel lecharny <elecha...@apache.org> AuthorDate: Wed May 10 05:44:04 2023 +0200 Applied patch for DIRMINA-1122 --- .../java/org/apache/mina/filter/ssl/SslFilter.java | 79 +++++- .../filter/ssl/SslIdentificationAlgorithmTest.java | 316 +++++++++++++++++++++ mina-core/src/test/resources/log4j.properties | 5 +- .../apache/mina/filter/ssl/client-cn.truststore | Bin 0 -> 951 bytes .../mina/filter/ssl/client-san-ext.truststore | Bin 0 -> 986 bytes .../apache/mina/filter/ssl/emptykeystore.sslTest | Bin 0 -> 32 bytes .../org/apache/mina/filter/ssl/server-cn.keystore | Bin 0 -> 2240 bytes .../apache/mina/filter/ssl/server-san-ext.keystore | Bin 0 -> 2276 bytes 8 files changed, 390 insertions(+), 10 deletions(-) diff --git a/mina-core/src/main/java/org/apache/mina/filter/ssl/SslFilter.java b/mina-core/src/main/java/org/apache/mina/filter/ssl/SslFilter.java index 0539c811d..052c899b3 100644 --- a/mina-core/src/main/java/org/apache/mina/filter/ssl/SslFilter.java +++ b/mina-core/src/main/java/org/apache/mina/filter/ssl/SslFilter.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.filterchain.IoFilterAdapter; @@ -98,6 +99,11 @@ public class SslFilter extends IoFilterAdapter { **/ protected String[] enabledProtocols; + /** + * EndPoint identification algorithms + */ + private String identificationAlgorithm; + /** * Creates a new SSL filter using the specified {@link SSLContext}. * @@ -166,6 +172,25 @@ public class SslFilter extends IoFilterAdapter { this.enabledCipherSuites = enabledCipherSuites; } + /** + * @return the endpoint identification algorithm to be used when {@link SSLEngine} + * is initialized. <tt>null</tt> means 'use {@link SSLEngine}'s default.' + */ + public String getEndpointIdentificationAlgorithm() { + return identificationAlgorithm; + } + + /** + * Sets the endpoint identification algorithm to be used when {@link SSLEngine} + * is initialized. + * + * @param identificationAlgorithm <tt>null</tt> means 'use {@link SSLEngine}'s default.' + */ + public void setEndpointIdentificationAlgorithm(String identificationAlgorithm) { + this.identificationAlgorithm = identificationAlgorithm; + } + + /** * @return the list of protocols to be enabled when {@link SSLEngine} is * initialized. <code>null</code> means 'use {@link SSLEngine}'s default.' @@ -206,7 +231,11 @@ public class SslFilter extends IoFilterAdapter { } if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Adding the SSL Filter {} to the chain", name); + if (parent.getSession().isServer()) { + LOGGER.debug("SERVER: Adding the SSL Filter '{}' to the chain", name); + } else { + LOGGER.debug("CLIENT: Adding the SSL Filter '{}' to the chain", name); + } } } @@ -279,8 +308,13 @@ public class SslFilter extends IoFilterAdapter { * @return an SSLEngine */ protected SSLEngine createEngine(IoSession session, InetSocketAddress addr) { - SSLEngine sslEngine = (addr != null) ? sslContext.createSSLEngine(addr.getHostString(), addr.getPort()) - : sslContext.createSSLEngine(); + SSLEngine sslEngine; + + if (addr != null) { + sslEngine = sslContext.createSSLEngine(addr.getHostName(), addr.getPort()); + } else { + sslEngine = sslContext.createSSLEngine(); + } // Always start with WANT, which will be squashed by NEED if NEED is true. // Actually, it makes not a lot of sense to select NEED and WANT. NEED >> WANT... @@ -299,6 +333,13 @@ public class SslFilter extends IoFilterAdapter { if (enabledProtocols != null) { sslEngine.setEnabledProtocols(enabledProtocols); } + + // Set the endpoint identification algorithm + if (getEndpointIdentificationAlgorithm() != null) { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm()); + sslEngine.setSSLParameters(sslParameters); + } sslEngine.setUseClientMode(!session.isServer()); @@ -311,7 +352,11 @@ public class SslFilter extends IoFilterAdapter { @Override public void sessionOpened(NextFilter next, IoSession session) throws Exception { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("session {} openend", session); + if (session.isServer()) { + LOGGER.debug("SERVER: Session {} openend", session); + } else { + LOGGER.debug("CLIENT: Session {} openend", session); + } } onConnected(next, session); @@ -324,9 +369,13 @@ public class SslFilter extends IoFilterAdapter { @Override public void sessionClosed(NextFilter next, IoSession session) throws Exception { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("session {} closed", session); + if (session.isServer()) { + LOGGER.debug("SERVER: Session {} closed", session); + } else { + LOGGER.debug("CLIENT: Session {} closed", session); + } } - + onClose(next, session, false); super.sessionClosed(next, session); } @@ -345,7 +394,11 @@ public class SslFilter extends IoFilterAdapter { //System.out.println( message ); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("session {} received {}", session, message); + if (session.isServer()) { + LOGGER.debug("SERVER: Session {} received {}", session, message); + } else { + LOGGER.debug("CLIENT: Session {} received {}", session, message); + } } SslHandler sslHandler = getSslHandler(session); @@ -358,7 +411,11 @@ public class SslFilter extends IoFilterAdapter { @Override public void messageSent(NextFilter next, IoSession session, WriteRequest request) throws Exception { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("session {} ack {}", session, request); + if (session.isServer()) { + LOGGER.debug("SERVER: Session {} ack {}", session, request); + } else { + LOGGER.debug("CLIENT: Session {} ack {}", session, request); + } } if (request instanceof EncryptedWriteRequest) { @@ -380,7 +437,11 @@ public class SslFilter extends IoFilterAdapter { @Override public void filterWrite(NextFilter next, IoSession session, WriteRequest request) throws Exception { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("session {} write {}", session, request); + if (session.isServer()) { + LOGGER.debug("SERVER: Session {} write {}", session, request); + } else { + LOGGER.debug("CLIENT: Session {} write {}", session, request); + } } if (request instanceof EncryptedWriteRequest || request instanceof DisableEncryptWriteRequest) { diff --git a/mina-core/src/test/java/org/apache/mina/filter/ssl/SslIdentificationAlgorithmTest.java b/mina-core/src/test/java/org/apache/mina/filter/ssl/SslIdentificationAlgorithmTest.java new file mode 100644 index 000000000..f946b7e09 --- /dev/null +++ b/mina-core/src/test/java/org/apache/mina/filter/ssl/SslIdentificationAlgorithmTest.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.mina.filter.ssl; + +import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder; +import org.apache.mina.core.filterchain.IoFilterChain; +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.FilterEvent; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.codec.textline.TextLineCodecFactory; +import org.apache.mina.transport.socket.nio.NioSocketAcceptor; +import org.apache.mina.transport.socket.nio.NioSocketConnector; +import org.apache.mina.util.AvailablePortFinder; +import org.junit.Before; +import org.junit.Test; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManagerFactory; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.security.Security; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Test SNI matching scenarios. (tests for DIRMINA-1122) + * + * <pre> + * emptykeystore.sslTest - empty keystore + * server-cn.keystore - keystore with single certificate chain (CN=mina) + * client-cn.truststore - keystore with trusted certificate + * server-san-ext.keystore - keystore with single certificate chain (CN=mina;SAN=*.bbb.ccc,xxx.yyy) + * client-san-ext.truststore - keystore with trusted certificate + * </pre> + */ +public class SslIdentificationAlgorithmTest { + + private static final String KEY_MANAGER_FACTORY_ALGORITHM; + + static { + String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); + + if (algorithm == null) { + algorithm = KeyManagerFactory.getDefaultAlgorithm(); + } + + KEY_MANAGER_FACTORY_ALGORITHM = algorithm; + } + + private int port; + private CountDownLatch handshakeDone; + + private class CustomSslFilter extends SslFilter { + public CustomSslFilter(SSLContext sslContext) { + super(sslContext); + } + + protected SSLEngine createEngine(IoSession session, InetSocketAddress addr) { + //Add your SNI host name and port in the IOSession + String sniHostNames = (String)session.getAttribute( "SNIHostNames" ); + int portNumber = (int)session.getAttribute( "PortNumber"); + InetSocketAddress peer = new InetSocketAddress( sniHostNames, portNumber); + + SSLEngine sslEngine; + + if (addr != null) { + sslEngine = sslContext.createSSLEngine(peer.getHostName(), peer.getPort()); + } else { + sslEngine = sslContext.createSSLEngine(); + } + + // Always start with WANT, which will be squashed by NEED if NEED is true. + // Actually, it makes not a lot of sense to select NEED and WANT. + // NEED >> WANT... + if (wantClientAuth) { + sslEngine.setWantClientAuth(true); + } + + if (needClientAuth) { + sslEngine.setNeedClientAuth(true); + } + + if (enabledCipherSuites != null) { + sslEngine.setEnabledCipherSuites(enabledCipherSuites); + } + + if (enabledProtocols != null) { + sslEngine.setEnabledProtocols(enabledProtocols); + } + + // Set the endpoint identification algorithm + if (getEndpointIdentificationAlgorithm() != null) { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm()); + sslEngine.setSSLParameters(sslParameters); + } + + sslEngine.setUseClientMode(!session.isServer()); + + return sslEngine; + } + } + + @Before + public void setUp() { + port = AvailablePortFinder.getNextAvailable(5555); + handshakeDone = new CountDownLatch(2); + } + + @Test + public void shouldAuthenticateWhenServerCertificateCommonNameMatchesClientSNI() throws Exception { + SSLContext acceptorContext = createSSLContext("server-cn.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-cn.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "mina"); + + assertTrue(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + @Test + public void shouldFailAuthenticationWhenServerCertificateCommonNameDoesNotMatchClientSNI() throws Exception { + SSLContext acceptorContext = createSSLContext("server-cn.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-cn.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "example.com"); + + assertFalse(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + @Test + public void shouldFailAuthenticationWhenClientMissingSNIAndIdentificationAlgorithmProvided() throws Exception { + SSLContext acceptorContext = createSSLContext("server-cn.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-cn.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, null); + + assertFalse(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + /** + * Subject Alternative Name (SAN) scenarios + */ + @Test + public void shouldAuthenticateWhenServerCertificateAlternativeNameMatchesClientSNIExactly() throws Exception { + SSLContext acceptorContext = createSSLContext("server-san-ext.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-san-ext.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "xxx.yyy"); + + assertTrue(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + @Test + public void shouldAuthenticateWhenServerCertificateAlternativeNameMatchesClientSNIViaWildcard() throws Exception { + SSLContext acceptorContext = createSSLContext("server-san-ext.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-san-ext.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "aaa.bbb.ccc"); + + assertTrue(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + @Test + public void shouldFailAuthenticationWhenServerCommonNameMatchesSNIAndSNINotInAlternativeName() throws Exception { + SSLContext acceptorContext = createSSLContext("server-san-ext.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-san-ext.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "mina"); + + assertFalse(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + @Test + public void shouldFailAuthenticationWhenMatchingAlternativeNameWildcardExactly() throws Exception { + SSLContext acceptorContext = createSSLContext("server-san-ext.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-san-ext.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "*.bbb.ccc"); + + assertFalse(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + @Test + public void shouldFailAuthenticationWhenMatchingAlternativeNameWithTooManyLabels() throws Exception { + SSLContext acceptorContext = createSSLContext("server-san-ext.keystore", "emptykeystore.sslTest"); + SSLContext connectorContext = createSSLContext("emptykeystore.sslTest", "client-san-ext.truststore"); + + startAcceptor(acceptorContext); + startConnector(connectorContext, "mmm.nnn.bbb.ccc"); + + assertFalse(handshakeDone.await(10, TimeUnit.SECONDS)); + } + + private void startAcceptor(SSLContext sslContext) throws Exception { + NioSocketAcceptor acceptor = new NioSocketAcceptor(); + acceptor.setReuseAddress(true); + + SslFilter sslFilter = new SslFilter(sslContext); + sslFilter.setEnabledProtocols(new String[] {"TLSv1.2"}); + + DefaultIoFilterChainBuilder filters = acceptor.getFilterChain(); + filters.addLast("ssl", sslFilter); + filters.addLast("text", new ProtocolCodecFilter(new TextLineCodecFactory())); + + acceptor.setHandler(new IoHandlerAdapter() { + + @Override + public void sessionOpened(IoSession session) { + session.write("acceptor write"); + } + + @Override + public void event(IoSession session, FilterEvent event) { + if (event == SslEvent.SECURED) { + handshakeDone.countDown(); + } + } + }); + + acceptor.bind(new InetSocketAddress(port)); + } + + private void startConnector(SSLContext sslContext, String sni) { + NioSocketConnector connector = new NioSocketConnector(); + + SslFilter sslFilter = new CustomSslFilter(sslContext) { + @Override + public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception { + if (sni != null) { + IoSession session = parent.getSession(); + + session.setAttribute("SNIHostNames", sni ); + session.setAttribute("PortNumber", port); + } + + super.onPreAdd(parent, name, nextFilter); + } + }; + + sslFilter.setEndpointIdentificationAlgorithm("HTTPS"); + sslFilter.setEnabledProtocols(new String[] {"TLSv1.2"}); + + DefaultIoFilterChainBuilder filters = connector.getFilterChain(); + filters.addLast("ssl", sslFilter); + filters.addLast("text", new ProtocolCodecFilter(new TextLineCodecFactory())); + + connector.setHandler(new IoHandlerAdapter() { + + @Override + public void sessionOpened(IoSession session) { + session.write("connector write"); + } + + @Override + public void event(IoSession session, FilterEvent event) { + if (event == SslEvent.SECURED) { + handshakeDone.countDown(); + } + } + }); + + connector.connect(new InetSocketAddress("localhost", port)); + } + + private SSLContext createSSLContext(String keyStorePath, String trustStorePath) throws Exception { + char[] password = "password".toCharArray(); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(SslIdentificationAlgorithmTest.class.getResourceAsStream(keyStorePath), password); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEY_MANAGER_FACTORY_ALGORITHM); + kmf.init(keyStore, password); + + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(SslIdentificationAlgorithmTest.class.getResourceAsStream(trustStorePath), password); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(KEY_MANAGER_FACTORY_ALGORITHM); + tmf.init(trustStore); + + SSLContext ctx = SSLContext.getInstance("TLSv1.2"); + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + return ctx; + } +} diff --git a/mina-core/src/test/resources/log4j.properties b/mina-core/src/test/resources/log4j.properties index 1aa61ea95..d97f14ff3 100644 --- a/mina-core/src/test/resources/log4j.properties +++ b/mina-core/src/test/resources/log4j.properties @@ -23,4 +23,7 @@ log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=[%d{HH:mm:ss}] [%t] %p %c - %m%n # you could use this pattern to test the MDC with the Chat server -#log4j.appender.stdout.layout.ConversionPattern=[%d{HH:mm:ss}] %t %p %X{name} [%X{user}] [%X{remoteIp}:%X{remotePort}] [%c] - %m%n \ No newline at end of file +#log4j.appender.stdout.layout.ConversionPattern=[%d{HH:mm:ss}] %t %p %X{name} [%X{user}] [%X{remoteIp}:%X{remotePort}] [%c] - %m%n + +#log4j.logger.org.apache.mina.filter.ssl.SSLFilter=DEBUG +#log4j.logger.org.apache.mina.filter.ssl.SSLHandler=DEBUG diff --git a/mina-core/src/test/resources/org/apache/mina/filter/ssl/client-cn.truststore b/mina-core/src/test/resources/org/apache/mina/filter/ssl/client-cn.truststore new file mode 100644 index 000000000..b9d3be86c Binary files /dev/null and b/mina-core/src/test/resources/org/apache/mina/filter/ssl/client-cn.truststore differ diff --git a/mina-core/src/test/resources/org/apache/mina/filter/ssl/client-san-ext.truststore b/mina-core/src/test/resources/org/apache/mina/filter/ssl/client-san-ext.truststore new file mode 100644 index 000000000..d6495dc1c Binary files /dev/null and b/mina-core/src/test/resources/org/apache/mina/filter/ssl/client-san-ext.truststore differ diff --git a/mina-core/src/test/resources/org/apache/mina/filter/ssl/emptykeystore.sslTest b/mina-core/src/test/resources/org/apache/mina/filter/ssl/emptykeystore.sslTest new file mode 100644 index 000000000..65d4b6528 Binary files /dev/null and b/mina-core/src/test/resources/org/apache/mina/filter/ssl/emptykeystore.sslTest differ diff --git a/mina-core/src/test/resources/org/apache/mina/filter/ssl/server-cn.keystore b/mina-core/src/test/resources/org/apache/mina/filter/ssl/server-cn.keystore new file mode 100644 index 000000000..ffb935eaa Binary files /dev/null and b/mina-core/src/test/resources/org/apache/mina/filter/ssl/server-cn.keystore differ diff --git a/mina-core/src/test/resources/org/apache/mina/filter/ssl/server-san-ext.keystore b/mina-core/src/test/resources/org/apache/mina/filter/ssl/server-san-ext.keystore new file mode 100644 index 000000000..600ef3a7a Binary files /dev/null and b/mina-core/src/test/resources/org/apache/mina/filter/ssl/server-san-ext.keystore differ