- Role
- Personal project
- Status
- Released
- Release
- v1.6.0
- Key outcome
- In-CI leak-regression gates for secret-touching paths.
- Stack
- RustCryptographyno_std
cargo add gmcrypto-core
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 no_std + alloc, builds on
wasm32-unknown-unknown, and ships RustCrypto-trait
fits for digest, mac, and cipher.
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 begins
TLCP (GB/T 38636) support: the key schedule plus a no-confirmation
SM2 key-exchange path.
A runnable consumer demo — hash / sign /
verify over SM3 and SM2 — lives at
gm-crypto-rs-demo.
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 SDK 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
what keeps it constant-time on every commit.
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
exercises nineteen secret-touching paths on every run; a core set
is gated at |τ| < 0.20, so a
timing regression fails the build instead of waiting for someone
to notice. 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 PR pays the measurement budget, and
statistics can never turn "not detected" into "absent."
Gate the core, observe the rest. Not every path
fits a PR's time budget, so the harness splits them. Gated
(build-failing): 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 and buffered
SM4-GCM / SM4-CCM decrypt, SM4-XTS decrypt, HMAC-SM3, and
encrypted-PKCS#8 decrypt. Telemetry-only: the field-inversion
diagnostics and the k-class sign, watched on the nightly run
against a 0.55 gross-regression sentinel rather than
the 0.20 gate — shared-runner class-split noise made
their tighter gates fire falsely, so they were 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 skip releases that
change nothing. 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. When a cycle changes no
output bytes — v0.14 was a cargo-fuzz parser-fuzzing
sweep that found zero crashes — it merges as assurance and is never
published, so crates.io skips 0.14.0. The same rule then
scaled up across the 0.17–0.23 gap — chiefly the
v0.21–v0.23 readiness work (public-API freeze,
crypto-bigint decoupling, a multi-model adversarial pre-1.0
re-audit) — which 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
Every output is checked for byte-identity: against the standard
KAT vectors, against gmssl 3.1.1 for interop, and against
OpenSSL 3.x SM4-XTS (xts_standard=GB)
for the tweakable mode. The leak-regression gates above run in CI
on every commit, 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,
twenty-seven as of v1.3 — 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.7.0, each a link
to the exact line:
- Secret-dependent comparison is constant-time
(
subtle'sct_eq) — cipher.rs:627 - The core crate forbids unsafe code — Cargo.toml:151
- A dudect timing-leak gate runs on every PR — dudect-pr.yml:166
Two more, specific to the constant-time claim — the parts that are easy to assert and hard to prove:
- The gate has teeth — a deliberately-leaky
negative_controlthe harness must flag, or CI fails — timing_leaks.rs:167 - Stated honestly — the harness detects leaks; it does not prove constant-time — README.md:44
Each secret-touching path is timed against the |τ| gate. Low |τ| = no leak detected under the budget; core paths fail the build if they cross the gate (a few carry looser nightly or telemetry policy).
dudect reports detection events — a low |τ| means no leak was detected under the budget given, not that none exists.
| Target | What it measures | |τ| | Gate | Status |
|---|---|---|---|---|
ct_sign |
SM2 sign, split by private key d | 0.0044 | < 0.2 | under gate |
ct_sign_k_class |
SM2 sign, split by nonce k magnitude | 0.0708 | < 0.2 | under gate |
ct_fn_invert |
Direct Fn::invert diagnostic | 0.0071 | < 0.2 | under gate |
ct_fp_invert |
Direct Fp::invert diagnostic | 0.0063 | < 0.2 | under gate |
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.2 | must fire |
crypto-bigint 0.6 ConstMontyForm::invert |
after (0.7.3 upgrade) | ≈0.006 | < 0.2 | under gate |
Measured: v0.2 W0 harness, 100K samples (pre-2026-05-12 runner). The two invert diagnostics later moved to telemetry + a |τ| ≥ 0.55 sentinel. Source: gm-crypto-rs SECURITY.md @ v1.2.0 ↗
Earlier releases (v0.6.0 – v0.13.0)
- v0.6.0
- 2026-05-14 — AVX2
sbox_x32, NEONsbox_x16, CBC-decrypt fanout. - v0.7.0
- 2026-05-14 — user-callable cipher modes: public batch APIs, single-shot & streaming SM4-CTR.
- v0.8.0
- 2026-05-15 — AEAD core: single-shot SM4-GCM + SM4-CCM, constant-time GHASH.
- v0.9.0
- 2026-05-17 — GCM tag-length parameterization, incremental-input buffered GCM, single-shot AEAD C FFI.
- v0.10.0
- 2026-05-21 — streaming SM4-GCM AEAD FFI (C / C++ / Go / Zig / Python).
- v0.11.0
- 2026-05-23 — RustCrypto trait fit on
digest 0.11/cipher 0.5; outputs byte-identical (KAT + gmssl 3.1.1). - v0.12.0
- 2026-05-23 — SM4-XTS core (GB/T 17964-2021): single-shot, full ciphertext stealing, byte-identical to OpenSSL SM4-XTS (
xts_standard=GB); opt-insm4-xts. - v0.13.0
- 2026-05-24 — single-shot SM4-XTS C ABI (the deferred FFI half of v0.12); default build byte-unchanged.
- v0.14.0
- Not published. A
cargo-fuzzparser-fuzzing sweep (16 targets, zero crashes) merged as assurance — no output change, no version bump. - v0.15.0
- 2026-05-28 — in-place multi-sector (disk) SM4-XTS helper; pure-core, opt-in
sm4-xts. - v0.16.0
- 2026-05-29 — multi-sector SM4-XTS C FFI; every cipher mode now reachable from C / C++ / Go / Zig / Python.
- 0.17 – 0.23
- Not published. Assurance & API-finalization cycles — public-API freeze with a
cargo-public-apidrift-check,crypto-bigintdecoupled from the always-on surface, and a multi-model adversarial pre-1.0 re-audit. crates.io skips them; their changes ship together in1.0.0. - v1.0.0
- 2026-06-01 — first stable release: all three crates published lockstep at
=1.0.0. The only published migration is 0.16 → 1.0; the runtime wire output (SM2 signatures / ciphertexts, SM4 mode bytes) is byte-identical to 0.16.0 (KAT + gmssl 3.1.1 interop 11/11), so the breaking changes are API shape only. From 1.0,cargo-semver-checksgates forward breaking changes. - v1.0.1
- 2026-06-03 — patch: the v1.0 readiness cleanup. No API / ABI / wire change (
cargo-public-api+cargo-semver-checksstayed green; a 1.0.0 consumer upgrades freely). Fixes the C ABIgmcrypto_version()(it returned a stale0.4.0) and lands docs + CI hardening — an x86_64 SIMD test job, ECB-misuse warnings on the raw single-block API, and dependency-coupling disclosures. - v1.1.0
- 2026-06-10 — SM2 key exchange (GM/T 0003.3) with key confirmation — the missing third of the SM2 family, after sign and encrypt. Opt-in
sm2-key-exchangefeature; a new gated dudect target (ct_sm2_key_exchange) and fuzz target (fuzz_sm2_kx). The default-features build is byte-identical to 1.0.1;cargo-semver-checkspasses as non-breaking. - v1.2.0
- 2026-06-11 — the C ABI for SM2 key exchange, on the usual core-in-vN / FFI-in-vN+1 cadence (63 → 72 FFI entry points). The GM/T 0003.5 recommended-curve KAT reproduces byte-for-byte through the C ABI, and FFI↔Rust handshakes cross-check in both directions. The core build is identical to 1.1.0 — with this, the SM2 family is complete in both Rust and C.
- v1.3.0
- 2026-06-11 — X.509-with-SM2: strict-DER parse of a v3 leaf certificate (GM/T 0015 profile) plus SM2-with-SM3 signature verification over the exact wire
tbsCertificatebytes. No trust decisions — no chain building, no clock, no extension interpretation, no revocation. New opt-inx509feature ongmcrypto-core(no new dependency); a new fuzz target (fuzz_x509, census 26 → 27) and no new dudect target — certificate verification consumes only public inputs. Default build byte-identical; all three crates bump lockstep to 1.3.0. - v1.4.0
- 2026-06-12 — the C ABI for X.509-with-SM2, on the usual core-in-vN / FFI-in-vN+1 cadence (72 → 85 FFI entry points): an opaque certificate handle, signature verify, and raw copy-out accessors. The no-trust-decisions contract crosses the ABI intact. The core build is identical to 1.3.0.
- v1.5.0
- Not published. The TLCP decomposition design cycle — scope and gap-mapping only, no output change. crates.io skips it; its plan ships with
1.6.0. - v1.6.0
- 2026-06-13 — the first code cycle of TLCP (GB/T 38636): the
tlcp::key_schedule(the TLS-1.2-style PRF over HMAC-SM3 — master secret, key block, Finished verify-data) plus no-confirmation SM2 key-exchange completers on the existingsm2-key-exchangetypestates. New opt-intlcpfeature (pure-core,no_std); key-schedule KATs from OpenSSL 3.xTLS1-PRFwithdigest:SM3. No new dudect target; the confirmed key-exchange flow is unchanged and remains the default. Default build byte-identical.
Next
- Next
- With the SM2 family and X.509-with-SM2 certificate verification both complete in Rust and C, the current direction is the rest of the TLCP (GB/T 38636) toolkit arc begun in v1.6 — the remaining building blocks are mapped in the project's decomposition doc but not yet committed to specific versions. Still parked: RustCrypto
aead 0.6trait fit (upstream remains on0.6.0-rc; v0.11 landed thecrypto-common 0.2line it needs), AVX-512sbox_x64, and streaming / incremental CCM.
What it isn't
- Not a TLS/TLCP stack — the v1.6
tlcpwork is key-schedule and key-exchange primitives, not a handshake or record layer. - Not SM9, ZUC, or post-quantum.
- Not an HSM / SDF / SKF integration.
- Not a certified cryptographic module.
- Not constant-time on CPUs with data-dependent multiply latencies (some older x86, some embedded).
Personal project. Cross-checked for byte-identity against gmssl 3.1.1 and OpenSSL, but not affiliated with, endorsed by, or certified by either project — or by any standards body or vendor.