gmcrypto-c 1.0.0

C ABI for gmcrypto-core — cdylib + staticlib exposing SM2/SM3/SM4/HMAC/PBKDF2 to C / C++ / Python / Go / Zig callers via opaque handles
Documentation

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).
cargo build -p gmcrypto-c --release

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
cargo build -p gmcrypto-c --release   # the complete ABI — no feature flags

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:

cargo build -p gmcrypto-c --features regen-header

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.
gcc -I crates/gmcrypto-c/include \
    -L target/release -lgmcrypto_c \
    examples/sm2_sign.c -o sm2_sign

# Run (set LD_LIBRARY_PATH on Linux / DYLD_LIBRARY_PATH on macOS):
LD_LIBRARY_PATH=target/release ./sm2_sign

Static linking:

gcc -I crates/gmcrypto-c/include \
    examples/sm2_sign.c \
    target/release/libgmcrypto_c.a \
    -o sm2_sign-static
./sm2_sign-static

Failure-mode invariant

Every entry point returning int follows the workspace failure-mode discipline:

  • 0 (= GMCRYPTO_OK) on success.
  • Non-zero (= GMCRYPTO_ERR etc.) 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 = gmcrypto_sm2_sign(
    key, NULL, 0,         /* signer_id = DEFAULT */
    msg, msg_len,
    sig_buf, sizeof sig_buf,
    &actual);
if (rc != GMCRYPTO_OK) {
    if (actual > sizeof sig_buf) {
        /* too-small buffer; `actual` carries the required length */
    } else {
        /* other failure */
    }
}

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.