OpenSSL RSA相關基本介面和程式設計示例

AndGate發表於2024-07-16

本文測試程式碼基於Openssl版本:1.1.1f

RSA介面

介面簡介

  1. RSA物件建立
int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);

功能:建立⼀對rsa的公鑰私鑰
引數:RSA金鑰指標,金鑰bit位數,公鑰指數的⼤數形式指標,回撥函式
返回:成功返回1,失敗返回0
e主要有兩個取值:第二個更常用
# define RSA_3 0x3L
# define RSA_F4 0x10001L
注意1:舊介面RSA_generate_key已經被廢棄
注意2:回撥函式可為null,在key的生成過程中會生成素數,cb會在生成素數之後對其進行處理
  1. 加密解密接⼝
  • 公鑰加密--私鑰解密
int RSA_public_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);

功能:公鑰加密,將⻓度為flen的from字串加密,使用to指標返回密文,返回to的⻓度等於RSA_size(rsa)
引數:明⽂⻓度(flen需要滿⾜padding的限制規則),明⽂,密⽂,金鑰,padding填充模式
padding填充模式有:
    RSA_PKCS1_PADDING: flen <= RSA_size(rsa) - 11
    RSA_PKCS1_OAEP_PADDING: flen < RSA_size(rsa) - 42
    RSA_NO_PADDING: flen == RSA_size(rsa)
    RSA_SSLV23_PADDING
返回:成功返回密⽂⻓度,失敗返回-1


int RSA_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);

功能:私鑰解密,將⻓度為flen的from密文解密,使用to指標返回明文
引數:密⽂⻓度,密⽂,明⽂, 金鑰,padding填充模式
padding填充模式:
RSA_PKCS1_PADDING、RSA_PKCS1_OAEP_PADDING、RSA_SSLV23_PADDING、RSA_NO_PADDING
返回:成功返回明⽂⻓度,失敗返回-1
  • 私鑰加密--公鑰解密
int RSA_private_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);

功能:私鑰加密,將⻓度為flen的from字串加密,使用to指標返回密文,返回to的⻓度等於RSA_size(rsa)
引數:明⽂⻓度,明⽂,密⽂,金鑰,padding填充模式
padding填充模式:
RSA_PKCS1_PADDING、RSA_PKCS1_OAEP_PADDING、RSA_SSLV23_PADDING、RSA_NO_PADDING
返回:成功返回明⽂⻓度,失敗返回-1


int RSA_public_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);

功能:公鑰解密,將⻓度為flen的from密文解密,使用to指標返回明文
引數:明⽂⻓度,密⽂,明⽂輸出,金鑰,padding填充模式
padding填充模式:
RSA_PKCS1_PADDING、RSA_PKCS1_OAEP_PADDING、RSA_SSLV23_PADDING、RSA_NO_PADDING
返回:成功返回明⽂⻓度,失敗返回-1
  1. 簽名驗籤接⼝
int RSA_sign(int type, const unsigned char *m, unsigned int m_len, unsigned char *sigret, unsigned int *siglen, RSA *rsa);

功能:RSA簽名,輸⼊摘要資料,返回簽名sigret
引數:type表⽰⽣成m使⽤的摘要演算法型別,m表⽰摘要(hash)資料,m_len表示摘要資料⻓度 sigret是返回簽名的指標(指向的記憶體長度需要等於RSA_size(rsa)),siglen是簽名⻓度,rsa是簽名者的公鑰
type型別:NID_md5、NID_sha、NID_sha1、NID_md5_sha1
注意1:摘要m需要和type型別保持一致,介面內部會針對不同的hash長度做padding
注意2:RSA型別本身可同時儲存私鑰和公鑰資訊,根據實際介面使用不同的能力
返回:成功返回1


int RSA_verify(int type, const unsigned char *m, unsigned int m_len, unsigned char *sigret, unsigned int siglen, RSA *rsa);

功能:RSA驗證簽名,輸⼊摘要m和簽名sigret,返回驗籤是否透過,即m和解密的sigret是否匹配
引數:type表⽰⽣成m使⽤的摘要演算法型別,m表⽰摘要資料,m_len為摘要⻓度,sigret簽名
siglen為簽名長度,rsa是簽名者的公鑰
type:NID_md5、NID_sha、NID_sha1、NID_md5_sha1
返回:成功返回1
 
  1. RSA pem⽂件相關
RSA *PEM_read_RSAPublicKey(FILE *fp, RSA **x, pem_password_cb *cb, void *u);
RSA *PEM_read_RSAPrivateKey(FILE *fp, RSA **x, pem_password_cb *cb, void *u);

