开源项目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以前的版本可能与后面的代码示例有所不同。

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, 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>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值