netty tls单向认证通讯

发布时间 2023-11-16 20:14:12作者: 飞奔的企鹅~
  • 需求背景
    项目主要分为监管侧和企业侧,企业侧实时上传数据到云端,云端汇聚业务数据,上传过程需要保证传输的安全性。

  • 技术实现
    数据上传考虑到用HTTPS或者是TCP + TLS传输。其实使用HTTPS传输协议是比较简单的,但是项目硬件使用的4G无线网卡,而且需要实时检测设备运行状态,所以使用了TCP + TLS的方式,实时性更高并且减少了传输流量。然后考虑是单向认证还是双向认证,考虑为简化交互过程就采用了单向认证,在服务端生成自签证书分发给企业侧,企业侧(客户端)使用该证书发起认证即可。时间有限,本文主要介绍整体使用技术及TLS部分,其余技术细节就不展开了。

  1. 使用技术
    jdk 1.8,netty 4.1.100.Final,boringssl(openssl), openssl 1.1.1,snakeyaml,logback,msgpack 0.6.12,mybatis-plus,Wireshark。
    netty、boringssl和openssl主要实现传输层交互和证书生成
    snakeyaml做配置管理
    logback做日志输出
    msgpack 做二进制的编解码;也有考虑用Protobuf但是操作复杂,要额外配置协议格式生成代码,不能动态实现任意类的编解码工作。
    mybatis-plus做数据库的操作
    Wireshark做网络分析

  2. 首先生成自签证书
    2.1 生成pkcs8格式的证书
    openssl genrsa -out rsa_private.key 2048
    openssl pkcs8 -topk8 -nocrypt -in rsa_private.key -out private_key_pkcs8.pem
    2.2 生成公钥
    openssl rsa -in private_key_pkcs8.pem -pubout -out public_key.pem
    2.3 使用私钥生成证书
    openssl req -new -key private_key_pkcs8.pem -x509 -days 365 -out certificate.crt -subj "/C=CN/ST=SC/L=CD/O=csin/OU=test/CN=test.com/emailAddress=test@test.com"
    我们会得到三个文件private_key_pkcs8.pem,public_key.pem,certificate.crt。这里私钥没有加密处理,后面会说明原因

  3. ssl部分代码
    3.1 服务端

    String serverCert = "/cert/certificate.crt";
    String serverKey = "/cert/private_key_pkcs8.pem";
    List<String> ciphers = Lists.newArrayList("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA");
    SslContext sslContext = SslContextBuilder.forServer(file(serverCert), file(serverKey))
            .sslProvider(SslProvider.OPENSSL)
            .ciphers(ciphers)
            .protocols("TLSv1.2")  // 指定支持的协议版本
            .build();
    //initChannel
    ...
    ChannelPipeline pipeline = ch.pipeline();
    //ssl处理
    SSLEngine sslEngine = sslContext.newEngine(ch.alloc());
    sslEngine.setUseClientMode(false); // 设置为服务器模式
    sslEngine.setNeedClientAuth(false); // 需要客户端验证
    SslHandler sslHandler = new SslHandler(sslEngine);
    pipeline.addFirst(sslHandler);
    ...

3.2 客户端

      List<String> ciphers = Lists.newArrayList("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA");
      SslContext sslContext = SslContextBuilder.forClient()
              .trustManager(file("/cert/certificate.crt"))
              .sslProvider(SslProvider.OPENSSL)
              .ciphers(ciphers)
              .protocols("TLSv1.2")  // 指定支持的协议版本
              .build();
     //initChannel
    ...
    ChannelPipeline pipeline = socketChannel.pipeline();
    //ssl处理
    SSLEngine sslEngine = sslContext.newEngine(socketChannel.alloc());
    sslEngine.setUseClientMode(true);
    SslHandler sslHandler = new SslHandler(sslEngine, true);
    pipeline.addLast(sslHandler);
    ...
    //监听tcp连接成功后flush一下,很重要
    future.addListener((ChannelFutureListener) futureListener -> {
      if (futureListener.isSuccess()) {
          channel = futureListener.channel();
          //刷新触发tls握手
          channel.flush();
          //连接成功后,启动定时任务
          log.info("Connect to server successfully!");
      } else {
          log.error("Failed to connect to server, try connect after 10s");
          futureListener.channel().eventLoop().schedule(this::doConnect, 10, TimeUnit.SECONDS);
      }
   });
  • 踩坑
  1. TLS协议版本使用错误
    解决:使用了TLSv1.3的版本.protocols("TLSv1.2", "TLSv1.3") // 指定支持的协议版本,而项目中jdk用的1.8,只支持TLSv1.2,所以只是用TLSv1.2就可以了.protocols("TLSv1.2") // 指定支持的协议版本
  2. 采用了密钥加密后一直以下报错,可能是本人使用方式不对,有遇到过的同学望告知,谢谢。
