OpenSSL 国密算法实战:SM2/SM3/SM4 文档与代码资源

发布时间:2026/10/26 21:25:52
OpenSSL 国密算法实战:SM2/SM3/SM4 文档与代码资源

简介:这份资源面向需要在Windows平台落地国密算法的开发者与安全工程师,围绕SM2、SM3、SM4三套算法提供基于OpenSSL的完整实现与VC工程示例,可用于身份认证、数据加密、完整性校验及消息认证码等场景,帮助读者在满足国内密码技术合规要求的同时兼顾与国际标准库的兼容。压缩包共64个文件,约4.65MB,以c源码与h头文件为核心,配套dsp、dsw等VC工程文件、obj与pdb等编译调试产物、exe可执行程序、dll与lib动态静态库,以及4份PDF技术规范文档,覆盖从算法原理到工程编译的完整链路。目前已有732人学习下载。读者可借助SM2加解密与签名、SM3摘要、SM4对称加密的示例代码,结合OpenSSL接口快速集成到实际项目中,并通过工程文件在Visual Studio中直接构建调试,是理解与应用国密算法较为实用的参考材料。

1. 国密算法遇上 OpenSSL:一套能直接跑通的文档与代码资源

手里有一套基于 OpenSSL 实现的国密算法文档和代码,这事本身就值得聊。国密算法(SM2/SM3/SM4)在国内金融、政务、物联网场景里已经是硬性要求,但真正落到工程上,很多人卡在同一个地方:标准文档看得懂,OpenSSL 的接口对不上。这套资源解决的正是这个断层——它不是纯理论教材,也不是一个孤立的 demo,而是把 SM2 签名验签、SM3 摘要、SM4 加解密在 OpenSSL 框架下的实现路径、参数配置和调用方式整理成了可复现的文档加代码。适合谁?正在做国密改造的后端工程师、需要过密评的项目组、以及想搞清楚 OpenSSL 国密接口到底怎么调的人。下面按“先搞懂结构、再动手编译、然后踩坑排查、最后进阶验证”的顺序拆一遍。

2. OpenSSL 国密支持现状与资源结构拆解

2.1 为什么是 OpenSSL 而不是从零造轮子

国密算法的底层实现并不复杂,SM3 是哈希、SM4 是分组密码、SM2 是椭圆曲线公钥算法,理论上自己撸一遍也能跑。但工程上没人这么干,原因很实际:你需要的是经过验证的、能跟现有 TLS/SSL 体系对接的实现。OpenSSL 从 1.1.1 版本开始通过 provider 机制和 engine 机制支持国密算法,国内也有基于 OpenSSL 1.1.1 分支的国密改造版本。这套资源选择 OpenSSL 作为底座,核心逻辑是:你不需要重新发明 SM2 的曲线参数和 SM3 的压缩函数,你需要的是知道 OpenSSL 暴露了哪些接口、这些接口的参数怎么填、返回值怎么判断。

常见做法是直接使用 OpenSSL 的 EVP 高层接口来调用国密算法,而不是去碰底层的 BN(大数)和 EC(椭圆曲线)接口。EVP 接口的好处是抽象层次高、代码量少、跟其他算法的调用方式统一。但坑也在这里——国密算法在标准 OpenSSL 里的支持程度跟改造版不一样,有些函数在标准版里根本不存在,你得先确认自己用的是哪个版本。

2.2 资源里到底有什么

这套文档和代码的结构大致分三块。第一块是算法说明文档,覆盖 SM2 的密钥生成、签名、验签、加密、解密流程,SM3 的输入输出规格和分组填充规则,SM4 的 ECB/CBC 模式及填充方式。第二块是代码示例,按算法分文件,每个文件是一个独立可编译的 C 程序,调用 OpenSSL 的 EVP 接口完成对应操作。第三块是编译脚本和测试向量,用来验证你的环境是否配置正确。

代码的组织方式我比较认可:每个算法一个独立目录,目录里有.c源文件、Makefile、以及一个test_vector.txt存放标准测试向量。这种结构的好处是你不需要一次性把所有东西都跑通,可以一个算法一个算法地验证。SM3 最简单,先跑通它建立信心;SM4 次之,对称加密逻辑直观;SM2 最复杂,涉及曲线参数和密钥格式,放最后啃。

