本文将详细介绍如何在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直接通信接口,下面逐一分析:
- SSL_CTX_new:新版本摒弃了一些老的接口,目前建议基本统一使用此方法来创建通信上下文
- SSL_CTX_free:释放SSL_CTX*
- SSL_CTX_use_certificate_file:设置证书文件
- SSL_CTX_use_PrivateKey_file:设置私钥文件,与上面的证书文件必须配套否则检测不通过
- SSL_CTX_check_private_key:检查私钥和证书文件
- SSL_new:方法一创建完成的上下文在通过此方法创建配套的SSL*
- SSL_set_fd:与上面创建的SSL和socket_fd绑定
- SSL_accept:服务端握手方法
- SSL_connect:客户端握手方法
- SSL_write:消息发送,内部会对明文消息加密并调用socket发送
- SSL_read:消息接收,内部会从socket接收到密文数据再解码成文明返回
- SSL_shutdown:通知对方关闭本次加密会话
- 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] = '