本文将详细介绍如何在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以前的版本可能与后面的代码示例有所不同。

electron桌面应用:electron是一套依赖google的V8引擎直接使用HTML/JS/CSS创建桌面应用的跨平台解决方案。如果你需要开发轻量化的桌面端应用,electron基本是不二选择。从个人的实践来看,无论是开发生态还是开发效率都强于Qt。使用electron可以调用nodejs相关接口完成与系统的交互。

Java-nio开发包:基本是现在作为Java中高级开发的必备技能。

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

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

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

二 libevent和openssl

生成x.509证书

首选在安装好openssl的机器上创建私钥文件:server.key

> openssl genrsa -out server.key 2048

得到私钥文件后我们需要一个证书请求文件:server.csr,将来你可以拿这个证书请求向正规的证书管理机构申请证书

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

最后我们生成自签名的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, );
sockaddr_in sin;
memset(&sin, , sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(); // 指定通信端口
int res = ::bind(sock, (sockaddr *)&sin, sizeof(sin));
if (res == -)
{
return -;
}
listen(sock, ); // 开始监听
// 只接受一次客户端的连接
int client_fd = accept(sock, , );
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 != )
{
ERR_print_errors_fp(stderr);
return -;
}
// 握手完成,接受消息并发送一次应答
char buf[] = {};
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 ;
} 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 != )
{
ERR_print_errors_fp(stderr);
return nullptr;
}
res = SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM);
if (res != )
{
ERR_print_errors_fp(stderr);
return nullptr;
}
res = SSL_CTX_check_private_key(ssl_ctx);
if (res != )
{
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, );
sockaddr_in sin;
memset(&sin, , sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr("127.0.0.1");
sin.sin_port = htons();
// 首先执行socket连接
int res = connect(sock, (sockaddr *)&sin, sizeof(sin));
if (res != )
{
return -;
}
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 != )
{
ERR_print_errors_fp(stderr);
return -;
}
string send_msg = "Hello Server, I'm CppSSLSocket Client.";
SSL_write(ssl, send_msg.c_str(), send_msg.size());
char recv_msg[] = {};
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 ;
} 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 == )
{
if (strcmp(argv[], "SSL") == )
{
isSsl = true;
}
}
// 创建event_base
event_base *base = event_base_new();
if (!base)
{
cout << "event_base_new fail" << endl;
return -;
}
// 创建SSL_CTX通信上下文
ssl_ctx_st *ssl_ctx = InitServer("../server.crt", "../server.key");
// 创建socket
sockaddr_in addr;
memset(&addr, , sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons();
evconnlistener *listener = evconnlistener_new_bind(
base,
EvconnlistenerCB,
ssl_ctx,
LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
,
(sockaddr *)&addr,
sizeof(addr)); // 阻塞当前线程执行事件循环
event_base_dispatch(base);
// 释放资源
SSL_CTX_free(ssl_ctx);
event_base_free(base);
return ;
} 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[] = {};
int len = bufferevent_read(bev, buf, sizeof(buf) - );
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开发指南》