功能:從pem檔案中讀取公鑰私鑰
引數:fp為⽂件,x是輸出的RSA型別指標,cb⽤於對加密的pem解密,u是cb函式的引數
注意:當x不為null時,讀取的金鑰將輸出到RSA型別金鑰x中;當設定x為null,介面將在返回值中return rsa指標
返回:nullptr 為讀取失敗


int PEM_write_RSAPublicKey(FILE *fp, RSA *x);
int PEM_write_RSAPrivateKey(FILE *fp, RSA *x, const EVP_CIPHER *enc, unsigned
char *kstr, int klen, pem_password_cb *cb, void *u);
引數:fp輸⼊⽂件,x為待寫入的金鑰,enc為指定要使用的加密演算法使得私鑰檔案不為明文儲存,後續引數均可以設定為null
功能:寫⼊公鑰私鑰
返回:1 success,0 failed

程式設計示例

#include <iostream>
#include <string>
#include <cstring>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/err.h>
#include <openssl/pem.h> //NID_md5_sha1
#include <openssl/md5.h>

using namespace std;

void dump_hex(const uint8_t *hex, uint32_t size) {
  uint32_t i = 0;
  for (i = 0; i < size; ++i) {
    if ((i % 8) == 0) {
      printf("\n");
    }
    printf("0x%02x ", hex[i]);
  }
  printf("\n");
}

RSA* rsa_create(){
    RSA* rsa = RSA_new();//分配空間
    BIGNUM* pBNe = BN_new();//分配空間
    BN_set_word(pBNe, RSA_F4);
    int ret = RSA_generate_key_ex(rsa, 1024, pBNe, NULL);
    if(ret < 0 ){
        ERR_print_errors_fp(stderr);
        return nullptr;
    }
    
    BN_free(pBNe);

    return rsa;
}

void PubEnc_PriDec(RSA* rsa, unsigned char* plaintext, int plaintext_len){
    //公鑰加密
    int ciphertext_len = RSA_size(rsa);
    std::cout << "ciphertext_len: " << ciphertext_len << std::endl;
    unsigned char ciphertext[ciphertext_len]{}; //加密後密文長度需要等於RSA_size(rsa)
    int ret = RSA_public_encrypt(plaintext_len,plaintext,ciphertext,rsa,RSA_PKCS1_PADDING);
    if(ret < 0){
        ERR_print_errors_fp(stderr);
        return;
    }
    std::cout << "ciphertext: " << std::endl;
    dump_hex(ciphertext, ciphertext_len);

    //私鑰解密
    unsigned char plaintext_decrypt[plaintext_len]{};
    int plaintext_decrypt_len{};
    ret = RSA_private_decrypt(ciphertext_len,ciphertext,plaintext_decrypt,rsa,RSA_PKCS1_PADDING);
    if(ret < 0 ){
        ERR_print_errors_fp(stderr);
        return;
    }
    std::cout << "decrypt plaintext: " << std::endl;
    std::cout << plaintext_decrypt << std::endl;
}

void PriEnc_PubDec(RSA* rsa, unsigned char* plaintext, int plaintext_len){
    //私鑰加密
    int ciphertext_len = RSA_size(rsa);
    unsigned char ciphertext[ciphertext_len]{}; //加密後密文長度需要等於RSA_size(rsa)
    int ret = RSA_private_encrypt(plaintext_len,plaintext,ciphertext,rsa,RSA_PKCS1_PADDING);
    if(ret < 0 ){
        ERR_print_errors_fp(stderr);
    }
    std::cout << "ciphertext: " << std::endl;
    dump_hex(ciphertext, ciphertext_len);

    //公鑰解密
    unsigned char plaintext_decrypt[plaintext_len]{};
    int plaintext_decrypt_len{};
    ret = RSA_public_decrypt(ciphertext_len,ciphertext,plaintext_decrypt,rsa,RSA_PKCS1_PADDING);
    if(ret < 0 ){
        ERR_print_errors_fp(stderr);
    }
    std::cout << "decrypt plaintext: " << std::endl;
    std::cout << plaintext_decrypt << std::endl;
}

void Sign_Verify(RSA* rsa, unsigned char* plaintext, unsigned int plaintext_len){
    unsigned int sign_text_len{RSA_size(rsa)};
    unsigned char sign_text[sign_text_len];

    unsigned char md5[16];
    MD5(plaintext,plaintext_len,md5);

    int ret = RSA_sign(NID_md5,md5,16,sign_text,&sign_text_len,rsa);
    if(ret != 1 ){
        ERR_print_errors_fp(stderr);
    }
    std::cout << "sign_text_len: " <<sign_text_len<< std::endl;
    dump_hex(sign_text, sign_text_len);

    ret = RSA_verify(NID_md5,md5,16,sign_text,sign_text_len,rsa);
    if(ret != 1 ){
        ERR_print_errors_fp(stderr);
    }
    std::cout << "verify result: " << ret << std::endl;
}

