为什么做这个项目。在使用国密工具的过程中,我发现 Rust 生态中的相关工具还比较少,于是开始了这个项目。
我负责的部分。我提出了最初的需求,并借助 AI 编程助手实现功能。我是 Rust 新手,主要关注实际使用体验,以及与 GmSSL 等现有实现的兼容性,并不经常逐行细审代码。起初,我自己复核改动;后来,为了提高效率,我把更多代码评审交给了遵循既定规则的 AI Agent。
目前的结果。gmcrypto-core v1.17.0 已于 2026 年 10 月 26 日(UTC)在 crates.io 发布。CI 会核对标准测试向量,并将输出与 GmSSL 比对。GmSSL 对照实现(oracle)准备就绪且版本校验通过后,才会执行这项比对。如果准备或版本校验失败,CI 会发出警告并跳过比对,任务仍会通过。统计时序测试则在 x86-64 Linux runner 上,对选定路径检查时序泄漏回归。
使用限制。这个库没有经过独立审计;1.0 之前的那轮评审由两个 AI 模型以对抗方式进行。时序测试通过,只能作为测试条件下的证据,不能证明常量时间行为。
- 角色
- 个人项目
- 状态
- 已发布
- 发布
- v1.17.0
- 关键结果
- 在 CI 里对涉密路径做时序泄漏检查。
- 技术栈
- Rust密码学no_std
cargo add gmcrypto-core
在线试用:国密在线工具。在浏览器里调试 SM2/SM3/SM4,内置自检 11/11(SM3、SM4 对国标向量,SM2 做往返校验);全部在本地计算,不上传数据。这个工具未经审计,请不要用生产密钥。
是什么
gm-crypto-rs 是一套纯 Rust 写的国密算法库,覆盖
中国商用密码杂凑(哈希)标准(GB/T 32905),输出 256 位摘要,在国密体系中大致对应 SHA-256 的角色。 哈希(GB/T 32905)、
中国商用密码公钥标准(GB/T 32918),基于椭圆曲线,用于数字签名、密钥交换和公钥加密。 公钥算法(GB/T 32918:签名 / 验签 /
加密 / 解密,v1.1 起还包括带密钥确认的密钥交换)和
中国商用密码分组密码标准(GB/T 32907),分组长 128 位、密钥长 128 位,用于批量对称加密。 分组密码(GB/T 32907)。
整套依赖都不碰标准库(不链接标准库的 Rust 构建,代码因而能运行在嵌入式、内核、WebAssembly 等受限环境中。它只能使用 core(标准库的子集);需要堆分配时才显式引入 alloc 并提供分配器。 + alloc),能编译到 wasm32-unknown-unknown,也实现了 RustCrypto 的
digest / mac / cipher trait,方便接入现有生态;从 v1.11 起,还可选适配 aead 0.6(SM4-GCM / SM4-CCM)。v1.12 和 v1.13 先后在 Rust 和 C 里加上长度预先承诺的流式 SM4-CCM,所有 SM4 流式类型在 drop 时都会清零;从 v1.16 起,可选的 SIMD SM4 路径在支持的 x86_64 CPU 上改用 GFNI 计算 S 盒;v1.17 修复了两条依赖安全公告,MSRV 升到 Rust 1.87,代价是 SM2 的域运算变慢。
从 v1.3 起,它还能解析 X.509 v3 证书并验证其 SM2-with-SM3 签名(GM/T 0015 profile,严格 DER,不做任何信任判定);对应的 C ABI 接口(在 gmcrypto-c crate 里)在 v1.4 跟上。v1.6 到 v1.9 把 TLCP(GB/T 38636)铺成了一套工具集:密钥派生(key schedule)与一条免密钥确认的 SM2 密钥交换路径(v1.6)、记录层的保护 / 解保护(v1.7)、证书对与证书链的签名验证(v1.8),
v1.9 起整套都能从 C 调用。
已发布的
gm-crypto-rs-demo
是面向使用者的示例与冒烟测试导览,覆盖散列、签名与验签、加解密、密钥交换和编码等代表性能力。它把依赖固定为
gmcrypto-core = "=1.17.0",即当前版本;下文的源码行证据仍固定在 v1.11.0。
要解决的问题
国密这几个算法,Rust 社区其实早有实现,写得好的那些,本来也都按常量时间设计——处理密钥的那段代码,跑多久跟密钥无关,免得别人靠测耗时反推出密钥。
难点在于,这种「常量时间」常常只在代码评审那一刻成立;之后随便一次重构、新加一条快路径、一次依赖升级,时序泄漏都可能悄悄溜回来,当场没人发现。这个库偏偏又卡在一堆容易让它松动的约束里:要逐字节对上 GB/T 标准、核心要能跑在 no_std / wasm 上、涉密路径在一个个版本之间都得保持运行耗时不随密钥等秘密值变化的代码,这样攻击者就无法靠测量耗时来还原秘密。它是一个设计目标,而非保证——某些硬件仍可能泄漏。的设计,还要通过一层 C ABI
把这些路径暴露出来,供 C、C++、Go、Zig、Python 调用。
所以它真正要回答的,不是「今天是不是常量时间」,而是「哪次改动把时序泄漏带回来时,怎样能被发现」——范围只限测试实际覆盖的路径和硬件。
约束与关键决策
持续验证,而不是一次性拍板。常量时间不该是评审会上签个字就算数的结论,而该是 CI 反复重新核验的性质。dudect-bencher(时序侧信道检测工具)一共测 20 条涉密路径。凡是改动密码代码的 PR,核心那一组的统计量在三轮测量中的中位数都必须满足 |τ| ≤ 0.20——
一旦越界,这项检查就失败,不用等人去发现。它不是合并的必需检查:PR 标题里带 [skip ci] 之类的跳过标记也会跳过它;失败会把 PR 标红,但本身不会挡住合并。要说清楚的是,它报的是「检测事件」:|τ| 低,只说明在当前的测量预算下没测到泄漏,并不能证明泄漏不存在(措辞沿用
dudect-bencher 自己的文档)。代价:每个改动密码代码的
PR 都要付出这份测量开销;而且统计永远只能说「没测到」,证明不了「不存在」。
核心路径做检查,其余先观察。不是每条路径都塞得进一个 PR 的时间预算,于是 harness 给它们分了层。
在 PR 上检查(不达标则这项检查失败)的有 16 条:两条标量乘(定基与变基)、SM2 签名、解密与密钥交换、SM4 密钥扩展、SM4
加密(线性扫描和位切片 SIMD S-box 两条都算)、SM4-CTR、CBC 解密扇出(fanout)、单次 SM4-GCM / SM4-CCM 解密、缓冲式 SM4-GCM 解密、SM4-XTS
解密、加密 PKCS#8 解密,以及 v1.7 起新增的
TLCP CBC 记录层解保护(Lucky13 残余风险的约束)。其中 CBC 解密扇出在 runner 的 CPU 是 AMD EPYC 9V74 时,阈值放宽为 0.55 而不是 0.20,因为这款 CPU 上无改动时的噪声带更宽。
只做遥测的有 4 条:域求逆诊断、k-class 签名,以及 2026-06-17 起的
HMAC-SM3——在 PR 上只报告、不检查;在每日 100K 采样的运行里用 0.55 的粗回归哨兵值监视,而不是 0.20 的阈值。共享 runner 上的类分割噪声让它们更紧的阈值频繁误报,所以逐个降级处理,理由也公开写明了。代价:遥测那部分只是监视、并不拦截,而且这套分层本身也得长期维护。
核心禁止 unsafe,SIMD 单独按需开。
gmcrypto-core 在 [lints] 表里禁掉了 unsafe
(unsafe_code = "forbid")——整个核心一行
unsafe 都不许有。可位切片
SIMD 的 S-box 偏偏离不开 unsafe,于是把它整段挪进单独的
gmcrypto-simd crate,用时再通过
sm4-bitsliced-simd 特性打开。代价:快路径不是默认值——你得自己去开 SIMD,否则开箱跑的是更慢的线性扫描 S-box。
算术按常量时间设计,但前提写在明处。涉密的大数运算搭在 subtle 和 crypto-bigint
之上,密钥材料一到 Drop 就由 zeroize 抹零。整体是奔着常量时间设计的,但有些地方确实做不到。代价:在那些「乘法耗时跟操作数有关」的 CPU 上(部分较老的 x86、部分嵌入式芯片),这些时序性质并不成立——这一条我明写在「它不是什么」里,没藏着掖着。
先发核心、再发 FFI——有些轮次不单独发版。每个密码模式都按「核心在 vN、FFI 在 vN+1」的节奏走:SM4-XTS 的核心在 v0.12、对应的 C ABI 在 v0.13;面向磁盘的多扇区原地(in-place)辅助函数在 v0.15、它的 FFI 在 v0.16;SM2 密钥交换在 v1.1、它的 C ABI 在 v1.2;X.509 与 SM2 的结合在 v1.3、它的 C ABI 在 v1.4。
有些轮次只并入了主干、没有单独发版,原因都按轮次记在项目记录里:v0.14 那一轮只做了
cargo-fuzz 解析器模糊测试,零崩溃,已发布 crate 的字节一个没变,所以 crates.io 上直接跳过了 0.14.0;
0.17–0.23 这一段也都没单独发版——主要是
v0.21–v0.23 的发版准备工作(冻结公开 API、把
crypto-bigint 从常开接口解耦、再请两个 AI 模型做一轮 1.0 前的对抗式评审)——
一起并进了刻意的 1.0.0 正式发布。代价:版本号因此留了个需要解释的缺口;而且多出一层 C ABI,每个模式都得在三个已发布的 crate 里测一遍、维护一遍,测试和维护面也随之翻倍。
证据
未经独立审计。没有做过任何第三方安全审计;下面列的都是项目自己的证据。每一处输出都逐字节比对过:标准 KAT 测试向量、GmSSL 3.2.0(互操作性)、以及 OpenSSL 3.x 的 SM4-XTS(可调模式,xts_standard=GB)。GmSSL 这项比对是 v1.10 起挂在 CI 里的任务,用从源码构建的 oracle 跑,截至 2026 年 10 月是合并的必需检查之一:字节对不上就失败,但钉住的 oracle 构建不出来或版本校验不过时,这组测试只会带着警告跳过,这项检查照样通过。
上面那套时序检查,会在改动密码代码的 PR 上跑,也会在 main 上每天跑一次,都跑在 GitHub 托管的 x86-64 Linux runner 上;核心在 [lints]
表里禁掉了 unsafe(unsafe_code = "forbid")。v0.14 那一轮还加了一套
cargo-fuzz(libFuzzer)模糊测试,覆盖不可信输入上的解码 / 解密路径——
当时 16 个目标,到 v1.8 扩到 30 个,到 v1.12 是 35 个——挂在 nightly 上扫。
这些都能自己去核对:源码、CI 工作流、一种检测时序泄漏的统计方法:对两类输入分别计时,看两组耗时分布是否有差异。它在给定的测量预算下检测泄漏,但无法证明泄漏不存在。 harness 全是公开的;已发布的 gmcrypto-core crate、它的 docs.rs 文档、以及能跑的示例也都在。
其中三条性质,固定在 v1.11.0,每条都直接链到对应的源码行:
- 涉密数据的相等判断走库里的常量时间比较路径;链接源码能核对这一实现选择,下面的 dudect 结果仍是经验性证据,不是证明: cipher.rs:627 。
- 核心 crate 禁止 unsafe 代码: Cargo.toml:177 。
- 改动密码代码的 PR 会跑 dudect 时序泄漏检查: dudect-pr.yml:169 。
再补两条,专门针对「常量时间」这个说法——也是最容易嘴上说、最难证明的部分:
- 检查中设有一个刻意引入时序泄漏的负对照
negative_control,harness 必须检测到这处泄漏;否则,该项时序检查失败( timing_leaks.rs:167 )。 - 也说得老实:这套 harness 只能检测泄漏,并不能证明常量时间( README.md:76 )。
有四篇笔记展开证据边界:CI 时序检查是怎么接线的、为什么值得展示的是负控制,而不是那排绿勾、把字节一致性当作互操作测试,以及把 unsafe 变成显式选择。
这张图把已发布测量值与当前策略放在一起:观测值来自一轮明确的运行,PR 检查阈值与每日哨兵值都只是阈值,不是证明。
dudect 报的是检测事件——|τ| 低只说明在给定预算下没测到泄漏,并不等于不存在。
| 目标 | 测的是什么 | |τ| | 阈值 | 状态 |
|---|---|---|---|---|
ct_sign |
SM2 签名,按私钥 d 分类 | 0.0044 | ≤ 0.20 | 在阈值内 |
ct_sign_k_class |
SM2 签名,按随机数 k 的量级分类 | 0.0708 | ≤ 0.55 (nightly 哨兵) | 在哨兵值内 |
ct_fn_invert |
Fn::invert 直接域求逆诊断 | 0.0071 | ≤ 0.55 (nightly 哨兵) | 在哨兵值内 |
ct_fp_invert |
Fp::invert 直接域求逆诊断 | 0.0063 | ≤ 0.55 (nightly 哨兵) | 在哨兵值内 |
negative_control |
negative_control:必须检测到此处刻意引入的泄漏(确认检测器能识别这处泄漏) | > 1.0 | > 1.0 | 必须检出泄漏 |
crypto-bigint 0.6 的 ConstMontyForm::invert |
修复前(crypto-bigint 0.6) | ≈0.7 | ≤ 0.20 | 超过阈值——已修复 |
crypto-bigint 0.6 的 ConstMontyForm::invert |
修复后(升级到 0.7.3) | ≈0.006 | ≤ 0.20 | 在阈值内 |
测量环境:v0.2 W0 harness,100K 采样(2026-05-12 之前的 runner)。k-class 签名和两个域求逆诊断后来转为遥测 + 0.55 的哨兵值。 来源:gm-crypto-rs SECURITY.md @ v1.2.0 ↗
有些轮次合进了主干,却没有在 crates.io 上单独发版:模糊测试、时序研究这类保障性工作,1.0 之前一起并进 1.0.0 的 API 定稿轮次,以及一轮只做设计的轮次。每一轮的原因都写在项目记录里;本站不把它们归纳成一条通用规则。来龙去脉,一篇笔记讲清。
- 当前版本
- v1.17.0,2026-10-26(UTC)发布——把
crypto-bigint最低版本提到 0.7.6,修复 RUSTSEC-2026-0387 和 RUSTSEC-2026-0385;MSRV 随之升到 1.87,SM2 变慢,输出的字节格式不变。C ABI 仍是 112 个入口。 - 版本缺口
- 已发布的版本线上有五处缺口——v0.14、0.17–0.23、v1.5、v1.10、v1.14–v1.15——它们都没改动任何输出字节。
- 完整版本线
- 从 v0.6.0 起的每一轮 →
下一步
- 下一步
- v1.6 起步的 TLCP(GB/T 38636)路线已在 v1.9 收官;v1.11 又落地了长期搁置的 RustCrypto
aead0.6 trait 适配;v1.12–v1.13 交付了流式 SM4-CCM(先 Rust,后 C);v1.16 用 GFNI S 盒取代了搁置已久的 AVX-512sbox_x64。目前没有下一条路线定到具体版本。另有一条是永久不做、而非暂缓:端点身份绑定——相当于 TLCP 里的主机名验证(hostname)——始终留给调用方。
它不是什么
- 未经独立审计——没有做过任何第三方安全审计。1.0 前那轮是两个 AI 模型做的对抗式评审,不是审计。
- 不是 TLS / TLCP 协议栈——
tlcp这部分是一套原语工具集(密钥派生、记录层保护 / 解保护、链与证书对验证)。没有握手状态机、没有协商、也没有会话管理:协议要由调用方自己驱动,v1.9 的 C 示例就是这么做的。 - 不是端点身份认证——
verify_chain/verify_pair只建立证书结构上的信任。把验证过的证书链绑定到具体的对端身份(相当于 TLCP 里的主机名验证)永远是调用方的事。 - 不包括 SM9、ZUC、后量子算法。
- 不是 HSM / SDF / SKF 集成。
- 不是通过美国联邦信息处理标准(FIPS)或其他项目认证的密码模块。
- 在乘法耗时跟操作数有关的 CPU 上(部分较老的 x86、部分嵌入式),不是常量时间的。
- 不对其他硬件做时序方面的说法:本页的时序结论只依据列出的 CI 证据,这些证据来自 x86-64 Linux runner。
个人项目。与 GmSSL、OpenSSL 做过逐字节一致性比对,但并不隶属于这两个项目,也未获它们或任何标准机构、厂商的背书或认证。