gmcrypto-core 1.11.1

Constant-time-designed pure-Rust SM2/SM3/SM4 primitives (no_std + alloc) with an in-CI dudect timing-leak regression harness
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# 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, the TLCP (GB/T 38636) key
schedule; SM4-CBC / CTR / GCM / CCM / XTS (single-shot and streaming);
HMAC-SM3, PBKDF2-HMAC-SM3; plus a complete C ABI (`gmcrypto-c`, 104 entry
points) — all secret-touching paths guarded by an in-CI `dudect-bencher`
detectable-leak regression harness.

[![Crates.io](https://img.shields.io/crates/v/gmcrypto-core.svg)](https://crates.io/crates/gmcrypto-core)
[![Documentation](https://docs.rs/gmcrypto-core/badge.svg)](https://docs.rs/gmcrypto-core)
[![License](https://img.shields.io/crates/l/gmcrypto-core.svg)](https://crates.io/crates/gmcrypto-core)

**Personal project notice:** not affiliated with, endorsed by, sponsored by, or
certified by any upstream cryptography project, payment gateway, standards body,
or vendor.

Official ecosystem membership, layering, versioning, and compatibility gates are defined in the [gmcrypto Rust ecosystem charter](docs/ECOSYSTEM.md).

> ⚠️ **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`]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-CI `dudect`
> timing-leak harness, and a 33-target `cargo-fuzz` suite. This is a solo-maintained, best-effort open-source
> project with no support SLA. Review the code and **use at your own risk.** See
> [`SECURITY.md`]SECURITY.md for the threat model and disclosure process.

**Status:** actively maintained as of July 2026. The 1.x line is feature-complete
for its stated scope — v1.9.0 closed the TLCP arc — so commit volume is low by
design rather than by neglect. Issues and PRs get a response.

## 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, gated |||
| Fuzzing | ✅ 33 targets, nightly |||
| C ABI | ✅ 104 entry points |||
| TLCP (GB/T 38636) toolkit ||||
| `no_std` || not advertised ||
| Enforced SemVer (`cargo-semver-checks`) ||||
| Latest release | **1.11.1** | 0.6.1 | `sm4` 0.6.0, `sm2` 0.14.0-rc |
| **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.

## 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`](https://docs.rs/dudect-bencher/)
detectable-leak regression harness**: 20 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` + 1 cfg-gated under `tlcp`) plus a deliberately-leaky
`negative_control` that proves
the harness can detect leaks. Most real targets gate at `|tau| <= 0.20`; four
(`ct_fn_invert`, `ct_fp_invert`, `ct_sign_k_class`, `ct_hmac_sm3`) carry
target-specific gate policy after the 2026-05-12 / 06-07 / 06-17
recalibrations — see [`SECURITY.md`](SECURITY.md) and
[`docs/v0.5-dudect-recalibration.md`](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`), v0.12's SM4-XTS
decrypt over a ciphertext-stealing data unit (`ct_sm4_xts_decrypt`,
cfg-gated on `sm4-xts` — the CTS tail is the riskiest tweak arithmetic, so
that is what gates), 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), v1.7's Lucky13-hardened TLCP record deprotect
(`ct_tlcp_cbc_deprotect`, cfg-gated on `tlcp` — the residual guard behind an
equalisation that is enforced separately by an equivalence test), 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 and two always-on
`noise_floor_*` probes that cannot leak by construction and exist to
characterise runner noise. 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`](docs/v0.5-dudect-recalibration.md)
for the data and posture. See [`SECURITY.md`](SECURITY.md) for the full
constant-time discipline.

The differentiator vs. existing Rust SM2 crates (notably
[`RustCrypto/sm2`](https://docs.rs/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 protocol implementation (the `tlcp` feature ships cryptographic building blocks, including record protection, but no handshake state machine, record framing, connection/session orchestration, or transport I/O).
- 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

```rust
use gmcrypto_core::sm2::{
    sign_with_id, verify_with_id, Sm2PrivateKey, DEFAULT_SIGNER_ID,
};
use getrandom::SysRng;
use hex_literal::hex;

// `from_bytes_be` is the recommended constructor: always available, and it
// keeps `crypto_bigint::U256` out of your code.
let d_be: [u8; 32] = hex!(
    "3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8"
);
let key = Sm2PrivateKey::from_bytes_be(&d_be).expect("d in [1, n-2]");
let public = key.public_key();

// Signing takes a fallible `rand_core::TryCryptoRng`, so an RNG failure
// surfaces as an error rather than a panic. `SysRng` satisfies it directly.
let mut rng = SysRng;
let sig = sign_with_id(&key, DEFAULT_SIGNER_ID, b"hello", &mut rng).unwrap();
assert!(verify_with_id(&public, DEFAULT_SIGNER_ID, b"hello", &sig));
```

**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:

```rust
use gmcrypto_core::sm2::key_exchange::{Sm2KxInitiator, Sm2KxResponder};

// A (initiator) and B (responder) hold each other's static public keys.
let init = Sm2KxInitiator::new(&key_a, &pub_b, b"A-id", b"B-id", 32)?;
let (r_a, init_waiting) = init.produce_ephemeral(&mut rng)?; // R_A -> B

let resp = Sm2KxResponder::new(&key_b, &pub_a, b"A-id", b"B-id", 32)?;
let (r_b, s_b, resp_waiting) = resp.respond(&r_a, &mut rng)?; // (R_B, S_B) -> A

let (k_a, s_a) = init_waiting.confirm(&r_b, &s_b)?; // verifies S_B; S_A -> B
let k_b = resp_waiting.finish(&s_a)?;               // verifies S_A
assert_eq!(k_a.as_bytes(), k_b.as_bytes());         // 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":

```rust
use gmcrypto_core::x509::Certificate;

let cert = Certificate::from_der(&leaf_der).ok_or("not a GM/T 0015 cert")?;
assert!(cert.verify_signature(&issuer_public_key));
let _validity = (cert.not_before(), cert.not_after()); // exposed; no clock
```

v1.8 adds a deliberately narrow chain layer: `x509::verify_chain` walks a
caller-ordered chain to a trusted anchor (per-edge signature, keyUsage /
basicConstraints, optional comparison time), and `tlcp::chain::verify_pair`
(with `tlcp` + `x509`) verifies a TLCP [sign, enc] double-cert pair. Both
return a single `bool` and make **structural** trust decisions only —
**endpoint identity binding stays the caller's, permanently** (a `true` is
never "this is the peer I dialed").

The same surfaces are reachable from C / C++ / Python / Go / Zig through
`gmcrypto-c` — see [`crates/gmcrypto-c/README.md`](crates/gmcrypto-c/README.md)
and the doc-only examples under
[`crates/gmcrypto-c/examples/`](crates/gmcrypto-c/examples/)
(`sm2_sign.c`, `sm4_gcm_streaming.c`, `sm4_ccm.c`, `sm4_xts_sector.c`,
`sm4_xts_multisector.c`, `sm2_key_exchange.c`, `x509_verify.c`,
`tlcp_handshake.c`, `tlcp_verify_pair.c`).

## Crates & features

Three crates, released together at one lockstep version:

| Crate | Role |
|---|---|
| [`gmcrypto-core`]https://crates.io/crates/gmcrypto-core | The `no_std + alloc` crypto core (`unsafe_code = "forbid"`). The Rust API. |
| [`gmcrypto-c`]https://crates.io/crates/gmcrypto-c | C ABI shim (cdylib + staticlib): 104 entry points, committed [`gmcrypto.h`]crates/gmcrypto-c/include/gmcrypto.h drift-checked in CI. **Always-on**: a default build exports the full surface. |
| [`gmcrypto-simd`]https://crates.io/crates/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 (typestate role state-machines): confirmed flow by default + the standard-permitted no-confirmation completers (v1.6). |
| `x509` | X.509-with-SM2 leaf parse + signature verify; v1.8 adds linear `verify_chain` + keyUsage/basicConstraints readers. **Structural trust only — NOT endpoint authentication.** |
| `tlcp` | TLCP (GB/T 38636-2020) crypto toolkit: key schedule (P_SM3 PRF, master secret, key block, Finished) + **record protection** (SM4-CBC Lucky13-hardened deprotect; SM4-GCM record with `sm4-aead`) + **certificate-pair verification** (`tlcp::chain::verify_pair`, with `x509`). **Not a protocol implementation.** |
| `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, plus a four-byte serial-`tau` path so CCM CBC-MAC is not routed through wasted x8 lanes. Runtime AVX2 detection; scalar fallback off those targets. |
| `digest-traits` / `cipher-traits` | RustCrypto trait fit (`digest 0.11` / `cipher 0.5`) for `Sm3` / `HmacSm3` / `Sm4Cipher`. |
| `aead-traits` | RustCrypto trait fit (`aead 0.6`) for SM4-GCM / SM4-CCM: `Sm4Gcm` (12-byte nonce, 16-byte tag) and `Sm4Ccm<M, N>` (tag/nonce sizes as type parameters). Implies `sm4-aead`. |
| `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.11.1** (a patch: on the measured AArch64 host the opt-in `sm4-bitsliced-simd`
feature no longer makes serial SM4 — the CCM CBC-MAC path — slower than the scalar
build, issue #163 (x86_64 throughput unmeasured; correctness/KAT/fuzz-covered only);
and the TLCP SM4-CBC deprotect's Lucky13 inner-HMAC is now length-independent by
construction — every `tlcp` build including the C ABI — a residual the first fix made
measurable on Zen 4 / Xeon runners. Wire-identical to 1.11.0; the default core build
is behaviour-identical. **1.11.0** was the RustCrypto `aead` 0.6
trait fit; **1.9.0** the TLCP toolkit C FFI that closed the TLCP arc). 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 → 1.6.0 → 1.7.0 → 1.8.0 → 1.9.0 → 1.11.0 → 1.11.1**, skipping 0.17.0–0.23.0,
1.5.0, 1.9.1 and 1.10.0 (the 0.x run was the assurance +
API-finalization arc that shipped together in `1.0.0`; 1.5 was the TLCP-decomposition
design cycle, [`docs/tlcp-decomposition.md`](docs/tlcp-decomposition.md); 1.10 was an
assurance cycle that changed no published crate's runtime behavior, so its work ships
here in `1.11.0`. **1.9.1 is the one skip that was not planned as such**: it was a
fully prepared and verified licence-text packaging patch, superseded when `1.11.0`
shipped that same fix — see [`docs/v1.9.1-release-review.md`](docs/v1.9.1-release-review.md),
kept as the record). 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`](docs/v1.0-reaudit.md)) —
see [`docs/v1.0-readiness.md`](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.11.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 in [`docs/api-baseline/gmcrypto-core.txt`]docs/api-baseline/gmcrypto-core.txt,
  drift-checked in CI) and the `gmcrypto-c` **C ABI** (the committed
  `crates/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 arithmetic `sm2::curve` / `sm2::scalar_mul` /
  `ProjectivePoint::to_affine`, and (v0.23) the raw EC point surface
  `sm2::point` / `ProjectivePoint` (the type + module + re-export) +
  `Sm2PublicKey::{from_point, point}`, the low-level `asn1::{reader, writer, oid}`
  modules, and the in-crate `traits::{Hash, Mac, BlockCipher}` module (all kept
  `pub` only for in-repo dev crates); and the entire **`gmcrypto-simd`** crate, which
  is an internal acceleration backend with **no stable Rust API** (use `gmcrypto-core`
  from Rust, `gmcrypto-c` from C). These may change or be removed in any release.
- **High-level key path speaks keys, not points (v0.23).**
  `Sm2PrivateKey::public_key()` returns `Sm2PublicKey` (not the now-internal
  `ProjectivePoint`); `Sm2PublicKey::from_sec1_bytes` is the on-curve-checked public
  point constructor. `spki::{encode, decode}` and `sec1::EcPrivateKey.public` speak
  `Sm2PublicKey`.
- **RNG bound (v0.23).** `sm2::{sign_with_id, encrypt}` name the **fallible**
  `rand_core::TryCryptoRng` bound — a deliberate, documented ecosystem coupling
  (`rand_core` is the RNG interop point, the RustCrypto-wide convention; unlike the
  v0.22 `crypto-bigint` decoupling, replacing it would hurt interop). An RNG failure
  collapses to the single `Failed`, never a panic.
- **Single-shot SM4-GCM `encrypt` is fallible (v0.23).**
  `mode_gcm::{encrypt, encrypt_with_tag_len}` return `Option<…>`, rejecting plaintext
  past the `2^36 − 32`-byte GCM counter ceiling (matching the streaming path and
  `decrypt`).
- **Features are additive** (`default = []`; all 10 are opt-in) and the build is
  `no_std` + `alloc`-only with `unsafe_code = "forbid"` on the core.
- **MSRV is 1.85** (edition 2024); an MSRV bump is treated as a minor, not a patch.
- **`crypto-bigint` decoupling (v0.22):** the **always-on** (default-features) public
  API names **no** `crypto-bigint` types — 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 a
  `crypto-bigint 0.7` type appears in the public API is the **opt-in**
  `crypto-bigint-scalar` feature's `Sm2PrivateKey::from_scalar(U256)` — enabling that
  feature is an explicit opt-in to the `crypto-bigint 0.7` type contract (a
  `crypto-bigint` major bump would be breaking for that feature). The recommended
  always-on path (`Sm2PrivateKey::from_bytes_be`) avoids it entirely. See
  [`docs/v1.0-readiness.md`]docs/v1.0-readiness.md §3.A.

## Release history & roadmap

Per-release narratives live in [`CHANGELOG.md`](CHANGELOG.md) (every
published version, Keep-a-Changelog format) and in the per-cycle scope
documents under [`docs/`](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 throughout — SM2 key exchange (1.1) + its C FFI (1.2),
X.509-with-SM2 leaf parse/verify (1.3) + its C FFI (1.4), then the TLCP arc:
the key schedule and no-confirmation SM2-KX completers (1.6), record
protection with the Lucky13-hardened CBC deprotect (1.7), certificate-chain
and `[sign, enc]` pair verification (1.8), and the C FFI that exposes the
whole toolkit — 85 → 104 entry points (1.9).

**Direction: the TLCP (GB/T 38636) toolkit is complete**, in-core and from C.
A C client can run a full handshake end-to-end — key exchange, key schedule,
record protection, certificate verification. What it is *not* is a protocol
implementation: there is no handshake state machine, no record framing, and
no transport I/O, and adding them is a separate decision (a sans-I/O engine in
its own crate) that has **not** been committed to. Smaller parked items —
RustCrypto `aead` trait fit, AVX-512 `sbox_x64`, CCM buffered input, and a
class-split-aware dudect noise-twin — are tracked in the scope docs.

**How it was built:** for the verification-first development method behind
this library — pre-registered scope, multi-model adversarial review,
executable-evidence gates, and the failures kept as receipts — see
[`CASE-STUDY.md`](CASE-STUDY.md).

## Threat model

See [`SECURITY.md`](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

```bash
cargo test --workspace                                                          # unit + integration
cargo bench --bench timing_leaks --features crypto-bigint-scalar                # local timing harness (~75s)
DUDECT_SAMPLES=10000 cargo bench --bench timing_leaks --features crypto-bigint-scalar  # match CI smoke budget
```

`gmssl` interop test (gated; install [`gmssl`](https://github.com/guanzhi/GmSSL)
**v3.2.0** to enable — this is what Homebrew currently ships):

```bash
GMCRYPTO_GMSSL=1 cargo test --test interop_gmssl                    # 11 tests
GMCRYPTO_GMSSL=1 cargo test --test interop_gmssl --features sm4-aead  # 13 tests
```

The suite **pins its oracle version** and fails with an `ORACLE DRIFT` message
on any other build. That pin is load-bearing rather than fussy: GmSSL renames
subcommands and narrows accepted input ranges between releases, so an unpinned
oracle quietly changes what "interop passes" means. CI runs the same suite
against a from-source v3.2.0 build. 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` as of v0.4. CI gates
both stable and MSRV (1.85) builds on the target.

```bash
rustup target add wasm32-unknown-unknown
cargo build -p gmcrypto-core --target wasm32-unknown-unknown --no-default-features
```

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`:

```toml
[dependencies]
gmcrypto-core = "1.11"
rand_core = { version = "0.10", default-features = false }
getrandom = { version = "0.4", default-features = false, features = ["wasm_js"] }
```

```rust
use gmcrypto_core::sm2::{sign_with_id, Sm2PrivateKey, DEFAULT_SIGNER_ID};
use getrandom::SysRng;

let mut rng = SysRng; // wasm_js-backed when targeting wasm32
let sig = sign_with_id(&priv_key, DEFAULT_SIGNER_ID, b"msg", &mut rng).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

Dual-licensed under either of

- Apache License, Version 2.0 ([`LICENSE-APACHE`]LICENSE-APACHE or
  <https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([`LICENSE-MIT`]LICENSE-MIT or
  <https://opensource.org/licenses/MIT>)

at your option. This is the Rust ecosystem convention: Apache-2.0 carries the
express patent grant, MIT is the permissive path for downstreams whose legal
review fast-tracks it.

**Starting with 1.11.0, both texts ship inside every published crate.** Every
release up to and including 1.9.0 shipped none — the licence existed only at the
repository root and nothing pointed cargo at it — so those archives on crates.io
do not contain it. 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.

Some reference outputs use the upstream [`gmssl`](https://github.com/guanzhi/GmSSL)
tool. This project is independent of that project.