RSA* Read_Key(){
    FILE *fp_pub;
	if ((fp_pub = fopen("/your_path/rsa_public_key.pem", "r")) == NULL) {
        return nullptr;
    }

    FILE *fp_pri;
    if ((fp_pri = fopen("/your_path/rsa_private_key.pem", "r")) == NULL) {
        return nullptr;
    }

    RSA* rsa{PEM_read_RSAPublicKey(fp_pub, nullptr, nullptr,nullptr)};
    PEM_read_RSAPrivateKey(fp_pri,&rsa,nullptr,nullptr);
    fclose(fp_pub);
    fclose(fp_pri);
    return rsa;
}

void Write_Key(RSA* rsa){
    FILE *fp_pub;
	if ((fp_pub = fopen("/your_path/rsa_public_key1.pem", "wt")) == NULL) {
        return;
    }
    int ret{PEM_write_RSAPublicKey(fp_pub,rsa)};
    std::cout << "PEM_write_RSAPublicKey: " << ret << std::endl;
    fclose(fp_pub);
    
    FILE *fp_pri;
	if ((fp_pri = fopen("/your_path/rsa_private_key1.pem", "wt")) == NULL) {
        return;
    }
    ret = PEM_write_RSAPrivateKey(fp_pri,rsa,nullptr,nullptr,0,nullptr,nullptr);
    std::cout << "PEM_write_RSAPrivateKey: " << ret << std::endl;
    fclose(fp_pri);
}

int main(){
    // RSA* rsa = rsa_create();
    RSA* rsa = Read_Key();

    int plaintext_len = 100;
    // RSA要求明文長度 < 金鑰長度,假如明文過長,則需要分段加密解密
    // 根據填充方式不同,明文長度小於等於RSA_size(rsa)-x
    unsigned char plaintext[plaintext_len]{"123"};
    std::cout << "plaintext: " << std::endl;
    std::cout << plaintext << std::endl;

    PubEnc_PriDec(rsa,plaintext,plaintext_len);
    PriEnc_PubDec(rsa,plaintext,plaintext_len);

    Sign_Verify(rsa,plaintext,plaintext_len);

    Write_Key(rsa);

    RSA_free(rsa);

    return 0;
}


//編譯 g++ rsa.cpp -lcrypto

RSA EVP介面

EVP:Openssl實現了密碼學中非常多種演算法實現,例如上一節中的RSA介面就是一種非對稱演算法的介面,此類介面和演算法較近,需要使用者瞭解演算法引數意義,EVP則是將此類的底層演算法進行了封裝,遮蔽演算法細節,易於使用者直接使用
下面介紹EVP--RSA的基本介面使用和程式設計示例

介面簡介

  1. 初始化相關接⼝
EVP_PKEY *EVP_PKEY_new(void);

功能:返回⼀個EVP_PKEY* 物件


EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);

功能:建立⼀個EVP_PKEY_CTX 上下⽂
引數:EVP_PKEY 該結構⽤來存放⾮對稱金鑰資訊,可以是 RSA、DSA、DH 或 ECC 金鑰, 也可以指定使⽤engine的演算法,不需要載入engine可設定e為null
返回:nullptr則為失敗


EVP_PKEY_CTX *EVP_PKEY_CTX_new_id(int id, ENGINE *e);

功能:建立⼀個EVP_PKEY_CTX 上下⽂
引數:id:指定的演算法型別,也可以指定使⽤engine的演算法,不需要載入engine可設定e為null
返回:nullptr則為失敗


int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *ctx);

功能:利⽤ctx上下文初始化加密環境
返回:1為成功
  1. RSA相關接⼝
int EVP_PKEY_assign_RSA(EVP_PKEY *pkey, RSA *key);
RSA *EVP_PKEY_get1_RSA(EVP_PKEY *pkey);

功能:金鑰型別相互轉換
返回:1為success,非null為success


int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);

功能:設定ctx的rsa加密的padding模式
padding模式有:
RSA_PKCS1_PADDING、RSA_SSLV23_PADDING、RSA_NO_PADDING、RSA_PKCS1_OAEP_PADDING、RSA_X931_PADDING、RSA_PKCS1_PSS_PADDING
返回:1為成功


int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen, const unsigned char *in, size_t inlen);

