开源项目SMSS发开指南(四)——SSL/TLS加密通讯详解

本文将详细介绍如何在Java端、C++端和NodeJs端实现基于SSL/TLS的加密通讯,重点分析Java端利用SocketChannel和SSLEngine从握手到数据发送/接收的完整过程。本文也涵盖了在Ubuntu系统上利用OpenSSL和Libevent如何建立一个支持SSL的服务端。文章中介绍的知识点并未所有在SMSS项目中实现,所以笔者会列出全部相关源码以方便读者查阅。提醒:因为知识点较多,分享涵盖了多种语言。预计的学习时间可能会大于3小时,为了保证读者能有良好的学习体验,继续前请先安排好时间。若是遇到困难,您也能够根据本身的实际状况有选择的学习,也欢迎与我交流。

一 相关前置知识

libevent网络库:libevent是一个用c语言编写的高性能支持事件响应的网络库,编译libevent前须要确保目标机器上已经完成对openssl的编译。不然生成的动态库中可能会缺乏调用openssl的接口。这里选择的openssl版本为1.1.1d,若是你选择1.0之前的版本可能与后面的代码示例有所不一样。html

electron桌面应用:electron是一套依赖google的V8引擎直接使用HTML/JS/CSS建立桌面应用的跨平台解决方案。若是你须要开发轻量化的桌面端应用,electron基本是不二选择。从我的的实践来看,不管是开发生态仍是开发效率都强于Qt。使用electron能够调用nodejs相关接口完成与系统的交互。vue

Java-nio开发包:基本是如今做为Java中高级开发的必备技能。java

javax.net.ssl开发包:属于Java对SSL/TLS支持的比较底层的开发包。目前在应用中更多会选择Netty等集成式框架,若是你的项目中须要一些定制化功能能够选择它做为支持。建议在项目中慎重使用。因为一些特殊缘由,Java只提供了SSLSocket对象,底层只支持阻塞式访问。文章最后会提供一个我我的实现的SSLSocketChannel对象,方便读者在基础上进行二次封装。node

SSL/TLS通讯:安全通讯的目的是在原有的tcp/ip层和应用层之间增长了一个称之为SSL/TLS的加/解密层来实现的。在网络协议层中的位置大体以下:ios

在OSI七层网络协议的定义中,它处于表示层。程序开发的方式通常是在完成tcp/ip创建链接后,开始ssl/tls握手。发布ssl的服务端须要具有一个私钥文件(.key)以及与私钥配套的证书文件(.crt)。证书包含了公钥和对公钥的签名,还有一些用来证实源安全的信息。证书须要到专门的机构申请而且有年费要求,鉴于各位读者仅用于自学,后面生成的证书咱们会作自签名。ssl/tls握手的目的是在客户端和服务端之间协商一个安全的对称秘钥,用来为本次会话的消息加解密,因为这对秘钥仅通讯的服务端和客户端持有,会话结束即消失。c++

二 libevent和openssl

生成x.509证书

首选在安装好openssl的机器上建立私钥文件:server.keyvue-cli

> openssl genrsa -out server.key 2048数据库

获得私钥文件后咱们须要一个证书请求文件:server.csr,未来你能够拿这个证书请求向正规的证书管理机构申请证书编程

> openssl req -new -key server.key -out server.csrubuntu

最后咱们生成自签名的x.509证书(有效期365天):server.crt

> openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

x.509证书是密码学里公钥证书的格式标准,被应用在包括ssl/tls等多项场景中。

OpenSSL加密通讯接口分析

与ssl/tls通讯相关的接口基本能够分为两大类,SSL_CTX通讯上下文和SSL直接通讯接口,下面逐一分析:

  1. SSL_CTX_new:新版本摒弃了一些老的接口,目前建议基本统一使用此方法来建立通讯上下文
  2. SSL_CTX_free:释放SSL_CTX*
  3. SSL_CTX_use_certificate_file:设置证书文件
  4. SSL_CTX_use_PrivateKey_file:设置私钥文件,与上面的证书文件必须配套不然检测不经过
  5. SSL_CTX_check_private_key:检查私钥和证书文件
  6. SSL_new:方法一建立完成的上下文在经过此方法建立配套的SSL*
  7. SSL_set_fd:与上面建立的SSL和socket_fd绑定
  8. SSL_accept:服务端握手方法
  9. SSL_connect:客户端握手方法
  10. SSL_write:消息发送,内部会对明文消息加密并调用socket发送
  11. SSL_read:消息接收,内部会从socket接收到密文数据再解码成文明返回
  12. SSL_shutdown:通知对方关闭本次加密会话
  13. SSL_free:释放SSL*

C++编写socket利用openssl接口开发测试代码

