0

I've been playing with Netty's (version 4.0.24) SecureChatServer examples and i've plugged in my own keystore (based on the answers found on the following two posts post 1 and post2. See code snippets below.

The issue i'm seeing is that everything works as expected when I run one instance of the client, but when i try to launch more client instances i get exceptions on both the server and client (see below for exception text) and at that point the server stops responding.

I'm hoping someone here could shed some light into what i'm doing wrong.

This is the class that handles loading my keyStore and creating SSLContext instances used by both client/server (for obvious reasons i've omitted my keystore values):

package com.test.securechat;

import java.io.ByteArrayInputStream;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import com.test.util.Key;
import com.pragmafs.util.encryption.Base64Coder;

/**
 * Creates SSLContext instances from a static (String representation) keystore
 * See http://maxrohde.com/2013/09/07/setting-up-ssl-with-netty/
 */
public class SslContextFactory {
    private static final String PROTOCOL = "TLS";
    private static final SSLContext SERVER_CONTEXT;
    private static final SSLContext CLIENT_CONTEXT;
    private static final TrustManagerFactory trustManagerFactory;

    static {

        SSLContext serverContext = null;
        SSLContext clientContext = null;

        try {

            KeyStore ks = KeyStore.getInstance("JKS");
            ks.load(new ByteArrayInputStream(Base64Coder.decode(Key.SSL.getKey())), Base64Coder.decodeString(Key.SSL.getPwd()).toCharArray());

            // Set up key manager factory to use our key store
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(ks, Base64Coder.decodeString(Key.SSL.getPwd()).toCharArray());

            // truststore
            KeyStore ts = KeyStore.getInstance("JKS");
            ts.load(new ByteArrayInputStream(Base64Coder.decode(Key.SSL.getKey())), Base64Coder.decodeString(Key.SSL.getPwd()).toCharArray());

            // set up trust manager factory to use our trust store
            trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(ts);

            // Initialize the SSLContext to work with our key managers.
            serverContext = SSLContext.getInstance(PROTOCOL);
            serverContext.init(kmf.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

            clientContext = SSLContext.getInstance(PROTOCOL);
            clientContext.init(null, trustManagerFactory.getTrustManagers(), null);
        } catch (Exception e) {
            throw new Error("Failed to initialize the SSLContext", e);
        }

        SERVER_CONTEXT = serverContext;
        CLIENT_CONTEXT = clientContext;
    }


    public static SSLContext getServerContext() {
        return SERVER_CONTEXT;
    }

    public static SSLContext getClientContext() {
        return CLIENT_CONTEXT;
    }

    public static TrustManagerFactory getTrustManagerFactory() {
        return trustManagerFactory;
    }

    private SslContextFactory() {
        // Unused
    }
}

This is the server code:

package com.test.securechat;

import java.net.InetAddress;
import javax.net.ssl.SSLEngine;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.GlobalEventExecutor;


/**
 * Simple SSL chat server
 */
public final class SecureChatServer {

    static final int PORT = Integer.parseInt(System.getProperty("port", "8992"));

    public static void main(String[] args) throws Exception {

        SSLEngine sslEngine = SslContextFactory.getServerContext().createSSLEngine();
        sslEngine.setUseClientMode(false);

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new SecureChatServerInitializer(sslEngine));

            b.bind(PORT).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private static class SecureChatServerInitializer extends ChannelInitializer<SocketChannel> {

        private final SSLEngine sslCtx;

        public SecureChatServerInitializer(SSLEngine sslCtx) {
            this.sslCtx = sslCtx;
        }

        @Override
        public void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();

            pipeline.addLast(new SslHandler(sslCtx));
            pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
            pipeline.addLast(new StringDecoder());
            pipeline.addLast(new StringEncoder());

            // and then business logic.
            pipeline.addLast(new SecureChatServerHandler());
        }
    }

    private static class SecureChatServerHandler extends SimpleChannelInboundHandler<String> {

        static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

        @Override
        public void channelActive(final ChannelHandlerContext ctx) {
            // Once session is secured, send a greeting and register the channel to the global channel
            // list so the channel received the messages from others.
            ctx.pipeline().get(SslHandler.class).handshakeFuture().addListener(
                    new GenericFutureListener<Future<Channel>>() {
                        @Override
                        public void operationComplete(Future<Channel> future) throws Exception {
                            ctx.writeAndFlush(
                                    "Welcome to " + InetAddress.getLocalHost().getHostName() + " secure chat service!\n");
                            ctx.writeAndFlush(
                                    "Your session is protected by " +
                                            ctx.pipeline().get(SslHandler.class).engine().getSession().getCipherSuite() +
                                            " cipher suite.\n");

                            channels.add(ctx.channel());
                        }
                    });
        }

        @Override
        public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            // Send the received message to all channels but the current one.
            for (Channel c : channels) {
                if (c != ctx.channel()) {
                    c.writeAndFlush("[" + ctx.channel().remoteAddress() + "] " + msg + '\n');
                } else {
                    c.writeAndFlush("[you] " + msg + '\n');
                }
            }

            // Close the connection if the client has sent 'bye'.
            if ("bye".equals(msg.toLowerCase())) {
                ctx.close();
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

Client code:

package com.test.securechat;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.net.ssl.SSLEngine;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.ssl.SslHandler;

/**
 * Simple SSL chat client
 */
public final class SecureChatClient {

    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8992"));

    public static void main(String[] args) throws Exception {

        SSLEngine engine = SslContextFactory.getClientContext().createSSLEngine();
        engine.setUseClientMode(true);

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new SecureChatClientInitializer(engine));

            // Start the connection attempt.
            Channel ch = b.connect(HOST, PORT).sync().channel();

            // Read commands from the stdin.
            ChannelFuture lastWriteFuture = null;
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            for (; ; ) {
                String line = in.readLine();
                if (line == null) {
                    break;
                }

                // Sends the received line to the server.
                lastWriteFuture = ch.writeAndFlush(line + "\r\n");

                // If user typed the 'bye' command, wait until the server closes
                // the connection.
                if ("bye".equals(line.toLowerCase())) {
                    ch.closeFuture().sync();
                    break;
                }
            }

            // Wait until all messages are flushed before closing the channel.
            if (lastWriteFuture != null) {
                lastWriteFuture.sync();
            }
        } finally {
            // The connection is closed automatically on shutdown.
            group.shutdownGracefully();
        }
    }

    private static class SecureChatClientInitializer extends ChannelInitializer<SocketChannel> {

        private final SSLEngine sslCtx;

        public SecureChatClientInitializer(SSLEngine sslCtx) {
            this.sslCtx = sslCtx;
        }

        @Override
        public void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();

            // Add SSL handler first to encrypt and decrypt everything.
            // In this example, we use a bogus certificate in the server side
            // and accept any invalid certificates in the client side.
            // You will need something more complicated to identify both
            // and server in the real world.
            //pipeline.addLast(sslCtx.newHandler(ch.alloc(), SecureChatClient.HOST, SecureChatClient.PORT));
            pipeline.addLast(new SslHandler(sslCtx));

            // On top of the SSL handler, add the text line codec.
            pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
            pipeline.addLast(new StringDecoder());
            pipeline.addLast(new StringEncoder());
            //pipeline.addLast(new LoggingHandler(LogLevel.INFO));


            // and then business logic.
            pipeline.addLast(new SecureChatClientHandler());
        }
    }

    private static class SecureChatClientHandler extends SimpleChannelInboundHandler<String> {

        @Override
        public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.err.println(msg);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

Server Exception:

INFO: [id: 0x5eb2ac7a, /0:0:0:0:0:0:0:0:8992] RECEIVED: [id: 0x7d7a7dca, /127.0.0.1:55932 => /127.0.0.1:8992]
io.netty.handler.codec.DecoderException: javax.net.ssl.SSLHandshakeException: ciphertext sanity check failed
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:278)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:147)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:787)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:130)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354)
    at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116)
    at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
    at java.lang.Thread.run(Thread.java:744)
