在C++中调用OpenSSL库进行编程

目录

  • OpenSSL简介
  • 下载OpenSSL库并配置实验环境
  • OpenSSL库的加密函数的认识
    • 使用 EVP 库实现 DES 和 AES 加密
    • EVP_EncryptUpdate 函数参数详解
  • OpenSSL加密实践
    • RSA 密钥生成
    • RSA 公钥提取
    • 聊天过程中的消息使用 RSA 进行签名和验签
    • 聊天信息的 3DES_CBC 加密
  • ​​​​​​参考文献

在C++中调用OpenSSL库进行编程_第1张图片

OpenSSL简介

  OpenSSL库是由C语言实现,整个软件包大概可以分成三个主要的功能部分:密码算法库、SSL协议库以及应用程序。OpenSSL是目前主流的基于密码学的安全开发包,提供的功能相当强大和全面,包括了主要的密码算法、常用的密钥和证书封装管理功能以及SSL协议,并提供了丰富的应用程序供测试或其它目的使用。
  OpenSSL库具有以下优点:1.功能全面,支持大部分主流密码算法、相关标准协议和SSL协议;2.开放源代码,可信任,能根据自己需要进行修改,对技术人员有借鉴和研究的价值;3.具备应用程序,既能直接使用,也可方便地进行二次开发;4.免费,也可用作非商业用途;5.应用广泛且持续更新