2.3 环境准备与版本确认

在动手之前,有一件事必须先做:确认你的 OpenSSL 版本和支持的算法列表。标准 OpenSSL 3.x 和国密改造版的行为差异很大,不确认版本就编译,后面报错会把你带偏。

# 查看 OpenSSL 版本 openssl version -a # 查看是否支持 SM3 openssl list -digest-algorithms | grep -i sm3 # 查看是否支持 SM4 openssl list -cipher-algorithms | grep -i sm4 # 查看是否支持 SM2 openssl list -public-key-algorithms | grep -i sm2

如果上面几条命令有输出,说明你的 OpenSSL 已经具备国密能力。如果list命令报错,说明版本太老(低于 1.1.1),需要升级或换用国密改造版。注意,有些系统自带的 OpenSSL 是 1.1.1 但编译时没开国密选项,这种情况下list能跑但 grep 不到 sm3/sm4,需要重新编译。

编译代码时,Makefile 里通常需要显式链接 crypto 库:

gcc -o sm3_test sm3_test.c -lssl -lcrypto

如果报undefined reference to EVP_sm3,说明链接的 OpenSSL 库不支持 SM3,不是代码写错了。这个区分很重要,很多人在这里浪费时间改代码,实际上是库的问题。

3. SM2/SM3/SM4 的 OpenSSL 调用实操

3.1 SM3 摘要:最简单的入口

SM3 是国密哈希算法,输出 256 位摘要,跟 SHA-256 同级别。用 OpenSSL 的 EVP 接口调用 SM3,代码结构跟调用 SHA-256 几乎一样,只需要把算法名换掉。

#include <openssl/evp.h> #include <stdio.h> #include <string.h> int main() { // 待摘要的数据 const char *msg = "abc"; unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digest_len = 0; // 获取 SM3 摘要算法 EVP_MD_CTX *ctx = EVP_MD_CTX_new(); if (ctx == NULL) { printf("EVP_MD_CTX_new failed\n"); return 1; } // 初始化:指定 SM3 算法 if (EVP_DigestInit_ex(ctx, EVP_sm3(), NULL) != 1) { printf("EVP_DigestInit_ex failed, SM3 not supported?\n"); EVP_MD_CTX_free(ctx); return 1; } // 更新数据 if (EVP_DigestUpdate(ctx, msg, strlen(msg)) != 1) { printf("EVP_DigestUpdate failed\n"); EVP_MD_CTX_free(ctx); return 1; } // 输出摘要 if (EVP_DigestFinal_ex(ctx, digest, &digest_len) != 1) { printf("EVP_DigestFinal_ex failed\n"); EVP_MD_CTX_free(ctx); return 1; } // 打印十六进制结果 for (unsigned int i = 0; i < digest_len; i++) { printf("%02x", digest[i]); } printf("\n"); EVP_MD_CTX_free(ctx); return 0; }

这段代码的逻辑很直白:创建上下文、初始化指定算法、喂数据、取结果、释放上下文。关键参数是EVP_sm3(),它返回 SM3 的算法描述符。如果这个函数返回 NULL 或者EVP_DigestInit_ex失败,基本可以断定当前 OpenSSL 不支持 SM3。

对 "abc" 做 SM3,标准结果应该是66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0。如果你跑出来的不是这个值,先检查输入数据有没有多算换行符或空格。测试向量文件里一般会给出输入和期望输出,逐字节对比。

3.2 SM4 加解密:分组模式和填充的坑

SM4 是 128 位分组密码,密钥也是 128 位。OpenSSL 里通过 EVP 接口调用 SM4,支持 ECB 和 CBC 模式。ECB 模式不需要 IV,CBC 模式需要 16 字节 IV。