Caused by: javax.net.ssl.SSLHandshakeException: ciphertext sanity check failed
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1683)
    at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:959)
    at sun.security.ssl.SSLEngineImpl.readNetRecord(SSLEngineImpl.java:884)
    at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:758)
    at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:624)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:995)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:921)
    at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:867)
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:247)
    ... 12 more
Caused by: javax.crypto.BadPaddingException: ciphertext sanity check failed
    at sun.security.ssl.InputRecord.decrypt(InputRecord.java:147)
    at sun.security.ssl.EngineInputRecord.decrypt(EngineInputRecord.java:192)
    at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:953)
    ... 19 more

Client exception:

io.netty.handler.codec.DecoderException: javax.net.ssl.SSLException: Received fatal alert: <UNKNOWN ALERT: 170>
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:278)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:147)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:787)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:130)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354)
    at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116)
    at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
    at java.lang.Thread.run(Thread.java:744)
Caused by: javax.net.ssl.SSLException: Received fatal alert: <UNKNOWN ALERT: 170>
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
    at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1619)
    at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1587)
    at sun.security.ssl.SSLEngineImpl.recvAlert(SSLEngineImpl.java:1756)
    at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:1060)
    at sun.security.ssl.SSLEngineImpl.readNetRecord(SSLEngineImpl.java:884)
    at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:758)
    at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:624)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:995)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:921)
    at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:867)
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:247)
    ... 12 more
Community
  • 1
  • 1
JDR
  • 33
  • 1
  • 3
  • 8

2 Answers2

0

Can you explain why you are initializing your server SSLContext with a trust factory but then not supplying any key factory to the client SSLContext initialization method?

Are you attempting to do mutual authentication? If so you will also need to call SSLEngine.setNeedClientAuth(true).

Have you seen SslContext.java? This may be able to simplify your initialization process and alleviate the need for you to build your own SSLContext objects. If your certificates are X509 and your keys are in PKCS#8 format then you don't have to worry about trust manager or key managers because SslContext can read these and initialize everything for you (via newClientContext and newServerContext). These interfaces also support mutual authentication if needed.

Scott Mitchell
  • 716
  • 4
  • 7
  • I was attempting to do a 1-way authentication that's why i didn't initially provide a key factory to the client context. I ended up doing what you suggested and i got the example to work. Thanks! – JDR Nov 05 '14 at 19:33
  • Can you explain what exact the solution was. I am having the same problem. I believe the fix should be done on the server side ? – Subash Chaturanga Apr 15 '15 at 19:07
0

An SSLEngine object is a statefull object, it cannot be shared between multiple connections. Inside your server code, you are sharing this object between multiple connections, so the data the new client sends does not match the state on the server, causing weird cryptic errors.

Inside the initizer, change it to accept an SSLContext object, and call the create method manually

Ferrybig
  • 18,194
  • 6
  • 57
  • 79