OpenSSL 文件验签与字符串验签原理及 C 语言实现详解
摘要
本文深入探讨了基于 OpenSSL 的文件验签与字符串验签原理及 C 语言实现方法。先阐述数字签名验证原理,包括基于非对称加密算法和哈希算法的签名生成与验证流程,着重介绍 OpenSSL 中利用 EVP 高级接口实现数字签名验证的关键函数及其作用。随后分别提供文件验签和字符串验签的完整 C 语言代码示例,详细解析各步骤要点,如公钥加载、数据读取、验证上下文初始化、更新验证数据及完成验证等,并给出代码步骤详解。此外,还涵盖错误处理与调试技巧,通过 OpenSSL 错误报告机制排查验证失败原因,列举常见错误类型及处理方式。同时对比文件验签与字符串验签特性,从数据来源、加载方式、大数据处理、适用场景及性能角度分析差异。最后总结最佳实践,涵盖算法选择、密钥管理、错误处理、资源管理和性能优化等方面,助力开发者在 OpenSSL 中高效实现文件和字符串签名验证功能,保障数据完整性和真实性。
数字签名验证原理
签名生成原理
- 哈希计算 :对原始数据使用哈希算法(如 SHA - 256)生成固定长度的消息摘要。
- 私钥加密 :使用签名者的私钥对消息摘要进行加密,生成数字签名。
- 签名存储 :将数字签名与原始数据一起存储或传输。
签名验证原理
- 哈希计算 :对接收到的原始数据使用相同的哈希算法生成消息摘要。
- 公钥解密 :使用签名者的公钥对数字签名进行解密,得到原始消息摘要。
- 摘要比对 :比较计算得到的消息摘要和解密得到的消息摘要,如果一致则验证通过。
在 OpenSSL 中,借助 EVP(Envelope)高级接口实现上述流程,主要涉及以下函数:
- EVP_DigestVerifyInit() :初始化验证上下文。
- EVP_DigestVerifyUpdate() :输入待验证数据。
- EVP_DigestVerifyFinal() :完成验证并返回结果。
文件验签 C 语言实现
以下是使用 OpenSSL EVP 接口实现文件验签的完整 C 代码示例:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
// 验证文件签名
int verify_file_signature(const char* pubkey_file,
const char* data_file,
const char* sig_file) {
EVP_MD_CTX* mdctx = NULL;
EVP_PKEY* pubkey = NULL;
FILE* pubkey_fp = NULL;
FILE* data_fp = NULL;
FILE* sig_fp = NULL;
unsigned char* data = NULL;
unsigned char* sig = NULL;
size_t data_len = 0;
size_t sig_len = 0;
int result = -1;
// 1. 读取公钥文件
pubkey_fp = fopen(pubkey_file, "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;
}
// 2. 读取数据文件
data_fp = fopen(data_file, "rb");
if (!data_fp) {
fprintf(stderr, "Error opening data file\n");
goto cleanup;
}
fseek(data_fp, 0, SEEK_END);
data_len = ftell(data_fp);
fseek(data_fp, 0, SEEK_SET);
data = (unsigned char*)malloc(data_len);
if (!data) {
fprintf(stderr, "Memory allocation error\n");
goto cleanup;
}
if (fread(data, 1, data_len, data_fp) != data_len) {
fprintf(stderr, "Error reading data file\n");
goto cleanup;
}
// 3. 读取签名文件
sig_fp = fopen(sig_file, "rb");
if (!sig_fp) {
fprintf(stderr, "Error opening signature file\n");
goto cleanup;
}
fseek(sig_fp, 0, SEEK_END);
sig_len = ftell(sig_fp);
fseek(sig_fp, 0, SEEK_SET);
sig = (unsigned char*)malloc(sig_len);
if (!sig) {
fprintf(stderr, "Memory allocation error\n");
goto cleanup;
}
if (fread(sig, 1, sig_len, sig_fp) != sig_len) {
fprintf(stderr, "Error reading signature file\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, EVP_sha256(), NULL, pubkey) != 1) {
fprintf(stderr, "Error initializing verification\n");
goto cleanup;
}
// 5. 更新验证数据
if (EVP_DigestVerifyUpdate(mdctx, data, data_len) != 1) {
fprintf(stderr, "Error updating verification data\n");
goto cleanup;
}
// 6. 完成验证
result = EVP_DigestVerifyFinal(mdctx, sig, sig_len);
if (result == 1) {
printf("Signature verification successful\n");
} else if (result == 0) {
printf("Signature verification failed\n");
} else {
fprintf(stderr, "Error during verification\n");
result = -1;
}
cleanup:
// 7. 清理资源
if (mdctx) EVP_MD_CTX_free(mdctx);
if (pubkey) EVP_PKEY_free(pubkey);
if (pubkey_fp) fclose(pubkey_fp);
if (data_fp) fclose(data_fp);
if (sig_fp) fclose(sig_fp);
if (data) free(data);
if (sig) free(sig);
return result;
}
int main(int argc, char* argv[]) {
if (argc != 4) {
printf("Usage: %s <public_key.pem> <data_file> <signature_file>\n", argv[0]);
return 1;
}
return verify_file_signature(argv[1], argv[2], argv[3]) != 1;
}
代码步骤详解
- 加载公钥 :利用
PEM_read_PUBKEY
从 PEM 格式的公钥文件中读取公钥。 - 读取数据文件 :将待验证的文件内容完整读入内存缓冲区。
- 读取签名文件 :把签名数据读入内存缓冲区。
- 初始化验证上下文 :创建
EVP_MD_CTX
结构,并借助EVP_DigestVerifyInit
进行初始化。 - 更新验证数据 :调用
EVP_DigestVerifyUpdate
将文件数据输入验证过程。 - 完成验证 :通过
EVP_DigestVerifyFinal
进行最终验证。 - 资源清理 :释放所有分配的内存和文件资源。
字符串验签 C 语言实现
字符串验签与文件验签主要区别在于数据来源不同,以下是处理内存中字符串数据的验签实现:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
// 验证字符串签名
int verify_string_signature(const char* pubkey_file,
const char* message,
const unsigned char* sig,
size_t sig_len) {
EVP_MD_CTX* mdctx = NULL;
EVP_PKEY* pubkey = NULL;
FILE* pubkey_fp = NULL;
int result = -1;
// 1. 读取公钥文件
pubkey_fp = fopen(pubkey_file, "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;
}
// 2. 初始化验证上下文
mdctx = EVP_MD_CTX_new();
if (!mdctx) {
fprintf(stderr, "Error creating EVP_MD_CTX\n");
goto cleanup;
}
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubkey) != 1) {
fprintf(stderr, "Error initializing verification\n");
goto cleanup;
}
// 3. 更新验证数据(直接使用字符串)
if (EVP_DigestVerifyUpdate(mdctx, message, strlen(message)) != 1) {
fprintf(stderr, "Error updating verification data\n");
goto cleanup;
}
// 4. 完成验证
result = EVP_DigestVerifyFinal(mdctx, sig, sig_len);
if (result == 1) {
printf("Signature verification successful\n");
} else if (result == 0) {
printf("Signature verification failed\n");
} else {
fprintf(stderr, "Error during verification\n");
result = -1;
}
cleanup:
// 5. 清理资源
if (mdctx) EVP_MD_CTX_free(mdctx);
if (pubkey) EVP_PKEY_free(pubkey);
if (pubkey_fp) fclose(pubkey_fp);
return result;
}
// Base64解码函数
int base64_decode(const char* encoded, unsigned char** decoded, size_t* decoded_len) {
BIO* bio = NULL;
BIO* b64 = NULL;
int len = strlen(encoded);
*decoded = (unsigned char*)malloc(len);
if (!*decoded) {
return -1;
}
b64 = BIO_new(BIO_f_base64());
bio = BIO_new_mem_buf((void*)encoded, len);
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
*decoded_len = BIO_read(bio, *decoded, len);
BIO_free_all(bio);
return (*decoded_len > 0) ? 0 : -1;
}
int main() {
const char* pubkey_file = "public_key.pem";
const char* message = "This is a test message to verify";
const char* base64_sig = "MEUCIQD..."; // Base64编码的签名
unsigned char* sig = NULL;
size_t sig_len = 0;
// 解码Base64签名
if (base64_decode(base64_sig, &sig, &sig_len) != 0) {
fprintf(stderr, "Error decoding base64 signature\n");
return 1;
}
int ret = verify_string_signature(pubkey_file, message, sig, sig_len);
free(sig);
return ret != 1;
}
代码步骤详解
- 加载公钥 :与文件验签相同,运用
PEM_read_PUBKEY
加载公钥。 - 初始化验证上下文 :创建并初始化
EVP_MD_CTX
结构。 - 更新验证数据 :直接使用字符串数据调用
EVP_DigestVerifyUpdate
。 - Base64 解码 :提供辅助函数处理 Base64 编码的签名数据。
- 完成验证 :调用
EVP_DigestVerifyFinal
进行最终验证。
错误处理与调试
OpenSSL 提供详细的错误报告机制,可在验证失败时获取更多信息:
c
void print_openssl_error() {
unsigned long err;
const char* file = NULL;
const char* data = NULL;
int line, flags;
while ((err = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
fprintf(stderr, "OpenSSL error: %s\n", buf);
fprintf(stderr, " at %s:%d\n", file, line);
if (data && (flags & ERR_TXT_STRING)) {
fprintf(stderr, " data: %s\n", data);
}
}
}
// 在验证函数中使用示例
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubkey) != 1) {
fprintf(stderr, "Error initializing verification\n");
print_openssl_error();
goto cleanup;
}
常见错误处理
- 初始化失败 :检查公钥格式是否正确,算法是否支持。
- 更新数据失败 :检查数据缓冲区是否有效。
- 验证失败 :检查签名数据是否正确,公钥是否匹配私钥。
- 内存错误 :确保所有分配的资源都被正确释放。
文件验签与字符串验签对比
特性 | 文件验签 | 字符串验签 |
---|---|---|
数据来源 | 文件系统 | 内存中的字符串 |
数据加载 | 需要文件 I/O 操作 | 直接使用内存数据 |
大数据处理 | 可分块读取处理 | 通常一次性处理 |
适用场景 | 验证文件完整性 | 验证 API 消息、配置数据等 |
性能考虑 | 受磁盘 I/O 影响 | 纯内存操作,速度更快 |
总结与最佳实践
- 算法选择 :推荐使用 SHA - 256 或更强的哈希算法,避免 MD5 或 SHA1 等不安全算法。
- 密钥管理 :妥善保管私钥,公钥可通过证书链验证真实性。
- 错误处理 :始终检查 OpenSSL 函数返回值,实现全面错误处理。
- 资源管理 :确保分配资源正确释放,防止内存泄漏。
- 性能优化 :大文件可分块调用
EVP_DigestVerifyUpdate
,避免一次性加载。