diff --git a/CHANGELOG.md b/CHANGELOG.md index 97af202..6801b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **SM4 AEAD combined format** - `sm4_encrypt_gcm_combined` / `sm4_decrypt_gcm_combined`: TLS format (ciphertext||tag) - `sm4_encrypt_ccm_combined` / `sm4_decrypt_ccm_combined`: Same format for CCM + +## [0.2.0] - 2025-03-08 + +### Added + - **BLS signatures** (`bls` module, requires `alloc` feature) - `bls_keygen` / `bls_sign` / `bls_verify`: minimal-signature-size variant (sig ∈ G1, pk ∈ G2) - `bls_aggregate` / `bls_aggregate_verify`: multi-message aggregate signatures @@ -92,9 +97,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MSRV raised to 1.83.0 (required by crypto-bigint 0.6.x for ConstMontyForm constant-time Montgomery arithmetic) - Use Rust 1.83+ built-in `div_ceil` method instead of manual implementation - -### CI - - Optimized sanity_check.sh to skip test code, avoiding false positives ## [0.1.0] - 2025-03-07 diff --git a/Cargo.lock b/Cargo.lock index 4af12e6..4679a07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -310,6 +320,12 @@ dependencies = [ "syn", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "generic-array" version = "0.14.7" @@ -368,7 +384,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -437,6 +453,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.8.0" @@ -638,12 +660,29 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.24.0-dev.0" +source = "git+https://github.com/rustls/rustls.git?branch=main#c5a1a4a3f391718e0b5bf87de0cceabb67258ab7" dependencies = [ + "log", "once_cell", "rustls-pki-types", + "rustls-webpki", "subtle", "zeroize", ] @@ -657,6 +696,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.104.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58d0ba9664baf780379001ecb183f4b95dabec82d464bff14173dee21bf49e3" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -715,6 +765,12 @@ dependencies = [ "zmij", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "sm9_core" version = "0.5.0" @@ -775,6 +831,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "version_check" version = "0.9.5" @@ -858,7 +920,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -867,6 +929,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -876,6 +947,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zerocopy" version = "0.8.40" diff --git a/Cargo.toml b/Cargo.toml index 047c372..0e0651f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,14 @@ name = "libsmx" version = "0.3.0" edition = "2021" rust-version = "1.83.0" +authors = ["kintai "] license = "Apache-2.0" description = "Pure-Rust, no_std, constant-time SM2/SM3/SM4/SM9 Chinese cryptography (GB/T 32918/32905/32907/38635)" repository = "https://github.com/kintaiW/libsmx" documentation = "https://docs.rs/libsmx" -homepage = "https://github.com/kintaiW/libsmx" +homepage = "https://kintaiw.github.io/libsmx/" categories = ["cryptography", "no-std"] -keywords = ["sm2", "sm3", "sm4", "sm9", "gmssl"] +keywords = ["sm2", "sm3", "sm4", "sm9", "cryptography"] readme = "README.md" exclude = [ "benches/", @@ -35,7 +36,7 @@ crypto-bigint = { version = "0.6", default-features = false } subtle = { version = "2.6", default-features = false } zeroize = { version = "1.8", default-features = false, features = ["derive"] } rand_core = { version = "0.6", default-features = false } -rustls = { version = "0.24.0-dev.0", path = "../rustls/rustls", optional = true, default-features = false } +rustls = { git = "https://github.com/rustls/rustls.git", branch = "main", optional = true, default-features = false } pki-types = { package = "rustls-pki-types", version = "1", optional = true, features = ["alloc"] } getrandom = { version = "0.2", optional = true } @@ -51,6 +52,9 @@ hex = "0.4" criterion = { version = "0.5", features = ["html_reports"] } rand = "0.8" sm9_core = "0.5.0" +# rustls-provider 集成测试依赖(通过 feature 条件编译保护) +rustls = { git = "https://github.com/rustls/rustls.git", branch = "main", default-features = false, features = ["log", "webpki"] } +pki-types = { package = "rustls-pki-types", version = "1", features = ["alloc"] } [[bench]] name = "sm2_bench" diff --git a/README.md b/README.md index b059c1d..518327e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ [![License](https://img.shields.io/crates/l/libsmx.svg)](LICENSE) [![MSRV](https://img.shields.io/badge/MSRV-1.83.0-blue.svg)](https://blog.rust-lang.org/2024/11/28/Rust-1.83.0.html) -Pure-Rust, `#![no_std]` implementation of Chinese commercial cryptography standards with constant-time operations throughout. +Pure-Rust, `#![no_std]` implementation of Chinese commercial cryptography standards (国密/GuoMi) with constant-time operations throughout. Designed for embedded systems, WASM, and production use. + +> **Security Notice**: This library has **not** been independently audited. While all secret-dependent operations are constant-time and `#![forbid(unsafe_code)]` is enforced, you should evaluate the risks before using it in production. Report vulnerabilities via [SECURITY.md](SECURITY.md) or email [kintai@foxmail.com](mailto:kintai@foxmail.com). | Algorithm | Standard | Description | |-----------|----------|-------------| @@ -151,6 +153,7 @@ sm9_verify(msg, &h, &s, user_id, &sign_pub).unwrap(); |---------|---------|-------------| | `alloc` | Yes | Enables `Vec`-returning APIs (SM2/SM9 encrypt/decrypt, SM4 modes) | | `std` | No | Enables `std::error::Error` impl and re-exports `rand_core/std` | +| `rustls-provider` | No | Enables rustls `CryptoProvider` with RFC 8998 ShangMi TLS 1.3 cipher suites | For `no_std` without `alloc`: @@ -159,6 +162,13 @@ For `no_std` without `alloc`: libsmx = { version = "0.3", default-features = false } ``` +With rustls TLS 1.3 provider: + +```toml +[dependencies] +libsmx = { version = "0.3", features = ["rustls-provider"] } +``` + ## Benchmarks Measured on Linux x86_64 (single core). All operations are constant-time. @@ -211,6 +221,12 @@ cargo bench > **Disclaimer**: This library has **not** been independently audited. See [SECURITY.md](SECURITY.md) for vulnerability reporting. +## Contributing + +Bug reports, feature requests, and pull requests are welcome on [GitHub](https://github.com/kintaiW/libsmx). + +For **security vulnerabilities**, please do **not** open public issues. Instead, email [kintai@foxmail.com](mailto:kintai@foxmail.com) directly. See [SECURITY.md](SECURITY.md) for details. + ## MSRV Policy The minimum supported Rust version is **1.83.0**. MSRV bumps are treated as minor version changes. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..0b5b880 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,894 @@ + + + + + + libsmx — Pure-Rust Chinese Cryptography + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+ + + + +