功能:根據ctx的資訊進⾏加密
引數:上下文ctx,out是輸出的密⽂,outlen密⽂⻓度,in輸⼊明⽂,inlen明⽂⻓度
注意1:out是nullptr時,調⽤此接⼝會將密⽂⻓度透過outlen返回;out不為nullptr時,調⽤接⼝
會將密⽂返回到out中,同時密⽂⻓度等於outlen
注意2:這個接⼝可以調⽤兩次,第⼀次獲取密⽂⻓度,為返回陣列out分配記憶體後,第⼆次呼叫該介面再獲取密⽂
返回:1為成功

程式設計示例

#include <openssl/evp.h>
#include <openssl/engine.h>
#include <iostream>

void dump_hex(const uint8_t *hex, uint32_t size) {
  uint32_t i = 0;
  for (i = 0; i < size; ++i) {
    if ((i % 8) == 0) {
      printf("\n");
    }
    printf("0x%02x ", hex[i]);
  }
  printf("\n");
}

int rsa_pub_enc(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding){
  int ciphertext_len = RSA_size(rsa);
  for(int i{0};i<ciphertext_len;++i){
    to[i] = 'x';
  }
  std::cout << "rsa_pub_enc call " << std::endl;
  return ciphertext_len;
}


RSA* rsa_create(){
    RSA* rsa = RSA_new();//分配空間
    BIGNUM* pBNe = BN_new();//分配空間
    BN_set_word(pBNe, RSA_F4);
    int ret = RSA_generate_key_ex(rsa, 1024, pBNe, NULL);
    if(ret < 0 ){
        printf("encrypt failed, ret:%d \n", ret);
        return nullptr;
    }
    BN_free(pBNe);
    return rsa;
}


RSA* Read_Key(){
  FILE *fp_pub;
	if ((fp_pub = fopen("/your_path/rsa_public_key.pem", "r")) == NULL) {
        return nullptr;
    }

    FILE *fp_pri;
    if ((fp_pri = fopen("/your_path/rsa_private_key.pem", "r")) == NULL) {
        return nullptr;
    }

    RSA* rsa{PEM_read_RSAPublicKey(fp_pub, nullptr, nullptr,nullptr)};
    PEM_read_RSAPrivateKey(fp_pri,&rsa,nullptr,nullptr);
    fclose(fp_pub);
    fclose(fp_pri);
    return rsa;
}

int main(){
  //RSA* rsa = rsa_create();
  RSA* rsa = Read_Key();
  if(rsa == nullptr){
    std::cout << "Read_Key" << std::endl;
  }

  EVP_PKEY* pkey = EVP_PKEY_new();
  
  int ret;
  ret = EVP_PKEY_assign_RSA(pkey, rsa);
  if(ret == 0){
    std::cout << "EVP_PKEY_assign_RSA fail" << std::endl;
    ERR_print_errors_fp(stderr);
  }

  EVP_PKEY_CTX* ctx;
  ctx = EVP_PKEY_CTX_new(pkey, nullptr);
  if (ctx == nullptr) {
    std::cout << "EVP_PKEY_CTX_new fail" << std::endl;
    ERR_print_errors_fp(stderr);
  }

  ret = EVP_PKEY_encrypt_init(ctx);
  if (ret == 0) {
    std::cout << "EVP_PKEY_encrypt_init fail" << std::endl;
    ERR_print_errors_fp(stderr);
  }

  ret = EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING);
  if (ret == 0) {
    std::cout << "EVP_PKEY_CTX_set_rsa_padding fail" << std::endl;
    ERR_print_errors_fp(stderr);
  }

  int plaintext_len = 100;
  unsigned char plaintext[plaintext_len]{"123"};
  std::cout << "plaintext: " << std::endl;
  std::cout << plaintext << std::endl;

  size_t ciphertext_len;
  ret = EVP_PKEY_encrypt(ctx, nullptr, &ciphertext_len, plaintext, plaintext_len);//先獲取ciphertext_len
  if (ret == 0) {
    std::cout << "EVP_PKEY_encrypt fail" << std::endl;
  }

  std::cout << "ciphertext_len: " << ciphertext_len << std::endl;

  unsigned char ciphertext[ciphertext_len]{};//構造輸出陣列
  ret = EVP_PKEY_encrypt(ctx, ciphertext, &ciphertext_len, plaintext, plaintext_len);
  if (ret == 0) {
    std::cout << "EVP_PKEY_encrypt fail" << std::endl;
  }

  dump_hex(ciphertext, ciphertext_len);

  return 0;
}

總結

本文總結了RSA的基礎介面和EVP RSA介面的引數和程式設計示例
計劃下一篇對OpenSSL engine的幾種載入方式進行總結

參考官方文件:
https://www.openssl.org/docs/man1.1.1/man3/
https://openssl-programing.readthedocs.io/en/latest/index.html

相關文章