...
java.lang.IllegalArgumentException: Input stream does not contain valid private key.
	at io.netty.handler.ssl.SslContextBuilder.keyManager(SslContextBuilder.java:416)
	at io.netty.handler.ssl.SslContextBuilder.forServer(SslContextBuilder.java:138)
...
Caused by: java.io.IOException: ObjectIdentifier() -- data isn't an object ID (tag = 48)
	at sun.security.util.ObjectIdentifier.<init>(ObjectIdentifier.java:257)
	at sun.security.util.DerInputStream.getOID(DerInputStream.java:314)
	at com.sun.crypto.provider.PBES2Parameters.engineInit(PBES2Parameters.java:267)
	at java.security.AlgorithmParameters.init(AlgorithmParameters.java:293)
	at sun.security.x509.AlgorithmId.decodeParams(AlgorithmId.java:132)
	at sun.security.x509.AlgorithmId.<init>(AlgorithmId.java:114)
	at sun.security.x509.AlgorithmId.parse(AlgorithmId.java:372)
	at javax.crypto.EncryptedPrivateKeyInfo.<init>(EncryptedPrivateKeyInfo.java:95)
	at io.netty.handler.ssl.SslContext.generateKeySpec(SslContext.java:1082)
	at io.netty.handler.ssl.SslContext.getPrivateKeyFromByteBuffer(SslContext.java:1144)
	at io.netty.handler.ssl.SslContext.toPrivateKey(SslContext.java:1134)
	at io.netty.handler.ssl.SslContextBuilder.keyManager(SslContextBuilder.java:414)
	... 3 common frames omitted
...
  1. channelActive中直接发送消息,出现以下错误。例:sendPing(ctx)
    服务端报错:io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record:
    解决:原因是在tcp连接后,tls握手还没有完成成功,发送数据没有经过SslHandler处理。正确方式应该监听握手成功发送。例子如下:
    ...
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof SslHandshakeCompletionEvent) {
            if (((SslHandshakeCompletionEvent) evt).isSuccess()) {
                // SSL握手成功
                log.info("SSL handshake completed successfully");
                handshakeSuccess(ctx);
            } else {
                // SSL握手失败
                log.info("SSL handshake failed: " + ((SslHandshakeCompletionEvent) evt).cause());
                handshakeFailed(ctx);
            }
        }
    ...
  1. 在服务端错误配置startTls为true。例:SslHandler sslHandler = new SslHandler(sslEngine, true);
    解决:应该在客户端配置startTls为true,并且sslEngine.setUseClientMode(true);
  2. 客户端TCP连接成功后,不能发起握手,异步传输时候以下报错
    handshake failed: io.netty.handler.ssl.SslHandshakeTimeoutException: handshake timed out after 10000ms
    解决:在客户端TCP连接成功后,调用channel.flush();触发tls握手,如下:
    ...
    future.addListener((ChannelFutureListener) futureListener -> {
        if (futureListener.isSuccess()) {
            channel = futureListener.channel();
            //刷新触发tls握手
            channel.flush();
            log.info("Connect to server successfully!");
        } else {
            log.error("Failed to connect to server, try connect after 10s");
            futureListener.channel().eventLoop().schedule(this::doConnect, 10, TimeUnit.SECONDS);
        }
    });
    ...
  1. 证书的格式错误,刚开始生成PKCS#1格式证书,启动就出错
    解决:netty支持PKCS#8的格式,生成PKCS#8格式的证书。
  2. Wireshark看不到tls过程
    解决:导入生成的私钥就可以了
  • 写在最后
    文中代码实现是经笔者测试的结果,为回顾项目特此记录开发过程中的问题。由于个人精力有限,如有疏漏或者发现错误,望大家提出宝贵意见。