在熟悉以上基本概念以后,根据测试先行和敏捷开发的原则。咱们接下来就要直接使用c++开发一个socket测试程序,并利用openssl接口进行加密通讯。如下代码的开发和运行系统为ubuntu 16.04 LTS,openssl版本为1.1.1d 10 Sep 2019,开发工具为Visual Studio Code 1.41.1。

服务端源码 server.cpp

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <string>
#include "openssl/ssl.h"
#include "openssl/err.h"

using namespace std;

// 前置申明
struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file);

int main(int argc, char *argv[])
{
    ssl_ctx_st *ssl_ctx = InitSSLServer("../server.crt", "../server.key"); // 引入以前生成好的私钥文件和证书文件
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(10020); // 指定通讯端口
    int res = ::bind(sock, (sockaddr *)&sin, sizeof(sin));
    if (res == -1)
    {
        return -1;
    }
    listen(sock, 1); // 开始监听
    // 只接受一次客户端的链接
    int client_fd = accept(sock, 0, 0);
    cout << "Client accept success!" << endl;
    ssl_st *ssl = SSL_new(ssl_ctx);
    SSL_set_fd(ssl, client_fd);
    res = SSL_accept(ssl); // 执行SSL层握手
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return -1;
    }
    // 握手完成,接受消息并发送一次应答
    char buf[1024] = {0};
    int len = SSL_read(ssl, buf, sizeof(buf));
    cout << buf << endl;
    string s = "Hi Client, I'm CppSSLSocket Server.";
    SSL_write(ssl, s.c_str(), s.size());
    // 释放资源
    SSL_free(ssl);
    SSL_CTX_free(ssl_ctx);
    return 0;
}

struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file)
{
    // 建立通讯上下文
    ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method());
    if (!ssl_ctx)
    {
        cout << "ssl_ctx new failed" << endl;
        return nullptr;
    }
    int res = SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM);
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return nullptr;
    }
    res = SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM);
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return nullptr;
    }
    res = SSL_CTX_check_private_key(ssl_ctx);
    if (res != 1)
    {
        return nullptr;
    }
    return ssl_ctx;
}

客户端源码 client.cpp

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include "openssl/ssl.h"
#include "openssl/err.h"

using namespace std;

struct ssl_ctx_st *InitSSLClient();

int main(int argc, char *argv[])
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");
    sin.sin_port = htons(10020);
    // 首先执行socket链接
    int res = connect(sock, (sockaddr *)&sin, sizeof(sin));
    if (res != 0)
    {
        return -1;
    }
    cout << "Client connect success." << endl;

    ssl_ctx_st *ssl_ctx = InitSSLClient();
    ssl_st *ssl = SSL_new(ssl_ctx);
    SSL_set_fd(ssl, sock);
    // 进行SSL层握手
    res = SSL_connect(ssl);
    if (res != 1)
    {
        ERR_print_errors_fp(stderr);
        return -1;
    }
    string send_msg = "Hello Server, I'm CppSSLSocket Client.";
    SSL_write(ssl, send_msg.c_str(), send_msg.size());
    char recv_msg[1024] = {0};
    int recv_len = SSL_read(ssl, recv_msg, sizeof(recv_msg));
    recv_msg[recv_len] = '\0';
    cout << recv_msg << endl;
    SSL_shutdown(ssl);
    SSL_free(ssl);
    SSL_CTX_free(ssl_ctx);
    return 0;
}

struct ssl_ctx_st *InitSSLClient()
{
    // 建立一个ssl客户端的上下文
    ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_client_method());
    return ssl_ctx;
}

编译使用Makefile,客户端的修改TARGET便可

TARGET=server.x
SRC=$(wildcard *.cpp)
OBJS=$(patsubst %.cpp,%.o,$(SRC))
LIBS=-lssl -lcrypto
$(TARGET):$(SRC)
    g++ -std=c++11 $^ -o $@ $(LIBS)
clean:
    rm -fr $(TARGET) $(OBJS)

若是在服务端和客户端均可以正常发送和接收显示消息,即表示通讯正常。

C++编写openssl与libevent安全通讯服务端

当前项目使用的libevent版本为2.1,在编译的时候须要在目标机器上预先编译好openssl。不然编译时检测不到,没法生成对应接口。有关libevent的基础能够参考smss开源系列的前期文章,这里再也不赘述。考虑到同构系统的开发案例网上的资料相对丰富,同时笔者目前的工做大多为异构系统开发为主。所以这里选择使用C++做为服务端,Java和NodeJs为客户端的方式。若是读者有须要也能够给我留言,我会补充Java做为服务端C++做为客户端的相关案例。

