Problem. While working with Chinese cryptographic tools, I found relatively few related tools in the Rust ecosystem, which led me to start this project.

My part. I defined the initial requirements and used AI coding assistants to implement features. I am new to Rust and focus primarily on practical usability and compatibility with existing implementations such as GmSSL. I do not regularly review the code line by line. Initially, I reviewed changes myself; later, to improve efficiency, I shifted more of the code review to AI agents following defined rules.

Outcome. gmcrypto-core v1.17.0 is on crates.io (published 2026-10-26, UTC). CI checks the standard test vectors and compares outputs against GmSSL. That comparison runs only when the GmSSL oracle's provisioning and version check succeed; if either fails, the comparison is skipped with a warning while the job still passes. Statistical timing tests watch selected paths for regressions on x86-64 Linux runners.

Limits. Not independently audited; the pre-1.0 review was an adversarial review by two AI models. Passing timing tests is evidence under the tested conditions, not proof of constant-time behavior.

Role
Personal project
Status
Released
Release
v1.17.0
Key outcome
In-CI timing-leak checks on secret-handling paths.
Stack
RustCryptographyno_std
Install
cargo add gmcrypto-core

Try it in the browser: SM2 / SM3 / SM4 Online Tool. Debug SM2/SM3/SM4 in your browser. Built-in self-test 11/11: SM3/SM4 against the GB/T vectors, SM2 by round trip. Everything runs locally and nothing is uploaded. The tool is unaudited, so don't use production keys.

gm-crypto-web SM4 encryption in ECB mode with the GB/T 32907 test vector: key and plaintext 0123456789abcdeffedcba9876543210 give the output 681edf34d206965e86b3e94f536e4246, with a green check reading “Matches the official test vector (GB/T 32907)”.
The tool checks itself against the GB/T 32907 example vector.

What it is

gm-crypto-rs implements GB/T 32905 (A Chinese national cryptographic hash standard (GB/T 32905) producing a 256-bit digest — broadly the SM-family counterpart to SHA-256. hash), GB/T 32918 (A Chinese national public-key standard (GB/T 32918) built on elliptic curves — used for digital signatures, key exchange, and public-key encryption. public-key sign / verify / encrypt / decrypt and, since v1.1, key exchange with key confirmation), and GB/T 32907 (A Chinese national block cipher standard (GB/T 32907) with a 128-bit block and 128-bit key, used for bulk symmetric encryption. block cipher) in pure Rust. The crate graph is A Rust build that doesn't link the standard library, so the code can run in constrained environments — embedded targets, kernels, WebAssembly. It has only `core`; heap allocation is opt-in, requiring `alloc` and an explicit allocator. + alloc, builds on wasm32-unknown-unknown, and ships RustCrypto-trait fits for digest, mac, and cipher — plus, since v1.11, opt-in aead 0.6 for SM4-GCM / SM4-CCM. Since v1.3 it also parses X.509 v3 certificates and verifies their SM2-with-SM3 signatures (the GM/T 0015 profile — strict-DER, no trust decisions), with the matching C-ABI entry points (in the gmcrypto-c crate) following in v1.4. v1.6 through v1.9 build out a TLCP (GB/T 38636) toolkit: the key schedule and a no-confirmation SM2 key-exchange path (v1.6), record protect / deprotect (v1.7), certificate-pair and chain signature verification (v1.8), and — since v1.9 — the whole toolkit reachable from C. v1.12 and v1.13 add length-committed streaming SM4-CCM, in Rust and then from C, and every SM4 streaming type now zeroizes on drop. Since v1.16, the opt-in SIMD SM4 path computes its S-box with GFNI on capable x86_64 CPUs. v1.17 fixes two dependency advisories and raises the MSRV to Rust 1.87, at the cost of slower SM2 field arithmetic. The published gm-crypto-rs-demo is a consumer and smoke-test tour of representative crate capabilities, including hashing, signing and verification, encryption and decryption, key exchange, and encoding flows. Its dependency is pinned to gmcrypto-core = "=1.17.0", the current release; the source-line evidence below stays pinned to v1.11.0.

sm2 / sm3 / sm4 → gmcrypto-core → gmcrypto-c (C ABI) and gmcrypto-simd (opt-in SIMD) sm2 sm3 sm4 gmcrypto-core gmcrypto-c C ABI gmcrypto-simd opt-in SIMD
fig. 3.1 — crate & C ABI surface (85 entry points at v1.4.0, unchanged through v1.8; 104 as of v1.9.0, 112 as of v1.13.0, still 112 at v1.17.0)

The problem

Rust already has SM2 / SM3 / SM4 implementations, and the good ones are designed for Code whose running time does not depend on secret values, so an attacker can't recover secrets by measuring how long it takes. It's a design target, not a guarantee — some hardware can still leak. secret-dependent operations. But design intent is asserted once, at review time, and then erodes quietly — a refactor, a new fast path, a dependency bump, and a timing leak slips back in with nothing to catch it. This library targets a combination that makes that erosion easy: byte-level GB/T conformance, a no_std / wasm core, secret-touching paths that must stay constant-time-designed across releases, and a C ABI that hands those same paths to non-Rust callers (C, C++, Go, Zig, Python). The question it is built around isn't is it constant-time today — it's how a change that brings a timing leak back gets noticed, on the paths and hardware the tests actually cover.

