From 2580571881936a9c27169c8462b4db0afd5952be Mon Sep 17 00:00:00 2001 From: huangxt Date: Sun, 8 Mar 2026 20:58:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=20v0.2.1=EF=BC=9ASM2=20?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E4=BA=A4=E6=8D=A2=E3=80=81HKDF-SM3=E3=80=81D?= =?UTF-8?q?ER=20=E7=BC=96=E8=A7=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - SM2 密钥交换:GB/T 32918.3 完整协议(exchange_a/b)、ECDH - SM2 DER 编解码:签名 DER、SEC1/PKCS#8 私钥解析 - SM2 便捷 API:sign_message/verify_message 自动计算 Z 值 - HKDF-SM3:RFC 5869 兼容的 HKDF - SM3 Hasher 增强:reset/finalize_reset 流式复用 - SM4 AEAD 合并格式:GCM/CCM combined(TLS 格式) 文档更新: - CHANGELOG 添加 v0.2.1 变更记录 --- CHANGELOG.md | 21 +- CHANGELOG.zh-CN.md | 19 +- Cargo.lock | 2 +- Cargo.toml | 2 +- src/error.rs | 3 + src/sm2/der.rs | 438 ++++++++++++++++++++++++++++ src/sm2/key_exchange.rs | 620 ++++++++++++++++++++++++++++++++++++++++ src/sm2/mod.rs | 95 ++++++ src/sm3/hkdf.rs | 194 +++++++++++++ src/sm3/mod.rs | 85 +++++- src/sm4/modes.rs | 63 ++++ 11 files changed, 1525 insertions(+), 17 deletions(-) create mode 100644 src/sm2/der.rs create mode 100644 src/sm2/key_exchange.rs create mode 100644 src/sm3/hkdf.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c8eb2..e910192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - v0.2.0 +## [0.2.1] - 2025-03-08 ### Added +- **SM2 key exchange** (`sm2::key_exchange` module) + - `exchange_a` / `exchange_b`: GB/T 32918.3 full key exchange protocol with confirmation hash + - `ecdh`: Simple SM2-ECDH shared secret computation (TLS/rustls compatible) + - `ecdh_from_slice`: Slice-based ECDH for TLS integration + - `EphemeralKey`: Ephemeral key pair with `ZeroizeOnDrop` +- **SM2 DER codec** (`sm2::der` module) + - `sig_to_der` / `sig_from_der`: Signature DER encoding/decoding + - `private_key_from_sec1_der`: RFC 5915 SEC1 private key parsing + - `private_key_from_pkcs8_der`: RFC 5958 PKCS#8 private key parsing +- **SM2 convenience API** (`sm2` module) + - `sign_message` / `verify_message`: One-step sign/verify with automatic Z-value computation +- **HKDF-SM3** (`sm3::hkdf` module) + - `hkdf_extract` / `hkdf_expand` / `hkdf`: RFC 5869 compatible +- **SM3 Hasher enhancements** + - `reset()` / `finalize_reset()`: Streaming hasher reuse +- **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 - **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 @@ -102,6 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - XTS: reject non-16-byte-aligned input instead of silently truncating - SM9 `hash_to_range`: replaced variable-iteration `while` loop with constant-time conditional select +[0.2.1]: https://github.com/kintaiW/libsmx/releases/tag/v0.2.1 [0.2.0]: https://github.com/kintaiW/libsmx/releases/tag/v0.2.0 [0.1.1]: https://github.com/kintaiW/libsmx/releases/tag/v0.1.1 [0.1.0]: https://github.com/kintaiW/libsmx/releases/tag/v0.1.0 diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index ca12de1..39ac6f4 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,10 +5,26 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 -## [Unreleased] - v0.2.0 +## [0.2.1] - 2025-03-08 ### 新增 +- **SM2 密钥交换**(`sm2::key_exchange` 模块) + - `exchange_a` / `exchange_b`:GB/T 32918.3 完整密钥交换协议(带确认哈希) + - `ecdh`:简单 SM2-ECDH 共享密钥计算(适配 TLS/rustls) + - `EphemeralKey`:临时密钥对(`ZeroizeOnDrop`) +- **SM2 DER 编解码**(`sm2::der` 模块) + - `sig_to_der` / `sig_from_der`:签名 DER 编解码 + - `private_key_from_sec1_der` / `private_key_from_pkcs8_der`:私钥解析 +- **SM2 便捷 API**(`sm2` 模块) + - `sign_message` / `verify_message`:自动计算 Z 值的一步签名/验签 +- **HKDF-SM3**(`sm3::hkdf` 模块) + - `hkdf_extract` / `hkdf_expand` / `hkdf`:RFC 5869 兼容 +- **SM3 Hasher 增强** + - `reset()` / `finalize_reset()`:支持流式复用 +- **SM4 AEAD 合并格式** + - `sm4_encrypt_gcm_combined` / `sm4_decrypt_gcm_combined`:TLS 格式(密文||Tag) + - `sm4_encrypt_ccm_combined` / `sm4_decrypt_ccm_combined`:同上 - **BLS 签名**(`bls` 模块,需 `alloc` 特性) - `bls_keygen` / `bls_sign` / `bls_verify`:最小签名尺寸变体(签名 ∈ G1,公钥 ∈ G2) - `bls_aggregate` / `bls_aggregate_verify`:多消息聚合签名 @@ -102,6 +118,7 @@ - XTS:拒绝非 16 字节对齐输入,而非静默截断 - SM9 `hash_to_range`:用常量时间条件选择替换可变迭代 `while` 循环 +[0.2.1]: https://github.com/kintaiW/libsmx/releases/tag/v0.2.1 [0.2.0]: https://github.com/kintaiW/libsmx/releases/tag/v0.2.0 [0.1.1]: https://github.com/kintaiW/libsmx/releases/tag/v0.1.1 [0.1.0]: https://github.com/kintaiW/libsmx/releases/tag/v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index f544382..079fa37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,7 +422,7 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libsmx" -version = "0.2.0" +version = "0.2.1" dependencies = [ "criterion", "crypto-bigint", diff --git a/Cargo.toml b/Cargo.toml index f157bba..fd48767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libsmx" -version = "0.2.0" +version = "0.2.1" edition = "2021" rust-version = "1.83.0" license = "Apache-2.0" diff --git a/src/error.rs b/src/error.rs index e6d0941..087e69c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,6 +23,8 @@ pub enum Error { PointAtInfinity, /// 输入数据长度不合法 InvalidInputLength, + /// 密钥交换失败(共享点为无穷远或 KDF 输出全零) + KeyExchangeFailed, // ── SM4 错误 ──────────────────────────────────────────────────────────── /// AEAD 认证标签验证失败(GCM/CCM 解密时) @@ -55,6 +57,7 @@ impl fmt::Display for Error { Error::DecryptFailed => write!(f, "decryption failed"), Error::PointAtInfinity => write!(f, "point at infinity"), Error::InvalidInputLength => write!(f, "invalid input length"), + Error::KeyExchangeFailed => write!(f, "key exchange failed"), Error::AuthTagMismatch => write!(f, "authentication tag mismatch"), Error::NotOnCurve => write!(f, "point not on curve"), Error::ZeroScalar => write!(f, "zero scalar"), diff --git a/src/sm2/der.rs b/src/sm2/der.rs new file mode 100644 index 0000000..b80f1ee --- /dev/null +++ b/src/sm2/der.rs @@ -0,0 +1,438 @@ +//! SM2 签名与密钥 DER 编解码 +//! +//! ## 签名格式 +//! TLS 使用 ASN.1 DER 格式表示签名: +//! ```text +//! SEQUENCE { +//! INTEGER r, +//! INTEGER s +//! } +//! ``` +//! 而 libsmx 内部使用原始 `r||s`(64 字节)。本模块提供两者互转。 +//! +//! ## 私钥格式 +//! - **SEC1**(RFC 5915):`ECPrivateKey SEQUENCE { version INTEGER(1), privateKey OCTET STRING, ... }` +//! - **PKCS#8**(RFC 5958):`PrivateKeyInfo SEQUENCE { version INTEGER(0), algorithm, privateKey OCTET STRING(SEC1) }` +//! +//! ## DER INTEGER 编码规则 +//! - 去除前导零(但若最高位为 1,需在前补 0x00 防止被解析为负数) +//! - tag = 0x02,length 占 1 字节(r/s < 256 位时长度 ≤ 33) +//! - SEQUENCE tag = 0x30 + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +use crate::error::Error; +use crate::sm2::PrivateKey; + +/// 将原始签名 `r||s`(64 字节)编码为 DER SEQUENCE +/// +/// 输出格式:`30 02 02 ` +#[cfg(feature = "alloc")] +pub fn sig_to_der(raw: &[u8; 64]) -> Vec { + let r = &raw[..32]; + let s = &raw[32..]; + + let r_enc = encode_integer(r); + let s_enc = encode_integer(s); + + let inner_len = r_enc.len() + s_enc.len(); + let mut der = Vec::with_capacity(2 + inner_len); + der.push(0x30); // SEQUENCE tag + der.push(inner_len as u8); // SEQUENCE length(inner < 256 字节) + der.extend_from_slice(&r_enc); + der.extend_from_slice(&s_enc); + der +} + +/// 将 DER 编码签名解码为原始 `r||s`(64 字节) +/// +/// # 错误 +/// 格式不合法时返回 `Error::InvalidSignature` +pub fn sig_from_der(der: &[u8]) -> Result<[u8; 64], Error> { + let err = || Error::InvalidSignature; + + // SEQUENCE tag + let (tag, rest) = split_first(der).ok_or_else(err)?; + if *tag != 0x30 { + return Err(err()); + } + + // SEQUENCE length + let (seq_len, rest) = split_first(rest).ok_or_else(err)?; + let seq_len = *seq_len as usize; + if rest.len() < seq_len { + return Err(err()); + } + let body = &rest[..seq_len]; + + // 解析 r + let (r_bytes, body) = decode_integer(body).ok_or_else(err)?; + + // 解析 s + let (s_bytes, body) = decode_integer(body).ok_or_else(err)?; + + // 不应有多余数据 + if !body.is_empty() { + return Err(err()); + } + + // r 和 s 都必须是正整数,不超过 32 字节 + if r_bytes.is_empty() || r_bytes.len() > 33 || s_bytes.is_empty() || s_bytes.len() > 33 { + return Err(err()); + } + + let mut raw = [0u8; 64]; + // Reason: DER INTEGER 可能有前缀 0x00(最高位保护),去除后左对齐写入 32 字节槽 + let r_stripped = strip_leading_zero(r_bytes); + let s_stripped = strip_leading_zero(s_bytes); + if r_stripped.len() > 32 || s_stripped.len() > 32 { + return Err(err()); + } + let r_off = 32 - r_stripped.len(); + let s_off = 32 - s_stripped.len(); + raw[r_off..32].copy_from_slice(r_stripped); + raw[32 + s_off..64].copy_from_slice(s_stripped); + + Ok(raw) +} + +// ── 内部辅助 ────────────────────────────────────────────────────────────────── + +/// 将 32 字节大端整数编码为 DER INTEGER(带 tag 0x02 和 length) +#[cfg(feature = "alloc")] +fn encode_integer(bytes: &[u8]) -> Vec { + // 去除前导零(至少保留 1 字节) + let start = bytes + .iter() + .position(|&b| b != 0) + .unwrap_or(bytes.len() - 1); + let val = &bytes[start..]; + + // 最高位为 1 时需补 0x00,防止被解析为负数 + let needs_pad = val[0] & 0x80 != 0; + let val_len = val.len() + if needs_pad { 1 } else { 0 }; + + let mut enc = Vec::with_capacity(2 + val_len); + enc.push(0x02); // INTEGER tag + enc.push(val_len as u8); // length + if needs_pad { + enc.push(0x00); + } + enc.extend_from_slice(val); + enc +} + +/// 从字节流中解析一个 DER INTEGER,返回 (value_bytes, 剩余字节) +fn decode_integer(data: &[u8]) -> Option<(&[u8], &[u8])> { + let (tag, rest) = split_first(data)?; + if *tag != 0x02 { + return None; + } + let (len, rest) = split_first(rest)?; + let len = *len as usize; + if rest.len() < len { + return None; + } + Some((&rest[..len], &rest[len..])) +} + +/// 去除前导 0x00 字节 +fn strip_leading_zero(bytes: &[u8]) -> &[u8] { + match bytes.iter().position(|&b| b != 0) { + Some(i) => &bytes[i..], + None => &bytes[bytes.len().saturating_sub(1)..], // 全零时保留末字节 + } +} + +fn split_first(data: &[u8]) -> Option<(&u8, &[u8])> { + data.split_first() +} + +// ── DER 长度解码 ────────────────────────────────────────────────────────────── + +/// 解析 DER 长度字段,返回 (length, 剩余字节) +/// +/// 支持:单字节(< 0x80)、两字节(0x81 nn)、三字节(0x82 nn nn) +fn parse_length(data: &[u8]) -> Option<(usize, &[u8])> { + let (first, rest) = data.split_first()?; + if *first < 0x80 { + // Reason: 最高位为 0 时,本字节直接表示长度 + Some((*first as usize, rest)) + } else if *first == 0x81 { + let (len, rest) = rest.split_first()?; + Some((*len as usize, rest)) + } else if *first == 0x82 { + if rest.len() < 2 { + return None; + } + let len = (rest[0] as usize) << 8 | rest[1] as usize; + Some((len, &rest[2..])) + } else { + // 不支持更长或不定长编码 + None + } +} + +/// 解析一个 TLV(tag-length-value),返回 (value_bytes, 剩余字节) +fn parse_tlv(data: &[u8], expected_tag: u8) -> Option<(&[u8], &[u8])> { + let (tag, rest) = data.split_first()?; + if *tag != expected_tag { + return None; + } + let (len, rest) = parse_length(rest)?; + if rest.len() < len { + return None; + } + Some((&rest[..len], &rest[len..])) +} + +// ── 私钥 DER 解析 ───────────────────────────────────────────────────────────── + +/// 从 SEC1 DER 解析 SM2 私钥(RFC 5915) +/// +/// 格式: +/// ```text +/// ECPrivateKey ::= SEQUENCE { +/// version INTEGER { ecPrivkeyVer1(1) }, +/// privateKey OCTET STRING, -- 32 字节原始私钥 +/// [0] ECParameters OPTIONAL, +/// [1] BIT STRING OPTIONAL +/// } +/// ``` +/// +/// # 错误 +/// DER 格式不合法或私钥范围不合法时返回 `Error::InvalidPrivateKey` +pub fn private_key_from_sec1_der(der: &[u8]) -> Result { + let err = || Error::InvalidPrivateKey; + + // 解析外层 SEQUENCE + let (seq_body, _) = parse_tlv(der, 0x30).ok_or_else(err)?; + + // version INTEGER,值应为 1(ecPrivkeyVer1) + let (ver_bytes, rest) = parse_tlv(seq_body, 0x02).ok_or_else(err)?; + if ver_bytes != [0x01] { + return Err(err()); + } + + // privateKey OCTET STRING(32 字节) + let (key_bytes, _rest) = parse_tlv(rest, 0x04).ok_or_else(err)?; + if key_bytes.len() != 32 { + return Err(err()); + } + let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| err())?; + + PrivateKey::from_bytes(key_arr) +} + +/// 从 PKCS#8 DER 解析 SM2 私钥(RFC 5958) +/// +/// 格式: +/// ```text +/// PrivateKeyInfo ::= SEQUENCE { +/// version INTEGER (0), +/// algorithm AlgorithmIdentifier SEQUENCE { ... }, +/// privateKey OCTET STRING (SEC1 DER) +/// } +/// ``` +/// +/// # 错误 +/// DER 格式不合法或私钥范围不合法时返回 `Error::InvalidPrivateKey` +pub fn private_key_from_pkcs8_der(der: &[u8]) -> Result { + let err = || Error::InvalidPrivateKey; + + // 解析外层 SEQUENCE(PrivateKeyInfo) + let (seq_body, _) = parse_tlv(der, 0x30).ok_or_else(err)?; + + // version INTEGER,值应为 0 + let (ver_bytes, rest) = parse_tlv(seq_body, 0x02).ok_or_else(err)?; + if ver_bytes != [0x00] { + return Err(err()); + } + + // AlgorithmIdentifier SEQUENCE(跳过,不验证 OID) + let (_, rest) = parse_tlv(rest, 0x30).ok_or_else(err)?; + + // privateKey OCTET STRING(内含 SEC1 DER) + let (sec1_der, _) = parse_tlv(rest, 0x04).ok_or_else(err)?; + + private_key_from_sec1_der(sec1_der) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_raw(r: [u8; 32], s: [u8; 32]) -> [u8; 64] { + let mut raw = [0u8; 64]; + raw[..32].copy_from_slice(&r); + raw[32..].copy_from_slice(&s); + raw + } + + #[cfg(feature = "alloc")] + #[test] + fn test_der_roundtrip_basic() { + let r = [0x01u8; 32]; + let s = [0x02u8; 32]; + let raw = make_raw(r, s); + let der = sig_to_der(&raw); + let recovered = sig_from_der(&der).unwrap(); + assert_eq!(recovered, raw); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_der_roundtrip_high_bit_set() { + // r/s 最高位为 1,需要 DER 填充 0x00 + let mut r = [0u8; 32]; + r[0] = 0x80; // 最高位为 1 + let mut s = [0u8; 32]; + s[0] = 0xFF; + let raw = make_raw(r, s); + let der = sig_to_der(&raw); + // 验证 DER 中有 0x00 填充 + let recovered = sig_from_der(&der).unwrap(); + assert_eq!(recovered, raw); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_der_roundtrip_leading_zeros() { + // r 前有大量前导零 + let mut r = [0u8; 32]; + r[31] = 0x42; // 只有最后一字节非零 + let s = [0x01u8; 32]; + let raw = make_raw(r, s); + let der = sig_to_der(&raw); + let recovered = sig_from_der(&der).unwrap(); + assert_eq!(recovered, raw); + } + + #[test] + fn test_der_invalid_tag() { + // 非 SEQUENCE tag + let bad = [0x10, 0x08, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00]; + assert!(sig_from_der(&bad).is_err()); + } + + #[test] + fn test_der_truncated() { + let bad = [0x30, 0x10]; // length 声明 16 字节但无内容 + assert!(sig_from_der(&bad).is_err()); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_der_structure() { + // 验证 DER 字节结构符合 ASN.1 规范 + let r = [0x01u8; 32]; + let s = [0x01u8; 32]; + let raw = make_raw(r, s); + let der = sig_to_der(&raw); + assert_eq!(der[0], 0x30); // SEQUENCE + assert_eq!(der[2], 0x02); // INTEGER tag for r + // 长度字段合理(r/s 各最多 33 字节 + 2 字节头 = 35,×2 + 2 = 72) + assert!(der.len() <= 72); + assert!(der.len() >= 8); + } + + // ── 私钥 DER 解析测试 ────────────────────────────────────────────────────── + + // 已知 SM2 私钥原始字节(与其他测试共用) + const RAW_KEY: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, 0x9f, + 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, 0x4d, 0xf7, + 0xc5, 0xb8, + ]; + + /// 构造最小 SEC1 DER(只有 version + privateKey 字段) + #[cfg(feature = "alloc")] + fn make_sec1_der(key: &[u8; 32]) -> alloc::vec::Vec { + // version INTEGER = 1:02 01 01 + // privateKey OCTET STRING:04 20 <32 bytes> + // inner = 3 + 2 + 32 = 37 bytes → SEQUENCE 30 25 ... + let mut der = alloc::vec![0x30u8, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20]; + der.extend_from_slice(key); + der + } + + /// 构造最小 PKCS#8 DER(包含虚拟 AlgorithmIdentifier OID) + #[cfg(feature = "alloc")] + fn make_pkcs8_der(key: &[u8; 32]) -> alloc::vec::Vec { + let sec1 = make_sec1_der(key); + // AlgorithmIdentifier 最小化:30 06 06 01 00 06 01 00(两个 OID,各 1 字节占位) + let alg_id: &[u8] = &[0x30, 0x06, 0x06, 0x01, 0x00, 0x06, 0x01, 0x00]; + // version INTEGER = 0:02 01 00 + let version: &[u8] = &[0x02, 0x01, 0x00]; + // privateKey OCTET STRING 包装 sec1 + let mut priv_oct = alloc::vec![0x04u8, sec1.len() as u8]; + priv_oct.extend_from_slice(&sec1); + // inner = version + alg_id + priv_oct + let inner_len = version.len() + alg_id.len() + priv_oct.len(); + let mut der = alloc::vec![0x30u8, inner_len as u8]; + der.extend_from_slice(version); + der.extend_from_slice(alg_id); + der.extend_from_slice(&priv_oct); + der + } + + #[cfg(feature = "alloc")] + #[test] + fn test_sec1_der_roundtrip() { + let der = make_sec1_der(&RAW_KEY); + let key = private_key_from_sec1_der(&der).expect("SEC1 解析应成功"); + assert_eq!(key.as_bytes(), &RAW_KEY); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_pkcs8_der_roundtrip() { + let der = make_pkcs8_der(&RAW_KEY); + let key = private_key_from_pkcs8_der(&der).expect("PKCS#8 解析应成功"); + assert_eq!(key.as_bytes(), &RAW_KEY); + } + + #[test] + fn test_sec1_der_invalid_tag() { + // 首字节不是 SEQUENCE tag + let bad = [0x02u8, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20, 0x00]; + assert!(private_key_from_sec1_der(&bad).is_err()); + } + + #[test] + fn test_sec1_der_wrong_version() { + // version 应为 1,此处给 0;最后 32 字节填充为 RAW_KEY + let mut der = [0u8; 39]; + der[0] = 0x30; + der[1] = 0x25; // SEQUENCE length 37 + der[2] = 0x02; + der[3] = 0x01; + der[4] = 0x00; // version = 0(错误,应为 1) + der[5] = 0x04; + der[6] = 0x20; // OCTET STRING 32 字节 + der[7..39].copy_from_slice(&RAW_KEY); + assert!(private_key_from_sec1_der(&der).is_err()); + } + + #[test] + fn test_sec1_der_key_too_short() { + // privateKey 只有 16 字节(不足 32) + let der = [ + 0x30, 0x15, // SEQUENCE 21 字节 + 0x02, 0x01, 0x01, // version = 1 + 0x04, 0x10, // OCTET STRING 16 字节 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; + assert!(private_key_from_sec1_der(&der).is_err()); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_pkcs8_der_invalid_outer_tag() { + let mut der = make_pkcs8_der(&RAW_KEY); + der[0] = 0x04; // 破坏外层 SEQUENCE tag + assert!(private_key_from_pkcs8_der(&der).is_err()); + } +} diff --git a/src/sm2/key_exchange.rs b/src/sm2/key_exchange.rs new file mode 100644 index 0000000..53be3a5 --- /dev/null +++ b/src/sm2/key_exchange.rs @@ -0,0 +1,620 @@ +//! SM2 密钥交换协议(GB/T 32918.3-2016) +//! +//! 提供两种密钥交换方式: +//! - `ecdh`: 简单 SM2-ECDH 共享密钥计算(适配 TLS/rustls) +//! - `exchange_a` / `exchange_b`: 完整 GB/T 32918.3 密钥交换协议(带确认哈希) + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +use crypto_bigint::{Zero, U256}; +use rand_core::RngCore; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::error::Error; +use crate::sm2::ec::{AffinePoint, JacobianPoint}; +use crate::sm2::field::{fn_add, fn_mul, fp_to_bytes, Fn, GROUP_ORDER_MINUS_1}; +use crate::sm2::get_z; +use crate::sm3::Sm3Hasher; + +// ── x̄ 辅助函数(GB/T 32918.3 核心运算)───────────────────────────────────────── + +/// 计算 x̄ = 2^w + (x & (2^w - 1)),其中 w = ⌈(⌈log2(n)⌉ / 2)⌉ - 1 = 127 +/// +/// 对 SM2 256 位群阶,w=127。在大端 32 字节表示中: +/// - 清除高 128 位(bytes[0..16]),保留低 128 位 +/// - 设 bytes[16] 的 bit7 = 1(即加 2^127) +fn x_bar(x_bytes: &[u8; 32]) -> U256 { + let mut buf = [0u8; 32]; + // Reason: 保留 x 的低 128 位(bytes[16..32]),高 128 位清零 + buf[16..32].copy_from_slice(&x_bytes[16..32]); + // 设 bit 127(bytes[16] 的最高位) + buf[16] |= 0x80; + U256::from_be_slice(&buf) +} + +// ── EphemeralKey(临时密钥对)──────────────────────────────────────────────────── + +/// SM2 密钥交换临时密钥对(离开作用域自动清零) +/// +/// 用于密钥交换协议中的临时私钥和对应公钥。 +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct EphemeralKey { + r_bytes: [u8; 32], + #[zeroize(skip)] + r_point: [u8; 65], +} + +impl EphemeralKey { + /// 生成临时密钥对 + pub fn generate(rng: &mut R) -> Self { + loop { + let mut r_bytes = [0u8; 32]; + rng.fill_bytes(&mut r_bytes); + let r = U256::from_be_slice(&r_bytes); + if bool::from(r.is_zero()) || r >= GROUP_ORDER_MINUS_1 { + r_bytes.zeroize(); + continue; + } + let r_jac = JacobianPoint::scalar_mul_g(&r); + // Reason: r 在合法范围内,scalar_mul_g 不会产生无穷远点 + let r_aff = r_jac.to_affine().expect("valid r produces valid point"); + return EphemeralKey { + r_bytes, + r_point: r_aff.to_bytes(), + }; + } + } + + /// 从指定标量创建临时密钥对(测试用) + pub fn from_scalar(r: &U256) -> Result { + if bool::from(r.is_zero()) || *r >= GROUP_ORDER_MINUS_1 { + return Err(Error::InvalidPrivateKey); + } + let r_jac = JacobianPoint::scalar_mul_g(r); + let r_aff = r_jac.to_affine().map_err(|_| Error::InvalidPrivateKey)?; + Ok(EphemeralKey { + r_bytes: r.to_be_bytes(), + r_point: r_aff.to_bytes(), + }) + } + + /// 获取临时公钥(发送给对方) + pub fn public_key(&self) -> &[u8; 65] { + &self.r_point + } +} + +// ── 简单 ECDH ────────────────────────────────────────────────────────────────── + +/// 简单 SM2-ECDH 共享密钥计算 +/// +/// 计算 shared = my_priv · peer_pub,返回共享点的 x 坐标(32 字节)。 +/// 适用于 TLS/rustls 等只需要原始 ECDH 共享密钥的场景。 +/// +/// # 参数 +/// - `my_priv`: 己方私钥 +/// - `peer_pub`: 对方公钥(65 字节,04||x||y) +/// +/// # 错误 +/// - `InvalidPublicKey`: 公钥格式错误或不在曲线上 +/// - `PointAtInfinity`: 共享点为无穷远(不应发生于合法输入) +pub fn ecdh(my_priv: &crate::sm2::PrivateKey, peer_pub: &[u8; 65]) -> Result<[u8; 32], Error> { + let peer = AffinePoint::from_bytes(peer_pub)?; + let d = U256::from_be_slice(my_priv.as_bytes()); + let peer_jac = JacobianPoint::from_affine(&peer); + let shared = JacobianPoint::scalar_mul(&d, &peer_jac); + let shared_aff = shared.to_affine()?; + Ok(fp_to_bytes(&shared_aff.x)) +} + +/// 从变长切片执行 SM2-ECDH(rustls `ActiveKeyExchange::complete` 适配) +/// +/// 等同于 `ecdh`,但接受 `&[u8]` 而非 `&[u8; 65]`,省去调用方的长度转换。 +/// +/// # 错误 +/// - `InvalidInputLength`: peer_pub 长度不等于 65 +/// - `InvalidPublicKey` / `PointAtInfinity`: 同 `ecdh` +pub fn ecdh_from_slice( + my_priv: &crate::sm2::PrivateKey, + peer_pub: &[u8], +) -> Result<[u8; 32], Error> { + let pub_fixed: &[u8; 65] = peer_pub.try_into().map_err(|_| Error::InvalidInputLength)?; + ecdh(my_priv, pub_fixed) +} + +// ── 完整密钥交换协议(GB/T 32918.3)────────────────────────────────────────────── + +/// 密钥交换结果 +#[cfg(feature = "alloc")] +pub struct ExchangeResult { + /// 协商出的共享密钥 + pub key: Vec, + /// 己方确认哈希(发给对方验证) + pub s_self: [u8; 32], + /// 对方确认哈希(用于验证对方发来的值) + pub s_peer: [u8; 32], +} + +/// 发起方 A 执行密钥交换 +/// +/// # 参数 +/// - `klen`: 期望密钥长度(字节) +/// - `id_a`: 发起方用户 ID +/// - `id_b`: 响应方用户 ID +/// - `pri_key_a`: 发起方私钥 +/// - `pub_key_a`: 发起方公钥(65 字节) +/// - `pub_key_b`: 响应方公钥(65 字节) +/// - `eph_key_a`: 发起方临时密钥 +/// - `r_b`: 响应方临时公钥(65 字节) +#[cfg(feature = "alloc")] +#[allow(clippy::too_many_arguments)] +pub fn exchange_a( + klen: usize, + id_a: &[u8], + id_b: &[u8], + pri_key_a: &crate::sm2::PrivateKey, + pub_key_a: &[u8; 65], + pub_key_b: &[u8; 65], + eph_key_a: &EphemeralKey, + r_b: &[u8; 65], +) -> Result { + compute_shared( + true, klen, id_a, id_b, pri_key_a, pub_key_a, pub_key_b, eph_key_a, r_b, + ) +} + +/// 响应方 B 执行密钥交换 +/// +/// # 参数 +/// - `klen`: 期望密钥长度(字节) +/// - `id_a`: 发起方用户 ID +/// - `id_b`: 响应方用户 ID +/// - `pri_key_b`: 响应方私钥 +/// - `pub_key_a`: 发起方公钥(65 字节) +/// - `pub_key_b`: 响应方公钥(65 字节) +/// - `eph_key_b`: 响应方临时密钥 +/// - `r_a`: 发起方临时公钥(65 字节) +#[cfg(feature = "alloc")] +#[allow(clippy::too_many_arguments)] +pub fn exchange_b( + klen: usize, + id_a: &[u8], + id_b: &[u8], + pri_key_b: &crate::sm2::PrivateKey, + pub_key_a: &[u8; 65], + pub_key_b: &[u8; 65], + eph_key_b: &EphemeralKey, + r_a: &[u8; 65], +) -> Result { + compute_shared( + false, klen, id_a, id_b, pri_key_b, pub_key_a, pub_key_b, eph_key_b, r_a, + ) +} + +/// 内部共享计算 +/// +/// `is_initiator`: true 表示发起方 A,false 表示响应方 B +#[cfg(feature = "alloc")] +#[allow(clippy::too_many_arguments)] +fn compute_shared( + is_initiator: bool, + klen: usize, + id_a: &[u8], + id_b: &[u8], + pri_key_self: &crate::sm2::PrivateKey, + pub_key_a: &[u8; 65], + pub_key_b: &[u8; 65], + eph_key_self: &EphemeralKey, + r_peer: &[u8; 65], +) -> Result { + // 计算 ZA、ZB + let z_a = get_z(id_a, pub_key_a); + let z_b = get_z(id_b, pub_key_b); + + // 解析临时公钥坐标 + let r_self_aff = AffinePoint::from_bytes(eph_key_self.public_key())?; + let r_peer_aff = AffinePoint::from_bytes(r_peer)?; + + // 计算 x̄_self 和 x̄_peer + let x_self_bytes = fp_to_bytes(&r_self_aff.x); + let x_peer_bytes = fp_to_bytes(&r_peer_aff.x); + let x_bar_self = x_bar(&x_self_bytes); + let x_bar_peer = x_bar(&x_peer_bytes); + + // t = (d_self + x̄_self · r_self) mod n + let d_self = U256::from_be_slice(pri_key_self.as_bytes()); + let r_self = U256::from_be_slice(&eph_key_self.r_bytes); + let t_fn = fn_add( + &Fn::new(&d_self), + &fn_mul(&Fn::new(&x_bar_self), &Fn::new(&r_self)), + ); + + // V/U = t · (peer_pub + x̄_peer · R_peer) + // Reason: 先计算 x̄_peer · R_peer(标量乘),再加 peer_pub(仿射点) + let peer_pub_bytes = if is_initiator { pub_key_b } else { pub_key_a }; + let peer_pub_aff = AffinePoint::from_bytes(peer_pub_bytes)?; + let peer_pub_jac = JacobianPoint::from_affine(&peer_pub_aff); + let r_peer_jac = JacobianPoint::from_affine(&r_peer_aff); + let x_bar_peer_r = JacobianPoint::scalar_mul(&x_bar_peer, &r_peer_jac); + let combined = JacobianPoint::add(&peer_pub_jac, &x_bar_peer_r); + let t = t_fn.retrieve(); + let v_point = JacobianPoint::scalar_mul(&t, &combined); + let v_aff = v_point.to_affine().map_err(|_| Error::KeyExchangeFailed)?; + + let xv = fp_to_bytes(&v_aff.x); + let yv = fp_to_bytes(&v_aff.y); + + // K = KDF(xV || yV || ZA || ZB, klen) + let mut kdf_input = Vec::with_capacity(32 + 32 + 32 + 32); + kdf_input.extend_from_slice(&xv); + kdf_input.extend_from_slice(&yv); + kdf_input.extend_from_slice(&z_a); + kdf_input.extend_from_slice(&z_b); + let key = crate::sm2::kdf::kdf(&kdf_input, klen); + + // KDF 输出全零时返回错误(防弱密钥) + if key.iter().all(|&b| b == 0) { + return Err(Error::KeyExchangeFailed); + } + + // 确认哈希 + // (x1,y1) 始终是 RA(发起方),(x2,y2) 始终是 RB(响应方) + let (x1, y1, x2, y2) = if is_initiator { + ( + fp_to_bytes(&r_self_aff.x), + fp_to_bytes(&r_self_aff.y), + fp_to_bytes(&r_peer_aff.x), + fp_to_bytes(&r_peer_aff.y), + ) + } else { + ( + fp_to_bytes(&r_peer_aff.x), + fp_to_bytes(&r_peer_aff.y), + fp_to_bytes(&r_self_aff.x), + fp_to_bytes(&r_self_aff.y), + ) + }; + + // 内部哈希 hash_v = SM3(xV || ZA || ZB || x1 || y1 || x2 || y2) + let mut h = Sm3Hasher::new(); + h.update(&xv); + h.update(&z_a); + h.update(&z_b); + h.update(&x1); + h.update(&y1); + h.update(&x2); + h.update(&y2); + let hash_v = h.finalize(); + + // S1 = SM3(0x02 || yV || hash_v) — 己方若为 B,则 S1 是己方确认值 + let s1 = { + let mut h = Sm3Hasher::new(); + h.update(&[0x02]); + h.update(&yv); + h.update(&hash_v); + h.finalize() + }; + + // SA = SM3(0x03 || yV || hash_v) + let sa = { + let mut h = Sm3Hasher::new(); + h.update(&[0x03]); + h.update(&yv); + h.update(&hash_v); + h.finalize() + }; + + // Reason: 发起方 A 的确认哈希是 SA(0x03),响应方 B 的确认哈希是 S1(0x02) + let (s_self, s_peer) = if is_initiator { + (sa, s1) // A 发送 SA 给 B 验证,A 验证 B 发来的 S1 + } else { + (s1, sa) // B 发送 S1 给 A 验证,B 验证 A 发来的 SA + }; + + Ok(ExchangeResult { + key, + s_self, + s_peer, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sm2::PrivateKey; + + #[allow(dead_code)] + struct FakeRng(#[allow(dead_code)] [u8; 32]); + impl RngCore for FakeRng { + fn next_u32(&mut self) -> u32 { + 0 + } + fn next_u64(&mut self) -> u64 { + 0 + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + for (i, b) in dest.iter_mut().enumerate() { + *b = self.0[i % 32]; + } + } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } + } + + #[test] + fn test_x_bar() { + // x = 1(低 128 位只有 bit 0) + let mut x_bytes = [0u8; 32]; + x_bytes[31] = 0x01; + let result = x_bar(&x_bytes); + // 期望 2^127 + 1 + let mut expected = [0u8; 32]; + expected[16] = 0x80; + expected[31] = 0x01; + assert_eq!(result, U256::from_be_slice(&expected)); + } + + #[test] + fn test_x_bar_high_bits_cleared() { + // x 有高 128 位数据,应被清除 + let x_bytes = [0xFFu8; 32]; + let result = x_bar(&x_bytes); + // 低 128 位全 1 + 2^127 设置位 = 0x80 FF...FF 后 16 字节加上 2^127 + // 高 16 字节应为 0,bytes[16] = 0xFF | 0x80 = 0xFF + let mut expected = [0u8; 32]; + expected[16..32].copy_from_slice(&[0xFF; 16]); + expected[16] |= 0x80; // 已经是 0xFF,不变 + assert_eq!(result, U256::from_be_slice(&expected)); + } + + #[test] + fn test_ecdh_roundtrip() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let d_b: [u8; 32] = [ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]; + + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + let pri_b = PrivateKey::from_bytes(&d_b).unwrap(); + let pub_a = pri_a.public_key(); + let pub_b = pri_b.public_key(); + + // A 用 B 的公钥算 ECDH,B 用 A 的公钥算 ECDH,结果应一致 + let shared_a = ecdh(&pri_a, &pub_b).unwrap(); + let shared_b = ecdh(&pri_b, &pub_a).unwrap(); + assert_eq!(shared_a, shared_b); + } + + #[test] + fn test_ecdh_invalid_pubkey() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + + // 无效公钥(全零 y 坐标不在曲线上) + let mut bad_pub = [0x04u8; 65]; + bad_pub[1] = 0x01; + assert!(ecdh(&pri_a, &bad_pub).is_err()); + } + + #[test] + fn test_ecdh_from_slice_length_check() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + + // 长度不对应报 InvalidInputLength + assert!(ecdh_from_slice(&pri_a, &[0x04u8; 64]).is_err()); + assert!(ecdh_from_slice(&pri_a, &[0x04u8; 66]).is_err()); + } + + #[test] + fn test_ecdh_from_slice_equals_ecdh() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let d_b: [u8; 32] = [ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]; + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + let pri_b = PrivateKey::from_bytes(&d_b).unwrap(); + let pub_b = pri_b.public_key(); + + let r1 = ecdh(&pri_a, &pub_b).unwrap(); + let r2 = ecdh_from_slice(&pri_a, &pub_b).unwrap(); + assert_eq!(r1, r2); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_exchange_roundtrip() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let d_b: [u8; 32] = [ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]; + + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + let pri_b = PrivateKey::from_bytes(&d_b).unwrap(); + let pub_a = pri_a.public_key(); + let pub_b = pri_b.public_key(); + + let id_a = b"Alice@test.com"; + let id_b = b"Bob@test.com"; + + // 生成临时密钥 + let ra_scalar = + U256::from_be_hex("83A2C9C8B96E5AF70BD480B472409A9A327257F1EBB73F5B073354B248668563"); + let rb_scalar = + U256::from_be_hex("33FE21940342161C55619C4A0C060293D543C80AF19748CE176D83477DE71C80"); + let eph_a = EphemeralKey::from_scalar(&ra_scalar).unwrap(); + let eph_b = EphemeralKey::from_scalar(&rb_scalar).unwrap(); + + let result_a = exchange_a( + 16, + id_a, + id_b, + &pri_a, + &pub_a, + &pub_b, + &eph_a, + eph_b.public_key(), + ) + .unwrap(); + + let result_b = exchange_b( + 16, + id_a, + id_b, + &pri_b, + &pub_a, + &pub_b, + &eph_b, + eph_a.public_key(), + ) + .unwrap(); + + // 协商出的密钥应相同 + assert_eq!(result_a.key, result_b.key); + assert!(!result_a.key.is_empty()); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_exchange_confirmation() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let d_b: [u8; 32] = [ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]; + + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + let pri_b = PrivateKey::from_bytes(&d_b).unwrap(); + let pub_a = pri_a.public_key(); + let pub_b = pri_b.public_key(); + + let id_a = b"1234567812345678"; + let id_b = b"1234567812345678"; + + let ra_scalar = + U256::from_be_hex("83A2C9C8B96E5AF70BD480B472409A9A327257F1EBB73F5B073354B248668563"); + let rb_scalar = + U256::from_be_hex("33FE21940342161C55619C4A0C060293D543C80AF19748CE176D83477DE71C80"); + let eph_a = EphemeralKey::from_scalar(&ra_scalar).unwrap(); + let eph_b = EphemeralKey::from_scalar(&rb_scalar).unwrap(); + + let result_a = exchange_a( + 16, + id_a, + id_b, + &pri_a, + &pub_a, + &pub_b, + &eph_a, + eph_b.public_key(), + ) + .unwrap(); + + let result_b = exchange_b( + 16, + id_a, + id_b, + &pri_b, + &pub_a, + &pub_b, + &eph_b, + eph_a.public_key(), + ) + .unwrap(); + + // 确认哈希交叉验证:A.s_peer == B.s_self,B.s_peer == A.s_self + assert_eq!(result_a.s_peer, result_b.s_self); + assert_eq!(result_b.s_peer, result_a.s_self); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_exchange_different_ids() { + let d_a: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let d_b: [u8; 32] = [ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]; + + let pri_a = PrivateKey::from_bytes(&d_a).unwrap(); + let pri_b = PrivateKey::from_bytes(&d_b).unwrap(); + let pub_a = pri_a.public_key(); + let pub_b = pri_b.public_key(); + + let ra_scalar = + U256::from_be_hex("83A2C9C8B96E5AF70BD480B472409A9A327257F1EBB73F5B073354B248668563"); + let rb_scalar = + U256::from_be_hex("33FE21940342161C55619C4A0C060293D543C80AF19748CE176D83477DE71C80"); + + // 使用不同 ID 组合 + let eph_a1 = EphemeralKey::from_scalar(&ra_scalar).unwrap(); + let eph_b1 = EphemeralKey::from_scalar(&rb_scalar).unwrap(); + let result_1 = exchange_a( + 16, + b"ID_A_1", + b"ID_B_1", + &pri_a, + &pub_a, + &pub_b, + &eph_a1, + eph_b1.public_key(), + ) + .unwrap(); + + let eph_a2 = EphemeralKey::from_scalar(&ra_scalar).unwrap(); + let eph_b2 = EphemeralKey::from_scalar(&rb_scalar).unwrap(); + let result_2 = exchange_a( + 16, + b"ID_A_2", + b"ID_B_2", + &pri_a, + &pub_a, + &pub_b, + &eph_a2, + eph_b2.public_key(), + ) + .unwrap(); + + // 不同 ID 应产生不同密钥 + assert_ne!(result_1.key, result_2.key); + } +} diff --git a/src/sm2/mod.rs b/src/sm2/mod.rs index b983018..a51f7bd 100644 --- a/src/sm2/mod.rs +++ b/src/sm2/mod.rs @@ -10,9 +10,11 @@ //! 签名必须使用 `SM3(Z||M)` 作为消息摘要,而非直接 `SM3(M)`。 //! 所有公开签名接口均要求调用方提供用户 ID(或已计算好的 Z 值)。 +pub mod der; pub mod ec; pub mod field; pub mod kdf; +pub mod key_exchange; #[cfg(feature = "alloc")] use alloc::vec::Vec; @@ -174,6 +176,56 @@ pub fn sign_with_k(e: &[u8; 32], pri_key: &PrivateKey, k: &U256) -> Result<[u8; Ok(sig) } +/// SM2 签名(标准接口,随机 k) +/// +/// # 合规说明 +/// 此函数接受预计算好的消息摘要 `e = SM3(Z||M)`。 +/// 调用方应先用 `get_z` + `get_e` 计算 e,确保满足 GB/T 32918.2-2016 §5.5。 +/// SM2 签名(便捷接口,自动计算 Z 值与消息摘要) +/// +/// 等同于 `get_z` + `get_e` + `sign` 的组合,适合不需要手动管理摘要的场景。 +/// +/// # 参数 +/// - `msg`: 原始消息 +/// - `id`: 用户可辨别标识(通常使用 `b"1234567812345678"`) +/// - `pri_key`: 私钥 +/// - `rng`: 随机数生成器 +/// +/// # 合规说明 +/// 内部自动计算 `Z = SM3(ENTL||ID||a||b||Gx||Gy||Px||Py)` 和 `e = SM3(Z||M)`, +/// 符合 GB/T 32918.2-2016 §5.5。 +pub fn sign_message( + msg: &[u8], + id: &[u8], + pri_key: &PrivateKey, + rng: &mut R, +) -> [u8; 64] { + let pub_key = pri_key.public_key(); + let z = get_z(id, &pub_key); + let e = get_e(&z, msg); + sign(&e, pri_key, rng) +} + +/// SM2 验签(便捷接口,自动计算 Z 值与消息摘要) +/// +/// 等同于 `get_z` + `get_e` + `verify` 的组合。 +/// +/// # 参数 +/// - `msg`: 原始消息 +/// - `id`: 用户可辨别标识 +/// - `pub_key`: 公钥(65 字节,04||x||y) +/// - `sig`: 签名(64 字节,r||s) +pub fn verify_message( + msg: &[u8], + id: &[u8], + pub_key: &[u8; 65], + sig: &[u8; 64], +) -> Result<(), Error> { + let z = get_z(id, pub_key); + let e = get_e(&z, msg); + verify(&e, pub_key, sig) +} + /// SM2 签名(标准接口,随机 k) /// /// # 合规说明 @@ -481,6 +533,49 @@ mod tests { assert_eq!(plaintext, msg); } + #[test] + fn test_sign_message_verify_message_roundtrip() { + let d_bytes: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let pri_key = PrivateKey::from_bytes(&d_bytes).unwrap(); + let pub_key = pri_key.public_key(); + + let mut rng = FakeRng([ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]); + + let msg = b"hello sign_message"; + let sig = sign_message(msg, DEFAULT_ID, &pri_key, &mut rng); + verify_message(msg, DEFAULT_ID, &pub_key, &sig).expect("便捷验签应通过"); + } + + #[test] + fn test_verify_message_rejects_wrong_id() { + let d_bytes: [u8; 32] = [ + 0x39, 0x45, 0x20, 0x8f, 0x7b, 0x21, 0x44, 0xb1, 0x3f, 0x36, 0xe3, 0x8a, 0xc6, 0xd3, + 0x9f, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xb5, 0x1a, 0x42, 0xfb, 0x81, 0xef, + 0x4d, 0xf7, 0xc5, 0xb8, + ]; + let pri_key = PrivateKey::from_bytes(&d_bytes).unwrap(); + let pub_key = pri_key.public_key(); + + let mut rng = FakeRng([ + 0x59, 0x27, 0x6e, 0x27, 0xd5, 0x06, 0x86, 0x1a, 0x16, 0x68, 0x0f, 0x3a, 0xd9, 0xc0, + 0x2d, 0xcc, 0xef, 0x3c, 0xc1, 0xfa, 0x3c, 0xdb, 0xe4, 0xce, 0x6d, 0x54, 0xb8, 0x0d, + 0xea, 0xc1, 0xbc, 0x21, + ]); + + let msg = b"hello sign_message"; + let sig = sign_message(msg, DEFAULT_ID, &pri_key, &mut rng); + // 用错误 ID 验签应失败 + assert!(verify_message(msg, b"wrong-id", &pub_key, &sig).is_err()); + } + #[cfg(feature = "alloc")] #[test] fn test_decrypt_rejects_tampered_ciphertext() { diff --git a/src/sm3/hkdf.rs b/src/sm3/hkdf.rs new file mode 100644 index 0000000..a1f525d --- /dev/null +++ b/src/sm3/hkdf.rs @@ -0,0 +1,194 @@ +//! HKDF-SM3:基于 HMAC-SM3 的密钥派生函数(RFC 5869) +//! +//! TLS 1.3 的密钥调度完全基于 HKDF,因此本模块是 rustls 国密适配的基础依赖。 +//! +//! ## 协议 +//! +//! ```text +//! Extract: PRK = HMAC-SM3(salt, IKM) +//! Expand: T(0) = b"" +//! T(i) = HMAC-SM3(PRK, T(i-1) || info || i) i = 1, 2, ... +//! OKM = T(1) || T(2) || ... 取前 len 字节 +//! ``` +//! +//! 参考:[RFC 5869](https://www.rfc-editor.org/rfc/rfc5869) + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +use super::hmac_sm3; +use super::DIGEST_LEN; +#[cfg(feature = "alloc")] +use crate::error::Error; + +/// HKDF-SM3 Extract +/// +/// PRK = HMAC-SM3(salt, IKM) +/// +/// # 参数 +/// - `salt`:可选盐值;`None` 时视为全零 32 字节(RFC 5869 §2.2) +/// - `ikm`:输入密钥材料 +/// +/// # 返回 +/// 32 字节伪随机密钥(PRK) +pub fn hkdf_extract(salt: Option<&[u8]>, ikm: &[u8]) -> [u8; DIGEST_LEN] { + // Reason: RFC 5869 §2.2 规定 salt 缺省时视为长度为 HashLen 的零字节序列 + let zeros = [0u8; DIGEST_LEN]; + let salt = salt.unwrap_or(&zeros); + hmac_sm3(salt, ikm) +} + +/// HKDF-SM3 Expand +/// +/// OKM = T(1) || T(2) || ... 截取前 `len` 字节 +/// +/// # 参数 +/// - `prk`:32 字节伪随机密钥(来自 `hkdf_extract` 输出) +/// - `info`:上下文信息(可为空) +/// - `len`:期望输出长度(字节),不得超过 255 × 32 = 8160 +/// +/// # 错误 +/// `len > 255 * 32` 时返回 `Error::InvalidInputLength` +#[cfg(feature = "alloc")] +pub fn hkdf_expand(prk: &[u8; DIGEST_LEN], info: &[u8], len: usize) -> Result, Error> { + // Reason: RFC 5869 §2.3 限制最大输出为 255 × HashLen + const MAX_LEN: usize = 255 * DIGEST_LEN; + if len > MAX_LEN { + return Err(Error::InvalidInputLength); + } + + let mut okm = Vec::with_capacity(len + DIGEST_LEN); + let mut t_prev = [0u8; DIGEST_LEN]; // T(0) = b"" + let mut t_prev_len = 0usize; // 第一轮 T(0) 为空 + + let rounds = len.div_ceil(DIGEST_LEN); + for i in 1u8..=(rounds as u8) { + // HMAC-SM3(PRK, T(i-1) || info || i) + // Reason: 用拼接方式避免在 no_std 环境分配临时 Vec + let mut input = [0u8; DIGEST_LEN + 255 + 1]; // T_prev(32) + info(≤255) + counter(1) + let info_len = info.len().min(255); + input[..t_prev_len].copy_from_slice(&t_prev[..t_prev_len]); + input[t_prev_len..t_prev_len + info_len].copy_from_slice(&info[..info_len]); + input[t_prev_len + info_len] = i; + let t_i = hmac_sm3(prk, &input[..t_prev_len + info_len + 1]); + + okm.extend_from_slice(&t_i); + t_prev = t_i; + t_prev_len = DIGEST_LEN; // 第二轮起 T_prev 固定 32 字节 + } + + okm.truncate(len); + Ok(okm) +} + +/// HKDF-SM3 一步完成(extract + expand) +/// +/// 适合只需要派生一段密钥材料的场景。 +/// +/// # 参数 +/// - `salt`:可选盐值(`None` 视为 32 字节零) +/// - `ikm`:输入密钥材料 +/// - `info`:上下文绑定信息 +/// - `len`:输出长度(字节) +#[cfg(feature = "alloc")] +pub fn hkdf(salt: Option<&[u8]>, ikm: &[u8], info: &[u8], len: usize) -> Result, Error> { + let prk = hkdf_extract(salt, ikm); + hkdf_expand(&prk, info, len) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// RFC 5869 附录 A.1 测试向量(以 SHA-256 为基准验结构,此处用 SM3 验确定性和正确性) + #[test] + fn test_hkdf_extract_deterministic() { + let salt = b"test-salt"; + let ikm = b"input-key-material"; + let prk1 = hkdf_extract(Some(salt), ikm); + let prk2 = hkdf_extract(Some(salt), ikm); + assert_eq!(prk1, prk2); + assert_eq!(prk1.len(), 32); + } + + #[test] + fn test_hkdf_extract_none_salt_equals_zero_salt() { + let ikm = b"some ikm"; + let zeros = [0u8; 32]; + let prk_none = hkdf_extract(None, ikm); + let prk_zero = hkdf_extract(Some(&zeros), ikm); + assert_eq!(prk_none, prk_zero); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_expand_length() { + let prk = [0x42u8; 32]; + let info = b"test-info"; + assert_eq!(hkdf_expand(&prk, info, 16).unwrap().len(), 16); + assert_eq!(hkdf_expand(&prk, info, 32).unwrap().len(), 32); + assert_eq!(hkdf_expand(&prk, info, 48).unwrap().len(), 48); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_expand_deterministic() { + let prk = [0x11u8; 32]; + let info = b"ctx"; + let out1 = hkdf_expand(&prk, info, 32).unwrap(); + let out2 = hkdf_expand(&prk, info, 32).unwrap(); + assert_eq!(out1, out2); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_expand_prefix_consistency() { + // T(1..n) 前缀应与长度更短的输出一致 + let prk = [0x22u8; 32]; + let info = b"prefix-test"; + let short = hkdf_expand(&prk, info, 32).unwrap(); + let long = hkdf_expand(&prk, info, 64).unwrap(); + assert_eq!(&long[..32], &short[..]); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_expand_max_len_rejected() { + let prk = [0u8; 32]; + let result = hkdf_expand(&prk, b"", 255 * 32 + 1); + assert!(result.is_err()); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_expand_max_len_accepted() { + let prk = [0u8; 32]; + let result = hkdf_expand(&prk, b"", 255 * 32); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 255 * 32); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_different_info_different_output() { + let prk = [0x33u8; 32]; + let out1 = hkdf_expand(&prk, b"info-a", 32).unwrap(); + let out2 = hkdf_expand(&prk, b"info-b", 32).unwrap(); + assert_ne!(out1, out2); + } + + #[cfg(feature = "alloc")] + #[test] + fn test_hkdf_roundtrip_salt_info() { + // 模拟 TLS 1.3 密钥调度:extract 然后 expand 两种 label + let salt = b"tls13-early-secret-salt"; + let ikm = b"shared-secret-from-key-exchange"; + let prk = hkdf_extract(Some(salt), ikm); + + let key1 = hkdf_expand(&prk, b"tls13 key", 16).unwrap(); + let key2 = hkdf_expand(&prk, b"tls13 iv", 12).unwrap(); + assert_eq!(key1.len(), 16); + assert_eq!(key2.len(), 12); + assert_ne!(&key1[..12], &key2[..]); + } +} diff --git a/src/sm3/mod.rs b/src/sm3/mod.rs index 0c621c0..198a284 100644 --- a/src/sm3/mod.rs +++ b/src/sm3/mod.rs @@ -23,6 +23,7 @@ //! 如需 HMAC,请使用 [`hmac_sm3`]。 mod compress; +pub mod hkdf; use compress::{compress, IV}; @@ -104,33 +105,56 @@ impl Sm3Hasher { /// /// 调用后此 hasher 不应再使用(消耗所有权的版本请用 [`finalize`](Self::finalize))。 pub fn finalize(mut self) -> [u8; DIGEST_LEN] { + Self::finalize_inner(&mut self) + } + + /// 完成哈希并重置状态(供复用,无需重新构造) + /// + /// 等同于 `finalize()` 后调用 `reset()`,但只需一次操作。 + /// rustls `Hasher` trait 要求此语义(`finish(&mut self)`)。 + pub fn finalize_reset(&mut self) -> [u8; DIGEST_LEN] { + let out = Self::finalize_inner(self); + self.reset(); + out + } + + /// 重置为初始状态(等同于重新调用 `new()`,但复用已分配内存) + pub fn reset(&mut self) { + self.state = IV; + self.buffer = [0u8; 64]; + self.buf_len = 0; + self.bit_len = 0; + } + + /// 内部完成函数(同时供消耗版和借用版使用) + fn finalize_inner(h: &mut Self) -> [u8; DIGEST_LEN] { // 计算总位数(包含缓冲区中的字节) - let total_bits = self.bit_len.wrapping_add((self.buf_len as u64) * 8); + let total_bits = h.bit_len.wrapping_add((h.buf_len as u64) * 8); // Padding:追加 0x80 + 零字节,使消息长度 ≡ 56 (mod 64) - self.buffer[self.buf_len] = 0x80; - self.buf_len += 1; + h.buffer[h.buf_len] = 0x80; + h.buf_len += 1; - if self.buf_len > 56 { + if h.buf_len > 56 { // 当前块填不下长度字段,先处理这块,再开一块 - for i in self.buf_len..64 { - self.buffer[i] = 0; + for i in h.buf_len..64 { + h.buffer[i] = 0; } - compress(&mut self.state, &self.buffer); - self.buffer = [0u8; 64]; + compress(&mut h.state, &h.buffer); + h.buffer = [0u8; 64]; } else { - for i in self.buf_len..56 { - self.buffer[i] = 0; + for i in h.buf_len..56 { + h.buffer[i] = 0; } } // 最后 8 字节写入总位长(大端) - self.buffer[56..64].copy_from_slice(&total_bits.to_be_bytes()); - compress(&mut self.state, &self.buffer); + h.buffer[56..64].copy_from_slice(&total_bits.to_be_bytes()); + compress(&mut h.state, &h.buffer); // 输出:8 个 u32 大端序拼接 let mut out = [0u8; 32]; - for (i, &v) in self.state.iter().enumerate() { + for (i, &v) in h.state.iter().enumerate() { out[i * 4..i * 4 + 4].copy_from_slice(&v.to_be_bytes()); } out @@ -262,6 +286,41 @@ mod tests { assert_eq!(mac.len(), 32); } + /// reset() 后状态恢复为 new() 初始状态 + #[test] + fn test_reset_equals_new() { + let mut h = Sm3Hasher::new(); + h.update(b"some data"); + h.reset(); + let digest_after_reset = h.finalize(); + let digest_fresh = Sm3Hasher::digest(b""); + assert_eq!(digest_after_reset, digest_fresh); + } + + /// finalize_reset() 返回正确摘要,且随后状态已重置 + #[test] + fn test_finalize_reset_correctness() { + let mut h = Sm3Hasher::new(); + h.update(b"abc"); + let d1 = h.finalize_reset(); + // d1 应等于 SM3("abc") + assert_eq!(d1, Sm3Hasher::digest(b"abc")); + // 重置后哈希空消息应等于 SM3("") + let d2 = h.finalize(); + assert_eq!(d2, Sm3Hasher::digest(b"")); + } + + /// finalize_reset() 可连续使用两次,结果一致 + #[test] + fn test_finalize_reset_repeatable() { + let mut h = Sm3Hasher::new(); + h.update(b"test"); + let d1 = h.finalize_reset(); + h.update(b"test"); + let d2 = h.finalize_reset(); + assert_eq!(d1, d2); + } + // 辅助:从十六进制字符串构造 [u8; 32] fn hex_literal(s: &str) -> [u8; 32] { let mut out = [0u8; 32]; diff --git a/src/sm4/modes.rs b/src/sm4/modes.rs index 806b5b1..d230285 100644 --- a/src/sm4/modes.rs +++ b/src/sm4/modes.rs @@ -553,6 +553,69 @@ pub fn sm4_decrypt_ccm( Ok(plaintext) } +// ── GCM/CCM 合并格式(TLS 适配)──────────────────────────────────────────────── + +/// SM4-GCM 加密(合并输出格式:`ciphertext || tag`) +/// +/// TLS 记录层要求 AEAD 输出为单一缓冲区,此函数将密文和 16 字节 tag 合并返回。 +#[cfg(feature = "alloc")] +pub fn sm4_encrypt_gcm_combined( + key: &[u8; 16], + nonce: &[u8; 12], + aad: &[u8], + plaintext: &[u8], +) -> Vec { + let (mut ct, tag) = sm4_encrypt_gcm(key, nonce, aad, plaintext); + ct.extend_from_slice(&tag); + ct +} + +/// SM4-GCM 解密(合并输入格式:`ciphertext || tag`) +/// +/// 输入必须至少 16 字节(tag 长度);先验证 tag 再解密。 +#[cfg(feature = "alloc")] +pub fn sm4_decrypt_gcm_combined( + key: &[u8; 16], + nonce: &[u8; 12], + aad: &[u8], + ciphertext_with_tag: &[u8], +) -> Result, crate::error::Error> { + if ciphertext_with_tag.len() < 16 { + return Err(crate::error::Error::InvalidInputLength); + } + let ct_len = ciphertext_with_tag.len() - 16; + let ct = &ciphertext_with_tag[..ct_len]; + let tag: &[u8; 16] = ciphertext_with_tag[ct_len..].try_into().unwrap(); + sm4_decrypt_gcm(key, nonce, aad, ct, tag) +} + +/// SM4-CCM 加密(tag_len = 16,合并输出格式) +/// +/// TLS 1.3 `TLS_SM4_CCM_SM3` 使用 16 字节 tag。 +/// 等同于 `sm4_encrypt_ccm`(其输出已是 ciphertext||tag 合并格式)。 +#[cfg(feature = "alloc")] +pub fn sm4_encrypt_ccm_combined( + key: &[u8; 16], + nonce: &[u8; 12], + aad: &[u8], + plaintext: &[u8], +) -> Result, crate::error::Error> { + sm4_encrypt_ccm(key, nonce, aad, plaintext, 16) +} + +/// SM4-CCM 解密(tag_len = 16,合并输入格式) +/// +/// 等同于 `sm4_decrypt_ccm(..., 16)`。 +#[cfg(feature = "alloc")] +pub fn sm4_decrypt_ccm_combined( + key: &[u8; 16], + nonce: &[u8; 12], + aad: &[u8], + ciphertext_with_tag: &[u8], +) -> Result, crate::error::Error> { + sm4_decrypt_ccm(key, nonce, aad, ciphertext_with_tag, 16) +} + // ── XTS ────────────────────────────────────────────────────────────────────── /// GF(2^128) 乘以 α(XTS tweak 更新)