目前使用libevent和openssl做为通讯框架,在追求性能优先的物联网项目中应用普遍,开发难度也相对较低。libevent也提供了专门调用openssl的接口,它能够帮助咱们管理SSL对象,不过SSL_CTX的维护还须要咱们本身实现。与直接使用libevent建立服务端相比最大的区别在于咱们须要本身建立socket并同时交给event_base和SSL_CTX来使用。

服务端源码 libevent_server.cpp

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <string>
#include "openssl/ssl.h"
#include "event2/event.h"
#include "event2/listener.h"
#include "event2/bufferevent.h"
#include "event2/bufferevent_ssl.h"

using namespace std;

// 设置x.509证书文件和私钥文件
ssl_ctx_st *InitServer(const char *crt_file, const char *key_file);

// 建立通讯ssl
ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket);

// 服务端链接监听回调函数
void EvconnlistenerCB(struct evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx);

// 消息读、写和事件回调
void ReadCB(struct bufferevent *bev, void *ctx);
void WriteCB(struct bufferevent *bev, void *ctx);
void EventCB(struct bufferevent *bev, short what, void *ctx);

static bool isSsl = false;

int main(int argc, char *argv[])
{

    if (argc == 2)
    {
        if (strcmp(argv[1], "SSL") == 0)
        {
            isSsl = true;
        }
    }
    // 建立event_base
    event_base *base = event_base_new();
    if (!base)
    {
        cout << "event_base_new fail" << endl;
        return -1;
    }
    // 建立SSL_CTX通讯上下文
    ssl_ctx_st *ssl_ctx = InitServer("../server.crt", "../server.key");
    // 建立socket
    sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10020);
    evconnlistener *listener = evconnlistener_new_bind(
        base,
        EvconnlistenerCB,
        ssl_ctx,
        LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
        10,
        (sockaddr *)&addr,
        sizeof(addr));
    
    // 阻塞当前线程执行事件循环
    event_base_dispatch(base);
    // 释放资源
    SSL_CTX_free(ssl_ctx);
    event_base_free(base);
    return 0;
}

void EvconnlistenerCB(evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx)
{
    cout << "Server EvconnlistenerCB..." << endl;
    // 获取当前的事件循环上下文
    event_base *base = evconnlistener_get_base(listener);
    bufferevent *bev = nullptr;
    // 判断当前是否启用ssl通讯模式
    if (isSsl)
    {
        ssl_ctx_st *ssl_ctx = (ssl_ctx_st *)ctx;
        ssl_st *ssl = NewSSL(ssl_ctx, socket);
        // 建立bufferevent,当bufferevent关闭的时候,会同时释放ssl资源
        bev = bufferevent_openssl_socket_new(base, socket, ssl, BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE);
        bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, ssl);
    }
    else
    {
        bev = bufferevent_socket_new(base, socket, BEV_OPT_CLOSE_ON_FREE);
        bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, base);
    }
    // 注册事件类型
    bufferevent_enable(bev, EV_READ | EV_WRITE);
}

/**
 * ssl上下文初始化
 * 考虑测试简洁的须要,这里没有作多余判断
 */
ssl_ctx_st *InitServer(const char *crt_file, const char *key_file)
{
    ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method());
    SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM);
    SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM);
    SSL_CTX_check_private_key(ssl_ctx);
    return ssl_ctx;
}

/**
 * 建立ssl接口而且和socket绑定
 */
ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket)
{
    ssl_st *ssl = SSL_new(ssl_ctx);
    SSL_set_fd(ssl, socket);
    return ssl;
}

void ReadCB(bufferevent *bev, void *ctx)
{
    char buf[1024] = {0};
    int len = bufferevent_read(bev, buf, sizeof(buf) - 1);
    buf[len] = '\0';
    cout << buf << endl;
    string msg = "hello client, I'm server.\n";
    bufferevent_write(bev, msg.c_str(), msg.size());
    bufferevent_write(bev, buf, len);
}

void WriteCB(bufferevent *bev, void *ctx)
{
}

void EventCB(bufferevent *bev, short what, void *ctx)
{
    cout << "EventCB: " << what << endl;
    if (what & BEV_EVENT_CONNECTED)
    {
        cout << "Event:BEV_EVENT_CONNECTED" << endl;
    }
    if (what & BEV_EVENT_ERROR && what & BEV_EVENT_READING)
    {
        cout << "Event:BEV_EVENT_READING" << endl;
        bufferevent_free(bev);
    }
    if (what & BEV_EVENT_ERROR && what & BEV_EVENT_WRITING)
    {
        cout << "Event:BEV_EVENT_WRITING" << endl;
        bufferevent_free(bev);
    }
}

编译用的Makefile文件

