gm-crypto-rs
Constant-time-designed pure-Rust SM2 / SM3 / SM4 SDK for Chinese national
cryptography (GB/T 32905 / 32918 / 32907 / GM/T 0009). SM2 sign / verify,
public-key encrypt / decrypt, key exchange (GM/T 0003.3), X.509-with-SM2
leaf certificate parse + signature verify; SM4-CBC / CTR / GCM / CCM / XTS
(single-shot and streaming); HMAC-SM3, PBKDF2-HMAC-SM3; plus a complete C
ABI (gmcrypto-c, 85 entry points) — all secret-touching paths guarded by
an in-CI dudect-bencher detectable-leak regression harness.
Personal project notice: not affiliated with, endorsed by, sponsored by, or certified by any upstream cryptography project, payment gateway, standards body, or vendor.
⚠️ 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, maintainer-run gmssl 3.1.1 interop (11/11, gated onGMCRYPTO_GMSSL— not run in CI), an in-CIdudecttiming-leak harness, and a 27-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.
What this is
A small, auditable, pure-Rust SM2 / SM3 / SM4 SDK whose central
differentiating commitment is that secret-touching code paths are
constant-time-designed and guarded by an in-CI dudect-bencher
detectable-leak regression harness: 19 real ct_* targets (12
always-on + 2 cfg-gated under sm4-bitsliced-simd + 3 cfg-gated under
sm4-aead + 1 cfg-gated under sm4-xts + 1 cfg-gated under
sm2-key-exchange) plus a deliberately-leaky
negative_control that proves
the harness can detect leaks. Most real targets gate at |tau| < 0.20;
ct_sign_k_class and the direct ct_fn_invert / ct_fp_invert invert
diagnostics carry target-specific gate policy after the 2026-05-12
recalibration — see SECURITY.md and
docs/v0.5-dudect-recalibration.md.
The harness reports timing-leak detection events. It does not prove
constant-time. Low |tau| values mean the test could not detect a leak with
the budget given, not that no leak exists. Language taken directly from
dudect-bencher's own docs.
The harness covers: SM2 sign (split by both private key d and nonce
k magnitude, with both retry nonces class-tied), SM2 decrypt (split
by recipient d_B), SM4 key schedule + single-block encrypt (split by
master key, under default linear-scan and sm4-bitsliced paths), the
v0.5 SIMD-packed dispatch (ct_sm4_encrypt_block_bitsliced_simd,
cfg-gated), v0.6's batched CBC-decrypt fanout
(ct_sm4_cbc_decrypt_fanout, cfg-gated), v0.7's SM4-CTR encrypt
(ct_sm4_ctr_encrypt, exercising the public batch path on every
cipher matrix entry), v0.8's SM4-GCM + SM4-CCM decrypt
(ct_sm4_gcm_decrypt and ct_sm4_ccm_decrypt, cfg-gated on
sm4-aead), v0.9's incremental-input buffered SM4-GCM decrypt
(ct_sm4_gcm_decrypt_buffered, cfg-gated on sm4-aead), v1.1's full
SM2 key-exchange initiator flow (ct_sm2_key_exchange, cfg-gated on
sm2-key-exchange — split by static d_A with per-class valid
responder transcripts), HMAC-SM3
(split by key), encrypted-PKCS#8
decrypt (split by password bytes — both classes' blobs valid for their
class's password so both succeed via identical control flow), plus
direct Fn::invert and Fp::invert diagnostics. The ct_sign_k_class
target closes v0.1's structural blind spot to nonce-only leaks.
The crypto-bigint 0.6 → 0.7.3 upgrade resolved the v0.1-era
ConstMontyForm::invert leak directly: on the v0.2 W0 harness both
direct invert diagnostics measured under |tau| ≈ 0.01, two orders of
magnitude below the gate. Subsequent GH Actions runner-image drift on
2026-05-12 raised the empirical noise floor on ct_fn_invert /
ct_fp_invert — both targets moved to PR-smoke telemetry + a nightly
gross-regression sentinel at |tau| ≥ 0.55. See
docs/v0.5-dudect-recalibration.md
for the data and posture. See SECURITY.md for the full
constant-time discipline.
The differentiator vs. existing Rust SM2 crates (notably
RustCrypto/sm2, which already aims for constant-time
secret-dependent operations in its design) is the in-CI regression gate, not
the design intent in isolation.
What this isn't
- Not a TLS/TLCP implementation.
- Not SM9, ZUC, 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).
- Not a comprehensive SM-crypto library yet — see the roadmap below.
Quick-start
use ;
use SysRng;
use hex;
// v0.5 W5 — `from_bytes_be` is the recommended public constructor
// (always-on, doesn't expose `crypto_bigint::U256` to callers).
let d_be: = hex!;
let key = from_bytes_be.expect;
// `public_key()` returns an `Sm2PublicKey` directly (v0.23).
let public = key.public_key;
// SM2 sign/encrypt take a fallible `rand_core::TryCryptoRng` (v0.23), so
// `getrandom::SysRng` is passed directly — no `UnwrapErr` wrapper.
let mut rng = SysRng;
let sig = sign_with_id.unwrap;
assert!;
SM2 key exchange (v1.1, opt-in sm2-key-exchange): an authenticated
two-party key 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 ;
// A (initiator) and B (responder) hold each other's static public keys.
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!; // 32-byte agreed key
X.509-with-SM2 (v1.3, opt-in x509): parse a DER v3 leaf certificate
and verify its SM2-with-SM3 signature against an issuer public key. This
makes no trust decisions — no chains, no clock, no extension
interpretation, no revocation; true means exactly "this issuer key signed
these exact wire tbsCertificate bytes":
use Certificate;
let cert = from_der.ok_or?;
assert!;
let _validity = ; // exposed; no clock
The same surfaces are reachable from C / C++ / Python / Go / Zig through
gmcrypto-c — see crates/gmcrypto-c/README.md
and the doc-only examples under
crates/gmcrypto-c/examples/
(sm2_sign.c, sm4_gcm_streaming.c, sm4_xts_sector.c,
sm4_xts_multisector.c, sm2_key_exchange.c, x509_verify.c).
Crates & features
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 shim (cdylib + staticlib): 85 entry points, committed gmcrypto.h drift-checked in CI. Always-on: a default build exports the full surface. |
gmcrypto-simd |
Internal AVX2/NEON/CLMUL/PMULL acceleration backend. No stable Rust API — use gmcrypto-core. |
gmcrypto-core features (default = []; all additive, all opt-in):
| Feature | Adds |
|---|---|
sm4-aead |
SM4-GCM + SM4-CCM single-shot AEAD, incremental-input buffered GCM (pulls gmcrypto-simd for GHASH). |
sm4-xts |
SM4-XTS (GB/T 17964-2021, not IEEE 1619): single-shot + in-place multi-sector disk helpers. Confidentiality only. |
sm2-key-exchange |
GM/T 0003.3 key agreement with key confirmation (typestate role state-machines). |
x509 |
X.509-with-SM2 leaf certificate parse + signature verify. No trust decisions. |
sm4-bitsliced |
Table-less, gate-only SM4 S-box (constant-time by construction; byte-identical output). |
sm4-bitsliced-simd |
AVX2 (x86_64) / NEON (aarch64) packed bitsliced SM4 batches; runtime detection, scalar fallback. |
digest-traits / cipher-traits |
RustCrypto trait fit (digest 0.11 / cipher 0.5) for Sm3 / HmacSm3 / Sm4Cipher. |
crypto-bigint-scalar |
Sm2PrivateKey::from_scalar(U256) — the documented crypto-bigint 0.7 escape hatch. |
Stability & SemVer
The line graduated to 1.0 (stable) with the 1.0.0 release; the current release is
1.4.0 (the C FFI for X.509-with-SM2). crates.io history
goes 0.16.0 → 1.0.0 → 1.0.1 → 1.1.0 → 1.2.0 → 1.3.0 → 1.4.0, skipping 0.17.0–0.23.0 (those were
non-publishing assurance + API-finalization milestones; their changes all shipped together
in the first stable 1.0.0). Every post-1.0 release has been additive (SemVer-checked);
the only migration ever required is 0.16 → 1.0, a single major bump — no published 0.x
consumer ever saw an intermediate break. The public API had been stable in
practice since v0.5; the v1.0 readiness audit (v0.21) froze and tooling-guarded
it, the v0.22 API-tightening cycle decoupled it from crypto-bigint 0.7, and
the v0.23 pre-1.0 re-audit remediation cycle applied the API/ABI-finality +
hardening fixes from a multi-model adversarial re-audit
(docs/v1.0-reaudit.md) —
see docs/v1.0-readiness.md.
From 1.0, SemVer is enforced: breaking changes to the covered surface require a
major bump, and cargo-semver-checks runs as the forward breaking-change gate in
CI (the three crates always release together at one lockstep version, with
intra-workspace deps pinned exactly — =1.4.0). The runtime wire output (SM2
signatures / ciphertexts, SM4 mode bytes) is byte-identical to 0.16.0.
- What's covered by SemVer: the public Rust API of
gmcrypto-core(the surface snapshotted indocs/api-baseline/gmcrypto-core.txt, drift-checked in CI) and thegmcrypto-cC ABI (the committedcrates/gmcrypto-c/include/gmcrypto.h, drift-checked in CI). - What's NOT covered: anything
#[doc(hidden)]—sm2::sign_raw_with_id(the dudect harness hook),Sm4Cbc{Encryptor,Decryptor}::take_output(FFI-shim drains), (v0.22) the low-level SM2 curve arithmeticsm2::curve/sm2::scalar_mul/ProjectivePoint::to_affine, and (v0.23) the raw EC point surfacesm2::point/ProjectivePoint(the type + module + re-export) +Sm2PublicKey::{from_point, point}, the low-levelasn1::{reader, writer, oid}modules, and the in-cratetraits::{Hash, Mac, BlockCipher}module (all keptpubonly for in-repo dev crates); and the entiregmcrypto-simdcrate, which is an internal acceleration backend with no stable Rust API (usegmcrypto-corefrom Rust,gmcrypto-cfrom C). These may change or be removed in any release. - High-level key path speaks keys, not points (v0.23).
Sm2PrivateKey::public_key()returnsSm2PublicKey(not the now-internalProjectivePoint);Sm2PublicKey::from_sec1_bytesis the on-curve-checked public point constructor.spki::{encode, decode}andsec1::EcPrivateKey.publicspeakSm2PublicKey. - RNG bound (v0.23).
sm2::{sign_with_id, encrypt}name the falliblerand_core::TryCryptoRngbound — a deliberate, documented ecosystem coupling (rand_coreis the RNG interop point, the RustCrypto-wide convention; unlike the v0.22crypto-bigintdecoupling, replacing it would hurt interop). An RNG failure collapses to the singleFailed, never a panic. - Single-shot SM4-GCM
encryptis fallible (v0.23).mode_gcm::{encrypt, encrypt_with_tag_len}returnOption<…>, rejecting plaintext past the2^36 − 32-byte GCM counter ceiling (matching the streaming path anddecrypt). - Features are additive (
default = []; all 9 are opt-in) and the build isno_std+alloc-only withunsafe_code = "forbid"on the core. - MSRV is 1.85 (edition 2024); an MSRV bump is treated as a minor, not a patch.
crypto-bigintdecoupling (v0.22): the always-on (default-features) public API names nocrypto-biginttypes — the byte-adjacent types (asn1::{encode,decode}_sig,Sm2Ciphertext::{x,y}) take/return[u8; 32], and the curve/scalar arithmetic is#[doc(hidden)](above). The only place acrypto-bigint 0.7type appears in the public API is the opt-incrypto-bigint-scalarfeature'sSm2PrivateKey::from_scalar(U256)— enabling that feature is an explicit opt-in to thecrypto-bigint 0.7type contract (acrypto-bigintmajor bump would be breaking for that feature). The recommended always-on path (Sm2PrivateKey::from_bytes_be) avoids it entirely. Seedocs/v1.0-readiness.md§3.A.
Release history & roadmap
Per-release narratives live in CHANGELOG.md (every
published version, Keep-a-Changelog format) and in the per-cycle scope
documents under docs/ (vX.Y-scope.md — including the
non-publishing assurance milestones v0.14 and v0.17–v0.23: parser fuzzing,
the open-source flip, dudect-gate hardening, the v1.0 readiness audit and
remediation).
The arc so far: v0.1–v0.16 built the primitive surface (SM2/SM3/SM4, all SM4 cipher modes incl. AEAD + XTS, the C ABI, SIMD acceleration); v0.17–v0.23 were the assurance + API-finalization run-up to 1.0.0; the 1.x line has been strictly additive — SM2 key exchange (1.1) + its C FFI (1.2), X.509-with-SM2 leaf parse/verify (1.3) + its C FFI (1.4).
Direction: TLCP (GB/T 38636) is the headline candidate — its
cryptographic prerequisites (SM2-KX, X.509-with-SM2) are now shipped; X.509
chain validation is the remaining building block and a deliberate
non-feature so far ("no trust decisions"). Smaller parked items (RustCrypto
aead trait fit, AVX-512, CCM buffered input, a class-split-aware dudect
noise-twin) are tracked in the scope docs.
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 in scope.
Build & test
DUDECT_SAMPLES=10000
gmssl interop test (gated; install gmssl
v3.1.1 to enable):
GMCRYPTO_GMSSL=1
wasm32 support
gmcrypto-core builds on wasm32-unknown-unknown as of v0.4. CI gates
both stable and MSRV (1.85) builds on the target.
The crate is no_std + alloc only and does NOT pull getrandom's
wasm_js backend or wasm-bindgen / js-sys into its default dep
graph. Wasm callers wire their own rand_core::Rng impl — typically
by enabling getrandom's wasm_js feature in their Cargo.toml:
[]
= "1.4"
= { = "0.10", = false }
= { = "0.4", = false, = ["wasm_js"] }
use ;
use SysRng;
let mut rng = SysRng; // wasm_js-backed when targeting wasm32
let sig = sign_with_id.unwrap;
A wasm-bindgen-test-driven test runner (running KAT vectors under
Node or a headless browser) is post-v0.4 — v0.4 ships the build-target
gate only.
License
Apache-2.0. See LICENSE.
Some reference outputs use the upstream gmssl
tool. This project is independent of that project.