+ Pure-Rust Chinese Cryptography. +
+ Zero unsafe. + Constant-time. +

+ + +

+ SM2 / SM3 / SM4 / SM9 / BLS / FPE — six algorithms, fully implemented, + strictly conforming to GB/T national standards. +

+

+ #![no_std] ready. + From bare-metal MCUs to WASM — runs everywhere. +

+ + +
+ + + View on GitHub + +
+ + +
+ +
+
+
+ + + + +
+
+
+

Algorithm Coverage

+

Six Standards. One Crate.

+
+
+ +
+
+ SM2 + GB/T 32918 +
+

Elliptic Curve PKC — sign, verify, encrypt, decrypt, key exchange, DER codec

+
+ +
+
+ SM3 + GB/T 32905 +
+

256-bit cryptographic hash — streaming, one-shot, HMAC, HKDF

+
+ +
+
+ SM4 + GB/T 32907 +
+

128-bit block cipher — ECB, CBC, OFB, CFB, CTR, GCM, CCM, XTS

+
+ +
+
+ SM9 + GB/T 38635 +
+

Identity-based crypto — BN256 pairing, sign, encrypt, key encapsulation

+
+ +
+
+ BLS + RFC 9380 +
+

BLS signatures — aggregate verification, Shamir threshold signing

+
+ +
+
+ FPE + SP 800-38G +
+

Format-preserving encryption — FNR algorithm, 7-round Feistel with SM4

+
+
+
+
+ + + + +
+
+
+

Why libsmx

+

Built for Security Engineers

+
+
+ +
+
+ +
+

Full GB/T Compliance

+

+ Complete SM2/SM3/SM4/SM9 implementations, plus BLS signatures and format-preserving encryption. Every algorithm validated against official GB/T standard test vectors. +

+
+ +
+
+ +
+

Constant-Time Everywhere

+

+ All secret-dependent ops use subtle primitives. SM2 scalar multiplication iterates all 256 bits. SM4 S-box uses boolean-circuit bitslice — zero cache-timing leaks. +

+
+ +
+
+ +
+

Zero unsafe Code

+

+ #![forbid(unsafe_code)] enforced at the crate root. Private keys implement ZeroizeOnDrop — automatic cleanup on scope exit. +

+
+ +
+
+ +
+

#![no_std] Ready

+

+ Runs on embedded MCUs, WASM runtimes, and bare-metal targets. Optional alloc and std feature flags. Zero runtime dependencies. +

+
+ +
+
+ +
+

Production Performance

+

+ SM3 hashing at 374 MiB/s. SM2 signing in 258 µs. SM4-ECB at 27 MiB/s — all measured under constant-time constraints. No shortcuts. +

+
+ +
+
+ +
+

rustls TLS 1.3

+

+ Feature-gated rustls-provider delivers RFC 8998 ShangMi cipher suites (SM4-GCM-SM3 / SM4-CCM-SM3). Plug directly into the Rust TLS ecosystem. +

+
+
+
+
+ + + + +
+
+
+

Code Examples

+

Clean API. Real Cryptography.

+
+ + +
+ + +
+ + +
+ +
+
+ +
+ + + + sm2_example.rs +
+
use libsmx::sm2::{generate_keypair, get_z, get_e, sign, verify};
+
+let mut rng = rand::rngs::OsRng;
+
+// Key generation
+let (pri_key, pub_key) = generate_keypair(&mut rng);
+
+// Sign: compute Z value and message digest per GB/T 32918.2
+let z = get_z(b"1234567812345678", &pub_key);
+let e = get_e(&z, b"hello SM2");
+let sig = sign(&e, &pri_key, &mut rng);
+
+// Verify
+verify(&e, &pub_key, &sig).expect("signature valid");
+
+
+ +
+
+
+ 1 + generate_keypair +
+

OS-level CSPRNG generates a 256-bit SM2 key pair on the standard curve (GB/T 32918.5).

+
+
+
+ 2 + get_z +
+

Computes the user-distinguishing Z value per the national standard — SM3 hash of ID length, ID, curve params, and public key coordinates.

+
+
+
+ 3 + get_e → sign +
+

Digest e = SM3(Z || M), then sign. Scalar multiplication runs all 256 rounds — constant-time against timing attacks.

+
+
+
+ 4 + verify +
+

Constant-time verification returns Result<()>. Failures leak zero timing information.

+
+
+
+ + + +
+
+ + + + +
+
+
+

Performance

+

Security Is Not a Performance Trade-off

+

All measurements are constant-time. Linux x86_64, single core. Run cargo bench for your own numbers.

+
+ +
+ + +
+

+ Throughput +

+

64 KiB data, constant-time

+ + +
+
+ SM3 Hash + 374 MiB/s +
+
+
+
+
167 µs @ 64 KiB
+
+ + +
+
+ SM4-ECB + 27 MiB/s +
+
+
+
+
2.32 ms @ 64 KiB
+
+
+ + +
+