#include <openssl/evp.h> #include <stdio.h> #include <string.h> int main() { // 16 字节密钥 unsigned char key[16] = { 0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef, 0xfe,0xdc,0xba,0x98,0x76,0x54,0x32,0x10 }; // 16 字节 IV(CBC 模式使用) unsigned char iv[16] = { 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f }; // 待加密明文 unsigned char plaintext[] = "Hello SM4!"; unsigned char ciphertext[64]; unsigned char decrypted[64]; int out_len, dec_len; // 加密 EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_sm4_cbc(), NULL, key, iv); EVP_EncryptUpdate(ctx, ciphertext, &out_len, plaintext, strlen((char*)plaintext)); int total_len = out_len; EVP_EncryptFinal_ex(ctx, ciphertext + out_len, &out_len); total_len += out_len; EVP_CIPHER_CTX_free(ctx); printf("Ciphertext (%d bytes): ", total_len); for (int i = 0; i < total_len; i++) printf("%02x", ciphertext[i]); printf("\n"); // 解密 ctx = EVP_CIPHER_CTX_new(); EVP_DecryptInit_ex(ctx, EVP_sm4_cbc(), NULL, key, iv); EVP_DecryptUpdate(ctx, decrypted, &dec_len, ciphertext, total_len); int dec_total = dec_len; EVP_DecryptFinal_ex(ctx, decrypted + dec_len, &dec_len); dec_total += dec_len; EVP_CIPHER_CTX_free(ctx); decrypted[dec_total] = '\0'; printf("Decrypted: %s\n", decrypted); return 0; }

这里有几个参数必须说清楚。EVP_sm4_cbc()指定 CBC 模式,如果要 ECB 模式就换成EVP_sm4_ecb(),但 ECB 模式下 IV 参数传 NULL。密钥和 IV 都必须是 16 字节,少一个字节都会导致EVP_EncryptInit_ex返回失败。

填充方面,OpenSSL 默认使用 PKCS#7 填充。如果你跟其他系统对接,对方用的是零填充或无填充,就需要用EVP_CIPHER_CTX_set_padding(ctx, 0)关闭默认填充,然后自己处理数据对齐。这个坑在跨系统对接时特别常见——你这边加密出来的密文长度跟对方期望的对不上,八成是填充方式不一致。

3.3 SM2 签名验签:曲线参数和密钥格式

SM2 是基于椭圆曲线的公钥算法,OpenSSL 里通过 EVP 接口调用。SM2 的密钥生成、签名、验签流程比 SM3 和 SM4 复杂,主要复杂在密钥格式和曲线参数的指定上。

#include <openssl/evp.h> #include <openssl/pem.h> #include <stdio.h> int main() { // 生成 SM2 密钥对 EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_SM2, NULL); EVP_PKEY_keygen_init(pctx); EVP_PKEY *pkey = NULL; EVP_PKEY_keygen(pctx, &pkey); EVP_PKEY_CTX_free(pctx); // 待签名数据 const char *msg = "sign me"; unsigned char sig[256]; size_t sig_len = sizeof(sig); // 签名 EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); EVP_PKEY_CTX *sctx = NULL; EVP_DigestSignInit(mdctx, &sctx, EVP_sm3(), NULL, pkey); EVP_DigestSign(mdctx, sig, &sig_len, (unsigned char*)msg, strlen(msg)); EVP_MD_CTX_free(mdctx); printf("Signature length: %zu\n", sig_len); // 验签 mdctx = EVP_MD_CTX_new(); EVP_PKEY_CTX *vctx = NULL; EVP_DigestVerifyInit(mdctx, &vctx, EVP_sm3(), NULL, pkey); int ret = EVP_DigestVerify(mdctx, sig, sig_len, (unsigned char*)msg, strlen(msg)); EVP_MD_CTX_free(mdctx); printf("Verify result: %s\n", ret == 1 ? "OK" : "FAIL"); EVP_PKEY_free(pkey); return 0; }

这段代码里,EVP_PKEY_SM2指定密钥类型为 SM2,EVP_sm3()指定签名时使用的摘要算法。SM2 签名标准里默认用 SM3 做摘要,所以这两个要配对使用。

密钥格式是另一个容易翻车的地方。OpenSSL 生成的 SM2 密钥默认是 PEM 格式,但有些系统要求裸密钥或者特定编码的密钥。如果你需要把密钥导出给其他系统用,注意EVP_PKEY_get_raw_public_key和EVP_PKEY_get_raw_private_key这两个接口拿到的裸密钥格式,跟标准 SM2 密钥格式可能有差异。文档里一般会给出密钥格式转换的示例代码,照着改参数就行。