开源项目SMSS发开指南(四)——SSL/TLS加密通信详解的更多相关文章

  1. 开源项目SMSS发开指南(五)——SSL/TLS加密通信详解(下)

    继上一篇介绍如何在多种语言之间使用SSL加密通信,今天我们关注Java端的证书创建以及支持SSL的NioSocket服务端开发.完整源码 一.创建keystore文件 网上大多数是通过jdk命令创建秘 ...

  2. SSL/TLS 握手过程详解

    在现代社会,互联网已经渗透到人们日常生活的方方面面,娱乐.经济.社会关系等都离不开互联网的帮助.在这个背景下,互联网安全就显得十分重要,没有提供足够的安全保障,人们是不会如此依赖它的.幸运的是,在大牛 ...

  3. SSL/TLS 握手优化详解

    随着 HTTP/2 的逐渐普及,以及国内网络环境越来越糟糕(运营商劫持和篡改),HTTPS 已经开始成为主流.HTTPS 在 TCP 和 HTTP 之间增加了 TLS(Transport Layer ...

  4. 开源项目SMSS开发指南(二)——基于libevent的线程池

    libevent是一套轻量级的网络库,基于事件驱动开发.能够实现多线程的多路复用和注册事件响应.本文将介绍libevent的基本功能以及如何利用libevent开发一个线程池. 一. 使用指南 监听服 ...

  5. 开源项目SMSS开源项目(三)——protobuf协议设计

    本文的第一部分将介绍protobuf使用基础以及如何利用protobuf设计通信协议.第二部分会给出smss项目的协议设计规范和源码讲解. 一.Protobuf使用基础 什么是protobuf pro ...

  6. SSL及其加密通信过程

    SSL及其加密通信过程 什么是SSL SSL英文全称Secure Socket Layer,安全套接层,是一种为网络通信提供安全以及数据完整性的安全协议,它在传输层对网络进行加密.它主要是分为两层: ...

  7. SSL握手通信详解及linux下c/c++ SSL Socket代码举例(另附SSL双向认证客户端代码)

    SSL握手通信详解及linux下c/c++ SSL Socket代码举例(另附SSL双向认证客户端代码) 摘自: https://blog.csdn.net/sjin_1314/article/det ...

  8. SSL/TLS 加密新纪元 - Let's Encrypt

    转自: https://linux.cn/article-6565-1.html SSL/TLS 加密新纪元 - Let's Encrypt 根据 Let's Encrypt 官方博客消息,Let's ...

  9. scons用户指南翻译(附gcc/g++参数详解)

    scons用户指南 翻译 http://blog.csdn.net/andyelvis/article/category/948141 官网文档 http://www.scons.org/docume ...

随机推荐

  1. slim中的参数获取

    官方文档中对于get和post的参数有以下获取方式 $app->get('/', function (Request $req, Response $res, $args = []) { $my ...

  2. VMware卸载后再安装时网络连接处没有虚拟网卡

    解决: 1.打开虚拟机,点击编辑,再点击虚拟网络编辑器 2.将所有的虚拟网络删除 3.删除完所有的虚拟网络之后再添加虚拟网络 4.按照自己想要的的连接方式添加上,网络连接处就会有虚拟网卡

  3. mysql锁及四种事务隔离级别笔记

    前言 数据库是一个共享资源,为了充分利用数据库资源,发挥数据 库共享资源的特点,应该允许多个用户并行地存取数据库.但这样就会产生多个用户程序并 发存取同一数据的情况,为了避免破坏一致性,所以必须提供并 ...

  4. jenkins邮件通知html魔板

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  5. Alpha阶段中间产物提交入口

    此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/9865 git地址:https://e.coding.net/Eustia ...

  6. 爬虫 -- JS调试

    开发者工具(F12) 其中常用的有Elements(元素面板).Console(控制台面板).Sources(源代码面板).Network(网络面板) 找 JS 文件的几种方法 1.找发起地址 2.设 ...

  7. 当Parallel遇上了DI - Spring并行数据聚合最佳实践

    分析淘宝PDP 让我们先看个图, Taobao的PDP(Product Detail Page)页. 打开Chrome Network面板, 让我们来看taobao是怎么加载这个页面数据的. 根据经验 ...

  8. 开箱即用!使用Rancher 2.3 启用Istio初体验

    本文来自Rancher Labs Rancher的理念是Run Kubernetes Everywhere,Rancher 2.3中许多重大更新,让这一理念的实现又向前一步. 其中,最重要的两个特性是 ...

  9. realme X2谷歌套件

    目前市面上的很多手机是不支持谷歌相关组件的,经过不断的测试成功适配realme X2(真机测试完美适配) 为框架的GMS是用户想要体验整套Google服务不可绕开的一环,Google地图.Play商店 ...

  10. 你的java服务挂了吗

    问题背景 最近测试环境服务总是崩溃,运维小哥全部重启后还是崩溃,查看了服务运行情况占用内存确实挺高的,看来是时候优化一波jvm参数了. 优化前 top $(ps -e | grep java | aw ...