文章目录
填坑:VC++ 采用OpenSSL 3.0接口方式生成RSA密钥
上一篇博客VC++ 使用OpenSSL创建RSA密钥PEM文件埋了点雷,还是要填掉的,借助现在强大的AI工具,也帮了不少忙,于是把修改的内容记录下来。
一些变化
首先,OpenSSL 3.0引入了一些重要的API变更,主要是为了更好的模块化和安全性,比如:
- Provider (提供者) 机制:这是最大的变化之一。算法实现现在通过"提供者"加载,而不是全局加载。
RSA_generate_key_ex
的替代 :在某些情况下,EVP_PKEY_keygen_init
和EVP_PKEY_keygen
及其相关函数是更现代和推荐的密钥生成方式。虽然RSA_generate_key_ex
仍然可用,但使用EVP
接口更符合 OpenSSL 3.0 的设计哲学。- PEM 文件写入函数 :
PEM_write_bio_RSAPrivateKey
和PEM_write_bio_RSA_PUBKEY
仍然可用,但在EVP
框架下,更推荐使用PEM_write_bio_PKCS8PrivateKey
和PEM_write_bio_PUBKEY
。PKCS8
是更现代的私钥存储格式,可以包含更多元数据并支持多种加密算法。 - 初始化函数 :
OpenSSL_add_all_algorithms()
和ERR_load_crypto_strings()
等函数在 OpenSSL 3.0 中仍然存在,但更推荐使用OPENSSL_init_crypto()
来进行更精细的控制。 - 错误处理 :
ERR_print_errors_fp
依然有效。
为适配 OpenSSL 3.0,我主要使用了 EVP
接口进行密钥生成和 PKCS8
格式存储私钥:
cpp
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
// OpenSSL 头文件
#include <openssl/rsa.h> // 仍然需要,因为 RSA 结构可能在内部被使用
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/evp.h> // EVP 接口
// 辅助函数:显示OpenSSL错误
void printOpenSSLError(const std::string& msg) {
std::cerr << msg << std::endl;
ERR_print_errors_fp(stderr);
}
// 生成RSA密钥对并保存到PEM文件 (适配 OpenSSL 3.0)
bool generateRSAKeyPair(const std::string& privateKeyFile, const std::string& publicKeyFile, int bits = 2048) {
EVP_PKEY_CTX* pctx = NULL; // EVP_PKEY context for key generation
EVP_PKEY* pkey = NULL; // EVP_PKEY for the generated key pair
BIO* bp_public = NULL;
BIO* bp_private = NULL;
int ret = 0; // 返回值
// 写入加密的私钥,需要密码
const char* password = "mypassword"; // 替换为你的密码,或者在实际应用中动态获取
// 1. 创建 EVP_PKEY_CTX 用于 RSA 密钥生成
// 使用 EVP_PKEY_RSA 来指定生成 RSA 密钥
pctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL); // 或者 EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
if (!pctx) {
printOpenSSLError("Error creating EVP_PKEY_CTX");
goto err;
}
if (EVP_PKEY_keygen_init(pctx) <= 0) {
printOpenSSLError("Error initializing EVP_PKEY_keygen");
goto err;
}
// 设置密钥位数
if (EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, bits) <= 0) {
printOpenSSLError("Error setting RSA keygen bits");
goto err;
}
// 可以选择设置公钥指数,例如 RSA_F4 (65537)
// BIGNUM* e = BN_new();
// BN_set_word(e, RSA_F4);
// EVP_PKEY_CTX_set_rsa_keygen_pubexp(pctx, e);
// BN_free(e); // 如果设置了,记得释放
// 2. 生成 RSA 密钥对
if (EVP_PKEY_keygen(pctx, &pkey) <= 0) {
printOpenSSLError("Error generating RSA key pair");
goto err;
}
std::cout << "RSA key pair generated successfully (bits: " << bits << ")." << std::endl;
// 3. 保存私钥到 PEM 文件 (PKCS#8 格式,带加密)
bp_private = BIO_new_file(privateKeyFile.c_str(), "w+");
if (bp_private == NULL) {
printOpenSSLError("Error creating private key file BIO");
goto err;
}
// 使用 PEM_write_bio_PKCS8PrivateKey,它更通用且推荐
// EVP_des_ede3_cbc() 是一个旧的加密算法,OpenSSL 3.0 推荐使用更强的算法,例如 EVP_aes_256_cbc()
if (!PEM_write_bio_PKCS8PrivateKey(bp_private, pkey, EVP_aes_256_cbc(), (char*)password, strlen(password), NULL, NULL)) {
printOpenSSLError("Error writing PKCS#8 private key to file");
goto err;
}
std::cout << "Private key saved to " << privateKeyFile << " (PKCS#8 encrypted with AES-256-CBC)." << std::endl;
// 4. 保存公钥到 PEM 文件
bp_public = BIO_new_file(publicKeyFile.c_str(), "w+");
if (bp_public == NULL) {
printOpenSSLError("Error creating public key file BIO");
goto err;
}
// 使用 PEM_write_bio_PUBKEY
if (!PEM_write_bio_PUBKEY(bp_public, pkey)) {
printOpenSSLError("Error writing public key to file");
goto err;
}
std::cout << "Public key saved to " << publicKeyFile << std::endl;
ret = 1; // 成功
err:
// 清理资源
if (bp_private != NULL) BIO_free_all(bp_private);
if (bp_public != NULL) BIO_free_all(bp_public);
if (pctx != NULL) EVP_PKEY_CTX_free(pctx);
if (pkey != NULL) EVP_PKEY_free(pkey);
return (ret == 1);
}
int main() {
// 1. 初始化 OpenSSL 库
OpenSSL_add_all_algorithms();
// 加载错误字符串,以便 ERR_print_errors_fp 可以打印有意义的信息
ERR_load_crypto_strings();
std::string privateKeyPath = "private_key.pem";
std::string publicKeyPath = "public_key.pem";
if (generateRSAKeyPair(privateKeyPath, publicKeyPath)) {
std::cout << "\nRSA key pair generation and saving completed successfully." << std::endl;
}
else {
std::cerr << "\nFailed to generate RSA key pair." << std::endl;
}
// 2. 清理 OpenSSL 库
// 释放错误字符串
ERR_free_strings();
EVP_cleanup(); // 清理所有算法
return 0;
}
关键修改点和解释:
-
EVP_PKEY_CTX
和EVP_PKEY
:- 在 OpenSSL 3.0 中,
EVP
接口是推荐的通用加密操作接口。 EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL)
用于创建一个用于 RSA 操作的上下文。"RSA"
是算法名称。EVP_PKEY_keygen_init(pctx)
初始化密钥生成过程。EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, bits)
设置密钥的位数,与RSA_generate_key_ex
中的bits
参数功能相同。EVP_PKEY_keygen(pctx, &pkey)
执行密钥生成,并将结果存储在EVP_PKEY* pkey
中。这个pkey
对象包含了公钥和私钥信息。
- 在 OpenSSL 3.0 中,
-
私钥保存 (PKCS#8):
PEM_write_bio_PKCS8PrivateKey
用于将私钥以 PKCS#8 格式写入 PEM 文件。PKCS#8 是一个更现代、更灵活的私钥存储标准,它本身可以包含加密信息。- 我将加密算法从
EVP_des_ede3_cbc()
改为EVP_aes_256_cbc()
,因为DES-EDE3-CBC
(即 Triple DES) 在现代标准下已不推荐,AES-256-CBC
更安全。 - PKCS#8 格式的私钥通常以
-----BEGIN ENCRYPTED PRIVATE KEY-----
开头,而不是-----BEGIN RSA PRIVATE KEY-----
。
-
公钥保存:
PEM_write_bio_PUBKEY
用于将公钥写入 PEM 文件。它会写入标准的-----BEGIN PUBLIC KEY-----
格式,该格式是基于EVP_PKEY
结构的。
-
错误处理:
ERR_print_errors_fp
仍然是打印 OpenSSL 错误的标准方式。
编译和运行:
- 头文件和库文件 :确保你的编译器能够找到 OpenSSL 3.0 的头文件 (
-I
参数) 和库文件 (-L
参数,以及-l
参数链接libcrypto
和libssl
)。 - Windows 用户 :对于在 Windows 上使用
MSVC
编译,你可能需要包含openssl/applink.c
或者在链接时处理一些特定的库。我的代码中已经添加了#include <openssl/applink.c>
,这通常能解决控制台应用程序的一些问题。 - 链接库 :
- Linux/macOS:
g++ your_code.cpp -o your_app -I/path/to/openssl/include -L/path/to/openssl/lib -lcrypto
- Windows (MinGW/MSYS2):
g++ your_code.cpp -o your_app -I<openssl_dir>/include -L<openssl_dir>/lib -lssl -lcrypto -Wl,--start-group -lws2_32 -lgdi32 -lcrypt32 -Wl,--end-group
(具体依赖可能因你的 OpenSSL 安装方式而异) - Windows (MSVC): 需要在项目属性中配置包含目录、库目录,并链接
libcrypto.lib
和libssl.lib
(或者crypt32.lib
,ws2_32.lib
等)。
- Linux/macOS:
这个修改后的版本遵循 OpenSSL 3.0 的现代实践,提供了更好的模块化和对新算法的支持。
小结
至此,我们填掉了之前warning: 4996
的坑,从 EVP_des_ede3_cbc()
改为 EVP_aes_256_cbc()
也体现到了私钥PEM文件里: