一. 生成密鑰對
在 OPENSSL 中, RSA
是一個很重要的結構體。它的定義在 rsa_locl.h
中,麪包含了在原理中提到的所有重要的變量 隨機質數 p
, q
, 公鑰指數 e
, 私鑰指數 d
, 以及模數 n
struct rsa_st {
// ...
BIGNUM *n;
BIGNUM *e;
BIGNUM *d;
BIGNUM *p;
BIGNUM *q;
// ...
};
生成密鑰函數:
int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
bits
密鑰的規模(modulus
)。小於 1028 位的密鑰是不安全的,小於 512 則會返回 0e
公開的指數。它應該是一個奇數(odd number), 一般是3, 17
或65537
cb
生成大隨機數的回調函數。一般使用 NULL 即可, 默認爲BN_GENCB_call()
生成公私鑰並寫入文件的代碼示例:
#include <openssl/rsa.h>
#include <openssl/evp.h>
#include <openssl/bn.h>
#include <openssl/pem.h>
#include <openssl/applink.c> //important!
void gen_rsa_key() {
RSA *rsa = RSA_new();
BIGNUM *e = BN_new();
BN_set_word(e, 17);
BN_GENCB* gencb = NULL;
EVP_PKEY* pkey = EVP_PKEY_new();
int rst = RSA_generate_key_ex(rsa, 3072, e, gencb);
rst = EVP_PKEY_set1_RSA(pkey, rsa);
FILE* f_pri = fopen("pri.key", "wb");
FILE* f_pub = fopen("pub.pem", "wb");
rst = PEM_write_RSAPublicKey(f_pub, rsa);
rst = PEM_write_RSAPrivateKey(f_pri, rsa, 0, 0, 0, 0, 0);
fclose(f_pri);
fclose(f_pub);
f_pri = NULL;
f_pub = NULL;
RSA_free(rsa);
BN_free(e);
EVP_PKEY_free(pkey);
BN_GENCB_free(gencb);
}
二. 公私鑰加解密
公私鑰加解密兩個步驟, 一是載入密鑰, 二是加解密。
載入密鑰使用 API :
PEM_read_RSAPublicKey()
PEM_read_RSAPrivateKey()
PEM_read_bio_RSAPrivateKey()
PEM_read_bio_RSAPublicKey()
當載入失敗時返回 NULL。
而加解密中兩個重要的 API 是:
int RSA_public_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
int RSA_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
flen
是輸入數據的長度,必須小於等於modulus = RSA_size(rsa)
個字節, 且和參數padding
相關。當 flen 不滿足要求時,將會出現"data too small for key size"
或"data too large for key size"
錯誤。 flen 與 padding 的對應關係如下:RSA_PKCS1_PADDING
, flen <= modulus – 11RSA_SSLV23_PADDING
, flen <= modulus – 11RSA_NO_PADDING
, flen = modulusRSA_PKCS1_OAEP_PADDING
, flen <= modulus – 41 (2 倍的 SHA1 長度 + 1)
padding
是填充模式,當 flen 滿足上述關係時,將會進行填充:RSA_PKCS1_PADDING
, 最常用的模式,使用 PKCS #1 v1.5 標準,前兩字節填充0x00, 0x02
,接着的modulus - flen - 3
字節使用隨機非 '\0'
值填充,接着填充一個字節的0x00
, 然後是from
數據RSA_SSLV23_PADDING
, 前兩個字節填充0x00,0x02
, 接着的modulus - flen - 3 - 8
字節填充隨機非 '\0'
值,然後填充 8 個0x03
, 接着填充一個字節的0x00
,然後是from
數據RSA_NO_PADDING
, 不需要填充RSA_PKCS1_OAEP_PADDING
, 前20字節使用 SHA1 填充, 接着填充至少 20 字節的0x00
, 最後填充0x01
,接着是from
數據
如圖所示:
公私鑰加解密示例:
#include <openssl/rsa.h>
#include <openssl/evp.h>
#include <openssl/bn.h>
#include <openssl/pem.h>
#include <openssl/applink.c> //important!
const char* pri_key_path = "pri.key";
const char* pub_key_path = "pub.pem";
const char *plain_text = "The EVP interface supports the ability to perform authenticated encryption and decryption, as well as the option to attach unencrypted,"
" associated data to the message. Such Authenticated-Encryption with Associated-Data (AEAD) schemes provide confidentiality by encrypting the data, and also"
" provide authenticity assurances by creating a MAC tag over the encrypted data. The MAC tag will ensure the data is not accidentally altered or maliciously"
" tampered during transmission and storage.";
bool pub_encrypt(const char* pub_key_path, const unsigned char* in, int in_len, unsigned char*& out, int& out_len) {
bool ret = false;
RSA* rsa = RSA_new();
FILE* f = fopen(pub_key_path, "r");
if (!f) goto ERR;
rsa = PEM_read_RSAPublicKey(f, &rsa, 0, 0);
if (!rsa) goto ERR;
fclose(f);
f = NULL;
out_len = RSA_size(rsa);
if (in_len > out_len) goto ERR;
out = (unsigned char*)malloc(out_len);
int padding = RSA_PKCS1_PADDING;
int rst = RSA_public_encrypt(in_len, in, out, rsa, padding);
if (rst <= 0) {
free(out);
out = NULL;
printf("%s\n", ERR_reason_error_string(ERR_get_error()));
goto ERR;
}
ret = true;
ERR:
if(rsa)
RSA_free(rsa);
return ret;
}
bool pri_decrypt(const char* pri_key_path, const unsigned char* in, int in_len, unsigned char*& out, int& out_len) {
bool ret = false;
RSA* rsa = RSA_new();
FILE* f = fopen(pri_key_path, "r");
if (!f) goto ERR;
rsa = PEM_read_RSAPrivateKey(f, &rsa, 0, 0);
if (!rsa) goto ERR;
fclose(f);
f = NULL;
out_len = RSA_size(rsa);
if (in_len > out_len) goto ERR;
out = (unsigned char*)malloc(out_len);
int padding = RSA_PKCS1_PADDING;
int rst = RSA_private_decrypt(in_len, in, out, rsa, padding);
if (rst < 0) {
free(out);
out = NULL;
printf("%s", ERR_reason_error_string(ERR_get_error()));
goto ERR;
}
out_len = rst;
ret = true;
ERR:
if (rsa)
RSA_free(rsa);
return ret;
}
int main() {
//init_openssl();
unsigned char* cipher_text = NULL, *re_plain_text = NULL;
int enc_out_len = 0, dec_out_len;
bool rst = pub_encrypt(pub_key_path, reinterpret_cast<const unsigned char*>(plain_text), strlen(plain_text), cipher_text, enc_out_len);
assert(rst);
rst = pri_decrypt(pri_key_path, cipher_text, enc_out_len, re_plain_text, dec_out_len);
assert(rst);
assert(strlen(plain_text) == dec_out_len);
int cmp = memcmp(plain_text, re_plain_text, dec_out_len);
assert(cmp == 0);
free(cipher_text);
free(re_plain_text);
//clean_openssl();
return 0;
}
三. Tips
1. 在 Windows 下運行出現崩潰 "no OPENSSL_Applink"
多半是因爲代碼運行庫不匹配導致的。在Windows 中,運行時庫有這幾種:
- Single Threaded
/ML
– MS VC++ often defaults to this for the release version of a new project. - Debug Single Threaded
/MLd
– MS VC++ often defaults to this for the debug version of a new project. - Multithreaded
/MT
- Debug Multithreaded
/MTd
- Multithreaded DLL
/MD
– OpenSSL defaults to this. - Debug Multithreaded DLL
/MDd
運行庫不一致可能導致程序崩潰(通常是在進行IO操作時)。這種情況下可以重新編譯 OPENSSL, 或者修改程序的運行庫。更簡單的辦法,可以嘗試添加一個文件引用:
#include <openssl/applink.c>