+ SM2 (256-bit ECC) +

+

Elliptic curve operations

+ +
+
+ Key Generation + 221 µs +
+
+
+
+ +
+ Sign + 258 µs +
+
+
+
+ +
+ Verify + 316 µs +
+
+
+
+ +
+ Encrypt + 639 µs +
+
+
+
+ +
+ Decrypt + 417 µs +
+
+
+
+
+
+ + +
+

+ SM9 (BN256 Pairing) +

+

Identity-based cryptography

+ +
+
+ Master Keygen + 753 µs +
+
+
+
+ +
+ User Keygen + 324 µs +
+
+
+
+ +
+ Sign + 3.44 ms +
+
+
+
+ +
+ Verify + 5.50 ms +
+
+
+
+ +
+ Encrypt + 4.68 ms +
+
+
+
+ +
+ Decrypt + 1.54 ms +
+
+
+
+
+
+ +
+
+
+ + + + +
+
+
+

Trust

+

Why Trust libsmx?

+
+ +
+ +
+
+ +
+

Zero unsafe

+

+ #![forbid(unsafe_code)] is enforced at the crate root — compile-time guarantee. Private keys implement ZeroizeOnDrop. GCM/CCM tags verified in constant time. +

+
+ +
+
+ +
+

MSRV Policy

+

+ Minimum supported Rust version: 1.83.0. MSRV bumps are treated as minor version changes. CI matrix enforces compatibility. No nightly required — ever. +

+
+ +
+
+ +
+

Apache-2.0

+

+ Business-friendly license. No copyleft obligations. Free to use in commercial products, internal systems, and government compliance projects. +

+
+
+ + +
+

+ Disclaimer: This library has not been independently audited. + Report vulnerabilities via SECURITY.md. +

+
+
+
+ + + + +
+
+

Get Started in Seconds

+

Add libsmx to your project and start using Chinese cryptography today.

+ +
+
+ + + + terminal +
+
+

# Add to your project

+

$ cargo add libsmx

+

# Or with no_std (no alloc)

+

$ cargo add libsmx --no-default-features

+

# With rustls TLS provider

+

$ cargo add libsmx --features rustls-provider

