素槿
Published on 2025-08-22 / 6 Visits
0

国密SM3 OpenSSL EVP接口例子

简介

国产SM3是中国国家密码管理局2010年公布的中国商用密码杂凑算法标准。SM3算法适用于商用密码应用中的数字签名和验证,是在SHA-256基础上改进实现的一种算法。SM3算法采用Merkle-Damgard结构,消息分组长度为512位,摘要值长度为256位。

EVP是OpenSSL提供的一组高级接口,使用EVP可以通过一致简单的接口调用不同的算法。

单次哈希

单次哈希就是只计算一次哈希,适用于数据较短的情况,可以一次完成计算。

OpenSSL提供了接口来直接计算一段数据的哈希值:

int EVP_Digest(const void *data, size_t count, unsigned char *md, unsigned int *size,
               const EVP_MD *type, ENGINE *impl);

参数都比较好理解,datacount为输入数据和数据长度,mdsize分别哈希值和哈希值长度,type为使用的哈希算法,impl是要使用的计算引擎,一般设为 NULL使用默认的引擎。

一个简单的例子,计算"hello world"的哈希值:

// sm3_str.c
#include <openssl/evp.h>
#include <stdio.h>
#include <string.h>

static unsigned int HASH_RESULT_LEN = 32;

/**
 * @brief 计算一段数据的哈希值
 * 
 * @param str 数据
 * @param len 数据长度
 * @param hash_result 哈希值
 * @return unsigned int 哈希值长度
 */
unsigned int hash_str(const char* str, const size_t len,
                      unsigned char* hash_result)
{
    unsigned int ret;
    const EVP_MD* alg = EVP_sm3();
    EVP_Digest(str, len, hash_result, &ret, alg, NULL);
    return ret;
}

int main(int argc, char const* argv[])
{
    char* str = "hello world";
    // HASH_RESULT_LEN = EVP_MD_size(EVP_sm3());
    unsigned char hash_result[HASH_RESULT_LEN];
    unsigned int retlen = hash_str(str, strlen(str), hash_result);

    printf("hash '%s', return len=%d\nhash val=", str, retlen);
    for (int i = 0; i < HASH_RESULT_LEN; i++) {
        printf("%02x", hash_result[i]);
    }
    printf("\n");

    return 0;
}

我们在代码第20行调用 EVP_sm3()函数来使用SM3算法计算哈希,如果想使用其他的哈希算法,可以调用其他类似的函数,如:EVP_md5(), EVP_sha256()等。第28行的 EVP_MD_size用来计算相应哈希算法的哈希值长度,如果不知道一个算法返回的哈希值长度的话可以先用次函数得到。

编译执行:

$ gcc -Wall sm3_str.c -lcrypto -std=c11 -o sm3_str
$ ./sm3_str 
hash 'hello world', return len=32
hash val=44F0061E69FA6FDFC290C494654A05DC0C053DA7E5C52B84EF93A9D67D3FFF88

可以使用OpenSSL提供的命令行工具对结果进行验证:

$ echo -n "hello world" | openssl sm3
(stdin)= 44f0061e69fa6fdfc290c494654a05dc0c053da7e5c52b84ef93a9d67d3fff88

多次哈希

有时可能数据较大或其他原因不能将所有的数据放在读入内存,可以多次读取多次计算得到数据的数据值,OpenSSL提供了一组实现这种功能的函数:

int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl);
int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, size_t cnt);
int EVP_DigestFinal_ex(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s);

一个简单的例子:

// sm3_file.c
#include <openssl/evp.h>
#include <stdio.h>
#include <string.h>

// 哈希值长度(字节)
static const unsigned int HASH_RESULT_LEN = 32;
static const size_t BUFSIZE = 8;
/**
 * @brief 计算文件中的数据哈希值
 * 
 * @param fp 文件结构体
 * @param hash_result 文件哈希值
 */
void hash_file(FILE* fp, unsigned char* hash_result)
{
    const EVP_MD* md = EVP_sm3();
    EVP_MD_CTX* ctx  = EVP_MD_CTX_new();

    char buf[BUFSIZE];
    size_t nread = 0;

    EVP_DigestInit_ex(ctx, md, NULL);
    while ((nread = fread(buf, 1, BUFSIZE, fp)) > 0) {
        EVP_DigestUpdate(ctx, buf, nread);
    }
    EVP_DigestFinal_ex(ctx, hash_result, NULL);

    EVP_MD_CTX_free(ctx);
    return;
}

int main(int argc, char const* argv[])
{
    if (argc != 2) {
        printf("usage: %s <file name>\n", argv[0]);
        return 1;
    }
    FILE* fp = fopen(argv[1], "r");
    if (!fp) {
        printf("%s open failed.\n", argv[1]);
        return 1;
    }

    unsigned char hash_result[HASH_RESULT_LEN];
    hash_file(fp, hash_result);
    printf("SM3(%s)= ", argv[1]);
    for (int i = 0; i < HASH_RESULT_LEN; i++) {
        printf("%02x", hash_result[i]);
    }
    printf("\n");

    fclose(fp);
    return 0;
}

我们在第8行定义了每次读取8个字节,避免fread函数一次读取完文件的所有数据,达不到多次计算哈希的效果。

编译运行:

$ gcc -g -Wall sm3_file.c -lcrypto -std=c11 -o sm3_file
$ ./sm3_file sm3_file.c
SM3(sm3_file.c)= 124834c2c678680ef6ead002232f7d5acfb55778446c39719aca8821a68a7fdf

和OpenSSL命令行的结果进行对比:

$ cat sm3_file.c | openssl sm3
(stdin)= 124834c2c678680ef6ead002232f7d5acfb55778446c39719aca8821a68a7fdf

其他

相关代码位于gitee仓库,还包括其他SM2,SM3, SM4,数字信封,RSA,MD5,SHA等代码。