下载OpenSSL库并配置实验环境

  进入 OpenSSL 官网(https://www.openssl.org/),下载 OpenSSL1.0 安装包(注意下载完整版,不要下载轻量版,完整版中包括 include 和 lib 目录。)。
  在 VS 中创建新项目,进行项目属性的配置。

  1. include 目录地址添加:项目-> 属性-> C/C++ 常规-> 包含附加目录:添加下载的OpenSSL 库的 include 目录地址。在C++中调用OpenSSL库进行编程_第2张图片

  2. lib目录地址添加:项目-> 属性-> 连接器->常规->附加库目录:添加下载的OpenSSL库的lib目录地址。在这里插入图片描述

  3. lib文件名称添加:项目-> 属性-> 连接器->输入->附加依赖项:添加下载的OpenSSL库的lib文件名称。在C++中调用OpenSSL库进行编程_第3张图片

  4. dll文件移动:将OpenSSL库中的bin文件夹中的两个dll文件移动到项目所在的文件夹下。

  5. 配置解决方案平台:下载的OpenSSL库需要在x64系统上进行调试运行,所以调试的平台选择x64。

  6. 包含需要的头文件,进行使用即可,如:#include,#include

OpenSSL库的加密函数的认识

使用 EVP 库实现 DES 和 AES 加密

  Openssl EVP(high-level cryptographic functions) 提供了丰富的密码学中的各种函数,包括各种对称算法、摘要算法以及签名/验签算法。下面重点学习使用 DES 和 AES 两种对称加密算法。
  在使用 EVP 库进行对称加密时,通常需要创建一个EVP_CIPHER_CTX 结构体对象,并通过一系列函数对其进行初始化、配置,最终进行加密操作。常用的 EVP_CIPHER_CTX 结构体初始化函数为 EVP_CIPHER_CTX_new() 和 EVP_CIPHER_CTX_init()。
  EVP_CIPHER_CTX_new() 函数在堆上为 EVP_CIPHER_CTX 结构体分配内存空间,并将其所有成员变量初始化为 0;EVP_CIPHER_CTX_init() 函数则用于初始化 EVP_CIPHER_CTX结构体对象的成员变量。
  在加密操作完成后,需要通过调用 EVP_CIPHER_CTX_cleanup 函数释放 EVP_CIPHER_CTX结构体对象的内存空间,以免内存泄漏。下面为EVP库的初始化代码:

EVP_CIPHER_CTX∗ ctx = EVP_CIPHER_CTX_new() ; //结构体对象ctx分配内存空间
EVP_CIPHER_CTX_init( ctx ) ; //初始化结构体对象ctx
EVP_CIPHER_CTX_cleanup( ctx ) ; //释放结构体对象空间

EVP_EncryptUpdate 函数参数详解

  在 DES 加密中,常见的 EVP_CIPHER 对象的类型有 EVP_des_ecb(),进行一次 des 的 ecb 模式加密;EVP_des_cbc()、EVP_des_ede3_cbc()。

  在 AES 加密中,常见的 EVP_CIPHER 对象类型有:

  1. EVP_aes_128_cbc、EVP_aes_192_cbc、EVP_aes_256_cbc:分别表示使用 AES 算法和 CBC 模式进行加密和解密,密钥长度分别为
    128、192 和 256 位。
  2. EVP_aes_128_ecb、EVP_aes_192_ecb、EVP_aes_256_ecb:分别表示使用 AES 算法和 ECB 模式进行加密和解密,密钥长度分别为 128、192 和 256 位。
  3. EVP_aes_128_gcm、EVP_aes_256_gcm:分别表示使用 AES 算法和 GCM 模式进行加密和解密,密钥长度分别为 128 和 256 位。

OpenSSL加密实践

  使用OpenSSL实现一个加密的聊天过程,包括对发送信息的签名以及总体发送消息的加密,加解密流程如下图所示:在C++中调用OpenSSL库进行编程_第4张图片
注意:

  1. 在实际聊天过程中,双方首先要相互通知对方自己生成的公钥,然后再输入发送的消息。
  2. 在图中进行 RSA 签名的密钥和对会话密钥加密的密钥不是同一组密钥。需要注意,使用
    自己的私钥进行 RSA 签名,对方使用公布的公钥进行验证;使用对方的公钥加密会话密钥,对
    方使用自己的私钥解密会话密钥。

RSA 密钥生成

  调用 openssl/rsa.h 库中的 RSA_generate_key_ex 函数生成 RSA 密钥对,具体代码如下所示:

void RSA_Key_Create() {
	int bits = 1024;//密钥位数
	unsigned long e = RSA_3;//RSA加密算法中使用的一个固定公开指数,其值为65537
	BIGNUM* bne;//大整数结构体和初始化
	bne = BN_new();
	BN_set_word(bne, e);//将e转换为大整数BIGNUM类型
	r = RSA_new();//r是存储密钥的结构体
	if (1 != RSA_generate_key_ex(r, bits, bne, NULL))//密钥生成
		handleErrors();
}

  对于 RSA_generate_key_ex 函数中各参数含义:r 是一个 RSA 密钥存储的结构体指针,使用 RSA_new 来创建一个结构体指针,存储生成的 RSA 密钥对;bits 表示密钥长度,一般为1024、2048 等,在本实验中 bit 取 1024,需要注意的是,bits 的长度还与后面摘要算法开辟的数组大小有关,密钥越大,RSA 签名算法生成的数字签名就越长。
  bne 是 RSA 公钥指数,一般取值为 65537(也就是上面赋值时使用的 RSA_3,但是 bne 是一个 BIGNUM 类型的参数,所以需要将 unsigned int 类型的 e 使用 BN_set_word 函数转换成BIGINT 类型),此外,bne 也可以为 NULL,此时将默认为 65537;最后一个参数是指回调函数,设置为 NULL 即可。函数返回值为 1 表示成功,返回 0 表示失败。

RSA 公钥提取

  在生成 RSA 公钥对后,需要向聊天的对方发送自己的 RSA 公钥,就需要从 RSA 密钥存储的结构体中将公钥分离出来,具体代码如下所示:

char* RSA_Get_Public() {
	EVP_PKEY* public_key = EVP_PKEY_new();// 获取公钥
	EVP_PKEY_set1_RSA(public_key, r);
        //创建一个新的内存BIO对象并将其与相应的内存缓冲区相关联,然后可以使用BIO函数从内存中读取或写入数据
	BIO* bio = BIO_new(BIO_s_mem());
        // 将EVP_PKEY对象输出为PEM格式的字符串
	PEM_write_bio_PUBKEY(bio, public_key);
 
	BUF_MEM* mem = NULL;
	BIO* b64_pub = BIO_new(BIO_f_base64());
	BIO_set_flags(b64_pub, BIO_FLAGS_BASE64_NO_NL);
	bio = BIO_push(b64_pub, bio);// 将PEM格式的字符串进行base64编码
	BIO_get_mem_ptr(bio, &mem);
	char* buffer_pub = (char*)OPENSSL_malloc(mem->length + 1);
	memcpy(buffer_pub, mem->data, mem->length);
	buffer_pub[mem->length] = '\0';
	cout <<"发送的公钥为:"<< buffer_pub << endl;
	BIO_free_all(bio);
	return buffer_pub;
}
}

  使用 PEM_write_bio_PUBKEY 函数提取公钥,函数将公钥写入 BIO 对象的内存空间并使用 PEM 格式编码。函数的第一个参数 bio 就是 BIO 对象指针,使用 BIO_NEW 函数创建;第二个参数 public_key 就是公钥指针,它是 EVP_PKEY 对象结构。(EVP_PKEY 对象在 evp.h文件中定义,所以还要调用 openssl/evp.h 文件)。
  BIO 是 OpenSSL 中的一个 I/O 抽象,它可以用于文件、套接字、内存等多种数据源/目的地。使用 BIO_new 创建一个 BIO 对象的函数,使用 BIO_s_mem() 可以为创建的 BIO 对象分配一块内存空间。
  在将公钥分离出来后,使用 base64 编码,将公钥打印在屏幕上,在双方完成通信互相交换密钥后,命令行如下图所示:在C++中调用OpenSSL库进行编程_第5张图片