Constraints & key decisions

Verify continuously, don't assert once. Constant-time behavior is treated as a property CI re-checks, not a claim review signs off. A dudect-bencher harness measures twenty secret-touching paths. On pull requests that change the crypto code, a core set must keep a median |τ| ≤ 0.20 over three runs, so a timing regression on those paths fails that check instead of waiting for someone to notice. A skip marker such as [skip ci] in the pull-request title skips it, and it is not a required merge check: a failure flags the pull request rather than blocking the merge on its own. The harness reports detection events — a low |τ| means no leak was detected under the budget given, not that none exists (the wording is dudect-bencher's own). Cost: every crypto-touching PR pays the measurement budget, and statistics can never turn "not detected" into "absent."

Check the core, observe the rest. Not every path fits a PR's time budget, so the harness splits them. Checked on pull requests (a failure fails the check), sixteen paths: both scalar multiplications (fixed- and variable-base), SM2 sign, decrypt, and key exchange, SM4 key schedule, SM4 encrypt (default linear-scan and bitsliced SIMD S-box), SM4-CTR, CBC-decrypt fanout, single-shot SM4-GCM / SM4-CCM decrypt plus buffered SM4-GCM decrypt, SM4-XTS decrypt, encrypted-PKCS#8 decrypt, and — since v1.7 — the TLCP CBC record deprotect, the Lucky13 residual guard. One of them, CBC-decrypt fanout, is held to 0.55 instead of 0.20 when the runner's CPU is an AMD EPYC 9V74, whose no-change noise band is wider. Telemetry-only, four: the field-inversion diagnostics, the k-class sign, and — since 2026-06-17 — HMAC-SM3, reported but not checked on pull requests and held to a 0.55 gross-regression sentinel on the daily 100,000-sample run rather than the 0.20 limit — shared-runner class-split noise made their tighter gates fire falsely, so each was demoted with the reasoning published. Cost: the telemetry paths are watched, not enforced, and the two-tier split is a permanent surface to maintain.

A safe core; SIMD is opt-in. gmcrypto-core forbids unsafe code in its [lints] table (unsafe_code = "forbid"). The bitsliced SIMD S-box needs unsafe, so all of it is quarantined in a separate gmcrypto-simd crate behind the sm4-bitsliced-simd feature. Cost: the fast path isn't the default — you opt into SIMD, and the stock build runs the slower linear-scan S-box.

Constant-time-designed arithmetic, caveat stated. Secret-dependent arithmetic is built on subtle and crypto-bigint, and secret material is zeroized on drop via zeroize. The design targets constant time but can't guarantee it everywhere. Cost: on CPUs whose multiply latency is data-dependent (some older x86, some embedded), the timing properties don't hold — stated plainly under What it isn't rather than buried.

Ship the core first, the FFI next — and leave some cycles unpublished. Cipher modes land on a core-in-vN / FFI-in-vN+1 cadence: the SM4-XTS core shipped in v0.12 and its C ABI in v0.13; the in-place multi-sector disk helper in v0.15 and its FFI in v0.16; SM2 key exchange in v1.1 and its C ABI in v1.2; X.509-with-SM2 in v1.3 and its C ABI in v1.4. Some cycles merged without a published version, each for a reason the project records: v0.14, a cargo-fuzz parser-fuzzing sweep that found zero crashes, left the published crates byte-unchanged, so crates.io skips 0.14.0; and the 0.17–0.23 gap — chiefly the v0.21–v0.23 readiness work (public-API freeze, crypto-bigint decoupling, an adversarial pre-1.0 review by two AI models) — stayed unpublished and shipped together in the deliberate 1.0.0 release. Cost: the version line carries a deliberate gap to explain, and the C ABI doubles the surface every mode has to be tested and owned across three published crates.

Evidence

Not independently audited. No third-party security audit has been performed; everything below is the project's own evidence. Every output is checked for byte-identity: against the standard KAT vectors, against GmSSL 3.2.0 for interop, and against OpenSSL 3.x SM4-XTS (xts_standard=GB) for the tweakable mode. The GmSSL comparison is an in-CI job pinned to a from-source oracle build since v1.10 and, as of Oct 2026, one of the required checks for merging: a byte mismatch fails it, but if the pinned oracle cannot be built or fails its version check, the suite is skipped with a warning and the check still passes. The timing checks above run on pull requests that change the crypto code and daily on main, on GitHub-hosted x86-64 Linux runners, and the core forbids unsafe code via its [lints] table. The v0.14 cycle added a cargo-fuzz (libFuzzer) harness over the untrusted-input decode / decrypt surface — sixteen targets then, thirty as of v1.8, thirty-five as of v1.12 — run as a nightly sweep. All of it is inspectable: the source, the CI workflow, and the A statistical test for timing leaks: it times an operation over two input classes and checks whether their timing distributions differ. It detects leaks under a measurement budget — it can't prove none exist. harness are public, alongside the published gmcrypto-core crate, its docs.rs reference, and the runnable demo.

