gmcrypto-c
C ABI for gmcrypto-core — pure-Rust SM2 / SM3 / SM4 SDK exposed to
C / C++ / Python (cffi) / Go (cgo) / Zig / Ruby (FFI) callers via a
cdylib + staticlib and a cbindgen-generated header.
The library is a thin shim over gmcrypto-core;
every cryptographic operation runs in the dudect-gated Rust core,
not in this crate. The C ABI is the language-binding surface only.
Build
# cdylib (libgmcrypto_c.so / .dylib / .dll) + staticlib (.a / .lib) +
# rlib (for Rust consumers).
Output artifacts land in target/release/:
libgmcrypto_c.so(Linux) /libgmcrypto_c.dylib(macOS) /gmcrypto_c.dll(Windows) — shared library.libgmcrypto_c.a(Linux/macOS) /gmcrypto_c.lib(Windows) — static library.
Cipher-mode coverage (complete, always-on)
The C ABI is complete and always-on: a default cargo build -p gmcrypto-c
exports the full surface — SM2 / SM3 / SM4-ECB+CBC / HMAC-SM3 / PBKDF2-HMAC-SM3
plus the symmetric-cipher modes below. As of v0.23 the AEAD/XTS symbols
are no longer behind opt-in cargo features (the former sm4-aead / sm4-xts
forwarding features were removed), so the committed
include/gmcrypto.h matches the default build exactly —
no feature flags are needed.
| Mode | Symbols | Example |
|---|---|---|
| SM4-GCM / SM4-CCM AEAD (single-shot + streaming SM4-GCM) | gmcrypto_sm4_gcm_* / gmcrypto_sm4_ccm_* |
examples/sm4_gcm_streaming.c |
| SM4-XTS tweakable disk/sector mode (GB/T 17964-2021): single-shot + in-place multi-sector | gmcrypto_sm4_xts_{encrypt,decrypt} / gmcrypto_sm4_xts_{encrypt,decrypt}_sectors |
examples/sm4_xts_sector.c, examples/sm4_xts_multisector.c |
SM2 key exchange (GM/T 0003.3) with key confirmation (v1.2): two opaque role handles; _confirm/_finish consume + free; _with_rng variants for caller-supplied randomness |
gmcrypto_sm2_kx_initiator_{new,new_with_rng,confirm,free} / gmcrypto_sm2_kx_responder_{new,respond,respond_with_rng,finish,free} |
examples/sm2_key_exchange.c |
SM2 key exchange notes: the initiator handle is created already holding
its ephemeral (_new writes R_A); the agreed key is written to caller
memory and the caller owns wiping it; every failure (off-curve peer R,
tag mismatch, misuse ordering, null, RNG failure) is the single
GMCRYPTO_ERR. id_len == 0 selects the GM/T default ID
"1234567812345678".
SM4-XTS takes a 32-byte key (Key1 ‖ Key2; Key1 == Key2 is
rejected) and a 16-byte tweak (the per-sector data-unit identifier,
caller-unique per key). It is length-preserving and confidentiality
only — it does not authenticate; use an AEAD mode if you need
integrity. Sizes are exported as GMCRYPTO_SM4_XTS_KEY_SIZE (32) and
GMCRYPTO_SM4_BLOCK_SIZE (16).
The *_sectors variants encrypt/decrypt a contiguous run of equal-size
sectors in place (buf is both input and output; buf_len must be a
whole multiple of sector_size), deriving sector i's tweak as
little-endian-128(start_sector + i) — the standard disk-XTS LBA convention.
start_sector is a uint64_t. Sector numbers must be unique within an
XTS-key namespace — do not encrypt multiple devices / partitions / snapshots
under one key all starting at sector 0 (use absolute LBAs or a per-device
key); reuse leaks block-equality structure. On GMCRYPTO_ERR (bad
sector_size, bad buf_len multiple, weak key, null) buf is untouched; a
zero-length run is a vacuous success (but the key is still validated).
Header
The committed header at include/gmcrypto.h
is the v0.4 C ABI contract. To regenerate after editing the FFI
surface in src/lib.rs:
CI verifies the committed header is in sync via git diff --exit-code
on every PR.
Linking against C
# Compile a C consumer against the shared library.
# Run (set LD_LIBRARY_PATH on Linux / DYLD_LIBRARY_PATH on macOS):
LD_LIBRARY_PATH=target/release
Static linking:
Failure-mode invariant
Every entry point returning int follows the workspace
failure-mode discipline:
0(=GMCRYPTO_OK) on success.- Non-zero (=
GMCRYPTO_ERRetc.) on failure. All non-zero returns are equivalent; do not distinguish failure modes by the specific value.
Per Q4.8 in docs/v0.4-scope.md:
distinguishing failure modes (wrong password vs. malformed PEM
vs. inner-key-parse failure) would introduce a password-oracle
attack surface. C consumers MUST treat all non-zero returns as
opaque.
Output buffer convention
Variable-length output (signatures, ciphertexts, PKCS#8 blobs)
uses the (out_ptr, out_capacity, out_actual_len) shape per
Q4.13:
size_t actual = 0;
int rc = ;
if
On too-small buffer: rc != 0, *out_actual_len set to the
required length, out_ptr unmodified.
Handle ownership
Opaque handles (gmcrypto_sm2_privkey_t*, etc.) are heap-allocated
via Box::into_raw. Pair every _new with exactly one _free.
_free(NULL) is a no-op (mirrors C's free()).
Private-key handles inherit ZeroizeOnDrop from gmcrypto-core; the
inner scalar / round keys are zeroized before the heap slot is
freed.
RNG sourcing
gmcrypto_sm2_sign, gmcrypto_sm2_encrypt, and
gmcrypto_sm2_privkey_to_pkcs8 source randomness from
getrandom::SysRng internally. C consumers do not pass an RNG
callback in v0.4; a "register custom RNG" entry point may land
post-v0.4 if a use case surfaces.
unsafe_code posture
Per Q4.7 in the scope doc: gmcrypto-c cannot satisfy
unsafe_code = "forbid" because raw-pointer dereferencing and
Box::from_raw are inherent to FFI. Every unsafe block carries
a // SAFETY: comment naming the caller-side preconditions. The
core crate gmcrypto-core itself stays
unsafe_code = "forbid".
SemVer
The C ABI is the v0.4 baseline. Signature changes in v0.4.x are
breaking ABI changes; v0.5 may break the C ABI without a major
Rust-side bump (since gmcrypto-c is a separate crate from
gmcrypto-core).
gmcrypto_sm2_privkey_to_sec1_be is NOT SemVer-stable across
v0.4.x per Q4.19 — caller MUST zeroize the returned scalar bytes;
documented in the header.