gm-crypto-rs
Constant-time-designed pure-Rust SM2 / SM3 / SM4 SDK for Chinese national
cryptography (GB/T 32905 / 32918 / 32907 / GM/T 0009). Sign / verify,
public-key encrypt / decrypt, SM4-CBC, SM4-CTR (single-shot + streaming),
length-flexible batched SM4 block encryption, HMAC-SM3, PBKDF2-HMAC-SM3 —
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 19-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 milestone roadmap.
Stability & SemVer
The line graduates to 1.0 (stable) with the 1.0.0 release; the current release is
1.0.1, a readiness-cleanup patch (no API/ABI change — see the v1.0.1 scope below).
crates.io history goes 0.16.0 → 1.0.0 → 1.0.1, skipping 0.17.0–0.23.0 (those were non-publishing assurance +
API-finalization milestones; their changes all ship together in the first stable
1.0.0). The only migration 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.0.1). 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 8 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.
v1.1 scope — SM2 key exchange (GM/T 0003.3)
Completes the SM2 family: GM/T 0003.2 sign + 0003.4/.5 encrypt shipped long
ago; v1.1 adds the missing third — GM/T 0003.3 ≡ GB/T 32918.3-2016 key
agreement with key confirmation — behind the opt-in sm2-key-exchange
feature (pure-core, no new dependency; the default-features build is
byte-identical).
- API: two role state-machines —
Sm2KxInitiator→produce_ephemeral→confirm→(Sm2SharedKey, S_A), andSm2KxResponder→respond→finish→Sm2SharedKey. Each step consumesself: an ephemeral cannot be reused, and the key is unreachable before confirmation passes. The agreed key isZeroizeOnDrop; every failure (off-curve peerR, bad tag, RNG failure, identityU, badklen/id) collapses to the singleError::Failed. - Constant-time posture: ephemeral via the existing fixed-budget masked
sampler;
t = (d + x̄·r) mod nand the scalar mults branch-free; confirmation tags compared withsubtle::ConstantTimeEqonly;t, the KDF input, andx_U/y_Uwiped after use. New dudect targetct_sm2_key_exchange(10K smoke|tau| ≈ 0.02, gate< 0.20). - KAT: byte-identical to the GM/T 0003.5-2012 recommended-curve worked
example (
K,S_A,S_B, all intermediate points) — note the example uses the default ID1234567812345678for both parties; seedocs/v1.1-sm2kx-kat-sourcing.md. - Assurance: new fuzz target
fuzz_sm2_kx(adversarial peerR_B/S_Bbytes, no-panic invariant);sm2-key-exchangelegs across the clippy/deny/MSRV/wasm32/dudect CI matrices. - C FFI deferred to v1.2 (the core-in-vN / FFI-in-vN+1 cadence).
v1.0.1 scope (shipped)
Readiness-cleanup patch — the first post-1.0 publish. v1.0.1 ships the
GO-WITH-FOLLOWUP cleanup from a release-readiness synthesis of the prior audits
(docs/audits/2026-06-02-release-readiness-synthesis.md):
0 blockers, all non-blocking polish.
- Functional fix (the one behavior change): the
gmcrypto-cC ABIgmcrypto_version()returned a hardcoded"0.4.0"regardless of the built version — it now reports the realCARGO_PKG_VERSION(so a C caller linking 1.0.1 reads"1.0.1"). This is the single reason 1.0.1 is a crates.io release rather than a docs-only update. - Doc improvements: raw-block "not a cipher mode" ECB warnings on
Sm4Cipher::{encrypt,decrypt}_blockand the corresponding block FFI; cbindgen header pointer/length preconditions; FFI notes on the fallible RNG path and the XTSstart_sectorrange; pre-1.0-stability caveats on thedigest-traits/cipher-traitstrait impls; andSECURITY.md/README.md/deny.tomlcorrections. - CI-health fixes:
sm4-xtsadded to the MSRV / wasm32 /cargo denypasses; the dudect path-allowlist gainedgmcrypto-simd/src/**;cargo generate-lockfileruns beforecargo deny; a newsimd-x86job (cargo test -p gmcrypto-simdonubuntu-latest) that immediately caught a real latent bug — the x86-only SIMD test files lacked#![allow(unsafe_code)], so they had never compiled under CI's-D warnings(fixed); and thepull_requestpaths-ignorewas removed fromci.ymlso docs-only PRs are no longer permanently blocked by branch-protection required checks.
No API or ABI change; runtime crypto wire output is byte-identical to 1.0.0 —
cargo-semver-checks runs enforced as the patch-non-breaking gate. 6 merged PRs
(#87–#92). Consumers move 1.0.0 → 1.0.1 with a plain cargo update.
v0.16 scope (shipped)
C FFI for the SM4-XTS multi-sector helper. v0.16 exposes the v0.15
sm4::mode_xts::{encrypt_sectors, decrypt_sectors} through the gmcrypto-c C
ABI (behind the existing forwarding sm4-xts feature): two new symbols
gmcrypto_sm4_xts_encrypt_sectors / gmcrypto_sm4_xts_decrypt_sectors that
transform a contiguous run of equal-size sectors in place (buf: *mut u8 +
buf_len), deriving sector i's tweak as little-endian-128(start_sector + i)
— start_sector is a uint64_t LBA. Unlike the single-shot XTS FFI (uniformly
out-of-place), these are in-place — mirroring the core's &mut [u8] API so
disk callers never double-allocate. Byte-identical to the core helper; single
GMCRYPTO_ERR with buf untouched on error; confidentiality only (no auth).
The deferred FFI half of v0.15, on the established core-in-vN / FFI-in-vN+1
cadence — every cipher mode is now FFI-complete. No new dependency, no new
feature flag, no new gmcrypto-core API, no new dudect target. Design
rationale: docs/v0.16-scope.md (Q16.1–Q16.12).
v0.20 scope (infra-assurance, not a crates.io release) — streaming-decryptor differential fuzzing + coverage
Two new differential fuzz targets + cargo fuzz coverage + a codified v1.0
constant-time baseline. fuzz_sm4_cbc_streaming_decrypt and
fuzz_sm4_gcm_streaming_decrypt feed the ciphertext to the streaming
decryptors (Sm4CbcDecryptor / Sm4GcmDecryptor) in arbitrary chunk
boundaries and assert the result is byte-identical to the single-shot
mode_{cbc,gcm}::decrypt oracle — a differential invariant (catches the CBC
buffer-back-by-one PKCS#7 boundary and the GCM commit-on-verify GHASH
accumulator), stronger than v0.14's no-panic property. The nightly fuzz sweep
grows to 18 targets (initial sweep: zero crashes, zero divergences) and gains
a non-gating cargo fuzz coverage job that renders per-target llvm-cov
TOTALS over the committed seed corpus and uploads them (the report is the
deliverable, not a coverage-% gate). v0.20 also codifies the settled v1.0
constant-time baseline in SECURITY.md: composite dudect
targets stay gated |tau| < 0.20; the two single-inversion micro-diagnostics
remain telemetry + a |tau| ≥ 0.55 sentinel (the v0.19 falsification is the
evidence), with a narrow revisit door (a class-split-twin without the inversion
op, or offline/dedicated hardware — never PR-executing public self-hosted CI).
The theme was chosen after a Codex + Grok strategy discussion (one more assurance
cycle that feeds v1.0 readiness, over a third dudect cycle or new features). A
repository / infra-assurance milestone — only the workspace-excluded fuzz/
crate + fuzz-nightly.yml + docs change (workspace stays 0.16.0; crates.io
skips 0.20.0 per the v0.14/v0.17/v0.18/v0.19 precedent). Design + result:
docs/v0.20-scope.md (Q20.1–Q20.5). Next: v0.21 = the
v1.0 readiness audit, with v0.20's harnesses + coverage as input evidence.
v0.19 scope (infra-assurance, not a crates.io release) — relative gate tested and falsified
Self-calibrating relative dudect gate — TESTED and FALSIFIED → honest fallback.
v0.19 set out to re-promote the two direct-invert diagnostics
(ct_fn_invert / ct_fp_invert) off the v0.18 telemetry/sentinel posture by
adding two fix-vs-fix noise-floor probes (noise_floor_fn_invert /
noise_floor_fp_invert — each runs the same Fn/Fp inversion as its suspect
but feeds both dudect classes one identical input, so its |tau| is pure
measurement noise) and gating each target relatively:
median(target) ≤ max(0.20, 4·median(probe)) — a threshold that adapts to the
runner's own noise floor.
The 100K calibration on main falsified the matched-sensitivity premise: the
probes stay uniformly quiet (~0.005) while the real class-split targets spike
intermittently into [0.26–0.32] (ct_fp_invert reached a median of 0.2606 on
the sm4-bitsliced-simd leg, ratio 50). The runner noise lives in the two-input
class-split difference (z_small vs z_large), not the operation duration a
same-input probe can observe — so the probe cannot track it and the relative
threshold just pins at the 0.20 the noise already breaks. Per the pre-committed
honest-fallback path, the relative gate is demoted to non-blocking telemetry, the
two targets revert to telemetry (PR) / gross-regression sentinel @0.55
(nightly), and the probes are kept as telemetry — they are the evidence that
the noise is class-split-specific, the input to a v0.20 class-split-aware
"noise-twin" reference. A repository / infra-assurance milestone — the only
crate change is the dev-only bench harness (published library byte-unchanged;
workspace stays 0.16.0; crates.io skips 0.19.0 per the v0.14 / v0.17 / v0.18
precedent). Design + result:
docs/v0.19-scope.md (Q19.1–Q19.7) +
docs/v0.5-dudect-recalibration.md (v0.19
resolution).
Deferred to v0.20+: a class-split-aware "noise-twin" dudect reference (the
v0.19 successor that could finally re-promote the invert diagnostics);
round-trip / differential + streaming-decryptor parser fuzzing; RustCrypto aead
trait fit (still 0.6.0-rc.10); cargo fuzz coverage; AVX-512 sbox_x64; CCM
buffered input; a v1.0 readiness pass.
v0.18 scope (shipped — infra-assurance, not a crates.io release)
dudect-gate hardening. v0.18 pins the dudect CI workflows' drift axes
(ubuntu-24.04 OS-label + exact dtolnay/rust-toolchain@1.95.0) and gates on a
CI-level multi-run median |tau| (PR 3 runs / nightly 5 runs; the
required_low gates + the nightly gross-regression sentinel use the median,
negative_control uses the min, and any required target not measured on
every run fails). The bench harness timing_leaks.rs is byte-unchanged — the
loop and median live entirely in CI. A 100K×5 calibration measured the
ct_fn_invert/ct_fp_invert diagnostics back near their ~0.006 baseline, but
they were kept on the telemetry / sentinel posture (not re-promoted): the
noise that demoted them is runner-image-sensitive and would re-flake a tight gate
if it returns — robustness over a tighter gate. A repository / infra-assurance
milestone — no crate code change (workspace stays 0.16.0; crates.io skips
0.18.0 per the v0.14 / v0.17 precedent). Design rationale:
docs/v0.18-scope.md (Q18.1–Q18.7) +
docs/v0.5-dudect-recalibration.md (v0.18
resolution).
Deferred to v0.19+ (per docs/v0.18-scope.md §5/§6):
a self-calibrating relative dudect gate (the change that could safely re-promote
the invert diagnostics); round-trip / differential + streaming-decryptor parser
fuzzing; RustCrypto aead trait fit (still 0.6.0-rc.10); cargo fuzz coverage;
AVX-512 sbox_x64; CCM buffered input; a v1.0 readiness pass.
v0.15 scope (shipped)
SM4-XTS multi-sector (disk) helper. v0.15 adds
sm4::mode_xts::{encrypt_sectors, decrypt_sectors} (opt-in sm4-xts feature):
encrypt/decrypt a contiguous run of equal-size disk sectors in place
(&mut [u8] -> Option<()>), deriving sector i's tweak as the little-endian
128-bit encoding of start_sector + i (the standard disk-XTS data-unit
convention). It owns the sector-number → tweak encoding the single-shot v0.12 API
left to the caller, and is byte-identical to looping that API per sector. Single
None failure mode (buf untouched on validation failure); confidentiality
only (no authentication). Pure-core: no new dependency, no new feature flag, no
new SIMD, no new dudect target. Design rationale:
docs/v0.15-scope.md (Q15.1–Q15.12). The C FFI for the
sector helper shipped in v0.16 (above), on the established core-in-vN /
FFI-in-vN+1 cadence.
crates.io goes 0.13.0 → 0.15.0: 0.14.0 names the unpublished
parser-fuzzing assurance cycle (below) and is intentionally never published.
v0.14 — parser fuzzing (assurance; not a crates.io release)
Pre-v1.0 hardening. v0.14 adds a cargo-fuzz (libFuzzer) harness over the
entire untrusted-input decode/decrypt surface of gmcrypto-core — 16
targets covering PEM, PKCS#8 (incl. PBES2 decrypt), SPKI, SEC1, the DER reader
primitives, SM2 DER + raw ciphertext, SM2 decrypt + signature-verify, and the
SM4-CBC/GCM/CCM/XTS decrypts — proving the failure-mode invariant on adversarial
bytes: no panic, no unbounded allocation, no hang. A capped nightly job
(.github/workflows/fuzz-nightly.yml) runs them on a schedule.
The initial sweep found zero crashes across all 16 targets, so v0.14 makes
no code change to the published crates and is not cut as a crates.io
release (publishing byte-identical crypto is release noise) — it lands as an
assurance/infra change. The fuzz crate lives in a workspace-excluded fuzz/
(nightly-only; never enters the published dependency graph). Design rationale:
docs/v0.14-scope.md. Run it yourself:
fuzz/README.md.
Deferred to v0.15+ (per docs/v0.14-scope.md §5/§6):
the SM4-XTS per-sector helper (shipped in v0.15, above); round-trip /
differential parser fuzzing, streaming-decryptor fuzzing, RustCrypto aead
trait fit (still 0.6.0-rc.10), pinned dudect runner, cargo fuzz coverage in
CI, AVX-512 sbox_x64, a v1.0 readiness pass (now v0.16+).
v0.13 scope (shipped)
C ABI for SM4-XTS. v0.13 exposes the v0.12 sm4::mode_xts core through the
gmcrypto-c C ABI (gmcrypto_sm4_xts_encrypt / _decrypt) behind a new
forwarding sm4-xts feature — the deferred FFI half of v0.12, on the
established core-then-FFI cadence (SM4-GCM/CCM core in v0.8 → FFI in v0.10).
Design rationale: docs/v0.13-scope.md.
- Additive only — no public API breakage, no new dependency. The default
build of both crates is byte-unchanged;
sm4-xtsforwards to the pure-coregmcrypto-core/sm4-xts. - Single-shot, mirroring the single-shot SM4-GCM FFI shape minus nonce/AAD/tag:
32-byte key (
Key1 ‖ Key2), 16-byte tweak, length-preserving output via the(out, out_capacity, out_actual_len)convention. Byte-identical togmcrypto_core::sm4::mode_xts. NewGMCRYPTO_SM4_XTS_KEY_SIZEheader constant; singleGMCRYPTO_ERRfailure mode. Confidentiality only. - Doc-only C example
crates/gmcrypto-c/examples/sm4_xts_sector.c; 5 newc_smokeRust-equivalence tests. No newgmcrypto-coreAPI, no new dudect target (the FFI is a thin shim over the v0.12 core path).
Followed by v0.14 (per docs/v0.13-scope.md §5/§6):
parser fuzzing — the recommended pre-v1.0 assurance gate — landed as the v0.14
assurance cycle above. RustCrypto aead trait fit (still 0.6.0-rc.10),
pinned/noise-isolated dudect runner, and AVX-512 sbox_x64 remain deferred.
v0.12 scope (shipped)
SM4-XTS — tweakable mode for disk/sector encryption. v0.12 adds
sm4::mode_xts behind the new opt-in sm4-xts feature: single-shot, full
ciphertext stealing, GB/T 17964-2021 (GM-T OID 1.2.156.10197.1.104.10),
byte-identical to OpenSSL 3.x EVP SM4-XTS (xts_standard=GB). Design
rationale: docs/v0.12-scope.md; KAT sourcing:
docs/v0.12-xts-kat-sourcing.md.
- Default-features users are unaffected — additive, opt-in, no new
dependency (the XTS tweak doubling is a trivial bit-reflected
multiply-by-x, not GHASH, so no
gmcrypto-simddep). - GB/T 17964, not IEEE 1619 — the two standards differ in the GF(2¹²⁸) tweak-doubling convention (GB is the bit-reflected / GHASH-style one), so they produce different ciphertext for multi-block / non-aligned data. v0.12 targets GB (the SM4 national standard + OpenSSL's default for SM4-XTS).
- Confidentiality only — no authentication. XTS has no tag; callers needing integrity use an AEAD mode (GCM/CCM). The per-data-unit tweak-uniqueness contract is the caller's responsibility.
- 32-byte key (
Key1 ‖ Key2) + raw 16-byte tweak; lengths[16 B, 16 MiB]; singleNonefailure mode. New dudect targetct_sm4_xts_decrypt. The whole- block bulk rides theSm4Cipher::encrypt_blocksbatch API (picks up the SIMD fanout undersm4-bitsliced-simd).
Deferred to v0.13 (per docs/v0.12-scope.md §5/§6):
C FFI for SM4-XTS, RustCrypto aead trait fit, pinned/noise-isolated dudect
runner, AVX-512 sbox_x64, CCM incremental input.
v0.11 scope (shipped)
RustCrypto trait-fit modernization. v0.11 migrates the opt-in
digest-traits / cipher-traits impls from digest 0.10 / cipher 0.4 to
digest 0.11 / cipher 0.5 (the crypto-common 0.2 / hybrid-array
generation), in-place. Design rationale:
docs/v0.11-scope.md.
- Default-features users are unaffected — the trait fit is opt-in;
generic-array/hybrid-arraynever enter the default dep graph, and every SM2 / SM3 / SM4 / HMAC / AEAD output is byte-identical (validated against the full KAT suite + gmssl 3.1.1 interop). - BREAKING for trait-fit consumers only: code enabling
digest-traits/cipher-traitsmust bump its owndigest/cipherdeps to0.11/0.5. HMAC construction via theMactrait moves todigest::KeyInit::new_from_slice(digest 0.11'sMacdroppedKeyInit); thecipherblock traits renamedBlockEncrypt/BlockDecrypt→BlockCipherEncrypt/BlockCipherDecrypt. - MSRV stays 1.85. The RustCrypto
aead 0.6trait fit remains deferred (still0.6.0-rc.10); v0.11 lands thecrypto-common 0.2line it will need.
Deferred to v0.12 (per docs/v0.11-scope.md §5/§6):
RustCrypto aead trait fit, pinned/noise-isolated dudect runner, AVX-512
sbox_x64, SM4-XTS, CCM incremental input, Argon2-with-SM3.
v0.10 scope (shipped)
Streaming AEAD FFI. v0.10 exposes the v0.9 incremental-input buffered
SM4-GCM encryptor/decryptor through the gmcrypto-c C ABI — the item
v0.9 deferred (Q9.6) now that the Rust streaming API is proven. Additive
behind the existing sm4-aead feature. Design rationale:
docs/v0.10-scope.md.
- 9 streaming AEAD C FFI symbols + 2 opaque handle types —
gmcrypto_sm4_gcm_encryptor_t(output-streaming:new/update→ ciphertext per chunk /finalize+finalize_with_tag_len→ tag /free) andgmcrypto_sm4_gcm_decryptor_t(commit-on-verify:new/updatebuffers and emits nothing /finalize_verifyreleases plaintext only after the constant-time tag check /free)._finalize*consume+free the handle; singleGMCRYPTO_ERRon every failure (no tag-/length-oracle across the boundary). Mirrors the v0.5 CBC-streaming lifecycle. C example:examples/sm4_gcm_streaming.c.
No public API breakage — purely additive. v0.9.0 callers can
cargo update to v0.10.0 without migration. No new gmcrypto-core API;
no new dudect target (the FFI is a thin wrapper over the v0.9
ct_sm4_gcm_decrypt_buffered-gated path).
Deferred to v0.11 (per docs/v0.10-scope.md
§5/§6): streaming/incremental CCM, RustCrypto aead trait fit (upstream
still 0.6.0-rc.10), pinned dudect runner, AVX-512 sbox_x64, SM4-XTS,
Argon2-with-SM3.
v0.9 scope (shipped)
AEAD ergonomics. v0.9 extends the v0.8 AEAD core with the three
items v0.8 deferred: GCM tag-length parameterization, incremental-input
buffered SM4-GCM, and single-shot AEAD C FFI. All additive behind the
existing sm4-aead flag. Design rationale: docs/v0.9-scope.md.
sm4::GcmTagLen+mode_gcm::encrypt_with_tag_len/decrypt_with_tag_len— W1. Caller-chosen GCM tag length per NIST SP 800-38D §5.2.1.2 ({4, 8, 12, 13, 14, 15, 16}bytes; truncated tag =MSB_t(full_tag)).GcmTagLen::new(usize) -> Option<Self>centralizes the valid-length policy. The fixed-16-byteencrypt/decryptare unchanged.sm4::Sm4GcmEncryptor/Sm4GcmDecryptor— W2. Incremental- input buffered SM4-GCM (deliberately NOT "streaming"). The encryptor is output-streaming:update(chunk) -> Option<Vec<u8>>emits each chunk's ciphertext (Noneonce the cumulative plaintext would exceed the NIST §5.2.1.1 ceiling2^36 − 32bytes);finalize()/finalize_with_tag_len()emit the tag. The decryptor is input-incremental but output-BUFFERED:update(chunk)buffers ciphertext + folds GHASH, andfinalize_verify(tag) -> Option<Vec<u8>>releases the plaintext only after the constant-time tag check (commit-on-verify — never leaks pre-verify bytes). AAD is supplied at construction. Driven with any chunking, both reproduce the single-shot path byte-for-byte.- 6 single-shot AEAD C FFI entry points — W4.
gmcrypto_sm4_gcm_ encrypt/_decrypt/_encrypt_with_tag_len/_decrypt_with_tag_ len+gmcrypto_sm4_ccm_encrypt/_decrypt, behind a new forwardingsm4-aeadfeature ongmcrypto-c. Every error path returnsGMCRYPTO_ERR(single failure code). Streaming AEAD FFI is deferred to v0.10. - New dudect target
ct_sm4_gcm_decrypt_buffered— W3. Class-split by master key, drivesSm4GcmDecryptor;|tau| < 0.20(5K-sample smoke|τ| ≈ 0.029). No new CI matrix slot — rides the existingsm4-aeadentries.
No public API breakage — purely additive. v0.8.0 callers can
cargo update to v0.9.0 without migration; sm4-aead is opt-in.
Deferred to v0.10 (per docs/v0.9-scope.md
§5/§6): CCM incremental input, streaming AEAD FFI, RustCrypto aead
trait fit (upstream still on 0.6.0-rc), pinned dudect runner,
AVX-512 sbox_x64, SM4-XTS, Argon2-with-SM3.
v0.8 scope (shipped)
The AEAD core. v0.8 cashed in the cipher-mode surface that v0.7
opened up: SM4-GCM and SM4-CCM single-shot, plus a constant-time
GHASH primitive in gmcrypto-simd.
sm4::mode_gcm::encrypt/decrypt— W2. Single-shot SM4-GCM per NIST SP 800-38D / GM/T 0009 / RFC 8998.encrypt(key, nonce, aad, pt) -> (Vec<u8>, [u8; 16])returns(ciphertext, tag).decrypt(key, nonce, aad, ct, tag) -> Option<Vec<u8>>—Some(plaintext)only when the tag verifies (constant-time compare viasubtle::ConstantTimeEq). Both 12-byte canonical and arbitrary-length nonce paths supported. Tag length fixed at 128 bits in v0.8 (parameterized in v0.9 viaGcmTagLen). Byte-identical to gmssl 3.1.1sm4 -gcm— bidirectional interop validated.sm4::mode_ccm::encrypt/decrypt— W3. Single-shot SM4-CCM per NIST SP 800-38C / RFC 3610 / GM/T 0009 (OID1.2.156.10197.1.104.9).encrypt(key, nonce, aad, pt, tag_len) -> Option<Vec<u8>>(output:ciphertext ‖ tag).tag_len ∈ {4, 6, 8, 10, 12, 14, 16}per spec, validated at API entry.nonce.len() ∈ [7, 13]. Pure-Rust CBC-MAC + CTR over the existingSm4Cipherpath — no GHASH. Byte-identical to OpenSSL 3.x EVPSM4-CCMacross 8 KAT scenarios (gmssl 3.1.1 doesn't shipsm4 -ccmso the CCM reference oracle comes from OpenSSL; seedocs/v0.8-ccm-kat-sourcing.md).gmcrypto_simd::ghash::ghash_mul(h, x) -> [u8; 16]— W1. Constant-time GHASH multiplication overGF(2^128) / (x^128 + x^7 + x^2 + x + 1). Single dispatch entry point:ghash_mul_clmulonx86_64(PCLMULQDQ + SSE2; runtime cpufeatures detect; Intel Westmere+ / AMD Bulldozer+).ghash_mul_pmullonaarch64(ARMv8.0 AES extensionvmull_p64; runtime cpufeatures detect; Apple Silicon / most modern ARM chips).ghash_mul_software(bit-serial mask-XOR; constant-time over both inputs; available everywhere as fallback).
- New
sm4-aeadfeature flag — default-off; opt-in.sm4-aead = ["dep:gmcrypto-simd"]activatesmode_gcmandmode_ccm. Additive on the default-features build. - New dudect targets
ct_sm4_gcm_decrypt+ct_sm4_ccm_decrypt— W4. Class-split by master key over a fixed 256-byte plaintext + 16-byte AAD. Both classes'(ct, tag)pairs are valid encrypts under their own keys, so both decrypt paths reach the tag-compare via identical control flow. Same|tau| < 0.20gate as the rest of the SM4 surface; new CI matrix slotsm4-bitsliced-simd,sm4-aeadexercises the most-demanding cipher-stack combination.
No public API breakage — purely additive. v0.7.0 callers can
cargo update to v0.8.0 without migration; sm4-aead is opt-in.
Everything v0.4 shipped (wasm32-unknown-unknown build, RustCrypto
trait fit behind digest-traits / cipher-traits, bitsliced SM4
S-box behind sm4-bitsliced, gmcrypto-c C ABI crate) is unchanged
— see the Roadmap row for the compact reference and CHANGELOG.md
[0.4.0] for detail.
Everything v0.3 shipped is unchanged:
- Reusable strict-canonical DER reader / writer subset
(
gmcrypto_core::asn1::{reader, writer, oid}). - PEM + encrypted PKCS#8 + X.509 SPKI + SEC1 codecs
(
gmcrypto_core::{pem, pkcs8, spki, sec1}). - Full bidirectional gmssl 3.1.1 interop (SM2 sign / verify, SM2
encrypt / decrypt, SM4-CBC). Gated on
GMCRYPTO_GMSSL=1. - Raw byte-concat SM2 ciphertext helpers
(
gmcrypto_core::sm2::raw_ciphertext):C1 || C3 || C2emit + decode; legacyC1 || C2 || C3decrypt-only. - Streaming
HmacSm3+Sm4Cbc{En,De}cryptor. In-crateHash/Mac/BlockCiphertraits (gmcrypto_core::traits). - Comb-table
mul_g(~5× sign-side speedup). 64 sub-tables of 16 entries each, lazily built once per process viaspin::Once.
Everything v0.2 shipped is unchanged:
- SM3 hash function (
#![no_std]+alloc). - SM2 sign / verify with custom signer ID (default
1234567812345678per GM/T 0009). - SM2 public-key encrypt / decrypt with GM/T 0009-2012 ciphertext DER
(
SEQUENCE { x, y, hash, ciphertext }). Invalid-curve attack defense via on-curve check onC1before scalar mult; non-branching KDF-zero detection so a chosen-ciphertext attacker cannot distinguish it from a normal MAC failure. - SM4 block cipher (GB/T 32907-2016) and SM4-CBC (PKCS#7 padding,
caller-supplied unpredictable IV per NIST SP 800-38A Appendix C).
Constant-time-designed
subtlelinear-scan S-box (~1-2M blocks/s); opt-in bitsliced (table-less, gate-only) S-box via thesm4-bitslicedfeature (v0.4 W3). PKCS#7 strip uses a constant-time scan over the final block;decryptcollapses every failure mode to a singleNoneagainst padding-oracle attacks. - HMAC-SM3 per RFC 2104, gmssl-cross-validated KAT vectors. Hash-first
long-key path. v0.3 adds the streaming
HmacSm3shape alongside single-shothmac_sm3. - PBKDF2-HMAC-SM3 per RFC 8018 §5.2. Caller-supplied output buffer (no internal allocation, no iteration-count default).
- Constant-time-designed
FpandFnfield arithmetic viacrypto-bigint = 0.7.3. - Renes-Costello-Batina complete addition formulas for the SM2 curve (a=-3 specialized).
- Fixed-base (v0.3 comb-table) and variable-base scalar multiplication,
both constant-time-designed with
subtle::ConditionallySelectablelinear-scan table lookup. - Fixed-K masked-select signing retry: the retry loop runs
K=2iterations unconditionally, regardless of which iteration produced a valid signature. The constant-time contract holds for any RNG that respectsCryptoRng; pathological RNGs cannot leak the secret via observable retry count. - Strict canonical ASN.1 DER for
SEQUENCE { r, s }(signatures), the GM/T 0009 SM2 ciphertext SEQUENCE, and all v0.3 PEM / PKCS#8 / SPKI / SEC1 wire formats. Rejects non-canonical leading-zero padding, sign-bit-set first bytes, empty content, and (for ciphertext coordinates) values≥ p. - KAT vectors from GB/T 32905-2016 (SM3), GB/T 32918.2-2017 / .5-2017 (SM2), GB/T 32907-2016 Appendix A.1 (SM4 single-block + 1M-round), GM/T 0042-2015 (HMAC-SM3), GM/T 0091-2020 (PBKDF2-HMAC-SM3).
gmsslCLI cross-validation for HMAC-SM3, PBKDF2-HMAC-SM3, and (new in v0.3) SM2 sign/verify, SM2 encrypt/decrypt, and SM4-CBC in both directions. Gated onGMCRYPTO_GMSSL=1.dudect-bencherharness — 18 realct_*targets (12 always-on + 2 cfg-gated undersm4-bitsliced-simd+ 3 cfg-gated undersm4-aead+ 1 cfg-gated undersm4-xts) plus a deliberately-leakynegative_controlthat proves the harness can detect leaks. Matrix-run underfeatures=default,sm4-bitsliced,sm4-bitsliced-simd, andsm4-bitsliced-simd,sm4-aead,sm4-xts— PR-smoke 10⁴ samples; nightly 10⁵ samples (more samples = tighter empirical confidence at the same threshold). Most real targets gate at|tau| < 0.20; per-target policy inSECURITY.md.- Failure-mode invariant: every
Result-returning public API uses the workspace-widegmcrypto_core::Error(singleFailedvariant,#[non_exhaustive]); per-module aliasessm2::Error,pem::Error,pkcs8::Errorall point at the same type.verify_with_idreturnsbool; DER decode returnsOption. Defense against padding-oracle, malleability, and invalid-curve attacks. - Zeroization on private keys, SM4 round keys, HMAC
K'/K' XOR ipad/K' XOR opad, PBKDF2 intermediates, SM2 KDF buffers, and PKCS#8 inner-key scratch.
Roadmap
| Version | Scope |
|---|---|
| v0.2 (shipped) | SM4 + SM4-CBC, HMAC-SM3, PBKDF2-HMAC-SM3, SM2 encrypt/decrypt + GM/T 0009 ciphertext DER, dudect harness expansion to 11 targets. See CHANGELOG.md [0.2.0]. |
| v0.3 (shipped) | Reusable ASN.1 reader/writer subset; PEM, encrypted PKCS#8, X.509 SPKI, SEC1; full bidirectional gmssl interop (incl. SM2 sign/verify + SM2 encrypt/decrypt with PEM-wrapped keys + SM4-CBC); raw byte-concat ciphertext helpers (C1||C3||C2 modern + legacy C1||C2||C3 decrypt); streaming HmacSm3 / Sm4CbcEncryptor / Sm4CbcDecryptor + in-crate Hash/Mac/BlockCipher traits; comb-table mul_g (~5× sign-side speedup); dudect harness expanded to 12 targets. See CHANGELOG.md [0.3.0]. |
| v0.4 (shipped) | wasm32-unknown-unknown build target; RustCrypto-trait fit (digest::Digest / digest::Mac / cipher::BlockEncrypt/BlockDecrypt) behind opt-in digest-traits / cipher-traits feature flags; bitsliced (table-less, gate-only) SM4 S-box behind the opt-in sm4-bitsliced feature; new gmcrypto-c workspace member exposing the SM2/SM3/SM4/HMAC/PBKDF2 surface as a C ABI (cdylib + staticlib + cbindgen-generated header). See CHANGELOG.md [0.4.0]. |
| v0.5.0 (shipped) | C-ABI completeness (streaming CBC + raw-byte SM2 ciphertext + caller-supplied RNG callback); sm4-bitsliced-simd feature-flag scaffolding — v0.5.0 ships no SIMD fast path (the feature transparently delegates to the v0.4 single-block bitslice); BREAKING ergonomic cleanup — workspace-wide gmcrypto_core::Error, Sm2PrivateKey::new(U256) → from_scalar(U256) (gated behind crypto-bigint-scalar) + always-on from_bytes_be(&[u8; 32]) constructor, std feature removed. See CHANGELOG.md [0.5.0]. |
| v0.5.1 (shipped) | W4 phase 2 — new sibling crate gmcrypto-simd carrying an AVX2 8-way packed bitsliced SM4 S-box behind opt-in sm4-bitsliced-simd, with runtime CPU detection (cpufeatures) and silent scalar fallback on non-AVX2 hosts. v0.5.1's tau dispatch fed the AVX2 path with 7 wasted lanes; production throughput matched v0.4 single-block bitslice. Dudect calibration update — ct_fn_invert / ct_fp_invert moved to PR-smoke telemetry + 100K nightly gross-regression sentinel after a GH Actions ubuntu-24.04 runner-image shift on 2026-05-12 raised the empirical noise floor; see docs/v0.5-dudect-recalibration.md. See CHANGELOG.md [0.5.1]. |
| v0.6.0 (shipped) | W4 milestone close-out — the throughput-win release. W4 phase 3: NEON 4-way bitsliced SM4 on aarch64 (compile-time baseline) + AVX2 32-byte full-width packed S-box (sbox_x32) + Sm4CbcDecryptor::process_chunk SIMD fanout. Per round of the SM4 decrypt, batched blocks' tau inputs pack into one SIMD register (32 bytes on x86_64 / 8-block batch, 16 bytes on aarch64 / 4-block batch) — 32× fewer SIMD dispatches per 8-block batch than v0.5.1. CBC encryption stays single-block (chain-of-blocks defeats SIMD packing). New dudect target ct_sm4_cbc_decrypt_fanout (Q6.7) gates the fanout path at |tau| < 0.20. Exhaustive lane-position-shifted SIMD tests (8192 + 4096 cases) per Q6.8. No public API changes; no breaking changes — additive only. See CHANGELOG.md [0.6.0] and docs/v0.6-scope.md. |
| v0.7.0 (shipped) | Cipher-mode surface expansion. First version where v0.6's SIMD machinery is callable from user code outside the CBC-decrypt internal path. New: public length-flexible Sm4Cipher::encrypt_blocks / decrypt_blocks (W1; Q7.7); single-shot sm4::mode_ctr::encrypt / decrypt (W2; GM/T 0002-2012 §5.4); streaming sm4::ctr_streaming::Sm4CtrCipher (W3); new dudect target ct_sm4_ctr_encrypt (gates |tau| < 0.20 on every cipher path). Plus the v0.8 AEAD scope doc (docs/v0.7-aead-scope.md, Q8.1–Q8.8 sign-off + v0.9 candidate Q-list). No public API breakage — additive only. See CHANGELOG.md [0.7.0]. |
| v0.8.0 (shipped) | AEAD core — SM4-GCM + SM4-CCM. Per docs/v0.7-aead-scope.md Q8.1–Q8.8. New: gmcrypto_simd::ghash::ghash_mul constant-time GHASH primitive (CLMUL on x86_64 / PMULL on aarch64 / software Karatsuba fallback; W1); sm4::mode_gcm::encrypt / decrypt byte-identical to gmssl 3.1.1 sm4 -gcm with bidirectional interop (W2); sm4::mode_ccm::encrypt / decrypt byte-identical to OpenSSL 3.x EVP SM4-CCM across 8 KAT scenarios (W3; gmssl 3.1.1 lacks sm4 -ccm so OpenSSL is the oracle — see docs/v0.8-ccm-kat-sourcing.md); new dudect targets ct_sm4_gcm_decrypt + ct_sm4_ccm_decrypt + new CI matrix slot sm4-bitsliced-simd,sm4-aead (W4). Behind opt-in sm4-aead feature flag (additive; default-off). No public API breakage — additive only. See CHANGELOG.md [0.8.0]. |
| v0.9.0 (shipped) | AEAD ergonomics. Per docs/v0.9-scope.md Q9.1–Q9.10. New: sm4::GcmTagLen + mode_gcm::encrypt_with_tag_len / decrypt_with_tag_len (NIST SP 800-38D §5.2.1.2 truncated tags; W1); incremental-input buffered sm4::Sm4GcmEncryptor (output-streaming) / Sm4GcmDecryptor (output-buffered, commit-on-verify) — differential-KAT-equal to single-shot across arbitrary chunking (W2); new dudect target ct_sm4_gcm_decrypt_buffered (W3); 6 single-shot AEAD C FFI symbols (gmcrypto_sm4_gcm_* / gmcrypto_sm4_ccm_*) behind a forwarding sm4-aead feature on gmcrypto-c (W4). Behind the existing sm4-aead flag. No public API breakage — additive only. See CHANGELOG.md [0.9.0]. |
| v0.10.0 (shipped) | Streaming AEAD FFI — SM4-GCM. Per docs/v0.10-scope.md Q10.1–Q10.11. New: 9 gmcrypto-c FFI symbols + 2 opaque handle types exposing the v0.9 incremental-input buffered SM4-GCM encryptor (output-streaming) / decryptor (commit-on-verify) to C/C++/Go/Zig/Python — gmcrypto_sm4_gcm_encryptor_{new,update,finalize,finalize_with_tag_len,free} + gmcrypto_sm4_gcm_decryptor_{new,update,finalize_verify,free}, behind the existing sm4-aead feature on gmcrypto-c; _finalize* consume+free, single GMCRYPTO_ERR; C example examples/sm4_gcm_streaming.c. regen-header now implies sm4-aead (cbindgen drops cfg-gated opaque structs otherwise). No new gmcrypto-core API; no new dudect target. No public API breakage — additive only. See CHANGELOG.md [0.10.0]. |
| v0.11.0 (shipped) | RustCrypto trait-fit modernization. Per docs/v0.11-scope.md Q11.1–Q11.11. Migrates the opt-in digest-traits / cipher-traits impls from digest 0.10 / cipher 0.4 to digest 0.11 / cipher 0.5 (the crypto-common 0.2 / hybrid-array generation), in-place: cipher block backend reshaped to cipher 0.5's separate BlockCipherEncBackend / BlockCipherDecBackend; HMAC construction via digest::KeyInit::new_from_slice (digest 0.11 Mac dropped KeyInit). BREAKING for trait-fit consumers only (bump your own digest/cipher); default-features users unaffected, output byte-identical (full KAT + gmssl interop). MSRV stays 1.85; no new dudect target. See CHANGELOG.md [0.11.0]. |
| v0.12.0 (shipped) | SM4-XTS — tweakable disk/sector mode. Per docs/v0.12-scope.md Q12.1–Q12.13. New: sm4::mode_xts::{encrypt, decrypt} + XTS_KEY_SIZE behind the opt-in sm4-xts feature — GB/T 17964-2021 (GM-T OID 1.2.156.10197.1.104.10), full ciphertext stealing, byte-identical to OpenSSL 3.x EVP SM4-XTS (xts_standard=GB; not IEEE 1619 — they differ in the GF(2¹²⁸) tweak doubling). 32-byte key (Key1 ‖ Key2) + raw 16-byte tweak, lengths [16 B, 16 MiB], single None failure mode, confidentiality-only (no auth). Pure-core (no new dependency); rides the Sm4Cipher::encrypt_blocks batch API + SIMD fanout. New dudect target ct_sm4_xts_decrypt. Also fixes a latent CI bug where the feature-conditional dudect gates never fired. C FFI deferred to v0.13. Additive — no public API breakage. See CHANGELOG.md [0.12.0]. |
| v0.13.0 (shipped) | C ABI for SM4-XTS. Per docs/v0.13-scope.md Q13.1–Q13.12. New: gmcrypto_sm4_xts_encrypt / _decrypt + GMCRYPTO_SM4_XTS_KEY_SIZE in gmcrypto-c, behind a forwarding sm4-xts feature — single-shot, mirroring the single-shot SM4-GCM FFI shape minus nonce/AAD/tag (32-byte key, 16-byte tweak, length-preserving (out, out_capacity, out_actual_len) output), byte-identical to gmcrypto_core::sm4::mode_xts, single GMCRYPTO_ERR, confidentiality-only. The deferred FFI half of v0.12 (the v0.8-core → v0.10-FFI cadence). 5 new c_smoke tests + doc-only C example examples/sm4_xts_sector.c; regenerated header (no regen-header change needed — free fns + always-on const). No new gmcrypto-core API, no new dudect target, no new dependency. Additive — no public API breakage. See CHANGELOG.md [0.13.0]. |
| v0.14 (assurance; not published) | Parser fuzzing. Per docs/v0.14-scope.md Q14.1–Q14.12. A cargo-fuzz (libFuzzer) harness over the full untrusted-input decode/decrypt surface of gmcrypto-core (16 targets: PEM, PKCS#8 decode/decrypt, SPKI, SEC1, DER reader primitives, SM2 DER + raw ciphertext, SM2 decrypt + verify, SM4-CBC/GCM/CCM/XTS decrypt) proving the failure-mode invariant on adversarial bytes — no panic / no OOM / no hang. Workspace-excluded fuzz/ crate (nightly-only; never in the published dep graph) + a capped nightly CI job (.github/workflows/fuzz-nightly.yml). Initial sweep: zero crashes → no published-crate change, not a crates.io release (assurance/infra only). See fuzz/README.md. |
| v0.15.0 (shipped) | SM4-XTS multi-sector (disk) helper. Per docs/v0.15-scope.md Q15.1–Q15.12. New: sm4::mode_xts::{encrypt_sectors, decrypt_sectors} (opt-in sm4-xts) — encrypt/decrypt a contiguous run of equal-size disk sectors in place (&mut [u8] -> Option<()>), sector i under tweak = little-endian-128(start_sector + i) (the standard disk-XTS data-unit convention; owns the encoding the single-shot v0.12 API left to the caller). Byte-identical to looping the single-shot per sector (transitively OpenSSL xts_standard=GB-pinned); whole-block sectors (no ciphertext stealing); ciphers built once + reused scratch (no per-sector allocation); single None for all validation with buf untouched; confidentiality-only. Pure-core: no new dependency, no new feature flag, no new SIMD, no new dudect target (the existing ct_sm4_xts_decrypt covers it). C FFI deferred to v0.16. crates.io skips 0.14.0 (the unpublished fuzzing cycle). Additive — no public API breakage. See CHANGELOG.md [0.15.0]. |
| v0.16.0 (shipped) | C ABI for the SM4-XTS multi-sector helper. Per docs/v0.16-scope.md Q16.1–Q16.12. New: gmcrypto_sm4_xts_encrypt_sectors / _decrypt_sectors in gmcrypto-c, behind the existing forwarding sm4-xts feature — in-place over a contiguous run of equal-size sectors (buf: *mut u8 + buf_len; no out/out_capacity/out_actual_len, mirroring the core's &mut [u8] so disk callers never double-allocate), start_sector: uint64_t, tweak = LE-128(start_sector + i). Byte-identical to gmcrypto_core::sm4::mode_xts::{encrypt,decrypt}_sectors; single GMCRYPTO_ERR with buf untouched on error; confidentiality-only. The deferred FFI half of v0.15 — every cipher mode is now FFI-complete. 11 new c_smoke tests + doc-only C example examples/sm4_xts_multisector.c; regenerated header (no regen-header change — free fns, no new opaque structs). No new gmcrypto-core API, no new dudect target, no new dependency. Additive — no public API breakage. See CHANGELOG.md [0.16.0]. |
| v0.17 (public release; not a crates.io release) | Open-sourced the repository. Flipped the GitHub repo private → public on the 0.x line; CI migrated off the self-hosted macOS runner to GitHub-hosted (ci.yml → macos-14, fuzz-nightly.yml → ubuntu-latest). A repository milestone — no crate code changes (workspace stays 0.16.0; crates.io skips 0.17.0 per the v0.14 precedent); v1.0 reserved. Per docs/v0.17-scope.md. |
| v0.18 (infra-assurance; not a crates.io release) | dudect-gate hardening. Per docs/v0.18-scope.md Q18.1–Q18.7. Pinned the dudect CI workflows' drift axes (ubuntu-24.04 OS-label + exact dtolnay/rust-toolchain@1.95.0) and gate on a CI-level multi-run median |tau| (PR 3 runs / nightly 5 runs; required_low + the nightly sentinel on the median, negative_control on the min, completeness gate on < N runs). timing_leaks.rs byte-unchanged — the loop + median live in CI. A 100K×5 calibration showed ct_fn_invert/ct_fp_invert back near baseline (medians 0.006–0.028) but kept on telemetry / sentinel — not re-promoted (the noise is runner-image-sensitive; a tight gate would re-flake if it returns). Also a comma-free rust-cache shared-key. A repository / infra-assurance milestone — no crate code change (workspace stays 0.16.0; crates.io skips 0.18.0 per the v0.14 / v0.17 precedent). See docs/v0.5-dudect-recalibration.md (v0.18 resolution). |
| v0.19 (infra-assurance; not a crates.io release) | Self-calibrating relative dudect gate — TESTED and FALSIFIED → honest fallback. Per docs/v0.19-scope.md Q19.1–Q19.7. Added two fix-vs-fix noise-floor probes (noise_floor_f{n,p}_invert) + a relative gate median(target) ≤ max(0.20, 4·median(probe)) to re-promote ct_fn_invert/ct_fp_invert. The 100K calibration disproved the matched-sensitivity premise: the probes stay quiet (~0.005) while the targets spike to [0.26–0.32] (ct_fp_invert median 0.2606, ratio 50) — the noise is in the two-input class split, not the operation, so a same-input probe can't track it. Reverted to telemetry / sentinel @0.55; probes kept as telemetry (evidence for a v0.21+ class-split-aware "noise-twin"). Only the dev-only bench harness changed (workspace stays 0.16.0; crates.io skips 0.19.0). See docs/v0.5-dudect-recalibration.md (v0.19 resolution). |
| v0.20 (infra-assurance; not a crates.io release) | Streaming-decryptor differential fuzzing + cargo fuzz coverage + codified v1.0 CT baseline. Per docs/v0.20-scope.md Q20.1–Q20.5. Two new differential targets (fuzz_sm4_{cbc,gcm}_streaming_decrypt) assert the streaming decryptors fed in arbitrary chunks equal the single-shot oracle; fuzz sweep → 18 targets (zero crashes, zero divergences); a non-gating cargo fuzz coverage nightly job (llvm-cov TOTALS artifact). Codified the settled v1.0 CT baseline in SECURITY.md (composite targets gated <0.20; the two single-inversion diagnostics on telemetry/sentinel @0.55, narrow revisit door). Theme chosen after a Codex+Grok discussion. Only fuzz/ + fuzz-nightly.yml + docs changed (workspace stays 0.16.0; crates.io skips 0.20.0). |
| v0.21 (infra-assurance; not a crates.io release) | v1.0 readiness audit. Per docs/v0.21-scope.md Q21.1–Q21.9. Froze + tooling-guarded the public API ahead of 1.0: committed cargo-public-api baselines + an enforced drift-check, cargo-semver-checks (informational pre-1.0), a cargo doc -D warnings gate, and a --no-default-features/--all-features matrix (new .github/workflows/api-stability.yml); finalized the #[doc(hidden)] surface (3 core items + the whole gmcrypto-simd internal backend) with "not public / not SemVer" notes + existence tests; froze the docs. Non-publishing (doc-attributes + tests only, no behavior change; workspace stays 0.16.0, crates.io skips 0.21.0). Headline finding: the always-on public API names crypto-bigint 0.7 types — a decision to resolve before 1.0 (docs/v1.0-readiness.md §3.A). Deferred to post-1.0: class-split-aware "noise-twin" dudect reference; round-trip/differential parser fuzzing; aead 0.6 (upstream 0.6.0-rc.10); AVX-512 sbox_x64; CCM buffered input; the dudect-nightly leg-cancellation fix. |
| v0.22 (infra-assurance; not a crates.io release) | API-tightening — decouple crypto-bigint 0.7 from the 1.0 contract. Per docs/v0.22-scope.md Q22.1–Q22.8 (resolves the v0.21 §3.A finding via Option 2). Group A: #[doc(hidden)] (kept pub) the low-level sm2::curve / sm2::scalar_mul / ProjectivePoint::to_affine surface. Group B: reshape asn1::{encode,decode}_sig + Sm2Ciphertext::{x,y} from U256 to [u8; 32], byte-output-identical (KAT + gmssl interop 11/11). Group C: ProjectivePoint stays public + unchanged. The always-on (default-features) public API now names zero crypto-bigint types; only the opt-in crypto-bigint-scalar from_scalar(U256) retains it (documented escape hatch). BREAKING for consumers that named Fn/Fp/encode_sig/Sm2Ciphertext::x; ships with 1.0 (non-publishing — workspace stays 0.16.0, crates.io skips 0.22.0). |
| v0.23 (infra-assurance; not a crates.io release) | Pre-1.0 re-audit remediation. Per docs/v0.23-scope.md Q23.1–Q23.9 + docs/v1.0-reaudit.md. A multi-model adversarial pre-publish re-audit (Codex gpt-5.5 + Grok, source-verified) returned NO-GO as-is — core primitives sound, but 2 API/ABI BLOCKERs + API-finality / zeroize-on-failure / spec-ceiling / doc should-fixes. Remediated: W1 (API) Sm2PrivateKey::public_key() -> Sm2PublicKey, the raw ProjectivePoint surface + asn1::{reader,writer,oid} + traits::* made #[doc(hidden)]; W2 (crypto) single-shot SM4-GCM encrypt made fallible (2^36−32 ceiling), the fallible rand_core::TryCryptoRng bound on SM2 sign/encrypt (no-panic RNG-failure path), a fixed-budget constant-time SM2 nonce sampler, sign-nonce / CCM-tentative-plaintext / Sm3-on-drop zeroization, SM2 KDF wrap guard; W3 (C ABI) the SM4-GCM/CCM/XTS FFI symbols made always-on so gmcrypto.h == the default build. Runtime output byte-identical (gmssl interop 11/11) except the deliberately-changed signatures; the breaking API/ABI changes ship with 1.0 (non-publishing — workspace stays 0.16.0, crates.io skips 0.23.0). |
| v1.0 | API stabilization + crates.io publish (the deliberate cut after the audit + tightening + re-audit: the crypto-bigint-exposure decision is resolved [v0.22] and the pre-publish re-audit findings remediated [v0.23], bump 0.16.0 → 1.0.0 with exact sibling pins, publish gmcrypto-simd → core → c, flip cargo-semver-checks to enforced — see the runbook in docs/v1.0-readiness.md §4). |
| v1.0.1 (shipped) | Readiness-cleanup patch — first post-1.0 publish. Per the release-readiness synthesis docs/audits/2026-06-02-release-readiness-synthesis.md (GO-WITH-FOLLOWUP, 0 blockers). Functional fix: the gmcrypto-c gmcrypto_version() returned a hardcoded "0.4.0" → now the real CARGO_PKG_VERSION (the one behavior change justifying a patch publish). Plus doc improvements (raw-block ECB warnings, cbindgen header preconditions, FFI RNG/XTS notes, trait-stability caveats) + CI-health fixes (sm4-xts in MSRV/wasm/deny; dudect allowlist; generate-lockfile before deny; a new simd-x86 job that caught a latent unsafe_code compile bug; removed pull_request paths-ignore so docs PRs aren't blocked). No API/ABI change; wire output byte-identical to 1.0.0 (enforced cargo-semver-checks). 6 merged PRs (#87–#92). See CHANGELOG.md [1.0.1]. |
| v1.1.0 | SM2 key exchange (GM/T 0003.3) with key confirmation. Per docs/v1.1-sm2-key-exchange-design.md + docs/v1.1-scope.md. New sm2::key_exchange module behind the opt-in sm2-key-exchange feature (pure-core, no new dependency): Sm2KxInitiator/Sm2KxResponder role state-machines with typestate-enforced single-use ephemerals and commit-on-confirm key release; byte-identical to the GM/T 0003.5-2012 recommended-curve worked example (K + S_A/S_B); new dudect target ct_sm2_key_exchange + fuzz target fuzz_sm2_kx. C FFI deferred to v1.2. Additive — no public API breakage. |
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!;
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.0"
= { = "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.