- 角色
- 个人项目
- 状态
- 已发布
- 版本
- v1.6.0
- 关键结果
- 用 CI 泄漏回归门禁守住涉密路径。
- 技术栈
- Rust密码学no_std
cargo add gmcrypto-core
是什么
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)。整套依赖都不碰标准库
(no_std + alloc),能编译到 wasm32-unknown-unknown,
也实现了 RustCrypto 的 digest / mac /
cipher trait,方便直接接入现有的 Rust 生态。从 v1.3 起,
它还能解析 X.509 v3 证书并验证其 SM2-with-SM3 签名(GM/T 0015 profile,
严格 DER,不做任何信任判定),对应的 C ABI 入口(在 gmcrypto-c
crate 里)在 v1.4 跟上。v1.6 开始铺 TLCP(GB/T 38636):密钥派生(key schedule),
外加一条免密钥确认的 SM2 密钥交换路径。想上手跑
一下,gm-crypto-rs-demo
里有现成例子(拿 SM3、SM2 做 hash / sign /
verify)。
要解决的问题
国密这几个算法,Rust 社区其实早有实现,写得好的那些,本来也都按
常量时间设计——意思是处理密钥的那段代码,跑多久跟密钥
无关,免得别人靠测耗时反推出密钥。难点在于,这种「常量时间」常常
只在代码评审那一刻成立;之后随便一次重构、一条为了赶进度新增的路径、
一次依赖升级,时序泄漏都可能悄悄溜回来,还没人当场发现。这个库
偏偏又卡在一堆容易让它松动的约束里:要逐字节对上 GB/T 标准、核心
要能跑在 no_std / wasm 上、涉密路径在每次发版之间都得
守住运行耗时不随密钥等秘密值变化的代码,这样攻击者就无法靠测量耗时来还原秘密。它是一个设计目标,而非保证——某些硬件仍可能泄漏。,还要通过一层 C ABI 把这些路径暴露给 C、C++、Go、
Zig、Python 调用。所以它真正要回答的,不是「今天是不是常量
时间」,而是「靠什么保证每次提交之后它还是常量时间」。
约束与关键决策
持续验证,而不是一次性拍板。
常量时间不该是评审会上签个字就算数的结论,而该是 CI 每次都重新
核一遍的性质。所以每次 CI 都会用 dudect-bencher(时序
侧信道检测工具)把 19 条涉密路径跑一遍;其中核心那一组
带门禁,阈值是统计量 |τ| < 0.20——
一旦越界就直接让构建挂掉,不用等人去发现。要说清楚的是,它报的是
「检测事件」:|τ| 低,只说明在当前的检测强度下没测到
泄漏,并不能证明泄漏不存在(这句措辞直接照搬了
dudect-bencher 自己的文档)。代价:每个
PR 都要付出这份测量开销;而且统计永远只能说「没测到」,证明不了
「不存在」。
核心路径上门禁,其余先观察。
不是每条路径都塞得进一个 PR 的时间预算,于是 harness 给它们分了层。
带门禁(不达标就让构建失败)的是:SM2 签名、解密与密钥
交换、SM4 密钥扩展、SM4 加密(线性扫描和位切片 SIMD S-box 两条
都算)、SM4-CTR、CBC 解密扇出、单次及缓冲式 SM4-GCM / SM4-CCM
解密、SM4-XTS 解密、HMAC-SM3、加密 PKCS#8 解密。只做遥测
的是:域求逆诊断和 k-class 签名——挂在 nightly 上、用
0.55 的粗回归哨兵值盯着,而不是 0.20
的门禁。共享 runner 上的类分割噪声让它们更紧的阈值频繁误报,
所以降级处理,理由也公开写明了。代价:遥测那
部分只是盯着、并不拦截,而且这套分层本身也得长期维护。
核心保持可审计,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;面向磁盘的多扇区原地辅助在
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 解析器模糊测试,
零崩溃),就只作为保障性工作并入主干、不单独发版,所以 crates.io 上
直接跳过了 0.14.0。同一条规矩后来放大到更大尺度:
0.17–0.23 这一段都没单独发版——主要是
v0.21–v0.23 的发版准备工作(冻结公开 API、把
crypto-bigint 从常开接口解耦、再做一轮多模型对抗式的 1.0 前复审)——
一起并进了刻意的 1.0.0 正式发布。代价:版本号因此留了
个需要解释的缺口;而且多出一层 C ABI,每个模式都得在三个已发布的
crate 里测一遍、维护一遍,工作量等于翻倍。
证据
每一处输出都逐字节比对过:标准 KAT 测试向量、gmssl 3.1.1
(互操作性)、以及 OpenSSL 3.x 的 SM4-XTS
(xts_standard=GB 模式)。上面那套泄漏回归门禁,
每次提交的 CI 都会跑一遍,核心则在 [lints] 表里
禁掉了 unsafe(unsafe_code = "forbid")。v0.14 那一轮还加了一套
cargo-fuzz(libFuzzer)模糊测试,专门覆盖不可信
输入的解码、解密路径——当时 16 个目标,到 v1.3 已经扩到 27
个——挂在 nightly 上扫。这些都能自己去核:
源码、CI 工作流、一种检测时序泄漏的统计方法:对两类输入分别计时,看两组耗时分布是否有差异。它在给定的测量预算下检测泄漏,但无法证明泄漏不存在。 harness 全是公开的,已发布的
gmcrypto-core crate、它的 docs.rs 文档、
以及那个能跑的示例也都在。
其中三条性质,固定在 v1.7.0,每条都直接链到对应的源码行:
- 涉密比较是常量时间实现的(
subtle的ct_eq)—— cipher.rs:627 - 核心 crate 禁止 unsafe 代码—— Cargo.toml:151
- 每个 PR 都跑 dudect 计时泄漏门禁—— dudect-pr.yml:166
再补两条,专门针对“常量时间”这个说法——也是最容易嘴上说、最难证明的部分:
- 门禁不是摆设——专门放一个会漏时的
negative_control,dudect 必须把它揪出来,否则 CI 直接失败—— timing_leaks.rs:167 - 也说得老实——这套门禁只能检测泄露,并不能证明常量时间—— README.md:44
每条涉密路径都对着 |τ| 门禁计时。|τ| 低,表示在该预算下没测到泄漏;核心路径一旦越过门禁就让构建失败(少数几条走更宽松的 nightly 或遥测策略)。
dudect 报的是检测事件——|τ| 低只说明在给定预算下没测到泄漏,并不等于不存在。
| 目标 | 测的是什么 | |τ| | 门禁 | 状态 |
|---|---|---|---|---|
ct_sign |
SM2 签名,按私钥 d 分类 | 0.0044 | < 0.2 | 在门禁内 |
ct_sign_k_class |
SM2 签名,按随机数 k 的量级分类 | 0.0708 | < 0.2 | 在门禁内 |
ct_fn_invert |
Fn::invert 直接诊断 | 0.0071 | < 0.2 | 在门禁内 |
ct_fp_invert |
Fp::invert 直接诊断 | 0.0063 | < 0.2 | 在门禁内 |
negative_control |
negative_control——必须在这里触发(证明检测器不是瞎的) | > 1.0 | > 1.0 | 必须触发 |
crypto-bigint 0.6 的 ConstMontyForm::invert |
修复前(crypto-bigint 0.6) | ≈0.7 | < 0.2 | 必须触发 |
crypto-bigint 0.6 的 ConstMontyForm::invert |
修复后(升级到 0.7.3) | ≈0.006 | < 0.2 | 在门禁内 |
测量环境:v0.2 W0 harness,100K 采样(2026-05-12 之前的 runner)。两个 invert 诊断后来转为遥测 + |τ| ≥ 0.55 的兜底哨兵。 来源:gm-crypto-rs SECURITY.md @ v1.2.0 ↗
更早的版本(v0.6.0 – v0.13.0)
- v0.6.0
- 2026-05-14 — AVX2
sbox_x32、NEONsbox_x16、CBC 解密扇出。 - v0.7.0
- 2026-05-14 — 用户可直接调用的密码模式:公开批量分组 API、单次和流式 SM4-CTR。
- v0.8.0
- 2026-05-15 — AEAD 核心:单次 SM4-GCM + SM4-CCM、常量时间 GHASH。
- v0.9.0
- 2026-05-17 — GCM 认证标签长度参数化、增量输入缓冲式 GCM、单次 AEAD C FFI。
- v0.10.0
- 2026-05-21 — 流式 SM4-GCM AEAD FFI(C / C++ / Go / Zig / Python)。
- v0.11.0
- 2026-05-23 — RustCrypto trait 适配迁到
digest 0.11/cipher 0.5;输出逐字节一致(KAT + gmssl 3.1.1)。 - v0.12.0
- 2026-05-23 — SM4-XTS 核心(GB/T 17964-2021):单次、完整密文窃取,和 OpenSSL SM4-XTS(
xts_standard=GB)逐字节一致;可选sm4-xts特性。 - v0.13.0
- 2026-05-24 — 单次 SM4-XTS 的 C ABI(v0.12 延后的 FFI 部分);默认构建逐字节不变。
- v0.14.0
- 未发布。合入了一轮
cargo-fuzz解析器模糊测试(16 个目标,零崩溃),作为保障性工作;输出无变化,不升版本号。 - v0.15.0
- 2026-05-28 — 面向磁盘的原地多扇区 SM4-XTS 辅助;纯核心,可选
sm4-xts特性。 - v0.16.0
- 2026-05-29 — 多扇区 SM4-XTS 的 C FFI;现在每个密码模式都能从 C / C++ / Go / Zig / Python 调到。
- 0.17 – 0.23
- 未发布。都是保障与 API 收尾的轮次——用
cargo-public-api漂移检查冻结公开 API、把crypto-bigint从常开接口解耦、再做一轮多模型对抗式的 1.0 前复审。crates.io 全部跳过;这些改动一起并进1.0.0发布。 - v1.0.0
- 2026-06-01 — 首个稳定版:三个 crate 以
=1.0.0锁步发布。唯一一次已发布的迁移就是 0.16 → 1.0;运行时实际输出的字节(SM2 签名 / 密文、SM4 各模式字节)与 0.16.0 逐字节一致(KAT + gmssl 3.1.1 互操作 11/11),所以破坏性变更只动了 API 的形状。从 1.0 起,cargo-semver-checks把关后续的破坏性变更。 - v1.0.1
- 2026-06-03 — 补丁版:1.0 的收尾清理。API / ABI / 输出字节均无变化(
cargo-public-api与cargo-semver-checks全程绿;1.0.0 的使用者可直接升级)。修正了 C ABI 的gmcrypto_version()(之前误返回旧的0.4.0),并补上文档与 CI 加固——x86_64 的 SIMD 测试任务、对裸单块 API 的 ECB 误用警告、以及依赖耦合的披露说明。 - v1.1.0
- 2026-06-10 — SM2 密钥交换(GM/T 0003.3),带密钥确认——SM2 算法族里缺的第三块,签名和加密此前都已发布。可选
sm2-key-exchange特性;新增一个带门禁的 dudect 目标(ct_sm2_key_exchange)和模糊测试目标(fuzz_sm2_kx)。默认特性构建与 1.0.1 逐字节一致;cargo-semver-checks判定为非破坏性。 - v1.2.0
- 2026-06-11 — SM2 密钥交换的 C ABI,照旧走「核心在 vN、FFI 在 vN+1」的节奏(FFI 入口从 63 个扩到 72 个)。GM/T 0003.5 推荐曲线的 KAT 测试向量穿过 C ABI 也能逐字节复现,FFI 与 Rust 两侧还做了双向交叉握手验证。核心构建与 1.1.0 完全一致——至此,SM2 算法族在 Rust 和 C 两侧都齐了。
- v1.3.0
- 2026-06-11 — X.509 与 SM2 结合:按 GM/T 0015 profile 严格 DER 解析 v3 叶子证书,并在精确的
tbsCertificate原始字节上做 SM2-with-SM3 签名验证。不做任何信任判定——不构建证书链、不看时钟、不解释扩展、不查吊销。新增可选x509特性(纯核心,无新依赖);新增一个模糊测试目标(fuzz_x509,census 26 → 27),不新增 dudect 目标——证书验证只吃公开输入。默认构建逐字节不变;三个 crate 锁步升到 1.3.0。 - v1.4.0
- 2026-06-12 — X.509 与 SM2 结合的 C ABI,照旧走「核心在 vN、FFI 在 vN+1」的节奏(FFI 入口从 72 个扩到 85 个):一个不透明的证书句柄、签名验证,以及若干原始拷出访问器。「不做信任判定」这条约束完整跨过 ABI。核心构建与 1.3.0 一致。
- v1.5.0
- 未发布。TLCP 的分解设计轮次——只做范围划定和缺口梳理,输出无变化。crates.io 跳过;它的方案随
1.6.0一起落地。 - v1.6.0
- 2026-06-13 — TLCP(GB/T 38636)的第一个代码轮次:
tlcp::key_schedule(基于 HMAC-SM3 的 TLS-1.2 式 PRF——主密钥、密钥块、Finished 校验数据),外加在现有sm2-key-exchangetypestate 上的免密钥确认完成器。新增可选tlcp特性(纯核心、no_std);密钥派生的 KAT 取自 OpenSSL 3.x 的TLS1-PRF(digest:SM3)。不新增 dudect 目标;带密钥确认的交换流程不变,仍是默认。默认构建逐字节不变。
下一步
- 下一步
- SM2 算法族与 X.509-with-SM2 证书验证在 Rust 和 C 两侧都已齐备;当前方向是把 v1.6 起步的 TLCP(GB/T 38636)工具链继续做下去——其余构件已在项目的分解文档里梳理过,但还没定到具体版本。继续搁置的还有:RustCrypto
aead 0.6的 trait 适配(上游仍在0.6.0-rc;它需要的crypto-common 0.2那条线,v0.11 已经铺好了)、AVX-512 的sbox_x64,以及流式 / 增量 CCM。
它不是什么
- 不是 TLS / TLCP 协议栈——v1.6 的
tlcp工作是密钥派生与密钥交换原语,不是握手或记录层。 - 不包括 SM9、ZUC、后量子算法。
- 不是 HSM / SDF / SKF 集成。
- 不是经过认证的密码模块。
- 在前面提到的那类 CPU 上(乘法耗时跟操作数有关,常见于部分老 x86、部分嵌入式)不保证常量时间。
个人项目。与 gmssl 3.1.1、OpenSSL 做过逐字节一致性比对, 但并不隶属于这两个项目,也未获它们或任何标准机构、厂商的背书或认证。