TARGET=server.x
SRC=$(wildcard *.cpp)
OBJS=$(patsubst %.cpp,%.o,$(SRC))
LIBS=-lssl -lcrypto -levent -levent_openssl
$(TARGET):$(SRC)
    g++ -std=c++11 $^ -o $@ $(LIBS)
clean:
    rm -fr $(TARGET) $(OBJS)

特别须要注意bufferevent_openssl_socket_new方法包含了对bufferevent和SSL的管理,所以当链接关闭的时候再也不须要SSL_free。可执行文件server.x接收SSL做为参数,做为是否启用安全通讯的标识。

读者可使用上一节生成的client.x与本节的程序通讯,方便测试结果。

三 *基于Node.js的(加密)通讯测试

*注:若是您不熟悉electron能够跳过本节,不妨碍后面的学习

因为electron不是本文的重点,所以如何建立和开发electron项目作过过多介绍。本例使用electron-vue做为模板,使用vue-cli直接建立。咱们将分别使用Node.js的net包和tls包建立通讯客户端。

net.Socket链接示例:

this.socket = net.connect(10020, "127.0.0.1", () => {
  console.log("socket 服务器链接成功...");
  this.socket.write("Hello Server, I'm Nodejs.", () => {
    console.log("发送完成~");
  });
});

this.socket.on("data", data => {
  console.log(data.toString());
});

tls.connect链接示例:

this.socket = tls.connect(
  { host: "127.0.0.1", port: 10020, rejectUnauthorized: false },
  () => {
    console.log("ssl 服务器链接成功...");
    this.socket.write("Hello Server, I'm Nodejs.", () => {
      console.log("发送完成~");
    });
  }
);

this.socket.on("data", data => {
  console.log(data.toString());
});

因为以前咱们经过openssl生成的x.509证书为自签名证书,所以在使用tls.connect的时候须要指定rejectUnauthorized属性。

读者能够利用这套代码和上一节建立的server.x分别进行普统统信和安全通讯,以判断功能是否正常。

四 建立基于SSLEngine的NIO通讯

若是说以前的知识你都可以掌握,那么从这里开始才是本文的重点,也是难点所在。网上对于SSLEngine的介绍资料相对较少,且大多都没有通过完整测试,确实形成学习曲线过于陡峭。加之笔者认为Java对于SSLEngine的设计的确不太合理,所以强烈不建议读者在实际项目中使用。事实上,SSL/TLS协议的握手过程很是复杂,涉及到加密和秘钥交换等多个步骤。不管是基于C语言的openssl仍是基于Node.js的tls.connect都将握手的过程封装到内部。如今笔者将经过介绍SSLEngine让你对这一过程有所了解。

ByteBuffer分析

io面向流(stream)开发,而nio面向缓冲(buffer)开发。不少人对此也不陌生,可是在工做中我发现可以深刻理解这句话的人比较少。什么叫面向流(stream)?为何有区别于面向缓冲(buffer)?传统io在向文件或数据库请求数据的时候。因为须要请求操做系统资源,所以存在须要等待响应的过程。它不一样于单纯的代码执行只须要使用cpu资源,io操做还须要涉及总线资源,磁盘资源等。在这个过程当中,因为没法肯定数据何时会返回,只能作阻塞等待。nio的作法至关于告知操做系统:我已经在用户态申请好了一块内存空间(buffer),当内核接收到数据之后请直接写到个人空间中。所以,使用nio编程的特色之一就是对数据的处理每每须要经过回调函数(callback)。做为最经常使用的缓冲对象——ByteBuffer,你有多熟悉?

ByteBuffer最重要的三个属性:

  • capacity 表示该缓冲区的最大容量,任何操做最大容量的读写操做都属于非法
  • limit 若是当前是写入态,limit等于capacity。若是当前是读取态,limit表示当前一共有多少有效数据。注意,写入态和读取态是我创造的名词,buffer自己并不存在这两个状态
  • position 当前数据区的读/写位置指针

当你开始往buffer中写入数据的时候,pos会不断增长,limit等于cap。写入完成后,若是你想要读取数据,第一步必须进行翻转(flip)。翻转之后的数据区pos为0,而limit则等于以前写入的pos。若是在读取数据的时候,没法一次性处理完。咱们可使用compact()方法将已经读取的数据清除。

为了加深印象,请你们思考一个问题:若是我向一个ByteBuffer中写入了数据,假设当前缓冲区的状态为 java.nio.HeapByteBuffer[pos=1305 lim=16921 cap=16921]。我又读取了94个字节,当前缓冲区状态为 java.nio.HeapByteBuffer[pos=94 lim=1305 cap=16921]。此时调用compact(),缓冲区的状态是什么状况?