+
+
+ + +
+
+ + + + + + + + + + + + + diff --git a/docs/og-image.png b/docs/og-image.png new file mode 100644 index 0000000..a23e7eb Binary files /dev/null and b/docs/og-image.png differ diff --git a/docs/og-image.svg b/docs/og-image.svg new file mode 100644 index 0000000..6cba4b9 --- /dev/null +++ b/docs/og-image.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + </> libsmx + + + + + Pure-Rust Chinese Cryptography + + + + + + + Zero unsafe + + + + + + Constant-time + + + + + + #![no_std] + + + + + SM2 · SM3 · SM4 · SM9 · BLS · FPE + + + + + + SM3 374 MiB/s + SM2 Sign 258 µs + SM4 27 MiB/s + + + + + + + + + + github.com/kintaiW/libsmx + + diff --git a/src/rustls_provider/hash.rs b/src/rustls_provider/hash.rs index 5be7fb7..a3dd295 100644 --- a/src/rustls_provider/hash.rs +++ b/src/rustls_provider/hash.rs @@ -49,3 +49,59 @@ impl crypto::hash::Context for Sm3Context { self.0.update(data); } } + +#[cfg(test)] +mod tests { + use rustls::crypto::hash::Hash; + + use super::SM3; + + // GB/T 32905 标准向量:"abc" → SM3 哈希 + const ABC_DIGEST: &[u8] = &[ + 0x66, 0xc7, 0xf0, 0xf4, 0x62, 0xee, 0xed, 0xd9, 0xd1, 0xf2, 0xd4, 0x6b, 0xdc, 0x10, 0xe4, + 0xe2, 0x41, 0x67, 0xc4, 0x87, 0x5c, 0xf2, 0xf7, 0xa2, 0x29, 0x7d, 0xa0, 0x2b, 0x8f, 0x4b, + 0xa8, 0xe0, + ]; + + #[test] + fn test_hash_abc() { + let out = SM3.hash(b"abc"); + assert_eq!(out.as_ref(), ABC_DIGEST); + } + + #[test] + fn test_context_update_finish() { + let mut ctx = SM3.start(); + ctx.update(b"ab"); + ctx.update(b"c"); + let out = ctx.finish(); + assert_eq!(out.as_ref(), ABC_DIGEST); + } + + #[test] + fn test_context_fork_finish() { + let mut ctx = SM3.start(); + ctx.update(b"abc"); + // fork_finish 不消耗 ctx,fork 后原 ctx 可继续 finish + let out1 = ctx.fork_finish(); + let out2 = ctx.finish(); + assert_eq!(out1.as_ref(), ABC_DIGEST); + assert_eq!(out2.as_ref(), ABC_DIGEST); + } + + #[test] + fn test_context_fork() { + let mut ctx = SM3.start(); + ctx.update(b"abc"); + let forked = ctx.fork(); + let out1 = forked.finish(); + let out2 = ctx.finish(); + assert_eq!(out1.as_ref(), ABC_DIGEST); + assert_eq!(out2.as_ref(), ABC_DIGEST); + } + + #[test] + fn test_output_len() { + assert_eq!(SM3.output_len(), 32); + } +} diff --git a/src/rustls_provider/hmac.rs b/src/rustls_provider/hmac.rs index 4755578..fbf899b 100644 --- a/src/rustls_provider/hmac.rs +++ b/src/rustls_provider/hmac.rs @@ -43,3 +43,41 @@ impl hmac::Key for Sm3HmacKey { 32 } } + +#[cfg(test)] +mod tests { + use rustls::crypto::hmac::Hmac; + + use super::HMAC_SM3; + + // RFC 2104 HMAC-SM3 已知正确值(用 libsmx hmac_sm3 预计算) + // key = b"key", data = b"The quick brown fox jumps over the lazy dog" + // 通过 crate::sm3::hmac_sm3 验证 + fn reference_hmac(key: &[u8], data: &[u8]) -> [u8; 32] { + crate::sm3::hmac_sm3(key, data) + } + + #[test] + fn test_sign_concat_single_chunk() { + let key = b"secret"; + let data = b"hello world"; + let k = HMAC_SM3.with_key(key); + let tag = k.sign_concat(data, &[], &[]); + assert_eq!(tag.as_ref(), reference_hmac(key, data)); + } + + #[test] + fn test_sign_concat_split_matches_whole() { + // 分段 "hel" + ["lo "] + "world" 应与整体 "hello world" 结果相同 + let key = b"secret"; + let k = HMAC_SM3.with_key(key); + let tag = k.sign_concat(b"hel", &[b"lo "], b"world"); + assert_eq!(tag.as_ref(), reference_hmac(key, b"hello world")); + } + + #[test] + fn test_tag_len() { + let k = HMAC_SM3.with_key(b"k"); + assert_eq!(k.tag_len(), 32); + } +} diff --git a/src/rustls_provider/kx.rs b/src/rustls_provider/kx.rs index fbc1a0c..efa7dc4 100644 --- a/src/rustls_provider/kx.rs +++ b/src/rustls_provider/kx.rs @@ -3,7 +3,9 @@ use alloc::boxed::Box; use alloc::vec::Vec; -use rustls::crypto::kx::{ActiveKeyExchange, NamedGroup, SharedSecret, StartedKeyExchange, SupportedKxGroup}; +use rustls::crypto::kx::{ + ActiveKeyExchange, NamedGroup, SharedSecret, StartedKeyExchange, SupportedKxGroup, +}; use rustls::error::{Error, PeerMisbehaved}; use crate::sm2::{generate_keypair, key_exchange::ecdh_from_slice, PrivateKey}; @@ -77,7 +79,57 @@ impl rand_core::RngCore for Sm2Rng { getrandom::getrandom(dest).map_err(|e| { // Reason: rand_core::Error::new 接受 NonZeroU32,直接用 getrandom 错误码 use core::num::NonZeroU32; - rand_core::Error::from(NonZeroU32::new(e.raw_os_error().unwrap_or(1) as u32).unwrap_or(NonZeroU32::new(1).unwrap())) + rand_core::Error::from( + NonZeroU32::new(e.raw_os_error().unwrap_or(1) as u32) + .unwrap_or(NonZeroU32::new(1).unwrap()), + ) }) } } + +#[cfg(test)] +mod tests { + use rustls::crypto::kx::NamedGroup; + + use super::CURVE_SM2; + + #[test] + fn test_name() { + assert_eq!(CURVE_SM2.name(), NamedGroup::curveSM2); + } + + #[test] + fn test_ecdhe_roundtrip() { + // 模拟 TLS 握手:A 生成临时密钥对,B 生成临时密钥对,双方计算共享密钥 + let kx_a = CURVE_SM2.start().unwrap().into_single(); + let kx_b = CURVE_SM2.start().unwrap().into_single(); + + let pub_a = kx_a.pub_key().to_vec(); + let pub_b = kx_b.pub_key().to_vec(); + + // 65 字节非压缩公钥(04 || x || y) + assert_eq!(pub_a.len(), 65); + assert_eq!(pub_b.len(), 65); + assert_eq!(pub_a[0], 0x04); + + let secret_a = kx_a.complete(&pub_b).unwrap(); + let secret_b = kx_b.complete(&pub_a).unwrap(); + + // 两端共享密钥必须相同(32 字节 x 坐标) + assert_eq!(secret_a.secret_bytes(), secret_b.secret_bytes()); + assert_eq!(secret_a.secret_bytes().len(), 32); + } + + #[test] + fn test_invalid_peer_key_rejected() { + let kx = CURVE_SM2.start().unwrap().into_single(); + // 无效公钥(全零)应报错 + let result = kx.complete(&[0u8; 65]); + assert!(result.is_err()); + } + + #[test] + fn test_no_ffdhe_group() { + assert!(CURVE_SM2.ffdhe_group().is_none()); + } +} diff --git a/src/rustls_provider/mod.rs b/src/rustls_provider/mod.rs index 90499b2..11d0aa3 100644 --- a/src/rustls_provider/mod.rs +++ b/src/rustls_provider/mod.rs @@ -71,6 +71,8 @@ impl TicketerFactory for SmTicketerFactory { fn ticketer(&self) -> Result, Error> { // Reason: TLS session ticket 加密暂不支持,返回错误; // 后续可用 SM4-GCM 实现 ticket 加密 - Err(Error::General("SM ticket factory not yet implemented".into())) + Err(Error::General( + "SM ticket factory not yet implemented".into(), + )) } } diff --git a/src/rustls_provider/sign.rs b/src/rustls_provider/sign.rs index b243705..74b4456 100644 --- a/src/rustls_provider/sign.rs +++ b/src/rustls_provider/sign.rs @@ -18,10 +18,14 @@ pub(crate) fn load_private_key( key_der: PrivateKeyDer<'static>, ) -> Result, Error> { let pri_key = match &key_der { - PrivateKeyDer::Sec1(sec1) => crate::sm2::der::private_key_from_sec1_der(sec1.secret_sec1_der()) - .map_err(|_| Error::General("invalid SEC1 SM2 private key".into()))?, - PrivateKeyDer::Pkcs8(pkcs8) => crate::sm2::der::private_key_from_pkcs8_der(pkcs8.secret_pkcs8_der()) - .map_err(|_| Error::General("invalid PKCS#8 SM2 private key".into()))?, + PrivateKeyDer::Sec1(sec1) => { + crate::sm2::der::private_key_from_sec1_der(sec1.secret_sec1_der()) + .map_err(|_| Error::General("invalid SEC1 SM2 private key".into()))? + } + PrivateKeyDer::Pkcs8(pkcs8) => { + crate::sm2::der::private_key_from_pkcs8_der(pkcs8.secret_pkcs8_der()) + .map_err(|_| Error::General("invalid PKCS#8 SM2 private key".into()))? + } _ => return Err(Error::General("unsupported SM2 key format".into())), }; Ok(Box::new(Sm2SigningKey { pri_key })) @@ -78,3 +82,84 @@ impl Signer for Sm2Signer { SignatureScheme::SM2_SM3 } } + +#[cfg(test)] +mod tests { + use alloc::boxed::Box; + use alloc::vec::Vec; + + use rustls::crypto::SignatureScheme; + + use super::load_private_key; + use crate::sm2::{der::sig_from_der, generate_keypair, verify_message, PrivateKey, DEFAULT_ID}; + + /// 将 SM2 私钥编码为最小 SEC1 DER(无可选字段) + /// ECPrivateKey ::= SEQUENCE { version INTEGER(1), privateKey OCTET STRING(32B) } + fn encode_sec1_der(pri_key: &PrivateKey) -> Vec { + let key_bytes = pri_key.as_bytes(); + // SEQUENCE body = INTEGER(1)[3B] + OCTET STRING(32B)[34B] = 37 字节 + let mut der = Vec::with_capacity(39); + der.extend_from_slice(&[ + 0x30, 0x25, // SEQUENCE, length 37 + 0x02, 0x01, 0x01, // INTEGER(1) = version + 0x04, 0x20, // OCTET STRING, length 32 + ]); + der.extend_from_slice(key_bytes); + der + } + + fn make_test_key_der() -> (PrivateKey, [u8; 65], pki_types::PrivateKeyDer<'static>) { + let mut rng = super::super::kx::Sm2Rng; + let (pri_key, pub_key) = generate_keypair(&mut rng); + let sec1 = encode_sec1_der(&pri_key); + let key_der = pki_types::PrivateKeyDer::Sec1(pki_types::PrivateSec1KeyDer::from(sec1)); + (pri_key, pub_key, key_der) + } + + #[test] + fn test_choose_scheme_sm2_sm3() { + let (_, _, key_der) = make_test_key_der(); + let signing_key = load_private_key(key_der).unwrap(); + let signer = signing_key.choose_scheme(&[SignatureScheme::SM2_SM3]); + assert!(signer.is_some()); + assert_eq!(signer.unwrap().scheme(), SignatureScheme::SM2_SM3); + } + + #[test] + fn test_choose_scheme_unmatched_returns_none() { + let (_, _, key_der) = make_test_key_der(); + let signing_key = load_private_key(key_der).unwrap(); + // 不含 SM2_SM3 时应返回 None + let signer = signing_key.choose_scheme(&[SignatureScheme::ECDSA_NISTP256_SHA256]); + assert!(signer.is_none()); + } + + #[test] + fn test_public_key_is_spki() { + let (_, _, key_der) = make_test_key_der(); + let signing_key = load_private_key(key_der).unwrap(); + let spki = signing_key.public_key(); + assert!(spki.is_some()); + // SPKI DER 以 SEQUENCE (0x30) 开头 + let spki_bytes: &[u8] = spki.as_ref().unwrap().as_ref(); + assert_eq!(spki_bytes[0], 0x30); + // 完整 SM2 SPKI:alg(21B) + BIT STRING(68B) = 89B,外层 SEQUENCE header(2B) = 91B + assert_eq!(spki_bytes.len(), 91); + } + + #[test] + fn test_sign_produces_valid_der_signature() { + let (_, pub_key, key_der) = make_test_key_der(); + let signing_key = load_private_key(key_der).unwrap(); + + let signer = signing_key + .choose_scheme(&[SignatureScheme::SM2_SM3]) + .unwrap(); + let message = b"hello SM2"; + let sig_der = Box::new(signer).sign(message).unwrap(); + + // 解析 DER 签名并用底层 API 验签 + let sig_raw = sig_from_der(&sig_der).unwrap(); + verify_message(message, DEFAULT_ID, &pub_key, &sig_raw).unwrap(); + } +} diff --git a/src/rustls_provider/tls13.rs b/src/rustls_provider/tls13.rs index ac89350..5578114 100644 --- a/src/rustls_provider/tls13.rs +++ b/src/rustls_provider/tls13.rs @@ -4,9 +4,8 @@ use alloc::boxed::Box; use alloc::vec::Vec; use rustls::crypto::cipher::{ - make_tls13_aad, AeadKey, EncodedMessage, InboundOpaque, Iv, MessageDecrypter, - MessageEncrypter, Nonce, OutboundOpaque, OutboundPlain, Tls13AeadAlgorithm, - UnsupportedOperationError, NONCE_LEN, + make_tls13_aad, AeadKey, EncodedMessage, InboundOpaque, Iv, MessageDecrypter, MessageEncrypter, + Nonce, OutboundOpaque, OutboundPlain, Tls13AeadAlgorithm, UnsupportedOperationError, NONCE_LEN, }; use rustls::crypto::tls13::HkdfUsingHmac; use rustls::crypto::CipherSuite; @@ -144,10 +143,11 @@ impl MessageDecrypter for Sm4GcmDecrypter { let aad = make_tls13_aad(payload.len()); let ct_len = payload.len() - 16; - let tag: [u8; 16] = payload[ct_len..].try_into().map_err(|_| Error::DecryptError)?; - let plaintext = - sm4_decrypt_gcm(&self.key, &nonce, &aad, &payload[..ct_len], &tag) - .map_err(|_| Error::DecryptError)?; + let tag: [u8; 16] = payload[ct_len..] + .try_into() + .map_err(|_| Error::DecryptError)?; + let plaintext = sm4_decrypt_gcm(&self.key, &nonce, &aad, &payload[..ct_len], &tag) + .map_err(|_| Error::DecryptError)?; // 将明文写回 payload(in-place),然后截断 let plain_len = plaintext.len(); diff --git a/src/rustls_provider/verify.rs b/src/rustls_provider/verify.rs index f4571c1..170df50 100644 --- a/src/rustls_provider/verify.rs +++ b/src/rustls_provider/verify.rs @@ -1,7 +1,7 @@ //! SM2 验签 → rustls `SignatureVerificationAlgorithm` use pki_types::{AlgorithmIdentifier, SignatureVerificationAlgorithm}; -use rustls::crypto::{WebPkiSupportedAlgorithms, SignatureScheme}; +use rustls::crypto::{SignatureScheme, WebPkiSupportedAlgorithms}; use crate::sm2::{der::sig_from_der, verify_message, DEFAULT_ID}; @@ -31,8 +31,7 @@ impl SignatureVerificationAlgorithm for Sm2Sm3Algorithm { AlgorithmIdentifier::from_slice(&[ 0x30, 0x13, // SEQUENCE 0x06, 0x07, // OID id-ecPublicKey - 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, - 0x06, 0x08, // OID SM2 + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, // OID SM2 0x2a, 0x81, 0x1c, 0xcf, 0x55, 0x01, 0x82, 0x2d, ]) } @@ -53,11 +52,62 @@ impl SignatureVerificationAlgorithm for Sm2Sm3Algorithm { .map_err(|_| pki_types::InvalidSignature)?; // DER 解码签名 - let sig_raw = - sig_from_der(signature).map_err(|_| pki_types::InvalidSignature)?; + let sig_raw = sig_from_der(signature).map_err(|_| pki_types::InvalidSignature)?; // SM2 验签(使用默认 ID,GB/T 32918.2) verify_message(message, DEFAULT_ID, pub_key_arr, &sig_raw) .map_err(|_| pki_types::InvalidSignature) } } + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use pki_types::SignatureVerificationAlgorithm; + + use super::SM2_SM3_ALG; + use crate::sm2::{der::sig_to_der, generate_keypair, sign_message, DEFAULT_ID}; + + fn sign_with_der(message: &[u8]) -> ([u8; 65], Vec) { + let mut rng = crate::rustls_provider::kx::Sm2Rng; + let (pri_key, pub_key) = generate_keypair(&mut rng); + let sig_raw = sign_message(message, DEFAULT_ID, &pri_key, &mut rng); + (pub_key, sig_to_der(&sig_raw)) + } + + #[test] + fn test_verify_valid_signature() { + let msg = b"test message"; + let (pub_key, sig_der) = sign_with_der(msg); + SM2_SM3_ALG + .verify_signature(&pub_key, msg, &sig_der) + .unwrap(); + } + + #[test] + fn test_verify_wrong_message_fails() { + let (pub_key, sig_der) = sign_with_der(b"original"); + let result = SM2_SM3_ALG.verify_signature(&pub_key, b"tampered", &sig_der); + assert!(result.is_err()); + } + + #[test] + fn test_verify_wrong_pubkey_fails() { + let msg = b"hello"; + let (_, sig_der) = sign_with_der(msg); + // 用另一个公钥验证 + let mut rng = crate::rustls_provider::kx::Sm2Rng; + let (_, other_pub) = generate_keypair(&mut rng); + let result = SM2_SM3_ALG.verify_signature(&other_pub, msg, &sig_der); + assert!(result.is_err()); + } + + #[test] + fn test_verify_invalid_pubkey_len_fails() { + let (_, sig_der) = sign_with_der(b"msg"); + // 公钥长度不是 65 字节时应报错 + let result = SM2_SM3_ALG.verify_signature(&[0u8; 32], b"msg", &sig_der); + assert!(result.is_err()); + } +} diff --git a/src/sm3/mod.rs b/src/sm3/mod.rs index fe99df9..d8db8ad 100644 --- a/src/sm3/mod.rs +++ b/src/sm3/mod.rs @@ -291,8 +291,6 @@ impl zeroize::Zeroize for HmacSm3 { #[cfg(test)] mod tests { - #[cfg(feature = "alloc")] - extern crate alloc; use super::*; /// GB/T 32905-2016 附录 A 示例 1:SM3("abc") @@ -399,11 +397,7 @@ mod tests { let key = b"streaming-key"; let parts: &[&[u8]] = &[b"hello", b" ", b"world"]; - let mut all = alloc::vec![]; - for p in parts { - all.extend_from_slice(p); - } - let expected = hmac_sm3(key, &all); + let expected = hmac_sm3(key, b"hello world"); let mut h = HmacSm3::new(key); for p in parts { diff --git a/tests/rustls_provider.rs b/tests/rustls_provider.rs new file mode 100644 index 0000000..8c27643 --- /dev/null +++ b/tests/rustls_provider.rs @@ -0,0 +1,333 @@ +//! rustls CryptoProvider 端到端集成测试(阶段 L.2) +//! +//! 使用 Raw Public Key(RFC 7250)模式,无需 X.509 证书, +//! 直接用 SM2 密钥对作为身份凭证完成 TLS 1.3 握手自回环测试。 + +#![cfg(feature = "rustls-provider")] + +use std::io::{Read, Write}; +use std::sync::Arc; + +use pki_types::{PrivateSec1KeyDer, ServerName, SubjectPublicKeyInfoDer}; +use rustls::client::danger::{ + HandshakeSignatureValid, PeerVerified, ServerIdentity, ServerVerifier, +}; +use rustls::crypto::{CipherSuite, CryptoProvider, SignatureScheme}; +use rustls::enums::CertificateType; +use rustls::server::danger::SignatureVerificationInput; +use rustls::{ClientConfig, ClientConnection, Connection, ServerConfig, ServerConnection}; + +use libsmx::rustls_provider; +use libsmx::sm2::{ + der::{public_key_to_spki_der, sig_from_der}, + generate_keypair, verify_message, DEFAULT_ID, +}; + +// ── 测试用 RNG ──────────────────────────────────────────────────────────────── + +struct TestRng; + +impl rand_core::RngCore for TestRng { + fn next_u32(&mut self) -> u32 { + let mut b = [0u8; 4]; + getrandom::getrandom(&mut b).unwrap(); + u32::from_le_bytes(b) + } + fn next_u64(&mut self) -> u64 { + let mut b = [0u8; 8]; + getrandom::getrandom(&mut b).unwrap(); + u64::from_le_bytes(b) + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + getrandom::getrandom(dest).unwrap(); + } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + getrandom::getrandom(dest).map_err(|_| { + use core::num::NonZeroU32; + rand_core::Error::from(NonZeroU32::new(1).unwrap()) + }) + } +} + +// ── 测试工具 ────────────────────────────────────────────────────────────────── + +/// 生成 SM2 密钥对,返回 (SEC1 DER 私钥, SPKI DER 公钥) +fn make_sm2_keypair() -> (Vec, Vec) { + let mut rng = TestRng; + let (pri_key, pub_key) = generate_keypair(&mut rng); + + // 最小 SEC1 DER: SEQUENCE { INTEGER(1), OCTET STRING(32B) } + let mut sec1 = Vec::with_capacity(39); + sec1.extend_from_slice(&[ + 0x30, 0x25, // SEQUENCE, length 37 + 0x02, 0x01, 0x01, // INTEGER(1) version + 0x04, 0x20, // OCTET STRING, length 32 + ]); + sec1.extend_from_slice(pri_key.as_bytes()); + + let spki = public_key_to_spki_der(&pub_key); + (sec1, spki) +} + +/// 在内存中传输 TLS 记录(left → right) +fn transfer(left: &mut impl Connection, right: &mut impl Connection) -> usize { + let mut buf = [0u8; 65536]; + let mut total = 0; + while left.wants_write() { + let n = left.write_tls(&mut &mut buf[..]).unwrap(); + if n == 0 { + break; + } + total += n; + let mut offs = 0; + while offs < n { + offs += right.read_tls(&mut &buf[offs..n]).unwrap(); + } + } + total +} + +/// 驱动完整 TLS 握手 +fn do_handshake(client: &mut ClientConnection, server: &mut ServerConnection) { + while client.is_handshaking() || server.is_handshaking() { + transfer(client, server); + server.process_new_packets().unwrap(); + transfer(server, client); + client.process_new_packets().unwrap(); + } +} + +// ── 自定义 ServerVerifier(测试用,接受任意 SM2 Raw Public Key)────────────── + +#[derive(Debug)] +struct AcceptAllSm2ServerVerifier; + +impl AcceptAllSm2ServerVerifier { + fn new() -> Self { + Self + } +} + +impl ServerVerifier for AcceptAllSm2ServerVerifier { + fn verify_identity( + &self, + _identity: &ServerIdentity<'_>, + ) -> Result { + // Reason: 测试中不验证服务端身份(Raw Public Key 模式,无 CA 信任链) + Ok(PeerVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _input: &SignatureVerificationInput<'_>, + ) -> Result { + Err(rustls::Error::General("TLS 1.2 not supported".into())) + } + + fn verify_tls13_signature( + &self, + input: &SignatureVerificationInput<'_>, + ) -> Result { + // Reason: webpki 不支持 SM2 OID,直接调用 libsmx SM2 验签实现, + // 绕过 webpki 的 OID 匹配逻辑 + use rustls::SignerPublicKey; + let spki_der: &[u8] = match input.signer { + SignerPublicKey::RawPublicKey(spki) => spki.as_ref(), + _ => return Err(rustls::Error::General("expected Raw Public Key".into())), + }; + // SM2 SPKI 结构(91字节): + // SEQUENCE(2B) + AlgId SEQUENCE(21B) + BIT STRING tag+len+pad(3B) + 公钥(65B) + // 公钥从偏移 26 开始,共 65 字节 + if spki_der.len() != 91 { + return Err(rustls::Error::General(format!( + "unexpected SPKI length: {}", + spki_der.len() + ))); + } + let pub_key_bytes: &[u8; 65] = spki_der[26..91] + .try_into() + .map_err(|_| rustls::Error::General("bad pubkey slice".into()))?; + + let sig_der = input.signature.signature(); + let sig_raw = sig_from_der(sig_der) + .map_err(|_| rustls::Error::General("invalid DER signature".into()))?; + + verify_message(input.message, DEFAULT_ID, pub_key_bytes, &sig_raw) + .map(|_| HandshakeSignatureValid::assertion()) + .map_err(|_| rustls::Error::General("SM2 signature verification failed".into())) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![SignatureScheme::SM2_SM3] + } + + fn request_ocsp_response(&self) -> bool { + false + } + + fn supported_certificate_types(&self) -> &'static [CertificateType] { + &[CertificateType::RawPublicKey] + } + + fn hash_config(&self, _: &mut dyn core::hash::Hasher) {} +} + +// ── 辅助:用 Raw Public Key 模式构建 server/client Config ──────────────────── + +fn make_configs( + provider: &CryptoProvider, + server_sec1: Vec, + server_spki: Vec, +) -> (ServerConfig, ClientConfig) { + // 服务端:用 new_unchecked 绕过 webpki 对 SM2 OID 的校验 + let server_key_der = pki_types::PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(server_sec1)); + let server_signing_key = provider + .key_provider + .load_private_key(server_key_der) + .unwrap(); + let identity = Arc::new(rustls::crypto::Identity::RawPublicKey( + SubjectPublicKeyInfoDer::from(server_spki), + )); + // Reason: Credentials::new_unchecked 跳过 webpki 验证,因为 SM2 OID 尚未被 + // rustls-webpki 支持,但我们的 SignatureVerificationAlgorithm 实现是正确的 + let creds = rustls::crypto::Credentials::new_unchecked(identity, server_signing_key); + let cert_resolver = Arc::new(rustls::crypto::SingleCredential::from(creds)); + let server_config = ServerConfig::builder(provider.clone().into()) + .with_no_client_auth() + .with_server_credential_resolver(cert_resolver) + .unwrap(); + + // 客户端:自定义 verifier 跳过证书链校验 + let verifier = Arc::new(AcceptAllSm2ServerVerifier::new()); + let client_config = ClientConfig::builder(provider.clone().into()) + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_no_client_auth() + .unwrap(); + + (server_config, client_config) +} + +// ── 测试用例 ────────────────────────────────────────────────────────────────── + +/// TLS 1.3 SM4-GCM-SM3 握手自回环(Raw Public Key 模式) +#[test] +fn test_tls13_sm4_gcm_sm3_handshake() { + let provider = rustls_provider::provider(); + let (server_sec1, server_spki) = make_sm2_keypair(); + let (server_config, client_config) = make_configs(&provider, server_sec1, server_spki); + + let server_name = ServerName::try_from("localhost").unwrap(); + let client_arc = Arc::new(client_config); + let mut client = client_arc.connect(server_name).build().unwrap(); + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); + + do_handshake(&mut client, &mut server); + + assert_eq!( + client.negotiated_cipher_suite().unwrap().suite(), + CipherSuite::TLS13_SM4_GCM_SM3, + ); + + // server → client 数据传输 + let plaintext = b"Hello, SM2/SM4/SM3!"; + server.writer().write_all(plaintext).unwrap(); + transfer(&mut server, &mut client); + client.process_new_packets().unwrap(); + let mut received = vec![0u8; plaintext.len()]; + client.reader().read_exact(&mut received).unwrap(); + assert_eq!(received, plaintext.to_vec()); +} + +/// TLS 1.3 SM4-CCM-SM3 握手自回环 +#[test] +fn test_tls13_sm4_ccm_sm3_handshake() { + use std::borrow::Cow; + + static CCM_SUITES: &[&rustls::Tls13CipherSuite] = + &[libsmx::rustls_provider::tls13::TLS13_SM4_CCM_SM3]; + + let provider = CryptoProvider { + tls13_cipher_suites: Cow::Borrowed(CCM_SUITES), + ..rustls_provider::provider() + }; + + let (server_sec1, server_spki) = make_sm2_keypair(); + let (server_config, client_config) = make_configs(&provider, server_sec1, server_spki); + + let server_name = ServerName::try_from("localhost").unwrap(); + let client_arc = Arc::new(client_config); + let mut client = client_arc.connect(server_name).build().unwrap(); + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); + + do_handshake(&mut client, &mut server); + + assert_eq!( + client.negotiated_cipher_suite().unwrap().suite(), + CipherSuite::TLS13_SM4_CCM_SM3, + ); + + // client → server 数据传输 + let msg = b"SM4-CCM test"; + client.writer().write_all(msg).unwrap(); + transfer(&mut client, &mut server); + server.process_new_packets().unwrap(); + let mut buf = vec![0u8; msg.len()]; + server.reader().read_exact(&mut buf).unwrap(); + assert_eq!(buf, msg.to_vec()); +} + +/// 验证握手后密钥交换组为 curveSM2 +#[test] +fn test_kx_group_is_curve_sm2() { + let provider = rustls_provider::provider(); + let (server_sec1, server_spki) = make_sm2_keypair(); + let (server_config, client_config) = make_configs(&provider, server_sec1, server_spki); + + let server_name = ServerName::try_from("localhost").unwrap(); + let client_arc = Arc::new(client_config); + let mut client = client_arc.connect(server_name).build().unwrap(); + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); + + do_handshake(&mut client, &mut server); + + assert_eq!( + client + .negotiated_key_exchange_group() + .map(|g: &dyn rustls::crypto::kx::SupportedKxGroup| g.name()), + Some(rustls::crypto::kx::NamedGroup::curveSM2), + ); +} + +/// 双向数据传输(client ↔ server 各发一条消息) +#[test] +fn test_bidirectional_data_transfer() { + let provider = rustls_provider::provider(); + let (server_sec1, server_spki) = make_sm2_keypair(); + let (server_config, client_config) = make_configs(&provider, server_sec1, server_spki); + + let server_name = ServerName::try_from("localhost").unwrap(); + let client_arc = Arc::new(client_config); + let mut client = client_arc.connect(server_name).build().unwrap(); + let mut server = ServerConnection::new(Arc::new(server_config)).unwrap(); + + do_handshake(&mut client, &mut server); + + // client → server + let c2s = b"from client"; + client.writer().write_all(c2s).unwrap(); + transfer(&mut client, &mut server); + server.process_new_packets().unwrap(); + let mut buf = vec![0u8; c2s.len()]; + server.reader().read_exact(&mut buf).unwrap(); + assert_eq!(buf, c2s.to_vec()); + + // server → client + let s2c = b"from server"; + server.writer().write_all(s2c).unwrap(); + transfer(&mut server, &mut client); + client.process_new_packets().unwrap(); + let mut buf2 = vec![0u8; s2c.len()]; + client.reader().read_exact(&mut buf2).unwrap(); + assert_eq!(buf2, s2c.to_vec()); +}