001/*
002Copyright 2015 Hendrik Saly
003
004Licensed under the Apache License, Version 2.0 (the "License");
005you may not use this file except in compliance with the License.
006You may obtain a copy of the License at
007
008    http://www.apache.org/licenses/LICENSE-2.0
009
010Unless required by applicable law or agreed to in writing, software
011distributed under the License is distributed on an "AS IS" BASIS,
012WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013See the License for the specific language governing permissions and
014limitations under the License.
015 */
016
017package de.saly.es.example.tssl.netty;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.net.InetSocketAddress;
024import java.security.KeyManagementException;
025import java.security.KeyStore;
026import java.security.KeyStoreException;
027import java.security.NoSuchAlgorithmException;
028import java.security.UnrecoverableKeyException;
029import java.security.cert.CertificateException;
030import java.util.Map;
031
032import javax.net.ssl.KeyManagerFactory;
033import javax.net.ssl.SSLContext;
034import javax.net.ssl.SSLEngine;
035import javax.net.ssl.SSLParameters;
036import javax.net.ssl.TrustManagerFactory;
037
038import org.elasticsearch.Version;
039import org.elasticsearch.common.inject.Inject;
040import org.elasticsearch.common.lang3.StringUtils;
041import org.elasticsearch.common.netty.channel.ChannelHandlerContext;
042import org.elasticsearch.common.netty.channel.ChannelPipeline;
043import org.elasticsearch.common.netty.channel.ChannelPipelineFactory;
044import org.elasticsearch.common.netty.channel.ChannelStateEvent;
045import org.elasticsearch.common.netty.channel.SimpleChannelHandler;
046import org.elasticsearch.common.netty.handler.ssl.SslHandler;
047import org.elasticsearch.common.network.NetworkService;
048import org.elasticsearch.common.settings.Settings;
049import org.elasticsearch.common.util.BigArrays;
050import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
051import org.elasticsearch.threadpool.ThreadPool;
052import org.elasticsearch.transport.netty.NettyTransport;
053
054import de.saly.es.example.tssl.util.ConfigConstants;
055import de.saly.es.example.tssl.util.SecurityUtil;
056
057public class SSLNettyTransport extends SecureNettyTransport {
058
059    private static final int SECURITY_SSL_TRANSPORT_NODE_SESSION_CACHE_SIZE_DEFAULT = 1000; //number of sessions
060    private static final int SECURITY_SSL_TRANSPORT_NODE_SESSION_TIMEOUT_DEFAULT = 24 * 60 * 60; //24h in seconds
061
062    private final Map<String, SSLContext> contextCache = ConcurrentCollections.newConcurrentMap();
063
064    @Inject
065    public SSLNettyTransport(final Settings settings, final ThreadPool threadPool, final NetworkService networkService,
066            final BigArrays bigArrays, final Version version) {
067        super(settings, threadPool, networkService, bigArrays, version);
068    }
069
070    @Override
071    public ChannelPipelineFactory configureClientChannelPipelineFactory() {
072        logger.info("Node client configured for SSL");
073        return new SSLClientChannelPipelineFactory(this, this.settings);
074    }
075
076    @Override
077    public ChannelPipelineFactory configureServerChannelPipelineFactory(final String name, final Settings settings) {
078        logger.info("Node server configured for SSL");
079        return new SSLServerChannelPipelineFactory(this, name, settings, this.settings);
080    }
081
082    protected class SSLServerChannelPipelineFactory extends SecureServerChannelPipelineFactory {
083
084        private final String keystoreType;
085        private final String keystoreFilePath;
086        private final String keystorePassword;
087        private final boolean needClientAuth;
088
089        private final String truststoreType;
090        private final String truststoreFilePath;
091        private final String truststorePassword;
092
093        private final int sslSessionCacheSize;
094        private final int sslSessionTimeout;
095
096        public SSLServerChannelPipelineFactory(final NettyTransport nettyTransport, final String name, final Settings sslsettings,
097                final Settings essettings) {
098            super(nettyTransport, name, sslsettings);
099
100            keystoreType = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_TYPE,
101                    System.getProperty("javax.net.ssl.keyStoreType", "JKS"));
102            keystoreFilePath = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_FILEPATH,
103                    System.getProperty("javax.net.ssl.keyStore", null));
104            keystorePassword = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_PASSWORD,
105                    System.getProperty("javax.net.ssl.keyStorePassword", "changeit"));
106
107            truststoreType = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_TYPE,
108                    System.getProperty("javax.net.ssl.trustStoreType", "JKS"));
109            truststoreFilePath = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_FILEPATH,
110                    System.getProperty("javax.net.ssl.trustStore", null));
111            truststorePassword = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_PASSWORD,
112                    System.getProperty("javax.net.ssl.trustStorePassword", "changeit"));
113
114            sslSessionCacheSize = essettings.getAsInt(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_TYPE,
115                    SECURITY_SSL_TRANSPORT_NODE_SESSION_CACHE_SIZE_DEFAULT);
116            sslSessionTimeout = essettings.getAsInt(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_TYPE,
117                    SECURITY_SSL_TRANSPORT_NODE_SESSION_TIMEOUT_DEFAULT);
118
119            needClientAuth = essettings.getAsBoolean(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_NEED_CLIENTAUTH, true);
120
121        }
122
123        @Override
124        public ChannelPipeline getPipeline() throws Exception {
125            final ChannelPipeline pipeline = super.getPipeline();
126
127            //Setup serverside SSL
128            final SSLContext serverContext = SSLNettyTransport.this.createSSLContext(keystoreType, keystoreFilePath, keystorePassword,
129                    truststoreType, truststoreFilePath, truststorePassword, sslSessionCacheSize, sslSessionTimeout);
130
131            final SSLEngine engine = serverContext.createSSLEngine();
132            final SSLParameters sslParams = new SSLParameters();
133            sslParams.setCipherSuites(SecurityUtil.ENABLED_SSL_CIPHERS);
134            sslParams.setProtocols(SecurityUtil.ENABLED_SSL_PROTOCOLS);
135            sslParams.setNeedClientAuth(needClientAuth);
136            engine.setSSLParameters(sslParams);
137            engine.setUseClientMode(false);
138
139            final SslHandler sslHandler = new SslHandler(engine);
140            sslHandler.setEnableRenegotiation(true);
141            pipeline.addFirst("ssl_server", sslHandler);
142
143            return pipeline;
144        }
145
146    }
147
148    protected static class ClientSslHandler extends SimpleChannelHandler {
149        private final SSLContext serverContext;
150        private final boolean hostnameVerificationEnabled;
151        private final boolean hostnameVerificationResovleHostName;
152
153        private ClientSslHandler(final SSLContext serverContext, final boolean hostnameVerificationEnabled,
154                final boolean hostnameVerificationResovleHostName) {
155            this.hostnameVerificationEnabled = hostnameVerificationEnabled;
156            this.hostnameVerificationResovleHostName = hostnameVerificationResovleHostName;
157            this.serverContext = serverContext;
158        }
159
160        @Override
161        public void connectRequested(final ChannelHandlerContext ctx, final ChannelStateEvent event) {
162
163            //Setup clientside SSL
164
165            SSLEngine engine = null;
166            final SSLParameters sslParams = new SSLParameters();
167            sslParams.setCipherSuites(SecurityUtil.ENABLED_SSL_CIPHERS);
168            sslParams.setProtocols(SecurityUtil.ENABLED_SSL_PROTOCOLS);
169
170            if (hostnameVerificationEnabled) {
171                final InetSocketAddress inetSocketAddress = (InetSocketAddress) event.getValue();
172
173                String hostname = null;
174                if (hostnameVerificationResovleHostName) {
175                    hostname = inetSocketAddress.getHostName();
176                } else {
177                    hostname = inetSocketAddress.getHostString();
178                }
179
180                engine = serverContext.createSSLEngine(hostname, inetSocketAddress.getPort());
181                sslParams.setEndpointIdentificationAlgorithm("HTTPS");
182            } else {
183                engine = serverContext.createSSLEngine();
184            }
185
186            engine.setSSLParameters(sslParams);
187            engine.setUseClientMode(true);
188
189            final SslHandler sslHandler = new SslHandler(engine);
190            sslHandler.setEnableRenegotiation(true);
191            ctx.getPipeline().replace(this, "ssl_client", sslHandler);
192
193            ctx.sendDownstream(event);
194        }
195    }
196
197    protected class SSLClientChannelPipelineFactory extends SecureClientChannelPipelineFactory {
198
199        private final String keystoreType;
200        private final String keystoreFilePath;
201        private final String keystorePassword;
202        private final boolean hostnameVerificationEnabled;
203        private final boolean hostnameVerificationResovleHostName;
204        private final String truststoreType;
205        private final String truststoreFilePath;
206        private final String truststorePassword;
207        private final int sslSessionCacheSize;
208        private final int sslSessionTimeout;
209
210        public SSLClientChannelPipelineFactory(final NettyTransport nettyTransport, final Settings essettings) {
211            super(nettyTransport);
212
213            keystoreType = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_TYPE,
214                    System.getProperty("javax.net.ssl.keyStoreType", "JKS"));
215            keystoreFilePath = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_FILEPATH,
216                    System.getProperty("javax.net.ssl.keyStore", null));
217            keystorePassword = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_PASSWORD,
218                    System.getProperty("javax.net.ssl.keyStorePassword", "changeit"));
219
220            truststoreType = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_TYPE,
221                    System.getProperty("javax.net.ssl.trustStoreType", "JKS"));
222            truststoreFilePath = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_FILEPATH,
223                    System.getProperty("javax.net.ssl.trustStore", null));
224            truststorePassword = essettings.get(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_PASSWORD,
225                    System.getProperty("javax.net.ssl.trustStorePassword", "changeit"));
226
227            sslSessionCacheSize = essettings.getAsInt(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_TYPE,
228                    SECURITY_SSL_TRANSPORT_NODE_SESSION_CACHE_SIZE_DEFAULT);
229            sslSessionTimeout = essettings.getAsInt(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_TYPE,
230                    SECURITY_SSL_TRANSPORT_NODE_SESSION_TIMEOUT_DEFAULT);
231
232            hostnameVerificationEnabled = essettings.getAsBoolean(
233                    ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_ENCFORCE_HOSTNAME_VERIFICATION, true);
234            hostnameVerificationResovleHostName = essettings.getAsBoolean(
235                    ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_ENCFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, true);
236        }
237
238        @Override
239        public ChannelPipeline getPipeline() throws Exception {
240            final ChannelPipeline pipeline = super.getPipeline();
241
242            final SSLContext clientContext = SSLNettyTransport.this.createSSLContext(keystoreType, keystoreFilePath, keystorePassword,
243                    truststoreType, truststoreFilePath, truststorePassword, sslSessionCacheSize, sslSessionTimeout);
244
245            pipeline.addFirst("client_ssl_handler", new ClientSslHandler(clientContext, hostnameVerificationEnabled,
246                    hostnameVerificationResovleHostName));
247
248            return pipeline;
249        }
250
251    }
252
253    private SSLContext createSSLContext(final String keystoreType, final String keystoreFilePath, final String keystorePassword,
254            final String truststoreType, final String truststoreFilePath, final String truststorePassword, final int sslSessionCacheSize,
255            final int sslSessionTimeout) throws KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException,
256            FileNotFoundException, IOException, UnrecoverableKeyException {
257
258        if (StringUtils.isBlank(keystoreFilePath) || StringUtils.isBlank(truststoreFilePath)) {
259            logger.error(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_FILEPATH + " and "
260                    + ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_FILEPATH + " must be set if transport ssl is reqested.");
261            throw new IOException(ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_KEYSTORE_FILEPATH + " and "
262                    + ConfigConstants.SECURITY_SSL_TRANSPORT_NODE_TRUSTSTORE_FILEPATH + " must be set if transport ssl is reqested.");
263        }
264
265        final String contextKey = keystoreFilePath + truststoreFilePath;
266
267        if (contextCache.containsKey(contextKey)) {
268            return contextCache.get(contextKey);
269        }
270
271        //## Keystore ##
272        final KeyStore ks = KeyStore.getInstance(keystoreType);
273        ks.load(new FileInputStream(new File(keystoreFilePath)), keystorePassword.toCharArray());
274        final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
275        kmf.init(ks, keystorePassword.toCharArray());
276
277        //## Truststore ##
278        final KeyStore ts = KeyStore.getInstance(truststoreType);
279        ts.load(new FileInputStream(new File(truststoreFilePath)), truststorePassword.toCharArray());
280        final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
281        tmf.init(ts);
282
283        //## SSLContext ##
284        final SSLContext sslContext = SSLContext.getInstance("TLS");
285        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
286
287        sslContext.getServerSessionContext().setSessionCacheSize(sslSessionCacheSize);
288        sslContext.getServerSessionContext().setSessionTimeout(sslSessionTimeout);
289
290        contextCache.put(contextKey, sslContext);
291        return sslContext;
292    }
293}