OpenSSL3.5.2实现SM3数据摘要生成
OpenSSL库中使用EVP(Enhanced Verification Package增强验证包)进行SM3相关的摘要生成。
其逻辑为:可分为 初始化上下文→配置算法→分块处理数据→生成最终哈希→释放资源 五个步骤。
完整代码如下:
#include <QCoreApplication>
#include <iostream>
#include <openssl/opensslv.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/err.h>
using namespace std;// 计算 SM3 哈希值
int sm3_hash(const unsigned char *data, size_t data_len, unsigned char *hash, unsigned int *hash_len) {// 创建 EVP 上下文EVP_MD_CTX *ctx = EVP_MD_CTX_new();if (ctx == NULL) {fprintf(stderr, "无法创建 EVP 上下文\n");return 0;}// 初始化 SM3 哈希计算if (EVP_DigestInit_ex(ctx, EVP_sm3(), NULL) != 1) {fprintf(stderr, "SM3 初始化失败\n");ERR_print_errors_fp(stderr);EVP_MD_CTX_free(ctx);return 0;}// 更新哈希计算(可以分多次调用更新不同的数据块)if (EVP_DigestUpdate(ctx, data, data_len) != 1) {fprintf(stderr, "SM3 数据更新失败\n");ERR_print_errors_fp(stderr);EVP_MD_CTX_free(ctx);return 0;}// 完成哈希计算并获取结果if (EVP_DigestFinal_ex(ctx, hash, hash_len) != 1) {fprintf(stderr, "SM3 计算完成失败\n");ERR_print_errors_fp(stderr);EVP_MD_CTX_free(ctx);return 0;}// 释放上下文EVP_MD_CTX_free(ctx);return 1;
}int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);// 待计算哈希的数据const char *input = "Hello, SM3!";unsigned char hash[EVP_MAX_MD_SIZE];unsigned int hash_len;// 初始化 OpenSSL 错误信息ERR_load_crypto_strings();// 计算 SM3 哈希if (sm3_hash((const unsigned char *)input, strlen(input), hash, &hash_len)) {printf("input data: %s\n", input);printf("SM3 hash: ");for (unsigned int i = 0; i < hash_len; i++) {printf("%02x", hash[i]);}printf("\nhash length: %u byte\n", hash_len);}// 清理错误信息ERR_free_strings();return a.exec();
}
这里有一个需要注意的地方:
EVP_DigestUpdate 支持分多次调用,核心原因是为了处理大数据或流式数据:
- 对于大文件(如几个 GB 的视频)或网络流(如实时接收的数据),无法一次性将所有数据加载到内存中(会导致内存溢出)。
- 分多次调用时,每次只需传入一部分数据(例如每次 4KB),上下文 ctx 会保存中间状态,最终合并计算结果,与一次性处理的结果完全一致。
- 这种设计既节省内存,又支持流式处理(边接收数据边计算哈希),是处理大数据场景的必需特性。
那么问题来了
为什么需要分块更新哈希计算?
分块调用 EVP_DigestUpdate 与 “哈希长度是否固定” 无关,核心原因是哈希算法的底层设计原理和实际工程需求:
1. 哈希算法的 “迭代式计算” 本质
SM3(以及几乎所有现代哈希算法,如 SHA256、MD5 等)的计算过程是迭代式的,而非 “一次性计算”。其底层逻辑是:
- 将输入数据按固定大小(SM3 中是 512 位,即 64 字节)分成若干块(最后一块不足时会进行补位处理)。
- 从初始状态开始,每处理一块数据,就用该块数据更新内部状态(一个 256 位的中间变量)。
- 所有块处理完成后,将最终的内部状态转换为 32 字节的哈希值。
例如:
- 输入 “abc” 时,数据量小于 512 位,补位后形成一个块,一次处理完成。
- 输入 1GB 文件时,数据会被分成约 200 万个 512 位块,必须分块处理,每块更新一次内部状态,最终合并为同一个 32 字节哈希值。
EVP_DigestUpdate 的作用就是向算法传递这些分块数据,让算法逐步更新内部状态,它是哈希算法迭代特性的直接体现。
2. 工程上的 “内存限制” 需求
即使哈希算法支持一次性处理数据,实际工程中也必须分块:
- 对于大文件(如 10GB 的视频),无法将全部数据一次性加载到内存(会导致内存溢出)。
- 分块处理时,只需在内存中保留一个小块缓冲区(如 4KB),每次读取一块数据并传递给 EVP_DigestUpdate,内存占用始终保持在极低水平。
这种方式既符合哈希算法的迭代逻辑,又能高效处理任意大小的输入(从几字节到几十 GB)。
分块计算的例子。
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/err.h>// 计算文件的 SM3 哈希值(分块处理大文件)
int sm3_file_hash(const char *file_path, unsigned char *hash, unsigned int *hash_len) {// 打开文件(二进制模式,避免文本模式下的换行符转换)FILE *file = fopen(file_path, "rb");if (!file) {fprintf(stderr, "无法打开文件: %s\n", file_path);return 0;}// 创建 EVP 上下文EVP_MD_CTX *ctx = EVP_MD_CTX_new();if (!ctx) {fprintf(stderr, "无法创建 EVP 上下文\n");fclose(file);return 0;}// 初始化 SM3 算法if (EVP_DigestInit_ex(ctx, EVP_sm3(), NULL) != 1) {fprintf(stderr, "SM3 初始化失败\n");ERR_print_errors_fp(stderr);EVP_MD_CTX_free(ctx);fclose(file);return 0;}// 分块读取文件并更新哈希(每次读取 4KB 块)unsigned char buffer[4096]; // 缓冲区大小,可根据需求调整(如 8192、16384 等)size_t bytes_read;while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {// 每次读取一块数据,更新哈希计算if (EVP_DigestUpdate(ctx, buffer, bytes_read) != 1) {fprintf(stderr, "哈希更新失败(文件读取到 %zu 字节时)\n", bytes_read);ERR_print_errors_fp(stderr);EVP_MD_CTX_free(ctx);fclose(file);return 0;}}// 检查文件读取是否出错if (ferror(file)) {fprintf(stderr, "文件读取错误\n");EVP_MD_CTX_free(ctx);fclose(file);return 0;}// 完成哈希计算并获取结果if (EVP_DigestFinal_ex(ctx, hash, hash_len) != 1) {fprintf(stderr, "SM3 计算完成失败\n");ERR_print_errors_fp(stderr);EVP_MD_CTX_free(ctx);fclose(file);return 0;}// 释放资源EVP_MD_CTX_free(ctx);fclose(file);return 1;
}int main(int argc, char *argv[]) {const char *file_path = "D:/Neo4j/neo4j-community-3.5.5-windows.zip";unsigned char hash[EVP_MAX_MD_SIZE];unsigned int hash_len;// 初始化 OpenSSL 错误信息ERR_load_crypto_strings();// 计算文件的 SM3 哈希if (sm3_file_hash(file_path, hash, &hash_len)) {printf("文件: %s\n", file_path);printf("SM3 哈希值: ");for (unsigned int i = 0; i < hash_len; i++) {printf("%02x", hash[i]);}printf("\n哈希长度: %u 字节\n", hash_len);}// 清理错误信息ERR_free_strings();return 0;
}运行截图如下:

