OpenSSL3.5.2实现SM3数据摘要生成

OpenSSL库中使用EVP(Enhanced Verification Package增强验证包)进行SM3相关的摘要生成。

其逻辑为:可分为 初始化上下文→配置算法→分块处理数据→生成最终哈希→释放资源 五个步骤。

完整代码如下:

cpp 复制代码
#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 的作用就是向算法传递这些分块数据,让算法逐步更新内部状态,它是哈希算法迭代特性的直接体现。

  1. 工程上的 "内存限制" 需求

即使哈希算法支持一次性处理数据,实际工程中也必须分块:

  • 对于大文件(如 10GB 的视频),无法将全部数据一次性加载到内存(会导致内存溢出)。
  • 分块处理时,只需在内存中保留一个小块缓冲区(如 4KB),每次读取一块数据并传递给 EVP_DigestUpdate,内存占用始终保持在极低水平。

这种方式既符合哈希算法的迭代逻辑,又能高效处理任意大小的输入(从几字节到几十 GB)。

分块计算的例子。

cpp 复制代码
#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;
}

运行截图如下:

相关推荐
Dave.B26 分钟前
用【vtk3DLinearGridCrinkleExtractor】快速提取3D网格相交面
算法·3d·vtk
yaoh.wang33 分钟前
力扣(LeetCode) 1: 两数之和 - 解法思路
python·程序人生·算法·leetcode·面试·跳槽·哈希算法
Code Slacker1 小时前
LeetCode Hot100 —— 滑动窗口(面试纯背版)(四)
数据结构·c++·算法·leetcode
brave and determined1 小时前
CANN训练营 学习(day8)昇腾大模型推理调优实战指南
人工智能·算法·机器学习·ai实战·昇腾ai·ai推理·实战记录
总爱写点小BUG2 小时前
打印不同的三角形(C语言)
java·c语言·算法
yaoh.wang2 小时前
力扣(LeetCode) 27: 移除元素 - 解法思路
python·程序人生·算法·leetcode·面试·职场和发展·双指针
2401_841495642 小时前
【自然语言处理】中文 n-gram 词模型
人工智能·python·算法·自然语言处理·n-gram·中文文本生成模型·kneser-ney平滑
San302 小时前
从零到一:彻底搞定面试高频算法——“列表转树”与“爬楼梯”全解析
javascript·算法·面试
F_D_Z2 小时前
最长连续序列(Longest Consecutive Sequence)
数据结构·算法·leetcode
ss2732 小时前
Java并发编程:DelayQueue延迟订单系统
java·python·算法