Netty SSL: пример клиента чата с пользовательским хранилищем ключей не может принимать несколько подключений

Я играл с примерами Netty (версия 4.0.24) SecureChatServer и подключил свое собственное хранилище ключей (на основе ответов, найденных в следующих двух сообщениях post 1 и post2. См. фрагменты кода ниже.

Проблема, которую я вижу, заключается в том, что все работает так, как ожидалось, когда я запускаю один экземпляр клиента, но когда я пытаюсь запустить больше экземпляров клиента, я получаю исключения как на сервере, так и на клиенте (см. текст исключения ниже) и в этот момент сервер перестает отвечать.

Я надеюсь, что кто-то здесь может пролить свет на то, что я делаю неправильно.

Это класс, который обрабатывает загрузку моего хранилища ключей и создание экземпляров SSLContext, используемых как клиентом, так и сервером (по понятным причинам я опустил значения хранилища ключей):

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
    }
}

Это код сервера:

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();
        }
    }
}

Код клиента:

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();
        }
    }
}

Исключение сервера:

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

Исключение клиента:

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

person JDR    schedule 04.11.2014    source источник


Ответы (1)


Можете ли вы объяснить, почему вы инициализируете свой сервер SSLContext с помощью фабрики доверия, но затем не предоставляете никакой фабрики ключей для метода инициализации клиента SSLContext?

Вы пытаетесь выполнить взаимную аутентификацию? Если это так, вам также нужно будет позвонить SSLEngine.setNeedClientAuth(true).

Вы видели SslContext.java? Это может упростить процесс инициализации и избавит вас от необходимости создавать собственные SSLContext объекты. Если ваши сертификаты X509 и ваши ключи в формате PKCS#8, вам не нужно беспокоиться о диспетчере доверия или менеджерах ключей, потому что SslContext может прочитать их и инициализировать все для вас (через newClientContext и newServerContext). Эти интерфейсы также поддерживают взаимную аутентификацию, если это необходимо.

person Scott Mitchell    schedule 05.11.2014
comment
Я пытался выполнить одностороннюю аутентификацию, поэтому изначально не предоставил фабрику ключей для клиентского контекста. Я закончил тем, что сделал то, что вы предложили, и я получил пример для работы. Спасибо! - person JDR; 05.11.2014
comment
Можете ли вы объяснить, какое именно решение было. Я с той же проблемой. Я считаю, что исправление должно быть сделано на стороне сервера? - person Subash Chaturanga; 15.04.2015