OpenSSL 簽名驗證詳解:PKCS7* p7、cafile 與 RSA 驗簽實現
摘要
本文深入剖析 OpenSSL 中 PKCS7* p7 數據結構和 cafile 的作用及相互關系,詳細講解基于 OpenSSL 的 RSA 驗簽字符串的 C 語言實現,涵蓋簽名解析、證書加載、驗證流程及關鍵要點,助力開發者掌握數字簽名驗證技術,確保數據完整性和來源可靠性。
一、PKCS7* p7 與 cafile 的關鍵作用
(一)PKCS7* p7:簽名數據的核心載體
-
結構與內容
- PKCS#7(Public Key Cryptography Standards #7)標準涵蓋數字簽名、證書、數據加密及消息認證。
PKCS7* p7
是處理 PKCS#7 格式數字簽名的關鍵數據結構,通過d2i_PKCS7_bio()
函數將 DER 編碼的 PKCS#7 數據解析為該結構體。 p7
包含豐富的簽名信息,如簽名者信息、簽名算法、簽名數據及證書鏈等。在驗證簽名時,PKCS7_verify()
函數利用這些信息驗證簽名有效性、檢查證書鏈完整性,確保數據未被篡改。
- PKCS#7(Public Key Cryptography Standards #7)標準涵蓋數字簽名、證書、數據加密及消息認證。
-
缺失影響
PKCS7* p7 = d2i_PKCS7_bio(signp7_mem, 0); if (p7 == NULL) {// 無法獲取簽名信息,驗證過程無法進行 }
- 若缺失
p7
,將無法讀取簽名數據,整個驗證過程直接失敗,相當于沒有驗證對象。
- 若缺失
-
釋放內存
- 使用完畢后,需調用
PKCS7_free(p7);
釋放結構體,避免內存泄漏。
- 使用完畢后,需調用
(二)cafile:信任鏈的根基
-
作用與使用
cafile
是包含根證書或中間證書的 CA(Certificate Authority,證書頒發機構)證書文件,用于建立信任鏈,驗證簽名者證書合法性,是數字簽名驗證的信任錨點。- 在代碼中,通過
X509_STORE_load_locations(store, cafile, NULL)
將 CA 證書加載到證書庫。PKCS7_verify()
函數利用加載的 CA 證書驗證簽名者證書,檢查證書鏈完整性,確認簽名者證書由可信 CA 簽發。
-
缺失影響
if (!X509_STORE_load_locations(store, cafile, NULL)) {LOG_ERR("Load CA file %s fail\n", cafile);// 無法建立信任鏈,驗證過程無法完成return CRYPTO_FAIL; }
- 若
cafile
缺失或無效,無法驗證簽名者證書真實性,無法確認簽名者身份,相當于有簽名但無法確認其真實性,驗證過程無法完成。
- 若
-
兩者關系
p7
是驗證對象(要驗證什么),cafile
是驗證依據(如何驗證),二者缺一不可,只有結合才能完成完整的簽名驗證過程。
二、OpenSSL RSA 驗簽字符串的 C 語言實現
(一)完整代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>// 錯誤處理函數
void handle_openssl_error() {ERR_print_errors_fp(stderr);exit(EXIT_FAILURE);
}// Base64解碼函數
int base64_decode(const char *base64_data, unsigned char **decoded_data, size_t *decoded_len) {BIO *bio, *b64;int len = strlen(base64_data);b64 = BIO_new(BIO_f_base64());BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);bio = BIO_new_mem_buf((void*)base64_data, len);bio = BIO_push(b64, bio);*decoded_data = (unsigned char*)malloc(len);if (!*decoded_data) {BIO_free_all(bio);return -1;}*decoded_len = BIO_read(bio, *decoded_data, len);BIO_free_all(bio);return (*decoded_len > 0) ? 0 : -1;
}// RSA驗簽函數
int rsa_verify_string(const char *pubkey_path, const char *message,const char *base64_signature,const char *hash_alg) {EVP_MD_CTX *mdctx = NULL;EVP_PKEY *pubkey = NULL;FILE *pubkey_fp = NULL;unsigned char *signature = NULL;size_t sig_len = 0;int ret = -1;const EVP_MD *md = NULL;// 1. 根據算法名稱獲取哈希算法if (strcmp(hash_alg, "sha1") == 0) {md = EVP_sha1();} else if (strcmp(hash_alg, "sha256") == 0) {md = EVP_sha256();} else {fprintf(stderr, "Unsupported hash algorithm: %s\n", hash_alg);goto cleanup;}// 2. 加載公鑰pubkey_fp = fopen(pubkey_path, "r");if (!pubkey_fp) {fprintf(stderr, "Error opening public key file\n");goto cleanup;}pubkey = PEM_read_PUBKEY(pubkey_fp, NULL, NULL, NULL);if (!pubkey) {fprintf(stderr, "Error reading public key\n");goto cleanup;}// 3. Base64解碼簽名if (base64_decode(base64_signature, &signature, &sig_len) != 0) {fprintf(stderr, "Error decoding base64 signature\n");goto cleanup;}// 4. 初始化驗簽上下文mdctx = EVP_MD_CTX_new();if (!mdctx) {fprintf(stderr, "Error creating EVP_MD_CTX\n");goto cleanup;}if (EVP_DigestVerifyInit(mdctx, NULL, md, NULL, pubkey) != 1) {fprintf(stderr, "Error initializing verification\n");goto cleanup;}// 5. 更新驗簽數據if (EVP_DigestVerifyUpdate(mdctx, message, strlen(message)) != 1) {fprintf(stderr, "Error updating verification data\n");goto cleanup;}// 6. 完成驗簽ret = EVP_DigestVerifyFinal(mdctx, signature, sig_len);if (ret == 1) {printf("Signature verification successful (%s)\n", hash_alg);} else if (ret == 0) {printf("Signature verification failed (%s)\n", hash_alg);} else {fprintf(stderr, "Error during verification\n");ret = -1;}cleanup:// 7. 清理資源if (mdctx) EVP_MD_CTX_free(mdctx);if (pubkey) EVP_PKEY_free(pubkey);if (pubkey_fp) fclose(pubkey_fp);if (signature) free(signature);return ret;
}int main(int argc, char *argv[]) {if (argc != 5) {printf("Usage: %s <public_key.pem> <message> <base64_signature> <sha1|sha256>\n", argv[0]);return 1;}// 初始化OpenSSLOpenSSL_add_all_algorithms();ERR_load_crypto_strings();int result = rsa_verify_string(argv[1], argv[2], argv[3], argv[4]);// 清理OpenSSLEVP_cleanup();ERR_free_strings();return (result != 1);
}
(二)代碼詳解
-
哈希算法選擇
if (strcmp(hash_alg, "sha1") == 0) {md = EVP_sha1(); } else if (strcmp(hash_alg, "sha256") == 0) {md = EVP_sha256(); }
- 根據傳入的哈希算法名稱(“sha1” 或 “sha256”),獲取對應的哈希算法結構體指針。也可使用
EVP_get_digestbyname("sha256")
動態獲取算法。
- 根據傳入的哈希算法名稱(“sha1” 或 “sha256”),獲取對應的哈希算法結構體指針。也可使用
-
公鑰加載
pubkey = PEM_read_PUBKEY(pubkey_fp, NULL, NULL, NULL);
- 從 PEM 格式的公鑰文件中讀取公鑰,支持 RSA、DSA、ECDSA 等多種公鑰類型。
-
Base64 解碼
BIO *b64 = BIO_new(BIO_f_base64()); BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
- 利用 OpenSSL 的 BIO 接口進行 Base64 解碼,
BIO_FLAGS_BASE64_NO_NL
標志表示不處理換行符。
- 利用 OpenSSL 的 BIO 接口進行 Base64 解碼,
-
驗簽流程
EVP_DigestVerifyInit(mdctx, NULL, md, NULL, pubkey); EVP_DigestVerifyUpdate(mdctx, message, strlen(message)); EVP_DigestVerifyFinal(mdctx, signature, sig_len);
EVP_DigestVerifyInit
:初始化驗簽上下文,指定哈希算法和公鑰。EVP_DigestVerifyUpdate
:輸入待驗證的數據,可多次調用以處理大消息。EVP_DigestVerifyFinal
:完成驗簽并返回結果。
-
錯誤處理
ERR_print_errors_fp(stderr);
- OpenSSL 錯誤處理機制可輸出詳細錯誤信息,每個 OpenSSL 函數調用后都應檢查返回值。
(三)使用示例
-
編譯命令
gcc rsa_verify.c -o rsa_verify -lssl -lcrypto
-
運行示例
- 使用 SHA256 驗證:
./rsa_verify public_key.pem "message to verify" "base64_signature" sha256
- 使用 SHA1 驗證:
./rsa_verify public_key.pem "message to verify" "base64_signature" sha1
- 使用 SHA256 驗證:
(四)關鍵點說明
-
哈希算法選擇
- SHA1 產生 160 位(20 字節)摘要,SHA256 產生 256 位(32 字節)摘要。現代應用推薦使用 SHA256,SHA1 已逐漸被淘汰。
-
簽名格式
- RSA 簽名通常是 PKCS#1 v1.5 格式,簽名長度等于 RSA 密鑰長度(如 2048 位 = 256 字節)。
-
性能考慮
- 對于大消息,可分塊調用
EVP_DigestVerifyUpdate
。SHA256 計算比 SHA1 稍慢但更安全。
- 對于大消息,可分塊調用
-
資源管理
- 必須正確釋放所有 OpenSSL 對象,使用
goto cleanup
模式集中處理資源釋放。
- 必須正確釋放所有 OpenSSL 對象,使用
-
Base64 處理
- 簽名通常以 Base64 編碼傳輸,驗簽前需要解碼為二進制格式。
通過上述內容,開發者可以全面了解 OpenSSL 中 PKCS7* p7 和 cafile 的作用、關系以及 RSA 驗簽的 C 語言實現細節,從而在實際項目中靈活應用數字簽名驗證技術,保障數據的安全性和完整性。