Three of those properties, pinned to v1.11.0, each a link to the exact line:

  • Secret-derived equality uses the crate's constant-time comparison path; the linked source shows that implementation choice, while the dudect evidence below remains empirical rather than a proof — cipher.rs:627 .
  • The core crate forbids unsafe code — Cargo.toml:177 .
  • A dudect timing-leak check runs on pull requests that change the crypto code — dudect-pr.yml:169 .

Two more, specific to the constant-time claim — the parts that are easy to assert and hard to prove:

  • The check has a built-in control — a deliberately-leaky negative_control the harness must flag, or the timing check fails — timing_leaks.rs:167 .
  • Stated honestly — the harness detects leaks; it does not prove constant-time — README.md:76 .

Four notes unpack the evidence boundary: how the CI timing check works, and why the negative control — not the green checks — is the part worth showing; byte identity as an interop test; and unsafe as an explicit opt-in.

fig. 3.2 — Constant-time, measured

Published measurements and current policy share one view: observed values are evidence from a named run, while the pull-request limit and the daily sentinel are thresholds, not proof.

PR check |τ| ≤ 0.20 Measured directly ≈0.7 ≈0.006 negative_control published ct_* — all under 0.20 0 0.55 1.0 |τ| (timing-leak statistic)

dudect reports detection events — a low |τ| means no leak was detected under the budget given, not that none exists.

Published dudect measurements (gm-crypto-rs, measured at v1.2.0; unchanged since)
Target What it measures |τ| Limit Status
ct_sign SM2 sign, split by private key d 0.0044≤ 0.20 within limit
ct_sign_k_class SM2 sign, split by nonce k magnitude 0.0708≤ 0.55 (nightly sentinel) within sentinel
ct_fn_invert Direct Fn::invert diagnostic 0.0071≤ 0.55 (nightly sentinel) within sentinel
ct_fp_invert Direct Fp::invert diagnostic 0.0063≤ 0.55 (nightly sentinel) within sentinel
negative_control negative_control — must fire here (proof the detector isn't blind) > 1.0 > 1.0 must fire
crypto-bigint 0.6 ConstMontyForm::invert before (crypto-bigint 0.6) ≈0.7 ≤ 0.20 Over limit — fixed
crypto-bigint 0.6 ConstMontyForm::invert after (0.7.3 upgrade) ≈0.006 ≤ 0.20 within limit

Measured: v0.2 W0 harness, 100K samples (pre-2026-05-12 runner). The k-class sign and the two invert diagnostics later moved to telemetry + a 0.55 sentinel. Source: gm-crypto-rs SECURITY.md @ v1.2.0 ↗

Some cycles merged without a new crates.io version: assurance work such as fuzzing sweeps and timing studies, the pre-1.0 API-finalization cycles that shipped together in 1.0.0, and one design-only cycle. The project records explain each one; the site does not state a general rule. The reasoning, in one note.

Current
v1.17.0, published 2026-10-26 (UTC) — fixes RUSTSEC-2026-0387 and RUSTSEC-2026-0385 by flooring crypto-bigint at 0.7.6, which raises the MSRV to 1.87 and makes SM2 slower; wire output is unchanged. The C ABI surface stays 112 entry points.
Unpublished
Five gaps in the published version line — v0.14, 0.17–0.23, v1.5, v1.10, v1.14–v1.15 — none of them changed an output byte.
Full history
Every cycle from v0.6.0 →

Next

Next
The TLCP (GB/T 38636) arc begun in v1.6 closed with v1.9; v1.11 then closed the long-parked RustCrypto aead 0.6 trait fit, v1.12–v1.13 shipped streaming SM4-CCM (Rust, then C), and v1.16 replaced the parked AVX-512 sbox_x64 item with a GFNI S-box. No successor arc is committed to a version yet. Permanently out of scope, not deferred: endpoint identity binding — the TLCP equivalent of hostname verification — stays the caller's.

What it isn't

  • Not independently audited — no third-party security audit has been performed. The pre-1.0 review was an adversarial review by two AI models, which is not an audit.
  • Not a TLS/TLCP stack — the tlcp work is a toolkit of primitives (key schedule, record protect / deprotect, chain and pair verification). There is no handshake state machine, no negotiation, and no session management: a caller drives the protocol, as the v1.9 C examples do.
  • Not endpoint authentication — verify_chain / verify_pair establish structural trust only. Binding a verified chain to a peer identity, the TLCP equivalent of hostname verification, stays the caller's permanently.
  • Not SM9, ZUC, or post-quantum.
  • Not an HSM / SDF / SKF integration.
  • Not a certified cryptographic module under the U.S. Federal Information Processing Standards (FIPS) or another certification program.
  • Not constant-time on CPUs with data-dependent multiply latencies (some older x86, some embedded).
  • No timing claim for other hardware: the timing claims here rest only on the CI evidence shown, which comes from x86-64 Linux runners.

Personal project. Cross-checked for byte-identity against GmSSL and OpenSSL, but not affiliated with, endorsed by, or certified by either project — or by any standards body or vendor.