4. 编译与运行时的避坑排查

4.1 现象:编译报 undefined reference

原因:链接阶段找不到 OpenSSL 的符号,通常是 Makefile 里没加-lcrypto,或者系统里装了多个 OpenSSL 版本,链接到了不支持国密的那个。

解决:先ldd看你的可执行文件链接的是哪个 libcrypto,再用openssl version确认那个库的版本。如果是多版本共存,在 Makefile 里用-L/path/to/correct/openssl/lib显式指定路径。

4.2 现象:EVP_sm3() 返回 NULL

原因:当前 OpenSSL 库编译时没有启用 SM3 支持。标准 OpenSSL 1.1.1 默认是支持的,但有些发行版的打包版本为了精简体积把国密算法裁掉了。

解决:用openssl list -digest-algorithms确认。如果没有 sm3,需要自己编译 OpenSSL 并开启国密选项,或者换用国密改造版。编译时加enable-sm2 enable-sm3 enable-sm4之类的配置参数(具体参数名看版本)。

4.3 现象:SM4 解密出来是乱码

原因:最常见的是 IV 不匹配或填充方式不一致。CBC 模式下加密和解密必须用同一个 IV,IV 错了第一个分组就解不对。填充方式不一致会导致最后一个分组解出来多几个字节或少几个字节。

解决:先确认加密和解密用的是同一组 key 和 IV,再确认两边填充方式一致。如果对方系统用的是零填充,你这边要关掉 OpenSSL 的默认填充,手动把数据补齐到 16 字节的整数倍。

4.4 现象:SM2 验签失败但签名长度正常

原因:SM2 签名涉及用户 ID 的默认值问题。标准 SM2 签名时有一个默认用户 ID(通常是 "1234567812345678"),如果签名和验签两边的用户 ID 不一致,验签就会失败。

解决:检查EVP_PKEY_CTX_set1_id是否在签名和验签两边都设置了相同的 ID。如果一方没设,用的就是默认值,另一方设了非默认值,就会对不上。

4.5 现象:程序运行时报 segment fault

原因:EVP 上下文没有正确初始化或提前释放。比如EVP_MD_CTX_new之后没有检查返回值就直接用,或者EVP_CIPHER_CTX_free之后又调用了该上下文。

解决:每个_new后面都加 NULL 检查,每个_free之后把指针置 NULL。EVP 接口的返回值检查不能省,国密算法在某些 OpenSSL 版本里可能返回特定错误码,不检查就会带着空指针往下跑。

5. 用测试向量验证实现正确性

代码能编译、能跑出结果,不代表结果是对的。国密算法有标准测试向量,文档里一般会附带。验证方法是:用测试向量里的输入跑你的程序,把输出跟期望值逐字节对比。

SM3 的标准测试向量:输入 "abc",期望输出66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0。输入为空字符串,期望输出1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b。

SM4 的标准测试向量:密钥和明文都是0123456789abcdeffedcba9876543210,ECB 模式加密后的密文是681edf34d206965e86b3e94f536e4246。这个向量可以用来验证你的 SM4 实现是否跟标准一致。

SM2 的验证稍微麻烦一点,因为涉及随机数。但签名验签的自洽性可以验证:用同一对密钥签名再验签,结果应该是 OK。如果要跟标准向量对比,需要用固定随机数的签名结果,文档里如果有提供就照着跑。

我一般会写一个简单的 shell 脚本把所有测试向量跑一遍:

#!/bin/bash # 依次运行各算法测试并对比期望值 echo "=== SM3 test ===" ./sm3_test echo "Expected: 66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0" echo "=== SM4 test ===" ./sm4_test echo "Expected: 681edf34d206965e86b3e94f536e4246" echo "=== SM2 test ===" ./sm2_test

跑完对比输出,全部匹配说明环境配置和代码实现都没问题。有一个不匹配就按第 4 章的排查思路定位。

从那以后我每次拿到新的国密代码包,都强制先跑一遍测试向量再动业务代码。这个习惯帮我省了很多来回折腾的时间——环境问题在测试向量阶段暴露,比在业务联调阶段暴露成本低得多。希望帮到你。

本文还有配套的精品资源,点击获取