gm-crypto-rs
Pure-Rust SM2 / SM3 / SM4 — the Chinese national cryptographic algorithms — with constant-time discipline that is measured in CI, not just intended.
For Rust services that must speak GB/T 32918 / 32905 / 32907 and want a
no_std core with no C dependency — and for C, C++, Python, Go and Zig
callers through a complete, always-on C ABI. Every secret-touching path is
written against subtle's constant-time primitives and guarded by a
dudect timing-leak harness that blocks merges.
use ;
use sm3;
use SysRng; // any `rand_core::TryCryptoRng`; this crate ships no RNG
⚠️ Not independently audited. No third-party / external security audit has been performed. Assurance is internal: a multi-model adversarial pre-publish re-audit (see
docs/v1.0-reaudit.md), in-CI KAT vectors, in-CI gmssl 3.2.0 interop (13/13, cross-validated against a pinned from-source build of the reference implementation; currently non-gating), an in-CIdudecttiming-leak harness, and a 35-targetcargo-fuzzsuite. This is a solo-maintained, best-effort open-source project with no support SLA. Review the code and use at your own risk. SeeSECURITY.mdfor the threat model and disclosure process.
Installation
[]
= "1.11"
= { = "0.4", = false, = ["sys_rng"] }
default = []. The base build is SM2, SM3, SM4-ECB/CBC/CTR, HMAC-SM3,
PBKDF2-HMAC-SM3 and the DER / PEM / PKCS#8 codecs, with no optional
dependency. Everything else below is behind a feature flag. The crate
deliberately pulls no RNG; SM2 signing and encryption take any
rand_core::TryCryptoRng, and getrandom's SysRng is the usual choice.
C / C++ / Python / Go / Zig callers want gmcrypto-c
instead. gmcrypto-simd is an internal backend — do not depend on it directly.
What's in the box
| Standard | Feature | |
|---|---|---|
| SM2 sign / verify, encrypt / decrypt | GB/T 32918, GM/T 0009 DER | default |
| SM3 hash, HMAC-SM3, PBKDF2-HMAC-SM3 | GB/T 32905, RFC 2104 / 8018 | default |
| SM4-ECB / CBC / CTR, single-shot and streaming | GB/T 32907 | default |
| DER / PEM / SPKI / SEC1 / PKCS#8 (incl. PBES2-encrypted) | RFC 5280 / 5915 / 5958 / 7468 | default |
| SM4-GCM / SM4-CCM AEAD, incremental-input GCM, length-committed streaming CCM | — | sm4-aead |
| SM4-XTS sector mode (confidentiality only) | GB/T 17964-2021 | sm4-xts |
| SM2 key exchange with key confirmation | GM/T 0003.3 | sm2-key-exchange |
| X.509-with-SM2 leaf parse + verify, linear chain verify | GM/T 0015 | x509 |
TLCP key schedule, record protection, [sign, enc] pair verify |
GB/T 38636-2020 | tlcp |
RustCrypto digest / cipher / aead trait fits |
— | *-traits |
| Table-less bitsliced SM4 S-box; AVX2 / NEON packed batches | — | sm4-bitsliced[-simd] |
Three crates, released together at one lockstep version:
| Crate | Role |
|---|---|
gmcrypto-core |
The no_std + alloc crypto core, unsafe_code = "forbid". The Rust API. |
gmcrypto-c |
C ABI, cdylib + staticlib: 104 entry points, committed gmcrypto.h drift-checked in CI. A default build exports the whole surface. |
gmcrypto-simd |
Internal AVX2 / NEON / CLMUL / PMULL backend. No stable Rust API. |
Why this rather than the alternatives
| gm-crypto-rs | libsm |
RustCrypto sm2 / sm4 |
|
|---|---|---|---|
| SM2 + SM3 + SM4 in one crate | ✅ | ✅ | separate crates |
| Timing-leak harness in CI | ✅ 20 dudect targets, 16 blocking |
— | — |
| Fuzzing | ✅ 35 targets, nightly | — | — |
| C ABI | ✅ 104 entry points | — | — |
| TLCP (GB/T 38636) toolkit | ✅ | — | — |
no_std |
✅ | not advertised | ✅ |
Enforced SemVer (cargo-semver-checks) |
✅ | — | — |
| External security audit | none | none | none |
| Production track record | thin — first published 2026 | years | years |
A dash means "not offered as a documented feature", checked against each project's crates.io metadata and repository in July 2026 — not a claim that the work is absent from someone's tree, and worth re-checking before you rely on it.
The last two rows are the honest counterweight and are meant to stay. If your
priority is the longest field exposure, libsm has years of it and this does
not. If your priority is verifiable constant-time discipline, a C ABI, or TLCP
building blocks, none of the alternatives offer them.
How "constant-time" is checked
The differentiator is not the design intent — RustCrypto/sm2
aims for constant-time too — it is the in-CI regression gate. Every PR
and every night, a dudect-bencher harness
times each secret-touching operation under two input classes and gates the
per-target |tau| statistic. A deliberately leaky negative_control must
fire on every run, proving the harness can still see a leak.
| Target | Secret it splits on | Gate |
|---|---|---|
ct_sign |
private key d and nonce k magnitude |
0.20 |
ct_sign_k_class |
nonce only, fixed d — the leak ct_sign cannot see |
sentinel 0.55 |
ct_mul_g, ct_mul_var |
scalar | 0.20 |
ct_sm2_decrypt |
recipient d_B |
0.20 |
ct_sm2_key_exchange (sm2-key-exchange) |
initiator's static d_A |
0.20 |
ct_sm4_key_schedule, ct_sm4_encrypt_block, ct_sm4_ctr_encrypt |
master key | 0.20 |
ct_sm4_encrypt_block_bitsliced_simd, ct_sm4_cbc_decrypt_fanout (sm4-bitsliced-simd) |
master key, through the SIMD batch path | 0.20 |
ct_sm4_gcm_decrypt, ct_sm4_gcm_decrypt_buffered, ct_sm4_ccm_decrypt (sm4-aead) |
master key, valid (ct, tag) for both classes |
0.20 |
ct_sm4_xts_decrypt (sm4-xts) |
master key, over a ciphertext-stealing tail | 0.20 |
ct_tlcp_cbc_deprotect (tlcp) |
recovered-fragment length, fixed key — the Lucky13 residual | 0.20 |
ct_hmac_sm3 |
key | sentinel 0.55 |
ct_pkcs8_decrypt |
password bytes, both blobs valid | 0.20 |
ct_fn_invert, ct_fp_invert |
field element, direct inversion diagnostics | sentinel 0.55 |
negative_control |
deliberately leaky | must fire, > 1.0 |
Sixteen of the twenty block a merge at |tau| <= 0.20. Four sit on a 0.55
gross-regression sentinel: they measure a composite window whose class split
reads as noise on some hosted-runner CPUs, so they report as telemetry and
only an egregious value fails. The harness reports detection events — it
does not prove constant-time. A low |tau| means no leak was detected with
the budget given, not that none exists. The full discipline, the hosted-runner
noise history, and every demotion with its data are in SECURITY.md
and docs/v0.5-dudect-recalibration.md.
Beyond timing: KAT vectors in CI, 35 cargo-fuzz targets run nightly, and an
interop suite against a pinned from-source GmSSL
3.2.0 — the reference implementation — cross-validating signatures,
ciphertexts and AEAD output byte-for-byte.
What this isn't
- Not a TLS/TLCP protocol implementation. The
tlcpfeature ships the cryptographic building blocks — key schedule, record protection, certificate-pair verification — but no handshake state machine, record framing, session orchestration or transport I/O. - Not SM9, ZUC, or post-quantum.
- Not an HSM / SDF / SKF integration, and not a certified cryptographic module.
- Not constant-time on CPUs with data-dependent multiply latencies (some older x86, some embedded).
verify_chain/verify_pairmake structural trust decisions only. Atruemeans the chain links to an anchor you supplied — never "this is the peer I dialed". Binding an identity to an endpoint is the caller's, permanently.
More examples
SM2 key exchange (sm2-key-exchange) — authenticated two-party agreement
with mandatory key confirmation. Each step consumes the state machine, so an
ephemeral cannot be reused and neither side sees the key before the peer's
confirmation tag verifies:
use ;
let init = new?;
let = init.produce_ephemeral?; // R_A -> B
let resp = new?;
let = resp.respond?; // (R_B, S_B) -> A
let = init_waiting.confirm?; // verifies S_B; S_A -> B
let k_b = resp_waiting.finish?; // verifies S_A
assert_eq!;
X.509-with-SM2 (x509) — parse a DER v3 leaf and verify its SM2-with-SM3
signature against an issuer key. true means exactly "this issuer key signed
these wire tbsCertificate bytes"; no clock, no extension interpretation, no
revocation:
use Certificate;
let cert = from_der.ok_or?;
assert!;
let _validity = ; // exposed, never compared
The same surfaces are reachable from C through gmcrypto-c; ten shipped
examples — SM2 signing, streaming GCM,
single-shot and length-committed streaming CCM, XTS sectors, key exchange,
X.509, a TLCP handshake and pair verification — are compiled in CI against the
committed header with -Wall -Wextra -Werror.
Stability & SemVer
- 1.x is stable. Every release since 1.0.0 has been additive;
cargo-semver-checksgates breaking changes in CI. The only migration ever required was 0.16 → 1.0. - Covered: the public Rust API of
gmcrypto-core(snapshotted indocs/api-baseline/, drift-checked in CI) and thegmcrypto-cC ABI (the committed header, drift-checked in CI). - Not covered: anything
#[doc(hidden)]— the low-level curve and point arithmetic, the raw DER reader / writer, the in-crate traits — and the whole ofgmcrypto-simd. These exist for in-repo dev crates and may change in any release. - Wire output is byte-identical to 0.16.0: SM2 signatures and ciphertexts, every SM4 mode.
- MSRV is 1.85 (edition 2024). An MSRV bump is a minor, not a patch.
- Features are additive,
default = [], all eleven opt-in. - No
crypto-biginttype in the always-on API. Byte-adjacent types take and return[u8; 32]. The one exception is the opt-incrypto-bigint-scalarfeature'sSm2PrivateKey::from_scalar(U256), and enabling it opts you into that crate's major-version contract. - Failures are opaque by design.
verify_with_idreturnsbool; every other fallible operation returns oneError::FailedorNone, never a reason. PRs that distinguish failure modes are rejected — seeSECURITY.md.
Release notes: CHANGELOG.md, every published version. The
per-cycle design records are under docs/; the verification-first
method behind them — pre-registered scope, adversarial review, executable
evidence gates, failures kept as receipts — is in CASE-STUDY.md.
The TLCP toolkit is complete as a toolkit; a sans-I/O protocol engine would be
a separate crate and has not been committed to.
Threat model
See SECURITY.md. Briefly: server-side use, dedicated host,
operator-trusted, network MITM in scope; side-channel attacks beyond what the
dudect harness covers are not.
Build & test
DUDECT_SAMPLES=10000
gmssl interop test (gated; install gmssl
v3.2.0 to enable — this is what Homebrew currently ships):
GMCRYPTO_GMSSL=1 GMCRYPTO_GMSSL=1
The suite pins its oracle version and fails with an ORACLE DRIFT message
on any other build. GmSSL renames subcommands and narrows accepted input
ranges between releases, so an unpinned oracle quietly changes what "interop
passes" means. To cross-validate against a different release deliberately,
set GMCRYPTO_GMSSL_VERSION (e.g. "GmSSL 3.1.1").
wasm32 support
gmcrypto-core builds on wasm32-unknown-unknown; CI gates both stable and
MSRV builds on the target.
The crate does not pull getrandom's wasm_js backend or wasm-bindgen into
its default graph. Wasm callers enable it in their Cargo.toml:
[]
= "1.11"
= { = "0.4", = false, = ["wasm_js"] }
There is no wasm-bindgen-test runner executing KAT vectors under Node or a
headless browser; adding one has not been committed to.
License
Dual-licensed under either of
- Apache License, Version 2.0 (
LICENSE-APACHEor https://www.apache.org/licenses/LICENSE-2.0) - MIT license (
LICENSE-MITor https://opensource.org/licenses/MIT)
at your option. Both texts ship inside every published crate from 1.11.0; archives up to 1.9.0 on crates.io do not contain them — the licence that governs those releases is unchanged, only the packaging was wrong.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.
Personal project notice: not affiliated with, endorsed by, sponsored by, or
certified by any upstream cryptography project, payment gateway, standards body,
or vendor. Some reference outputs use the upstream
gmssl tool; this project is independent of
it. Official ecosystem membership, layering, versioning and compatibility gates
are defined in the gmcrypto Rust ecosystem charter.