
简介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、怎么分组toolsgmssl命令行子命令某个开关不确定直接看解析代码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 SM4CBC 模式下的 key、IV 与填充SM4 是分组长度 128 位、密钥长度 128 位的分组密码。命令行下最常用的组合是 CBC 模式加 PKCS#7 填充加解密两边参数必须完全一致。KEY0123456789abcdeffedcba9876543210 IV000102030405060708090a0b0c0d0e0f 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参数含义需要说清楚因为这几个值错一个字节结果就全变参数含义约束-key128 位密钥的十六进制串必须 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的输出做字符串比较更直接。从那以后我每接一个新的国密库第一件事都是把这三条向量跑一遍再动业务代码因为密码学库的问题一旦混进业务逻辑排查成本会翻好几倍。希望帮到你。本文还有配套的精品资源点击获取