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

2023-11-10

本文将详细介绍如何在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>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 网络常考题

    45 当数据接收者不能处理更多数据时 哪一层发出停止信息给发送者 A 网络层 B 传输层 C 会话层 D 表示层 B 49 在传输层采用了以下哪些方法来保证接收缓冲区不溢出 多选 A 数据分段 B 确认机制 C 流量控制 D 滑动窗口 E
  • Spring中@JsonFormat与@DateTimeFormat注解使用简介说明

    转自 Spring中 JsonFormat与 DateTimeFormat注解使用简介说明 下文笔者讲述Spring中JsonFormat和DateTimeFormat注解的简介说明 如下所示 JsonFormat和DateTimeForm
  • C语言编译执行的全过程

    编译 编译程序读取源程序 字符流 对之进行词法和语法的分析 将高级语言指令转换为功能等效的汇编代码 再由汇编程序转换为机器语言 并且按照操作系统对可执行文件格式的要求链接生成可执行程序 C源程序头文件 gt 预编译处理 cpp gt 编译程
  • 高光谱遥感数值建模技术及在植被、水体、土壤信息提取领域应用

    在高光谱影像中 结合纹理 表面粒度 风化程度 作物密度等辅助信息 能估计出多种地物及其上覆作物的状态参量 提高遥感高定量分析的精度和可靠性 如何通过构建遥感光谱反射信号与地表参数之间的关系模型来实现数值计算 是举办本次培训班的主要目的 针对
  • 校园网络系统服务器配置摘要,校园网网络应用服务器配置

    浅谈校园网网络应用服务器配置 摘要 以某校园网为例 在网络应用服务器设计这个环节中 我们分别用到了web服务器 ftp服务器 dns服务器 dhcp服务器 mail服务器 并且在一台已经安装了windows 2003 server的计算机上
  • 前端框架——React 学习总结,这篇7000字全解决

    React组件复用 React组件复用 把多个组件中部分功能相似或者相同的状态或者逻辑进行复用 复用 state和操作state的方法 复用的方式 render props模式 高阶组件 HOC render props模式 用childr
  • Mysql 时间转换 && 时间函数

    1 时间转换 涉及的函数 DATE FORMAT date format MySQL日期格式化函数 STR TO DATE str format MySQL字符串格式化为日期 UNIX TIMESTAMP MySQL其他数据转换为时间戳 F
  • Vue的antd多选下拉框增加全选操作

    因为antd的多选下拉框没有提供全选操作 我做了一个简易的全选操作 data return categoryList 存放获取到的分选数据 category 已选分类数据
  • QT信号槽的5种连接方式

    在面试中 这是一个经常被问到的问题点 也是刚刚上qt的工程师不会去注意的一个点 qt源代码定义的连接方式如下 1 Qt AutoConnection 一般信号槽不会写第五个参数 其实使用的默认值 使用这个值则连接类型会在信号发送时决定 如果
  • markdown编辑数学公式

    在输入数学公式的时候 需要在数学公式的前后加入 符号 将需要输入的公式加入到 中间 上下标 上标 下标 名称 数学表达式 markdown公式 上标 ab a b a b 下标 ab a b a b 分数 frac 第一个 写分子 第二个
  • React Native(RN)-组件生命周期

    生命周期简介 像 Android 开发一样 React Native RN 中的组件也有生命周期 Lifecycle 借用大神流程图 这张图很简洁直观地告诉我们 生命周期的整个流程以及阶段划分 第一阶段 getDefaultProps gt
  • 目标检测入门

    目录 R CNN 1 1提取候选区域 1 1 1合并规则 1 1 2多样化与后处理 1 2特征提取 1 2 1预处理 2 Fast RCNN 2 1RoI Pooling Layer Faster RCNN 结构 RPN anchor 目标
  • Junit中使用线程池不执行任务代码

    1 在test中使用线程池发送MQ 没有报错 没有执行线程池中的代码 2 查资料 junit框架只要主线程结束完成 单元测试就会关闭 导致线程池中的线程没有执行代码就被销毁关闭了 可以在主线程中sleep一段时间 或者用main方法
  • 稳定ORACLE的执行计划

    很多时候可能我们都希望CBO能够帮我们生成正确 高效的执行计划 但是很多时候事实并非如此 可能因为各种各样的原因 如 统计信息不正确或者CBO天生的缺陷等 都会导致生成的执行计划特别的低效 之前的一家公司有一台专门用于批量做数据校验清洗的数
  • 大学四年自学走来,这些私藏的实用工具/学习网站我贡献出来了

    大学四年 看课本是不可能一直看课本的了 对于学习 特别是自学 善于搜索网上的一些资源来辅助 还是非常有必要的 下面我就把这几年私藏的各种资源 网站贡献出来给你们 主要有 电子书搜索 实用工具 在线视频学习网站 非视频学习网站 软件下载 面试
  • 'vue-cli-service' 不是内部或外部命令,也不是可运行的程序 或批处理文件。

    vue时 报 vue cli service 不是内部或外部命令 也不是可运行的程序 或批处理文件 罪该万死 怎么能忘记 npm install 如果你下载的淘宝镜像 也可以cnpm install 转载于 https www cnblog
  • Java设计模式-状态模式

    1 概述 定义 对有状态的对象 把复杂的 判断逻辑 提取到不同的状态对象中 允许状态对象在其内部状态发生改变时改变其行为 例 通过按钮来控制一个电梯的状态 一个电梯有开门状态 关门状态 停止状态 运行状态 每一种状态改变 都有可能要根据其他
  • STM32F031串口(RS485)中断+DMA发送(预备知识)

    STM32F031串口 RS485 中断 DMA发送 前言 GPIO移植过程 与F1系列的一些区别 串口 DMA 前言 最近在搞STM32F031的项目 F0系列与常用的F1系列有一定区别 在开发过程中遇到一些问题 而且花了好长花间在搜寻解
  • js操作剪贴板讲解

    文章目录 复制 剪切 到剪贴板 Document execCommand Clipboard复制 Clipboard writeText Clipboard write copy cut事件 从剪贴板进行粘贴 document execCo
  • 【E2EL5】A Year in Computer Vision中关于图像增强系列部分

    http www themtank org a year in computer vision 部分中文翻译汇总 https blog csdn net chengyq116 article details 78660521 The M T

随机推荐