文章目录
一、概述
OpenSSL支持多种非对称加密算法,包括RSA、DH、DSA、ECC和SM2等。本文主讲RSA非对称加解密的PKCS1填充模式。
1. RSA非对称加解密流程
- 生成密钥对
生成私钥:使用OpenSSL提供的命令行工具可以生成一对RSA密钥,包括一个私钥和一个公钥。生成私钥的命令是openssl genrsa -out rsa_private_key.pem 1024,其中1024是密钥的长度,可以根据安全需求选择不同的长度。
提取公钥:有了私钥文件后,可以通过命令openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem从私钥中提取出公钥。
密钥格式:在C/C++开发中,私钥和公钥通常以PEM格式存储,这种格式的密钥文件包含了起始和结束的标识,以及每64个字节的换行符,方便阅读和管理。 - 公钥加密过程
读取公钥文件:在进行加密操作前,需要通过fopen()函数打开公钥文件,并使用PEM_read_RSA_PUBKEY函数读入并初始化公钥。
执行加密操作:利用RSA_public_encrypt函数可以实现公钥加密,该函数需要指定明文的长度和密文的存储空间。加密过程中可能需要对明文进行适当的填充,以确保加密的正确性。
加密数据限制:由于RSA算法的限制,每次加密的数据长度不能超过密钥长度减去一定数值(如RSA_PKCS1_PADDING),对于较大的数据需要进行分块加密或采用其他策略。 - 私钥解密过程
读取私钥文件:与加密过程类似,解密前需要打开私钥文件,使用PEM_read_RSAPrivateKey函数读入私钥并进行初始化。
执行解密操作:通过RSA_private_decrypt函数可以实现私钥解密,解密过程中同样需要注意数据长度和填充规范,以确保解密的正确性。
解密数据限制:解密过程中也受到数据长度的限制,需要确保每次解密的数据块符合RSA算法的要求。
2. RSA非对称加解密的填充模式
以下是对每种填充模式的介绍:
- RSA_PKCS1_PADDING
特点: RSA_PKCS1_PADDING是RSA加密中一种传统的填充方式,它遵循PKCS#1标准。这种模式在数据块前添加了必要的填充字节,以确保数据符合RSA密钥长度的要求。
安全性分析: 虽然PKCS1填充广泛使用,但它可能受到选择密文攻击(CCA)的威胁。在某些情况下,不恰当的实现可能会暴露出安全漏洞。
适用场景: RSA_PKCS1_PADDING适用于那些不需要最高安全性的应用,或者在能够确保加密过程中其他环节安全性的情况下使用。 - RSA_PKCS1_OAEP_PADDING
特点: RSA_PKCS1_OAEP_PADDING是一种基于最优非对称加密填充(Optimal Asymmetric Encryption Padding, OAEP)的模式。它不仅提供数据填充,还加强了算法的整体安全性,通过在加密前对数据进行预处理来防止某些类型的攻击。
安全性分析: OAEP设计用于提高RSA加密的安全性,可以有效防御CCA和其他类型的攻击。它引入了额外的随机性和编码步骤,使得加密过程更难以被攻击者利用。
适用场景: RSA_PKCS1_OAEP_PADDING适用于需要较高安全性的应用场景,如敏感数据的加密传输和数字签名。 - RSA_NO_PADDING
特点: 如其名,RSA_NO_PADDING不使用任何填充。这意味着加密的数据必须严格符合RSA密钥的长度要求,否则将无法正确加密或解密。
安全性分析: 由于没有填充,这种模式容易受到多种攻击,特别是当处理未经过适当验证的数据时。因此,除非特殊情况,通常不推荐使用无填充模式。
适用场景: 应避免使用RSA_NO_PADDING,除非在非常特定的、已经充分考虑并管理了所有潜在风险的场景下。 - 注意事项
确保在实施加密时,密钥的长度和填充模式相匹配,以避免数据丢失或错误。
在选择填充模式时,应考虑到数据的特性和安全需求,合理选择最合适的加密策略。
对于涉及高安全性需求的场合,建议使用RSA_PKCS1_OAEP_PADDING以增强整体安全性。
本文代码示例中,使用RSA_PKCS1_PADDING举例。
二、调用函数介绍
本文主要使用EVP打头的函数,关于EVP函数的介绍,可参考我的博客: Linux C语言调用OpenSSL:EVP 接口概述
1. EVP_PKEY_new
动态地分配并初始化一个 EVP_PKEY 结构体。这个结构体通常与特定的密钥类型(如 RSA、DSA、EC 等)关联,并且包含密钥的实际数据。
#include <openssl/evp.h>
EVP_PKEY *EVP_PKEY_new(void);
返回值:
如果成功,返回一个指向新分配的 EVP_PKEY 结构体的指针。如果失败,返回 NULL。
注意:
(1)通常不会直接使用 EVP_PKEY_new 来生成密钥。应该使用特定于密钥类型的函数(如 RSA_generate_key_ex、EC_KEY_generate_key 等)来生成密钥,并将生成的密钥设置为 EVP_PKEY 结构体的内容。
(2)当使用完 EVP_PKEY 结构体后,应该使用 EVP_PKEY_free 函数来释放它,以避免内存泄漏。
(3)如果在创建 EVP_PKEY 结构体后没有设置其密钥类型或内容,那么它就是一个“空”的或“未初始化”的 EVP_PKEY 结构体,并且可能无法用于任何有意义的操作。
2. EVP_PKEY_set1_RSA
用于将RSA密钥(RSA *类型)设置为EVP密钥(EVP_PKEY *类型)的RSA部分。
#include <openssl/evp.h>
#include <openssl/rsa.h>
int EVP_PKEY_set1_RSA(EVP_PKEY *pkey, struct rsa_st *key)
参数:
*EVP_PKEY pkey:这是一个指向EVP_PKEY结构的指针,该结构表示一个公钥或私钥。这个指针指向的EVP_PKEY结构将被设置为包含提供的RSA密钥。
*RSA key:这是一个指向RSA结构的指针,该结构包含RSA密钥的具体信息。这个密钥将被复制到EVP_PKEY结构中。
返回值:
成功时返回1,表示密钥已成功设置。如果发生错误,则返回0,并可以通过OpenSSL的错误处理函数(如ERR_get_error)来获取更详细的错误信息。
注意:
EVP_PKEY_set1_RSA函数会增加key的引用计数(如果它尚未被设置为自动释放),并在不再需要时自动释放它。这有助于管理密钥的生命周期,并确保在不再需要密钥时正确地释放它。
3. EVP_PKEY_CTX_new
用于初始化一个密钥上下文 (EVP_PKEY_CTX)。这个上下文用于存储与特定公钥或私钥相关的操作(如签名、验证、加密、解密等)的状态和参数。
#include <openssl/evp.h>
EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);
参数:
EVP_PKEY *pkey:这是一个指向 EVP_PKEY 结构的指针,该结构包含了公钥或私钥的信息。这个参数通常是非空的,除非要在不涉及具体密钥的情况下设置某些通用的上下文选项。
ENGINE *e:这是一个指向 ENGINE 结构的指针,该结构用于硬件加速或实现特定的加密算法。在大多数情况下,可以将此参数设置为 NULL,除非正在使用特定的加密硬件或需要特定的算法实现。
返回值:
如果成功,EVP_PKEY_CTX_new 将返回一个指向新创建的 EVP_PKEY_CTX 结构的指针。如果失败,它将返回 NULL。
4. EVP_PKEY_encrypt_init
用于初始化一个公钥加密操作。使用公钥进行加密时(例如,在公钥密码体制如 RSA 或 ECC 中),需要首先使用这个函数来设置加密操作的上下文。
#include <openssl/evp.h>
int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *ctx, EVP_PKEY *pkey);
参数:
EVP_PKEY_CTX *ctx:这是一个指向 EVP_PKEY_CTX 结构的指针,该结构用于存储加密操作的上下文。这个上下文通常是通过 EVP_PKEY_CTX_new 函数创建的。
EVP_PKEY *pkey:这是一个指向 EVP_PKEY 结构的指针,该结构包含了公钥的信息。公钥用于加密数据。
返回值:
如果初始化成功,函数返回 1(真)。如果失败,它返回 0(假)并设置错误代码,可以通过 ERR_get_error 来获取这个错误代码。
5. EVP_PKEY_CTX_set_rsa_padding
用于在 RSA 加密或签名操作中设置填充模式(padding mode)。RSA 加密和签名操作通常需要某种形式的填充来确保安全性,因为 RSA 算法本身并不直接支持对任意长度的数据进行加密或签名。
#include <openssl/evp.h>
int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);
参数:
EVP_PKEY_CTX *ctx:这是一个指向 EVP_PKEY_CTX 结构的指针,该结构用于存储密钥操作的上下文。这个上下文通常是通过 EVP_PKEY_CTX_new 函数创建的,并且已经与 RSA 密钥相关联。
int pad:这是一个整数,指定了 RSA 操作的填充模式。OpenSSL 支持多种 RSA 填充模式,包括但不限于:
RSA_PKCS1_PADDING:传统的 PKCS#1 v1.5 填充模式。
RSA_PKCS1_OAEP_PADDING:PKCS#1 OAEP(Optimal Asymmetric Encryption Padding)填充模式,通常与 SHA-1、SHA-256 等哈希算法一起使用。
RSA_NO_PADDING:不进行填充(注意:这通常是不安全的,并且仅用于某些特定的、非标准的用途)。
其他填充模式可能也受支持,但上述是最常见的。
返回值:
如果设置成功,函数返回 1(真)。如果失败,它返回 0(假)并设置错误代码,可以通过 ERR_get_error 来获取这个错误代码。
6. EVP_PKEY_encrypt
用于执行公钥加密操作的函数。
#include <openssl/evp.h>
int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);
参数:
EVP_PKEY_CTX *ctx:这是一个指向 EVP_PKEY_CTX 结构的指针,它包含了加密操作的所有上下文信息,包括公钥、填充模式、加密算法等。
unsigned char *out:这是一个指向输出缓冲区的指针,加密后的数据将被写入这个缓冲区。
size_t *outlen:这是一个指向 size_t 类型的指针,它包含了输出缓冲区 out 的大小。在函数调用后,它将被设置为实际写入输出缓冲区的字节数。
const unsigned char *in:这是一个指向输入数据的指针,即需要被加密的数据。
size_t inlen:这是输入数据的长度(以字节为单位)。
返回值:
如果加密操作成功,函数返回 1(真)。如果失败,它返回 0(假)并设置错误代码,可以通过 ERR_get_error 来获取这个错误代码。
7. EVP_PKEY_CTX_free
用于释放由EVP_PKEY_CTX_new创建的EVP_PKEY_CTX结构的内存。当使用完一个EVP_PKEY_CTX上下文后,应该调用这个函数来释放它占用的资源,以防止内存泄漏。
#include <openssl/evp.h>
void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);
参数:
EVP_PKEY_CTX *ctx:指向要释放的EVP_PKEY_CTX结构的指针。这个指针必须是之前通过EVP_PKEY_CTX_new或相关函数(如EVP_PKEY_sign_init、EVP_PKEY_encrypt_init等)成功创建的。
返回值:
此函数没有返回值,因为它执行的是一个简单的内存释放操作。
8. EVP_PKEY_decrypt_init
用于初始化解密操作的函数。它设置了一个 EVP_PKEY_CTX 上下文,以便进行后续的解密操作
#include <openssl/evp.h>
int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *ctx);
参数:
EVP_PKEY_CTX *ctx:指向 EVP_PKEY_CTX 结构的指针,该结构将包含解密操作的上下文信息。
返回值:
如果初始化成功,函数返回 1(真)。如果失败,它返回 0(假)并设置错误代码。
9. EVP_PKEY_decrypt
用于执行私钥解密操作。EVP_PKEY_decrypt函数是一个通用的接口,它可以用于支持多种算法(如RSA、EC等)的解密操作。
#include <openssl/evp.h>
int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen,
const unsigned char *in, size_t inlen);
参数:
EVP_PKEY_CTX *ctx:指向已初始化解密操作的EVP_PKEY_CTX结构的指针。
unsigned char *out:指向输出缓冲区的指针,用于存放解密后的数据。
size_t *outlen:解密后数据的长度(以字节为单位)。这是一个输入/输出参数。在调用函数之前,它应该被设置为输出缓冲区out的大小。在函数返回后,它将被设置为实际解密数据的长度。
const unsigned char *in:指向要解密的密文的指针。
size_t inlen:密文的长度(以字节为单位)。
返回值:
如果解密操作成功,函数返回1(真)。如果失败,它返回0(假)并设置错误代码。
示例
代码
#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/bn.h>
#include <openssl/rsa.h>
#define WK_OK 0
#define WK_ERR (-1)
#define ANSI_COLOR_RESET "\x1b[0m"
#define ANSI_COLOR_RED "\x1b[31m"
#define log_err(...) (printf("[" ANSI_COLOR_RED "%s-%s-%d" ANSI_COLOR_RESET "]: ", __FILE__, __FUNCTION__, __LINE__), printf(__VA_ARGS__))
//打印
void my_printf(char* str, unsigned char* data, unsigned int len)
{
if(NULL != str)
{
printf("%s\n", str);
}
for(int i=0; i<len; i++)
{
printf("0x%02x, ", data[i]);
if(0 == (i+1)%32)
{
printf("\n");
}
}
printf("\n");
}
//生成RSA私钥
RSA* my_generate_rsa_key(int key_bit, unsigned long e_value)
{
RSA* r = RSA_new();//存放rsa密钥对
if(NULL == r)
{
return NULL;
}
BIGNUM* e = BN_new();
BN_set_word(e, e_value); //公钥指数 使用默认值RSA_F4 65537; 也可以采用随机数,性能不可控
RSA_generate_key_ex(r, key_bit, e, NULL);//生成私钥指数D 和 模数N(N=p*q)
BN_free(e);//释放空间
return r;
}
//提取公钥
//prikey:私钥
RSA* my_get_pub_form_pri(RSA* prikey)
{
BIGNUM *n_in_pri = NULL;//私钥中的模数
BIGNUM *e_in_pri = NULL;//私钥中的公钥指数
BIGNUM *n_in_pub = NULL;
BIGNUM *e_in_pub = NULL;
//存放rsa公钥
RSA* pubkey = RSA_new();
if(NULL == pubkey)
{
return NULL;
}
//获取私钥中的N和e(公钥由模数N和公钥指数E决定,所以只需要获取这两个参数)
RSA_get0_key(prikey, &n_in_pri, &e_in_pri, NULL);
//复制大数。注意:(1)不能直接使用memcpy,容易造成内存泄漏;(2)若使用BN_copy,需要提前给目标BINNUM申请空间
n_in_pub = BN_dup(n_in_pri);
e_in_pub = BN_dup(e_in_pri);
//设置公钥(pubkey会继承n_in_pub和e_in_pub空间,所以无需单独释放n_in_pub和e_in_pub空间)
RSA_set0_key(pubkey, n_in_pub, e_in_pub, NULL);
return pubkey;
}
//RSA加密
int my_RSA_en(EVP_PKEY *evpKey, unsigned char* src, unsigned int src_len, unsigned char* dst, unsigned int* dst_len)
{
int ret = 0;
char err[128] = {0};
EVP_PKEY_CTX *ctx = NULL;
int encrypted_len = 0;
ctx = EVP_PKEY_CTX_new(evpKey, NULL);
if (ctx == NULL)
{
log_err("err: EVP_PKEY_CTX_new\n");
goto err_handle;
}
//初始化加密上下文
ret = EVP_PKEY_encrypt_init(ctx);
if (ret <= 0)
{
log_err("err: EVP_PKEY_encrypt_init\n");
goto err_handle;
}
//设置填充模式
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
if (ret <= 0)
{
log_err("err: EVP_PKEY_CTX_set_rsa_padding\n");
goto err_handle;
}
//加密
ret = EVP_PKEY_encrypt(ctx, dst, dst_len, src, src_len);
if (ret <= 0)
{
log_err("err: EVP_PKEY_encrypt\n");
goto err_handle;
}
EVP_PKEY_CTX_free(ctx);
return WK_OK;
err_handle:
ERR_error_string(ERR_get_error(), err);
log_err("err: [%s]\n", err);
if(ctx)
{
EVP_PKEY_CTX_free(ctx);
}
return WK_ERR;
}
//RSA解密
int my_RSA_de(EVP_PKEY *evpKey, unsigned char* src, unsigned int src_len, unsigned char* dst, unsigned int* dst_len)
{
int ret = 0;
char err[128] = {0};
EVP_PKEY_CTX *ctx = NULL;
ctx = EVP_PKEY_CTX_new(evpKey, NULL);
if (ctx == NULL)
{
log_err("err: EVP_PKEY_CTX_new\n");
goto err_handle;
}
//初始化加密上下文
ret = EVP_PKEY_decrypt_init(ctx);
if (ret <= 0)
{
log_err("err: EVP_PKEY_decrypt_init\n");
goto err_handle;
}
//设置填充模式
ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
if (ret <= 0)
{
log_err("err: EVP_PKEY_CTX_set_rsa_padding\n");
goto err_handle;
}
//解密
ret = EVP_PKEY_decrypt(ctx, dst, dst_len, src, src_len);
if (ret <= 0)
{
log_err("err: EVP_PKEY_decrypt\n");
goto err_handle;
}
EVP_PKEY_CTX_free(ctx);
return WK_OK;
err_handle:
ERR_error_string(ERR_get_error(), err);
log_err("err: [%s]\n", err);
if(ctx)
{
EVP_PKEY_CTX_free(ctx);
}
return WK_ERR;
}
int main()
{
unsigned long long e_value = RSA_F4;//65537,rsa.h中的宏定义,加密指数(或称:公共指数)
int key_bit = 2048;
RSA* rsa_key = NULL;//私钥
RSA* rsa_pub = NULL;//公钥
unsigned char data[512] = {0};
unsigned int datalen = 245;
unsigned char cipher[512] = {0};
unsigned int cipher_len = 0;
unsigned char plain[512] = {0};
unsigned int plain_len = 0;
int ret = 0, i = 0;
char err[128] = {0};
EVP_PKEY *evpKey = EVP_PKEY_new();//EVP KEY结构体变量(私钥)
EVP_PKEY *evpKey_pub = EVP_PKEY_new();//EVP KEY结构体变量(公钥)
for(i=0; i<datalen; i++)
{
data[i] = i%100;
}
printf("data len [%d]\n", datalen);
my_printf("data:", data, datalen);
OpenSSL_add_all_algorithms();
/*===================================================================================================*/
//获取密钥(也可从文件中读取)
rsa_key = my_generate_rsa_key(key_bit, e_value);
if(NULL == rsa_key)
{
log_err("err: my_generate_rsa_key\n");
return WK_ERR;
}
rsa_pub = my_get_pub_form_pri(rsa_key);//提取公钥
if(NULL == rsa_pub)
{
log_err("err: my_get_pub_form_pri\n");
return WK_ERR;
}
//保存rsa密钥结构体,到EVP_PKEY 结构体中
ret = EVP_PKEY_set1_RSA(evpKey, rsa_key);
if(1 != ret)
{
log_err("err: EVP_PKEY_set1_RSA\n");
goto err_handle;
}
ret = EVP_PKEY_set1_RSA(evpKey_pub, rsa_pub);
if(1 != ret)
{
log_err("err: EVP_PKEY_set1_RSA\n");
goto err_handle;
}
/*==============================================================================================*/
//注意:对于RSA加解密,单包有长度限制,单包加密最大长度 = 私钥长度 - 填充长度
//比如:私钥的size = 2048,即256字节,选择RSA_PKCS1_PADDING填充模式,RSA_PKCS1_PADDING_SIZE = 11;于是单包加密最大长度 = 256 - 11 = 245
//若需对长报文进行非对称加解密,可以分包加密,然后依次解密后拼接
//加密
ret = my_RSA_en(evpKey_pub, data, datalen, cipher, &cipher_len);
if(WK_OK != ret)
{
log_err("err: my_RSA_PSS_sign\n");
goto err_handle;
}
printf("en result len [%d]\n", cipher_len);
my_printf("en result:", cipher, cipher_len);
//解密
ret = my_RSA_de(evpKey, cipher, cipher_len, plain, &plain_len);
if(WK_OK != ret)
{
log_err("err: my_RSA_PSS_ver\n");
goto err_handle;
}
printf("de result len [%d]\n", plain_len);
my_printf("de result:", plain, plain_len);
if(0 == memcmp(plain, data, datalen))
{
printf("succ: ========en and de========\n");
}
else
{
printf("err: ========en and de========\n");
}
EVP_PKEY_free(evpKey);
EVP_PKEY_free(evpKey_pub);
RSA_free(rsa_key);
RSA_free(rsa_pub);
return WK_OK;
err_handle:
ERR_error_string(ERR_get_error(), err);
log_err("err: [%s]\n", err);
if(NULL != evpKey)
{
EVP_PKEY_free(evpKey);
}
if(NULL != evpKey_pub)
{
EVP_PKEY_free(evpKey_pub);
}
if(NULL != rsa_key)
{
RSA_free(rsa_key);
}
if(NULL != rsa_pub)
{
RSA_free(rsa_pub);
}
return WK_ERR;
}
makefile
SRC := $(wildcard ./*.c)
#paramter
CC := gcc
target := app_openssl
#头文件和库路径(修改成安装openssl的路径)
DIR_LIB := -L /xxxxxxx/openssl/lib64
DIR_INCLUDE := -I /xxxxxx/openssl/include/
$(target):$(SRC)
$(CC) $(SRC) $(DIR_INCLUDE) $(DIR_LIB) -lssl -lcrypto -o $@
clean:
rm -rf $(target)
执行结果
每次的加密结果不一样。
参考链接:https://www.openssl.org/source/old/index.html