简介:GmSSL 3.1.1 源码包面向需要在应用中接入国密算法的开发者与安全研究人员,提供一套以 SSL/TLS 协议为核心、支持 SM2/SM3/SM4/SM9 等国密算法的开源实现,可用于构建国密 HTTPS 服务、证书体系与密码模块集成等场景,对具备一定 C 语言与密码学基础的读者尤为合适。压缩包共 329 个文件,以 218 个 C 源文件和 72 个头文件为主体,构成协议栈、X.509 证书与 ASN.1 编解码等核心模块;另有 18 个 shell 脚本与 3 个 CMake 配置文件,便于快速编译与功能验证,6 个 pem 证书、3 个 crl 吊销列表与 3 个 yml 配置可直接用于测试。整包约 397KB,体积轻量,源码结构清晰,适合按模块研读。已有 233 人学习下载,读者可从中获得国密 TLS 握手、SM9 算法、CMS 消息封装与证书扩展处理等完整实现思路,并借助示例脚本完成编译验证,是理解国密协议落地的实用参考。
1. 从一份 tar.gz 说起:为什么还要单独编一套 SM2/SM3/SM4 的库
去年给一个内网系统做签名网关,需求只有一句话:签名走 SM2,摘要走 SM3,报文加密走 SM4。系统自带的 openssl 对 SM2 的封装只到半成品,命令行能凑合,但 C 接口里 ID 和 Z 值要自己拼,调了两天全是玄学。后来换成自带国密算法工具集的 GmSSL,一个 tar.gz 解压、config、make 三步,库和命令行工具一起装上。GmSSL-3.1.1.tar.gz 是源码包,不是二进制分发包,它把 SM2/SM3/SM4/SM9 以及基于这些算法的 TLCP 协议栈打在一起,既能当命令行工具用,也能当 C 库链进工程。适合两类人:一类是要在国产化环境里做国密改造的后端和嵌入式工程师,一类是想拿标准测试向量把实现验证一遍的学习者。下面按我当初踩过来的顺序写:先编译,再命令行验证,然后落到代码里,最后是我返工过的那些坑。
2. 解包到 make install:依赖检查、config 开关与装完自检
源码包编译看着最没技术含量,但国密库出问题大多发生在这十分钟里:系统里已经有一份 OpenSSL 的头文件和动态库,编译器分不清谁是谁;装的路径不在动态链接器的搜索范围里,命令行工具跑得起来,自己写的程序一运行就报找不到库。我一般的顺序是先看目录确认版本和内容,再决定装到哪个 prefix,最后做一次最小自检,把不确定性锁死在这一步。
2.1 解包后先看三样东西
拿到源码别急着./config。GmSSL 3.x 的目录结构比较规矩,先ls一遍能省掉后面很多猜测。
tar -xzf GmSSL-3.1.1.tar.gz cd GmSSL-3.1.1 ls # 通常会看到:include/ src/ tools/ tests/ demos/ Makefile configure config 等这里面真正对后面有用的是tests和demos。tests里放的是标准测试向量,是自检的底稿;demos里是各算法的调用示例,当你不知道某个 API 该传几个参数时,它比任何文档都直接。tools是gmssl这个命令行程序的入口,想知道某个子命令底下还有哪些开关,去这里翻源码比猜快。
| 目录 | 是什么 | 什么时候会翻它 |
|---|---|---|
include/gmssl | 全部对外头文件 | 写 C 代码时确认函数原型和结构体 |
src | 算法与协议实现 | 想看某个模式到底怎么处理 padding、怎么分组 |
tools | gmssl命令行子命令 | 某个开关不确定,直接看解析代码 |
tests | 标准测试向量 | 编译完自检、怀疑实现有问题时 |
demos | 最小调用示例 | 第一次接 API,照抄改参数 |
判断版本也有个小技巧:解压后的目录名就是版本号,gmssl version输出要和它对得上。如果对不上,说明 PATH 里存在另一份更早安装的gmssl,后面所有验证都会失效,这一点在第 5 章会展开。
2.2 ./config 的开关与编译
GmSSL 3 的构建脚本叫config,不是常见的configure,这是第一个容易卡住的地方,敲错会直接提示找不到文件。
./config --prefix=/usr/local --openssldir=/usr/local/ssl make -j$(nproc) make test # 跑 tests/ 里的向量,第一次装建议别跳过 sudo make install sudo ldconfig # 让 /usr/local/lib 进入动态库搜索路径--prefix决定装到哪儿,默认是/usr/local。如果你的机器上还有别的密码学库要共存,建议改成独立目录(比如--prefix=/opt/gmssl),后面通过-I、-L和-Wl,-rpath精确指向,比事后从/usr/local/lib里往外摘干净得多。--openssldir是配置和证书的默认查找目录,命令行工具生成证书时如果没显式给路径,会往这里找。
make test这一步不要省。它跑的就是tests里那批向量,能第一时间告诉你编译器或者机器字长有没有导致实现跑偏。我见过一次在裁剪过的交叉编译环境里,make成功但make test挂掉,最后发现是某个依赖整数宽度的判断被编译器优化掉了,如果跳过这步,问题会在业务代码里以「签名偶尔验不过」的形式复现,难查十倍。
安装完还有个常见的小问题:/usr/local/lib不在ld.so.conf里,ldconfig跑了也没用。可以先把路径写进配置文件再刷新。
echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/gmssl.conf sudo ldconfig -v | grep -i gmssl # 确认动态链接器已经收录这段不写进构建脚本里也行,但换机器部署时一定要带上,否则在开发机上一切正常、到了测试环境启动就报错。
2.3 安装后的三分钟自检
很多人的「装好了」指的是make install没报错,这不等于可用。装完花三分钟跑下面这组命令,能把版本错乱、路径错乱、功能缺失三类问题一次排掉。
gmssl version # 版本号要和 3.1.1 对上 which -a gmssl # 有没有多个 gmssl 共存 gmssl help # 看子命令列表 ls -l /usr/local/lib/libgmssl* # 记下实际库文件名 ls /usr/local/include/gmssl/ | head # 确认头文件装到位gmssl help这一步很关键。GmSSL 的子命令集合会随构建配置变化,sm2keygen、sm3、sm4、sm2sign一般都在,SM9 相关的sm9setup、sm9keygen是否出现取决于编译时有没有打开对应模块。与其去背一份命令清单,不如让工具自己报一遍,后面写脚本时以这份输出为准。
ls -l /usr/local/lib/libgmssl*也别跳过。有些构建会产出带版本后缀的.so,有些只有静态库.a,链接时-l后面跟什么名字,得看实际产物,凭经验写-lssl -lcrypto是 OpenSSL 的习惯,套到这套库上必然失败。
提示:自检阶段发现版本号或子命令集合和预期不符,先别往下走。把
which -a gmssl的每一份都找出来删干净再重装,比在业务代码里绕一圈便宜得多。
3. 命令行跑通三件套:SM3 摘要、SM4 加解密、SM2 签验
在写第一行 C 代码之前,我习惯先用命令行把三条主链路各跑一遍。原因很现实:命令行和 C 库走的是同一套实现,但命令行的输入输出是人眼能核对的,一旦结果和标准向量对不上,能立刻判断是库的问题还是自己代码的问题。这一章的三段脚本可以直接抄进自己的自检脚本里。
3.1 SM3:先拿标准测试向量把工具链钉死
SM3 的输出是 256 位,标准里最常用的向量是字符串abc,摘要值固定。这条向量对不上,后面所有签名验签都没有意义。
printf 'abc' > abc.bin gmssl sm3 -in abc.bin # 期望输出: # 66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0这里有个血泪经验:一定要用printf,别用echo。echo 'abc'会多写一个换行符,输入变成 4 个字节,摘要完全不同。第一次对不上向量时我折腾了半天,最后发现是 shell 内置命令的差别。如果手头只有echo,用echo -n也行,但printf的行为在各家 shell 上更一致,脚本里统一用它省心。
需要把摘要落成二进制文件时,用输出开关而不是事后做十六进制转换:
gmssl sm3 -in abc.bin -bin -out abc.digest xxd abc.digest | head -2 # 应该看到 32 字节具体开关名以gmssl help里 sm3 那一行为准,不同构建可能略有差异,但-in、-out、-bin这组是通用的。二进制摘要的用处在于后续要做 HMAC 或者直接当 KDF 输入,十六进制文本夹在中间转换容易多一个大小写或空格的问题。
对文件做完整性校验时,SM3 和 SHA-256 的用法没有区别:对文件整体摘要,把结果和发布方给的值比对。区别只在于长度和算法本身,工具链层面完全同构。
3.2 SM4:CBC 模式下的 key、IV 与填充
SM4 是分组长度 128 位、密钥长度 128 位的分组密码。命令行下最常用的组合是 CBC 模式加 PKCS#7 填充,加解密两边参数必须完全一致。
KEY=0123456789abcdeffedcba9876543210 IV=000102030405060708090a0b0c0d0e0f printf 'GmSSL SM4 CBC demo' > plain.txt gmssl sm4 -cbc -encrypt -key $KEY -iv $IV -in plain.txt -out cipher.bin gmssl sm4 -cbc -decrypt -key $KEY -iv $IV -in cipher.bin -out plain.out cmp plain.txt plain.out && echo "roundtrip OK"参数含义需要说清楚,因为这几个值错一个字节结果就全变:
| 参数 | 含义 | 约束 |
|---|---|---|
-key | 128 位密钥的十六进制串 | 必须 32 个 hex 字符,即 16 字节 |
-iv | 初始向量 | CBC 模式必填,同样 16 字节 |
-cbc/-ctr | 工作模式 | CBC 需要填充,CTR 是流式、不需要 |
-encrypt/-decrypt | 方向 | 两个方向用同一组 key 和 iv |
-in/-out | 输入输出文件 | 不加-out时输出到标准输出 |
半块向量可以用来单独验证实现:密钥和明文都取0123456789abcdeffedcba9876543210,单块加密结果是681edf34d206965e86b3e94f536e4246。如果这一步对不上,先怀疑自己把 hex 串写错了(比如中间混了空格或大写),再怀疑构建。
CBC 模式下 IV 的作用是让相同明文块产生不同密文,所以 IV 不能固定复用。生产环境里每个会话或者每条报文都应该换一次 IV,并且 IV 本身不需要保密,可以跟密文一起传。真正需要保护的是 key,硬编码在脚本里只适合做验证。
还有一个容易被忽略的点:命令行默认会做填充,而 C 接口通常不做。同一份明文,命令行加密出来的结果和你在代码里直接调分组函数算出来的可能差一个块,这不是实现错了,是填充策略不同。这个坑在第 5 章单独说。
3.3 SM2:密钥、ID 与签名验签的完整闭环
SM2 是基于椭圆曲线的公钥算法,签名和加密是两套不同的流程。密钥对生成一次,签名和加密都能用。
# 生成密钥对,私钥用口令加密保存 gmssl sm2keygen -pass 12345678 -out sm2.key -pubout sm2.pub.pem # 签名 gmssl sm2sign -key sm2.key -pass 12345678 -in plain.txt -out sig.bin # 验签,只需要公钥 gmssl sm2verify -pubkey sm2.pub.pem -in plain.txt -sig sig.bin-pass不是随便设的装饰项,它用来加密私钥文件。后面每次用这个私钥(签名、解密)都要给同一个口令,给错会直接报私钥无法解密。这一点和某些库「口令只是保护文件、实际密钥明文存在里面」的行为不同,别把-pass当成可选项跳过。
sm2verify成功时会输出验签通过的提示,失败时返回非零。写自动化脚本时以退出码为准,不要用 grep 输出文本。
SM2 加密与解密是另一条链路,注意它的输入长度限制:
gmssl sm2encrypt -pubkey sm2.pub.pem -in plain.txt -out sm2.cipher gmssl sm2decrypt -key sm2.key -pass 12345678 -in sm2.cipher -out plain.decSM2 公钥加密适合处理短数据,比如一个对称密钥、一个随机数。几十 KB 的报文直接走 SM2 加密既慢又容易踩到长度边界,常见做法是「SM4 加密报文 + SM2 加密 SM4 会话密钥」的数字信封,TLCP 握手内部也是这个思路。
签名验签这里有一处很容易埋雷:SM2 签名时,参与运算的并不只是消息本身,还有一个用户 ID 参与杂凑,缺省值是固定的 16 字节串。签名方和验签方必须用同一个 ID,否则同样的消息、同样的密钥,验签就是不过。命令行工具默认用缺省 ID,所以两边都用工具时不会暴露这个问题;一旦一边换成自己写的代码,问题就来了。
4. 代码实现:C 接口加密与 TLCP 双向认证的最小骨架
命令行只能验证实现,真正落地还是要进代码。这一章分三层:先用最底层的分组接口把 SM4 跑通,弄清 padding 责任在谁;再用命令行造一条 SM2 证书链;最后给一份 TLCP 握手的调用骨架。
4.1 C 代码里调 SM4-CBC:错误栈怎么读
下面这段是最小可运行版本,只做一件事:32 字节明文加密再解密,比对一致。
#include <stdio.h> #include <string.h> #include <gmssl/sm4.h> int main(void) { uint8_t key[16], iv[16], in[32], out[32], back[32]; SM4_KEY sm4_key; memset(key, 0x11, sizeof(key)); /* 演示用固定值,生产必须随机 */ memset(iv, 0x22, sizeof(iv)); memset(in, 'A', sizeof(in)); /* 加密方向:用加密密钥表 */ sm4_set_encrypt_key(&sm4_key, key); sm4_cbc_encrypt(&sm4_key, iv, in, sizeof(in), out); /* 解密方向:必须重新用解密密钥表 */ sm4_set_decrypt_key(&sm4_key, key); sm4_cbc_decrypt(&sm4_key, iv, out, sizeof(out), back); printf("roundtrip %s\n", memcmp(in, back, sizeof(in)) == 0 ? "OK" : "FAIL"); return 0; }这段代码有三个地方值得展开。第一,sm4_set_encrypt_key和sm4_set_decrypt_key是两个不同的函数,密钥表内容不一样,加解密不能共用一份。见过有人只调了加密那个,解密结果全是乱码,然后怀疑 IV。第二,sm4_cbc_encrypt的最后一个参数是字节数,必须是 16 的整数倍,函数内部不做填充,也不检查长度是否对齐——传个 30 进去,行为就不确定了。第三,iv参数是const,函数内部自己拷贝,加密不会破坏调用方的 IV,所以解密可以直接复用同一份。
编译命令:
gcc demo_sm4.c -o demo_sm4 \ -I/usr/local/include \ -L/usr/local/lib \ -lgmssl \ -Wl,-rpath,/usr/local/lib-I指向头文件根目录,源码里写#include <gmssl/sm4.h>,路径是拼接出来的。-L和-lgmssl里的库名要跟 2.3 节ls出来的实际文件名对齐,如果产物是带版本后缀的.so,用-l:libgmssl.so.x这种写法更稳。-Wl,-rpath把运行时搜索路径直接编进可执行文件,省掉部署时再配LD_LIBRARY_PATH的麻烦,这一点在容器里尤其值。
如果要处理不定长数据,填充得自己做。PKCS#7 的规则很简单:缺几个字节就补几个值为「缺的字节数」的字节,整块对齐时补满一整块。补完再调sm4_cbc_encrypt,解密后按最后一个字节的值截断。写错这一步的典型症状是解密出来的尾部多几个0x04之类的字节。
4.2 证书链怎么造:从 SM2 自签根到站点证书
TLCP 双向认证需要两条证书链:服务端证书和客户端证书,各自都要能被对端信任。本地验证时最省事的做法是造一个自签根,用它签两张叶子证书。
# 1. 根 CA 密钥与自签证书 gmssl sm2keygen -pass 12345678 -out ca.key -pubout ca.pub.pem gmssl certgen -C CN -ST Beijing -L Haidian -O Demo -OU Lab \ -CN "Demo Root CA" -days 3650 \ -key ca.key -pass 12345678 -serial 0x01 -out ca.crt # 2. 服务端密钥与证书请求 gmssl sm2keygen -pass 12345678 -out server.key -pubout server.pub.pem gmssl reqgen -C CN -ST Beijing -L Haidian -O Demo -OU Lab \ -CN "localhost" -key server.key -pass 12345678 -out server.req # 3. 用根 CA 签发 gmssl certsign -in server.req -ca -in ca.crt \ -key ca.key -pass 12345678 -days 365 -serial 0x02 -out server.crt # 4. 校验签发结果 gmssl certverify -in server.crt -cacert ca.crt几个参数值得注意。-CN在服务端证书里要写成客户端实际连接使用的主机名,写localhost就连localhost,写成别的域名握手阶段的主机名校验会失败。-serial是证书序列号,同一套 PKI 里不能重复,手工造的时候按 1、2、3 递增即可。-days是有效期天数,根证书可以给长一点,叶子证书按业务需要给。-ca -in ca.crt这组参数是把签发者证书一起写进输出,方便对端直接构建链条。
客户端证书按同样的三步来,只是 CN 换成客户端标识。造完用certparse看一眼内容,确认公钥算法是 SM2、签名算法是 SM3-with-SM2,这一步比事后调握手便宜。
注意:
certgen、reqgen、certsign的开关名在 3.x 内部比较稳定,但必填项缺一个就报错退出。按报错提示补参数,不要凭 OpenSSL 的习惯硬套。
4.3 TLCP 握手:ctx 配置与双向认证
TLCP 是基于 SM2/SM3/SM4 的传输层协议,双向认证的调用顺序和常规 TLS 库很像,差别主要在证书和密钥的算法类型。写代码前先打开include/gmssl/tls.h看一遍函数原型,比我口述参数顺序可靠。
调用骨架大致是这样几步:
#include <gmssl/tls.h> /* 1. 建上下文,选客户端或服务端方法 */ TLS_CTX *ctx = tls_ctx_new(TLS_client_method(), 0); /* 2. 装自己的证书和私钥(双向认证时两边都要装) */ tls_ctx_set_certificate_and_key(ctx, "server.crt", "server.key", NULL); /* 3. 装信任的根证书,限制链深度 */ tls_ctx_set_ca_certificates(ctx, "ca.crt", 4); tls_ctx_set_verify_depth(ctx, 4); /* 4. 建连接对象,设主机名,握手 */ TLS *tls = tls_new(ctx); tls_set_hostname(tls, "localhost"); tls_connect(tls); tls_do_handshake(tls); /* 5. 收发数据 */ tls_send(tls, buf, len); tls_recv(tls, buf, sizeof(buf)); /* 6. 收尾 */ tls_shutdown(tls); tls_free(tls); tls_ctx_free(ctx);双向认证的关键在第 2 步和第 3 步:服务端要装自己的证书和私钥,同时装信任的根;客户端除了装根,还要装自己的证书和私钥,否则服务端要求客户端证书时握手会中断在 Certificate 消息之后。tls_ctx_set_verify_depth是链深度上限,设太小会导致中间 CA 存在时验不过,本地自签单层链设 1 都能过,给到 4 是留余量。
调试握手失败时,光看返回值不够。把错误栈打出来,能看到具体卡在证书校验还是密钥交换,ERR_print_errors_fp(stderr)这类做法在这里很省事。日志里出现「certificate verify failed」就往第 3 步查,出现密钥相关错误就往第 2 步查,比逐行读源码快。
5. 避坑与排查:五个让我返工的现场
5.1 编译链接到了系统 OpenSSL
现象:代码里写的是#include <gmssl/sm4.h>,编译通过,运行时行为却不对;或者链接阶段报一堆 undefined reference。原因:-I和-L没写或者顺序不对,编译器先找到了系统自带的 OpenSSL 头文件和库,函数名重名但 ABI 不同。解决:把 GmSSL 的-I、-L放在命令行最前面,链接时用ldd确认最终依赖的是哪一个;实在分不清就把 GmSSL 装到独立 prefix,用完整路径链接,彻底避开搜索顺序的博弈。
5.2 SM3 结果和标准向量差一个字节的长度
现象:gmssl sm3算出来的值和网上查到的向量对不上,逐位比对差异很大。原因:多半是输入多了一个看不见的换行符,echo或者编辑器保存文件时自动加的结尾换行都算。解决:统一用printf 'abc'造输入,用xxd或wc -c确认输入正好是 3 字节再算。养成「先看输入字节数,再看输出」的习惯,能省掉大量无效排查。
5.3 SM2 验签始终不通过
现象:签名命令返回成功,验签失败;或者两边都用自己的代码,互相验不过。原因有三类:一是签名方和验签方的用户 ID 不一致,一方用了缺省值一方用了自定义值;二是对摘要值签名而验签时喂的是原文,中间多了一层杂凑;三是密钥对不是同一对,-pubout导出的公钥和私钥文件内容错配。解决:先用命令行把「同一对密钥、同一份原文、都用缺省 ID」跑通,确认基础链路没问题,再把自定义 ID 一点点加进去,一次只改一个变量。
5.4 CLI 加密的结果和 API 加密的结果对不上
现象:同一份明文、同一个 key 和 IV,命令行加密出来的密文比代码里长一个块,或者干脆全不一样。原因:命令行默认做 PKCS#7 填充,底层分组接口不做填充,输入长度必须是 16 的整数倍。解决:要么在代码里自己补填充对齐算法,要么两边都统一走同一种封装。做互通测试时,先把明文裁成正好 16 字节的整数倍,排除填充因素,再验证模式本身。
5.5 装完 command not found 或运行时找不到动态库
现象:make install没报错,敲gmssl提示找不到命令;或者自己的程序编译通过,一运行就报error while loading shared libraries。原因:/usr/local/bin不在 PATH 里,/usr/local/lib不在动态链接器搜索路径里,这是「装好了但用不了」最常见的两副面孔。解决:命令找不到就sudo ldconfig后重开一个 shell 让 PATH 生效,或者把路径写进/etc/ld.so.conf.d/再执行ldconfig;库找不到优先用-Wl,-rpath把路径编进可执行文件,比依赖环境变量可靠,也更容易随产物一起交付。
6. 进阶:把 GmSSL 塞进既有工程的三条边界
第一是共存边界。工程里已经有一份 OpenSSL 时,别指望全局替换,用独立 prefix 装 GmSSL,在 CMake 里显式写target_include_directories和target_link_libraries,把路径钉死。静态链接能彻底消除运行时找错库的可能,代价是产物变大、升级要重新编,本地验证阶段我一般先用静态链确认逻辑正确,再切回动态库。
第二是性能边界。SM4 的软件实现吞吐和硬件指令支持关系很大,做高并发网关时先压一遍再定并发数,别拿对称加密的直觉去套 SM2——椭圆曲线签名的单次开销比 SM4 分组运算高好几个数量级,签名网关的瓶颈往往在这里。SM9 更特殊,主密钥生成和用户私钥派生的开销要单独测,不适合塞进每条请求的热路径。
第三是验证边界。每接一个新构建的密码学库,我都固定跑三条向量:SM3 对abc的摘要、SM4 单块加密、SM2 签名验签闭环。前两条能立刻暴露实现或输入处理的问题,第三条能暴露密钥和 ID 处理的问题。三条都过,才敢往业务代码里接。
# 一次跑完的三条向量,建议做成脚本进 CI printf 'abc' | sha256sum -c <(echo "66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0 -") 2>/dev/null || true gmssl sm4 -cbc -encrypt -key 0123456789abcdeffedcba9876543210 \ -iv 000102030405060708090a0b0c0d0e0f -in block.bin -out block.enc gmssl sm2sign -key sm2.key -pass 12345678 -in block.bin -out block.sig && \ gmssl sm2verify -pubkey sm2.pub.pem -in block.bin -sig block.sig上面第一行只是示意把期望值放在脚本里比对,实际用gmssl sm3 -in的输出做字符串比较更直接。从那以后我每接一个新的国密库,第一件事都是把这三条向量跑一遍再动业务代码,因为密码学库的问题一旦混进业务逻辑,排查成本会翻好几倍。希望帮到你。
本文还有配套的精品资源,点击获取