根据jdk官方文档上的解释,compact()方法会将缓冲区中的数据按位复制,pos复制到0,pos + 1复制到1,以此类推,最后是将limit-1复制到limit-pos。事实上方法内部还帮咱们作了一次翻转操做,当前的缓冲区状态为 java.nio.HeapByteBuffer[pos=1211 lim=16921 cap=16921]。

非阻塞SocketChannel

目前几乎全部支持非阻塞的通讯框架都基于React模式开发,经过在IO管道上注册多个事件回调以达到异步处理的效果。又由于回调的使用原来越多,所以Java 8也提出了函数式接口的概念,同时引入兰姆达表达式以让用户可以设计出更适合阅读和维护的代码。

NIO在socket上的运用Java提供了SocketChannel和Selector对象。

非阻塞客户端 NioSocket.java

package socket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Set;

public class NioSocket {
    /**
     * 链接方法
     * 
     * @param host 服务器主机地址
     * @param port 服务器端口
     */
    public static void connection(String host, int port) throws IOException {
        Selector sel = Selector.open(); // 建立事件选择器
        InetSocketAddress addr = new InetSocketAddress(host, port);
        SocketChannel socket = SocketChannel.open(); // 建立非阻塞socket对象
        socket.configureBlocking(false).register(sel,
                SelectionKey.OP_CONNECT | SelectionKey.OP_READ); // 配置非阻塞模式和向Selector注册链接事件与数据可读事件
        socket.connect(addr);
        while (true) {
            // 等待间隔
            if (sel.select(10) > 0) {
                Set<SelectionKey> keys = sel.selectedKeys();
                for(SelectionKey key : keys) {
                    keys.remove(key); // 移除事件并处理
                    if(key.isConnectable()) {
                        socket.finishConnect();
                        String reqMsg = "Hello Server, I'm JavaClient.";
                        ByteBuffer reqBuf = ByteBuffer.wrap(reqMsg.getBytes());
                        socket.write(reqBuf);
                    } else if(key.isReadable()) {
                        ByteBuffer respBuf = ByteBuffer.allocate(1024);
                        int length = socket.read(respBuf);
                        if(length > 0) {
                            String respMsg = new String(respBuf.array(), 0, length);
                            System.out.println(respMsg);
                        }
                    }
                }
            }
        }
    }
    