聊天过程中的消息使用 RSA 进行签名和验签

  用户命令行输入字符串,需要对字符串明文消息进行摘要和 RSA 签名。首先将字符串转换成字符型数组,然后 SHA256 函数实现消息摘要,使用 RSA_sign 函数实现数字签名,具体代码如下:

unsigned int RSA_Sign(unsigned char signret[200],int original_datalen, unsigned char original_data[800]) {
	//选择摘要算法类型
	int nid = NID_sha256;// NID_sha、NID_sha1
	//Hash摘要算法计算
	unsigned char data[32];
	int datalen = 32;
	SHA256(original_data, original_datalen, data);
	
	unsigned char Base64_data[200];//这里开辟的栈大小为200,理论上不需要这么大,但实际不开辟这么大会出现栈溢出
	int len = EVP_EncodeBlock(Base64_data, data, datalen);
	cout << "摘要结果:" << Base64_data << endl;

	unsigned int signlen=0;//初始化输出缓冲区
	int ret=RSA_sign(nid, data, SHA256_DIGEST_LENGTH, signret, &signlen, r);//签名函数
	len = EVP_EncodeBlock(Base64_data, signret, signlen);
	cout << "签名内容:" << Base64_data << endl;
	return signlen;

  这里选择 SHA256 摘要函数,它是 SHA-2 系列的一种摘要函数,相较于之前的 SHA-1,它的哈希长度更长(256 位),具有更高的安全性,更难被暴力破解。SHA256 函数有三个参数,分别为:第一个参数 data,用来存储需要计算的输入数据;第二个参数 len 是数据长度;第三个参数 md 用来存储计算出的摘要结果,长度为 32 字节(256 位)。(SHA256 函数在 openssl/sha.h文件中)
  使用 RSA_sign 函数进行 RSA 签名,它的参数分别为:nid 表示使用的签名算法类型,例如NID_sha256 就表示 SHA256 算法;m_len 表示待签名数据的长度;sigret 表示签名后的数据的输出缓冲区,必须预先分配足够的空间来存放签名结果;siglen 表示签名后的数据的长度,即输出缓冲区的长度;rsa 表示使用的 RSA 密钥。如果函数执行成功,返回值为 1;否则返回 0. 可以通过调用 ERR_get_error() 函数获取错误码。
  同样,在实验中会将摘要内容和生成的 RSA 签名内容使用 base64 编码打印出来,显示在屏幕上。
  RSA 验签过程就是 RSA 签名的反过程,使用 RSA_verify 函数实现,函数的参数含义和RSA_sign 函数参数基本相同,需要注意的是,这里使用的 rsa_key 虽然是一个 RSA 密钥对的结构,但其中只存储了对方的公钥。实现代码如下所示:

bool RSA_Verify(unsigned char* signret, int signlen, unsigned char original_data[800], int original_datalen) {
	bool ret = 0;
	//选择摘要算法类型
	int nid = NID_sha256;// NID_sha、NID_sha1
	//Hash摘要算法计算
	unsigned char data[32];
	int datalen = 32;
	SHA256(original_data, original_datalen, data);

	if (1 != RSA_verify(nid, data, SHA256_DIGEST_LENGTH, signret, signlen, rsa_key))//验证函数
		ret = 0;
	else
		ret = 1;
	return ret;
}

  在实际聊天过程中,接收方会对发送方发送的消息进行验签,如果验签失败,说明消息不是发送方发来的,就不输出消息;验签成功就输出消息。
在C++中调用OpenSSL库进行编程_第6张图片

聊天信息的 3DES_CBC 加密

  在对消息签名后,将签名内容附在消息签名,对这个整体进行3DES_CBC 模式加密。3DES(Triple Data Encryption Standard) 是一种对称加密算法,它是对 DES 算法的改进,将 DES 算法中的密钥长度增加到了 24 字节,更好地确保数据的完整性和安全性。
  使用的 EVP_CIPHER 对象类型为 EVP_des_ede3_cbc(),使用 EVP_EncryptUpdate 和 EVP_EncryptFinal_ex 函数进行加密,EVP_EncryptUpdate 和 EVP_EncryptFinal_ex 是 OpenSSL 库中进行加密的两个重要函数。它们的组合可以用于将数据进行加密并生成密文。

​​​​​​参考文献

《Openssl 编程》 赵春平著

你可能感兴趣的:(c++,ssl)