    public static void main(String[] args) {
        try {
            NioSocket.connection("127.0.0.1", 10020);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

当有注册的事件产生的时候,咱们可以经过selectedKey()方法获取完整的事件队列。若是事件没有被处理,会在下一次事件循环中从新触发,所以处理完成的事件须要从队列中删除。

阻塞式加密通讯 SSLSocket

接下来咱们将难度升级,看一下利用SSLSocket如何开发加密通讯的客户端。Java为咱们提供了javax.net.ssl包,里面都是与SSL/TLS加密通讯相关的组件。因为服务端使用的是自签名证书,所以咱们须要重写TrustManager的实现

package tls;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;

public class X509SelfSignTrustManager implements X509TrustManager {

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        for (int i = 0; i < chain.length; i++) {
            System.out.println(chain[i]);
        }
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }

}

做为客户端checkClientTrusted()和getAcceptedIssuers()方法都不会被调用。checkServerTrusted()方法用来检查服务端的证书,咱们只将证书内容打印出来。

package tls;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

public class Ssl {
    public SSLSocket connection(String host, int port) throws Exception {
        SSLContext context = SSLContext.getInstance("SSL");
        context.init(null, new TrustManager[] {new X509SelfSignTrustManager()}
         , new java.security.SecureRandom());
        SSLSocketFactory factory = context.getSocketFactory();
        return (SSLSocket) factory.createSocket(host, port);
    }

    public static void main(String[] args) {
        Ssl ssl = new Ssl();
        SSLSocket sslSocket = null;
        try {
            sslSocket = ssl.connection("127.0.0.1", 10020);
            OutputStream output = sslSocket.getOutputStream();
            String msg = "Hello Server, I'm BioSSLClient.";
            output.write(msg.getBytes());
            output.flush();
            InputStream input = sslSocket.getInputStream();
            byte[] buf = new byte[1024];
            int len = input.read(buf);
            String ss = new String(buf, 0, len);
            System.out.println(ss);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                sslSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

首先是须要建立基于SSL协议的上下文对象SSLContext

使用咱们本身实现的证书管理器进行初始化

建立SSLSocketFactory,并经过它实例化SSLSocket

通讯过程基本就是操做io流,这里不作赘述

SSLEngine——抽象化的握手和加/解密接口

先看一下规范的SSL/TLS握手步骤:

基本的通讯大体能够分为4个过程:

  1. 选择协议版本和会话ID
  2. 服务端发送证书和秘钥交换数据
  3. 客户端处理证书和生成秘钥交换数据并发送给服务端
  4. 会话秘钥协商成功,握手完成

由于SSLEngine仅仅是针对SSL层进行了抽象,所以底层通信接口须要本身建立。由于打算使用nio,我将建立一个SocketChannel。

SSLEngine也经过SSLContext实例化,SSLContext还可以实例化一个SSLSession对象,使用SSLSession帮助咱们建立两种缓存:应用数据缓存和网络数据缓存。顾名思义,应用数据缓存用来存储明文数据,网络数据缓存表明将要发送或接收到的密文数据。它们经过SSLEngine的wrap()和unwrap()方法相互转换。使用SSLEngine的难点是执行握手操做,关键点在于如何理解内部的两个枚举类型:

SSLEngineResult.HandshakeStatus:

  • NEED_WRAP 当前有数据须要被加密并发送
  • NEED_UNWRAP 当前有数据应该被读取并解密
  • NEED_TASK 须要执行运算任务
  • FINISHED 握手完成
  • NOT_HANDSHAKING 当前不处于握手状态中

特别注意,FINISHED状态只会在握手完成后的最后一步操做中出现,以后再获取状态都会显示为NOT_HANDSHAKING(SSLEngine为何会这样设计我也没看懂)。我曾经觉得NOT_HANDSHAKING状态表示握手已断开,一度很不理解。

SSLEngineResult.Status:在执行wrap()或unwrap()操做后

  • OK 执行成功
  • BUFFER_OVERFLOW 写入缓存区不足,一般表示unwrap()的第二个参数设置的buffer剩余空间不足
  • BUFFER_UNDERFLOW 输出缓冲区不足,一般表示wrap()的第一个参数设置的buffer中没有数据
  • CLOSED SSLEngine已经被关闭,没法执行任何方法

利用SSLEngine进行握手的时候,咱们会屡次使用wrap()和unwrap()方法。此时若是打开断点你会发现明明没有提供明文数据,通过wrap()后密文缓存中却有数据。或者接收到密文数据后通过unwrap()方法,却没获得任何明文数据。缘由是,握手阶段的任何数据都在SSLEngine内部处理(这个设计很奇怪,不明白Java的设计者们如此设计的初衷是什么)。

package tls;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;

public class NioSsl {
    private SocketChannel sc;
    private SSLEngine sslEngine;
    private Selector selector;
    private HandshakeStatus hsStatus;
    private Status status;
    private ByteBuffer localNetData;
    private ByteBuffer localAppData;
    private ByteBuffer remoteNetData;
    private ByteBuffer remoteAppData;

    public void connection(String host, int port) throws Exception {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom());
        sslEngine = sslContext.createSSLEngine();
        sslEngine.setUseClientMode(true);
        SSLSession session = sslEngine.getSession();
        localAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
        localNetData = ByteBuffer.allocate(session.getPacketBufferSize());
        remoteAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
        remoteNetData = ByteBuffer.allocate(session.getPacketBufferSize());
        remoteNetData.clear();
        SocketChannel channel = SocketChannel.open();
        selector = Selector.open();
        channel.configureBlocking(false).register(selector,
                SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        InetSocketAddress addr = new InetSocketAddress(host, port);
        channel.connect(addr);
        sslEngine.beginHandshake();
        hsStatus = sslEngine.getHandshakeStatus();
        while (true) {
            if (selector.select(10) > 0) {
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    it.remove();
                    handleSocketEvent(selectionKey);
                }
            }
        }
    }

    private void handleSocketEvent(SelectionKey key) throws IOException, InterruptedException {
        if (key.isConnectable()) {
            System.out.println("isConnectable...");
            sc = (SocketChannel) key.channel();
            sc.finishConnect();
            doHandshake();
            localAppData.clear();
            localAppData.put("Hello Server, I'm NioSslClient.".getBytes());
            localAppData.flip();
            localNetData.clear();
            SSLEngineResult result = sslEngine.wrap(localAppData, localNetData);
            hsStatus = result.getHandshakeStatus();
            status = result.getStatus();
            if (status == Status.OK) {
                localNetData.flip();
                while (localNetData.hasRemaining()) {
                    sc.write(localNetData);
                }
            }
        } else if (key.isReadable()) {
            System.out.println("isReadable...");
            sc = (SocketChannel) key.channel();
            remoteNetData.clear();
            remoteAppData.clear();
            int len = sc.read(remoteNetData);
            System.out.println("接受服务端加密数据长度:" + len);
            remoteNetData.flip();
            SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData);
            hsStatus = result.getHandshakeStatus();
            status = result.getStatus();
            remoteAppData.flip();
            byte[] buf = new byte[remoteAppData.limit()];
            remoteAppData.get(buf);
            System.out.println(new String(buf));
        }
    }

    private void doHandshake() throws IOException, InterruptedException {
        SSLEngineResult result;
        int count = 0;
        while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
            TimeUnit.MILLISECONDS.sleep(100);
            switch (hsStatus) {
            case NEED_TASK:
                System.out.println("当前握手状态:NEED_TASK");
                Runnable runnable;
                while ((runnable = sslEngine.getDelegatedTask()) != null) {
                    runnable.run();
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            case NEED_UNWRAP:
                System.out.println("当前握手状态:NEED_UNWRAP");
                count = sc.read(remoteNetData);
                System.out.println("获取字节数:" + count);
                remoteNetData.flip();
                remoteAppData.clear();

                do {
                    result = sslEngine.unwrap(remoteNetData, remoteAppData);
                } while (result.getStatus() == SSLEngineResult.Status.OK
                        && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);


                hsStatus = result.getHandshakeStatus();
                status = result.getStatus();

                remoteNetData.compact();
                if (hsStatus == SSLEngineResult.HandshakeStatus.FINISHED) {
                    System.out.println("===========" + hsStatus + "===========");
                    
                }
                break;
            case NEED_WRAP:
                System.out.println("当前握手状态:NEED_WRAP");
                localNetData.clear();
                result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData);
                hsStatus = result.getHandshakeStatus();
                status = result.getStatus();
                if (status != Status.OK) {
                    throw new RuntimeException("status: " + status);
                }
                localNetData.flip();
                while (localNetData.hasRemaining()) {
                    int len = sc.write(localNetData);
                    System.out.println("发送字节数:" + len);
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            default:
                break;
            }
        }
        hsStatus = sslEngine.getHandshakeStatus();
        System.out.println("===========" + hsStatus + "===========");
    }

    public static void main(String[] args) {
        NioSsl nioSsl = new NioSsl();
        try {
            nioSsl.connection("127.0.0.1", 10020);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

源码中我已经设置了睡眠时间和必要的消息输出。读者能够复制到IDE中结合C++端的服务协同测试。若是通讯成功,你应该能够在客户端看到x.509证书打印和13次状态改变。去除NEED_TASK状态,再对比SSL/TLS协议的握手规范学习。

SSLSocketChannel 源码

若是你可以顺利看到这里,那么恭喜你。在这篇知识分享的文章中,你应该多少有些收获。为了准备这些东西,我用了几乎整个2020年的春节假期(幸亏假期延长了,不然时间还不够)。最后是我本身封装的SSLSocketChannel,使用了函数式接口以及兰姆达表达式。

package tls;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;

public class SSLSocketChannel {
    private volatile boolean isQuit = false;
    private SocketChannel socket = null;
    private Selector selector = null;
    private ExecutorService pool = null;
    private LinkedList<Function<byte[], byte[]>> readBufQueue = new LinkedList<>();
    private LinkedList<Supplier<byte[]>> writeBufQueue = new LinkedList<>();
    private Lock writeLock = new ReentrantLock();
    private Lock readLock = new ReentrantLock();
    private SSLEngine sslEngine;
    private HandshakeStatus hsStatus;

    private ByteBuffer localAppData, remoteAppData;
    private ByteBuffer localNetData, remoteNetData;

    public SSLSocketChannel() throws IOException {
        this.selector = Selector.open(); // 打开事件选择器
        this.pool = Executors.newSingleThreadExecutor();
    }

    /**
     * 建立一个非堵塞的Socket并注册链接事件和读取事件
     * 
     * @param host
     * @param port
     * @throws IOException
     */
    public void connect(String host, int port) throws IOException {
        InetSocketAddress addr = new InetSocketAddress(host, port);
        socket = SocketChannel.open();
        socket.configureBlocking(false).register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
        socket.connect(addr);
    }

    /**
     * 网络事件循环线程
     * 
     * @return 线程结束
     */
    public Future<Void> dispatch() {
        Future<Void> fut = this.pool.submit(() -> {
            while (!isQuit) {
                if (selector.select(10) > 0) {
                    Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        if (key.isConnectable()) {
                            socket.finishConnect();
                            this.sslHandshake();
                        } else if (key.isReadable()) {
                            remoteNetData.clear();
                            int length = socket.read(remoteNetData);
                            if (length > 0) {
                                remoteNetData.flip();
                                remoteAppData.clear();
                                SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData);
                                if (handleResult(result)) {
                                    remoteAppData.flip();
                                    byte[] b = new byte[remoteAppData.limit()];
                                    remoteAppData.get(b);
                                    try {
                                        readLock.lock();
                                        for (Function<byte[], byte[]> fn : readBufQueue) {
                                            byte[] r = fn.apply(b);
                                            if (r != null) {
                                                ByteBuffer buf = ByteBuffer.wrap(r);
                                                socket.write(buf);
                                            }
                                        }
                                    } finally {
                                        readLock.unlock();
                                    }
                                }
                            }
                        }
                        iter.remove();
                    }
                }
                if (socket.isConnected() && writeBufQueue.size() > 0
                        && (hsStatus == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING
                                || hsStatus == SSLEngineResult.HandshakeStatus.FINISHED)) {
                    try {
                        writeLock.lock();
                        Supplier<byte[]> sup = null;
                        while ((sup = writeBufQueue.poll()) != null) {
                            localAppData.clear();
                            localAppData.put(sup.get());
                            localAppData.flip();
                            localNetData.clear();
                            SSLEngineResult result = sslEngine.wrap(localAppData, localNetData);
                            if (handleResult(result)) {
                                localNetData.flip();
                                while (localNetData.hasRemaining()) {
                                    socket.write(localNetData);
                                }
                            }
                        }
                    } finally {
                        writeLock.unlock();
                    }
                }
            }
            return null;
        });
        this.pool.shutdown();
        return fut;
    }

    /**
     * 添加数据进入发送队列
     * 
     * @param 函数式接口
     * @see Supplier
     */
    public void write(Supplier<byte[]> s) {
        try {
            writeLock.lock();
            writeBufQueue.push(s);
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 添加接收器进入接收队列
     * 
     * @param 函数式接口
     * @see Function
     */
    public void read(Function<byte[], byte[]> f) {
        try {
            readLock.lock();
            readBufQueue.push(f);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * SSL/TLS 握手
     * 
     * @throws InterruptedException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     * @throws IOException
     */
    public void sslHandshake()
            throws InterruptedException, NoSuchAlgorithmException, KeyManagementException, IOException {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom());
        sslEngine = sslContext.createSSLEngine();
        sslEngine.setUseClientMode(true);
        SSLSession sslSession = sslEngine.getSession();
        localAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 本地应用数据缓存
        localNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 本地加密数据缓存
        remoteAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 远端应用数据缓存
        remoteNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 远端加密数据缓存
        sslEngine.beginHandshake();
        hsStatus = sslEngine.getHandshakeStatus();
        SSLEngineResult result;
        // 循环判断指导握手完成
        while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
            switch (hsStatus) {
            case NEED_WRAP:
                localNetData.clear();
                result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData); // 第一个参数设置空包,SSLEngine会将握手数据写入网络包
                hsStatus = result.getHandshakeStatus();
                if (handleResult(result)) {
                    localNetData.flip();
                    // 确保数据所有发送完成
                    while (localNetData.hasRemaining()) {
                        socket.write(localNetData);
                    }
                }
                break;
            case NEED_UNWRAP:
                int len = socket.read(remoteNetData); // 读取网络数据
                if (len == -1) {
                    break;
                }
                remoteNetData.flip();
                remoteAppData.clear();
                do {
                    result = sslEngine.unwrap(remoteNetData, remoteAppData); // 与握手相关的数据SSLEngine会自行处理,不会输出至第二个参数
                    hsStatus = result.getHandshakeStatus();
                } while (handleResult(result) && hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
                // 一次性没有完成处理的数据经过压缩的方式处理,等待下一次数据写入
                remoteNetData.compact();
                break;
            case NEED_TASK:
                // SSLEngine后台任务
                Runnable runnable;
                while ((runnable = sslEngine.getDelegatedTask()) != null) {
                    runnable.run();
                }
                hsStatus = sslEngine.getHandshakeStatus();
                break;
            default:
                break;
            }
        }
        // 握手完成将全部缓存清空
        localAppData.clear();
        localNetData.clear();
        remoteAppData.clear();
        remoteNetData.clear();
    }

    private boolean handleResult(SSLEngineResult result) {
        switch (result.getStatus()) {
        case OK:
            return true;
        case BUFFER_OVERFLOW:
            return false;
        case BUFFER_UNDERFLOW:
            return false;
        case CLOSED:
            return false;
        default:
            return false;
        }
    }

    public static void main(String[] args) {
        try {
            SSLSocketChannel sslSocketChannel = new SSLSocketChannel();
            sslSocketChannel.connect("127.0.0.1", 10020);
            sslSocketChannel.dispatch();
            sslSocketChannel.read((b) -> {
                String s = new String(b);
                System.out.println(s);
                return null;
            });
            sslSocketChannel.write(() -> {
                return "hello ssl".getBytes();
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

相关文章:《开源项目SMSS开发指南》

相关文章
相关标签/搜索