diff --git a/src/error.rs b/src/error.rs index b98d7c3..e6d0941 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,5 +66,10 @@ impl fmt::Display for Error { } } +// Reason: std::error::Error 只在 std 环境可用;no_std 环境下仅提供 Display + Debug。 +// 条件编译确保 alloc-only 场景不引入 std 依赖。 +#[cfg(feature = "std")] +impl std::error::Error for Error {} + /// libsmx 统一 Result 类型 pub type Result = core::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 8fc02f8..b098946 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,9 @@ #[cfg(feature = "alloc")] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; + pub mod error; pub mod sm2; pub mod sm3; diff --git a/src/sm2/ec.rs b/src/sm2/ec.rs index bb65979..318d5f1 100644 --- a/src/sm2/ec.rs +++ b/src/sm2/ec.rs @@ -81,20 +81,30 @@ impl JacobianPoint { }) } - /// 判断是否为无穷远点(常量时间通过字节检查) + /// 判断是否为无穷远点(常量时间,公开接口) pub fn is_infinity(&self) -> bool { - // Reason: Z==0 等价于无穷远点,检查所有字节为 0 - fp_to_bytes(&self.z).iter().all(|&b| b == 0) + bool::from(self.ct_is_infinity()) } - /// 点倍运算(Jacobian 坐标,a=-3 优化公式) + /// 常量时间无穷远判断(内部辅助,返回 Choice) + /// + /// Reason: 返回 Choice 供 conditional_select 直接使用,避免 bool 转换后再转回 Choice + fn ct_is_infinity(&self) -> Choice { + // Reason: 用 ConstantTimeEq 比较所有 32 字节,执行时间与 Z 值无关, + // 替代 Iterator::all 的短路求值(后者泄露 Z 坐标前缀信息)。 + use subtle::ConstantTimeEq; + fp_to_bytes(&self.z).ct_eq(&[0u8; 32]) + } + + /// 点倍运算(Jacobian 坐标,a=-3 优化公式,完全常量时间) /// /// 公式来自 https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-3.html#doubling-dbl-2001-b /// SM2 曲线 a = p-3 ≡ -3 (mod p),使用 a=-3 特化公式降低乘法次数。 + /// + /// # 安全性 + /// 无条件执行完整运算,用 `conditional_select` 处理无穷远退化情况, + /// 消除 `if is_infinity()` 分支对标量前导零位的泄露。 pub fn double(&self) -> Self { - if self.is_infinity() { - return *self; - } let (x1, y1, z1) = (&self.x, &self.y, &self.z); let delta = fp_square(z1); // Z1² @@ -118,24 +128,28 @@ impl JacobianPoint { &double2(&double1(&gamma2)), ); - JacobianPoint { - x: x3, - y: y3, - z: z3, - } + let d = JacobianPoint { x: x3, y: y3, z: z3 }; + // Reason: 无穷远点的倍点仍为无穷远点;用掩码选择替代 if 分支, + // 避免 scalar_mul 热路径中泄露哪些迭代位为前导零。 + JacobianPoint::conditional_select(&d, self, self.ct_is_infinity()) } - /// 点加运算(完整 Jacobian 公式,处理特殊情况) + /// 点加运算(完全常量时间,无条件分支) /// /// 公式来自 https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-3.html#addition-add-2007-bl - /// 当 P==Q 退化为倍点,当 P==-Q 退化为无穷远点。 + /// + /// # 安全性 + /// 采用"计算所有情况 + 掩码选择"策略,消除全部退化情况的条件分支: + /// - P = ∞ → Q(无穷远加法单位元) + /// - Q = ∞ → P + /// - P = Q → double(P)(相同点,用 ct_eq 检测 H==0 且 R==0) + /// - P = -Q → ∞(互反点,用 ct_eq 检测 H==0 且 R≠0) + /// - 正常情况 → 标准 Jacobian 加法 + /// + /// Reason: 原实现的 3 处 `if` 分支(is_infinity、H==0、R==0) + /// 在 scalar_mul 热路径中泄露标量的汉明重量及位分布。 pub fn add(p: &JacobianPoint, q: &JacobianPoint) -> JacobianPoint { - if p.is_infinity() { - return *q; - } - if q.is_infinity() { - return *p; - } + use subtle::ConstantTimeEq; let z1sq = fp_square(&p.z); let z2sq = fp_square(&q.z); @@ -147,33 +161,41 @@ impl JacobianPoint { let h = fp_sub(&u2, &u1); let r = fp_sub(&s2, &s1); - // H==0 时 P、Q 在同一射影位置 - if fp_to_bytes(&h).iter().all(|&b| b == 0) { - return if fp_to_bytes(&r).iter().all(|&b| b == 0) { - p.double() // P == Q - } else { - JacobianPoint::INFINITY // P == -Q - }; - } + // 常量时间零判断(替代 Iterator::all 短路) + let h_is_zero = fp_to_bytes(&h).ct_eq(&[0u8; 32]); + let r_is_zero = fp_to_bytes(&r).ct_eq(&[0u8; 32]); + // 无条件执行标准 Jacobian 加法(当 h==0 时结果为垃圾值,后续掩码覆盖) let h2 = fp_square(&h); let h3 = fp_mul(&h, &h2); let u1h2 = fp_mul(&u1, &h2); // X3 = R² - H³ - 2·U1·H² let x3 = fp_sub(&fp_sub(&fp_square(&r), &h3), &double1(&u1h2)); - // Y3 = R·(U1·H² - X3) - S1·H³ let y3 = fp_sub(&fp_mul(&r, &fp_sub(&u1h2, &x3)), &fp_mul(&s1, &h3)); - - // Z3 = H·Z1·Z2 + // Z3 = H·Z1·Z2 (当 H==0 时 z3=0,即 INFINITY,与下面掩码一致) let z3 = fp_mul(&fp_mul(&h, &p.z), &q.z); + let normal = JacobianPoint { x: x3, y: y3, z: z3 }; - JacobianPoint { - x: x3, - y: y3, - z: z3, - } + // 预计算 P==Q 退化情况的结果(无条件执行,结果由掩码决定是否使用) + let double_p = p.double(); + + // 按优先级从低到高用 conditional_select 叠加(后面覆盖前面): + // 优先级 1(最低):正常 Jacobian 加法 + let result = normal; + // 优先级 2:P == -Q → INFINITY(h==0 且 r≠0) + let result = JacobianPoint::conditional_select( + &result, &JacobianPoint::INFINITY, h_is_zero & !r_is_zero, + ); + // 优先级 3:P == Q → double(P)(h==0 且 r==0) + let result = JacobianPoint::conditional_select( + &result, &double_p, h_is_zero & r_is_zero, + ); + // 优先级 4:Q 是无穷远 → P(加法单位元) + let result = JacobianPoint::conditional_select(&result, p, q.ct_is_infinity()); + // 优先级 5(最高):P 是无穷远 → Q + JacobianPoint::conditional_select(&result, q, p.ct_is_infinity()) } /// 标量乘 k·P(常量时间,固定 256 位迭代) @@ -422,4 +444,42 @@ mod tests { let rhs = fp_add(&fp_add(&x3, &ax), &CURVE_B); assert_eq!(rhs, fp_square(&pub_aff.y)); } + + /// 验证完备加法公式的退化情况(常量时间 add 的正确性) + #[test] + fn test_add_degenerate_cases() { + let g = JacobianPoint::from_affine(&AffinePoint::generator()); + let inf = JacobianPoint::INFINITY; + + // ∞ + G = G + let r = JacobianPoint::add(&inf, &g).to_affine().unwrap(); + assert_eq!(fp_to_bytes(&r.x), fp_to_bytes(&GX), "∞ + G 的 x 坐标错误"); + assert_eq!(fp_to_bytes(&r.y), fp_to_bytes(&GY), "∞ + G 的 y 坐标错误"); + + // G + ∞ = G + let r = JacobianPoint::add(&g, &inf).to_affine().unwrap(); + assert_eq!(fp_to_bytes(&r.x), fp_to_bytes(&GX), "G + ∞ 的 x 坐标错误"); + + // G + G = 2G(通过 add 和 double 各算一次,结果应相同) + let add_gg = JacobianPoint::add(&g, &g).to_affine().unwrap(); + let double_g = g.double().to_affine().unwrap(); + assert_eq!( + fp_to_bytes(&add_gg.x), + fp_to_bytes(&double_g.x), + "add(G,G) != double(G) 的 x 坐标" + ); + assert_eq!( + fp_to_bytes(&add_gg.y), + fp_to_bytes(&double_g.y), + "add(G,G) != double(G) 的 y 坐标" + ); + + // G + (-G) = ∞(互反点,y 取负) + let g_neg = JacobianPoint { + x: g.x, + y: fp_neg(&g.y), + z: g.z, + }; + assert!(JacobianPoint::add(&g, &g_neg).is_infinity(), "G + (-G) 应为无穷远点"); + } } diff --git a/src/sm3/mod.rs b/src/sm3/mod.rs index f49b1db..1974c3c 100644 --- a/src/sm3/mod.rs +++ b/src/sm3/mod.rs @@ -151,7 +151,13 @@ impl Default for Sm3Hasher { /// /// # 返回 /// 32 字节 HMAC 值 +/// +/// # 安全性 +/// `k_pad`/`ipad`/`opad` 含密钥派生材料,函数返回前用 `zeroize` 清零, +/// 防止密钥残留在栈上被后续代码或内存扫描工具读取。 pub fn hmac_sm3(key: &[u8], data: &[u8]) -> [u8; DIGEST_LEN] { + use zeroize::Zeroize; + // 将 key 标准化到 64 字节(不足补零,过长先哈希) let mut k_pad = [0u8; 64]; if key.len() > 64 { @@ -179,7 +185,14 @@ pub fn hmac_sm3(key: &[u8], data: &[u8]) -> [u8; DIGEST_LEN] { let mut outer = Sm3Hasher::new(); outer.update(&opad); outer.update(&inner_hash); - outer.finalize() + let result = outer.finalize(); + + // Reason: 清零栈上的密钥派生材料,防止密钥残留 + k_pad.zeroize(); + ipad.zeroize(); + opad.zeroize(); + + result } #[cfg(test)] diff --git a/src/sm4/cipher.rs b/src/sm4/cipher.rs index 681f214..2bd0043 100644 --- a/src/sm4/cipher.rs +++ b/src/sm4/cipher.rs @@ -2,8 +2,8 @@ //! //! # 安全说明 //! -//! S-box 使用**位切片(Bitslice)**实现,完全消除了缓存时序侧信道攻击面。 -//! 每次 S-box 查询的访存模式与输入无关。 +//! S-box 使用**纯布尔电路位切片**实现(路径 A),完全消除内存访问, +//! 仅使用 AND/XOR/OR/NOT 位运算,无缓存时序侧信道攻击面。 use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -25,72 +25,154 @@ const CK: [u32; 32] = [ 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279, ]; -// ── 常量时间 S-box(两级 4-bit 掩码查表)──────────────────────────────────── +// ── 纯布尔电路 S-box(路径 A:零内存访问位切片实现)───────────────────────── // -// 将 8 位输入拆分为高 4 位(行索引)和低 4 位(列索引), -// 分两级各做 16 次掩码操作,合计 32 次掩码操作,远少于原 256 次。 +// 仅使用 AND/XOR/OR/NOT 位运算,完全消除内存查表,无缓存时序侧信道。 // -// 安全性: -// - 无秘密依赖的条件分支 -// - 无秘密索引的内存访问(每次访问固定的 16×16 = 256 字节区域) -// - 掩码生成仅用算术运算(XOR / wrapping_sub / 右移),无分支 +// 算法来源:emmansun/sm4bs(sbox64 函数)经标量化提取并验证(256/256 全表正确)。 +// 结构:输入线性层 -> GF(2^4) 求逆(top+middle 函数)-> 输出线性层(bottom+output 函数) // -// Reason: 两级 4-bit 掩码查表是在不使用 SIMD 的前提下,兼顾性能与常量时间 -// 安全性的务实方案(路径 B)。真正的布尔电路位切片(路径 A)记录于 -// ROADMAP.md,待 v1.0 发布后评估。 +// Reason: 纯布尔电路(路径 A)完全消除内存访问,不依赖缓存行为, +// 在所有微架构上均无侧信道风险。 -/// SM4 S-box 常量时间查找:两级 4-bit 掩码(32 次掩码操作) +/// SM4 S-box 布尔电路实现(路径 A) /// -/// 将输入 `x` 拆分为高 4 位(行)和低 4 位(列), -/// - 第一级:16 次掩码操作选出正确的 16 字节行到栈上缓冲区 -/// - 第二级:16 次掩码操作从缓冲区选出正确的 1 字节输出 -/// -/// 全程无条件分支,无秘密依赖的内存地址计算。 +/// 仅使用 `&`/`^`/`|`/`!` 位运算,零内存访问,无条件分支。 +/// 每个中间变量为 0 或 1(对应输入字节的各个位平面)。 #[inline] pub(crate) fn sbox_ct(x: u8) -> u8 { - // SM4 S-box 按 16×16 组织(行 = 高半字节,列 = 低半字节) - #[rustfmt::skip] - const SBOX: [[u8; 16]; 16] = [ - [0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05], - [0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99], - [0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62], - [0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6], - [0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8], - [0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35], - [0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87], - [0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e], - [0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1], - [0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3], - [0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f], - [0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51], - [0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8], - [0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0], - [0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84], - [0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48], - ]; + // 提取输入字节的 8 个位(b0 = LSB, b7 = MSB) + let b0 = x & 1; + let b1 = (x >> 1) & 1; + let b2 = (x >> 2) & 1; + let b3 = (x >> 3) & 1; + let b4 = (x >> 4) & 1; + let b5 = (x >> 5) & 1; + let b6 = (x >> 6) & 1; + let b7 = (x >> 7) & 1; - let hi = (x >> 4) as usize; // 行索引(高 4 位) - let lo = (x & 0xF) as usize; // 列索引(低 4 位) + // ── 输入线性层(input function)────────────────────────────────────────── + // Reason: 将输入 8 位映射为中间变量 g0..g7, m0..m9,为 GF(2^4) 求逆做准备。 + let t1 = b7 ^ b5; + let t2 = 1 ^ (b5 ^ b1); // NOT(b5 ^ b1) = g4 + let g5 = 1 ^ b0; // NOT(b0) + let t3 = 1 ^ (b0 ^ t2); // NOT(b0 ^ t2) = m1 + let t4 = b6 ^ b2; // m4 + let t5 = b3 ^ t3; // g3 + let t6 = b4 ^ t1; // m0 + let t7 = b1 ^ t5; // g1 + let t8 = b1 ^ t4; // m2 + let t9 = t6 ^ t8; // m8 + let t10 = t6 ^ t7; // g0 + let t11 = 1 ^ (b3 ^ t1); // NOT(b3 ^ t1) = m5 + let t12 = 1 ^ (b6 ^ t9); // NOT(b6 ^ t9) = m9 - // 第一级:16 次掩码操作,选出行 hi 的 16 字节到栈缓冲区 - // Reason: mask = 0xFF 当且仅当 i == hi(无分支算术),遍历全部 16 行 - // 保证访问模式与 hi 无关。 - let mut row = [0u8; 16]; - for (i, srow) in SBOX.iter().enumerate() { - let mask = ((hi ^ i).wrapping_sub(1) >> 8) as u8; - for (j, &v) in srow.iter().enumerate() { - row[j] |= v & mask; - } - } + let g0 = t10; + let g1 = t7; + let g2 = t4 ^ t10; + let g3 = t5; + let g4 = t2; + let g6 = t11 ^ t2; + let g7 = t12 ^ (t11 ^ t2); + let m0 = t6; + let m1 = t3; + let m2 = t8; + let m3 = t3 ^ t12; + let m4 = t4; + let m5 = t11; + let m6 = b1; + let m7 = t11 ^ m3; + let m8 = t9; + let m9 = t12; - // 第二级:16 次掩码操作,从缓冲区选出列 lo 的字节 - // Reason: 同上,访问模式与 lo 无关。 - let mut result = 0u8; - for (j, &v) in row.iter().enumerate() { - let mask = ((lo ^ j).wrapping_sub(1) >> 8) as u8; - result |= v & mask; - } - result + // ── Top 函数(GF(2^4) 求逆的输入准备)──────────────────────────────────── + // Reason: 将 16 个中间变量组合为 p0..p3,供 GF(2^2) 中间层使用。 + let t2t = m0 & m1; + let t3t = g0 & g4; + let t4t = g3 & g7; + let t7t = g3 | g7; + let t11t = m4 & m5; + let t10t = m3 & m2; + let t12t = m3 | m2; + let t6t = g6 | g2; + let t9t = m6 | m7; + let t5t = m8 & m9; + let t8t = m8 | m9; + let t14t = t3t ^ t2t; + let t16t = t5t ^ t14t; + let t20t = t16t ^ t7t; + let t17t = t9t ^ t10t; + let t18t = t11t ^ t12t; + let p2 = t20t ^ t18t; + let p0 = t6t ^ t16t; + let t1t = g5 & g1; + let t13t = t1t ^ t2t; + let t15t = t13t ^ t4t; + let p3 = (t6t ^ t15t) ^ t17t; + let p1 = t8t ^ t15t; + + // ── Middle 函数(GF(2^2) 求逆)─────────────────────────────────────────── + // Reason: 在 GF(2^2) 上对 (p0,p1,p2,p3) 组成的元素进行求逆,输出 l0..l3。 + let t0m = p1 & p2; + let t1m = p3 & p0; + let t2m = p0 & p2; + let t3m = p1 & p3; + let t4m = t0m & t2m; + let t5m = t1m ^ t3m; + let t6m = t5m | p0; + let t7m = t2m | p3; + let l3 = t4m ^ t6m; + let t9m = t7m ^ t3m; + let l0 = t0m ^ t9m; + let t11m = p2 | t5m; + let l1 = t11m ^ t1m; + let t12m = p1 | t2m; + let l2 = t12m ^ t5m; + + // ── Bottom 函数(GF(2^4) 求逆的输出组合)───────────────────────────────── + // Reason: 将 l0..l3 与输入中间变量结合,得到 r0..r11(12 个中间结果)。 + let k4 = l2 ^ l3; + let k3 = l1 ^ l3; + let k2 = l0 ^ l2; + let k0 = l0 ^ l1; + let k1 = k2 ^ k3; + + let e0 = m1 & k0; let e1 = g5 & l1; let r0 = e0 ^ e1; + let e2 = g4 & l0; let r1 = e2 ^ e1; + let e3 = m7 & k3; let e4 = m5 & k2; let r2 = e3 ^ e4; + let e5 = m3 & k1; let r3 = e5 ^ e4; + let e6 = m9 & k4; let e7 = g7 & l3; let r4 = e6 ^ e7; + let e8 = g6 & l2; let r5 = e8 ^ e7; + let e9 = m0 & k0; let e10 = g1 & l1; let r6 = e9 ^ e10; + let e11 = g0 & l0; let r7 = e11 ^ e10; + let e12 = m6 & k3; let e13 = m4 & k2; let r8 = e12 ^ e13; + let e14 = m2 & k1; let r9 = e14 ^ e13; + let e15 = m8 & k4; let e16 = g3 & l3; let r10 = e15 ^ e16; + let e17 = g2 & l2; let r11 = e17 ^ e16; + + // ── 输出线性层(output function)────────────────────────────────────────── + // Reason: 将 r0..r11 组合为输出字节的 8 个位。 + let t1o = r7 ^ r9; + let t2o = r1 ^ t1o; + let t3o = r3 ^ t2o; + let t4o = r5 ^ r3; + let t5o = r4 ^ t4o; + let t6o = r0 ^ r4; + let t7o = r11 ^ r7; + + let b5o = t1o ^ t4o; + let b2o = t1o ^ t6o; + let t10o = r2 ^ t5o; + let b3o = r10 ^ r8; + let b1o = 1 ^ (t3o ^ b3o); + let b6o = t10o ^ b1o; + let b4o = 1 ^ (t3o ^ t7o); + let b0o = t6o ^ b4o; + let b7o = 1 ^ (r10 ^ r6); + + // 将 8 个输出位重组为字节 + b0o | (b1o << 1) | (b2o << 2) | (b3o << 3) + | (b4o << 4) | (b5o << 5) | (b6o << 6) | (b7o << 7) } /// SM4 τ 变换:对 u32 的 4 个字节分别做 S-box(常量时间) @@ -288,7 +370,7 @@ mod tests { assert_eq!(block, plain, "SM4 加解密往返不一致"); } - /// 常量时间 S-box 与标准 S-box 表一致性验证 + /// 布尔电路 S-box 与标准 S-box 表一致性验证(256 点全表) #[test] fn test_sbox_ct_correct() { #[rustfmt::skip] @@ -314,7 +396,7 @@ mod tests { assert_eq!( sbox_ct(i), REF[i as usize], - "S-box 常量时间实现在输入 {i:#04x} 处与标准不一致" + "S-box 布尔电路实现在输入 {i:#04x} 处与标准不一致" ); } } diff --git a/src/sm4/modes.rs b/src/sm4/modes.rs index 26e5fc7..567f5e2 100644 --- a/src/sm4/modes.rs +++ b/src/sm4/modes.rs @@ -186,26 +186,34 @@ pub fn sm4_crypt_ctr(key: &[u8; 16], nonce: &[u8; 16], data: &[u8]) -> Vec { // ── GCM ────────────────────────────────────────────────────────────────────── -/// GF(2^128) 乘法(NIST SP 800-38D Algorithm 1) -/// Reason: GHASH 的核心运算,不可约多项式 x^128 + x^7 + x^2 + x + 1 +/// GF(2^128) 乘法(NIST SP 800-38D Algorithm 1,常量时间) +/// +/// # 安全性 +/// 使用掩码算术替代秘密依赖的条件分支,消除时序侧信道: +/// - `mask_xi`:由当前标量位生成的 0x00/0xFF 掩码,替代 `if bit == 1` +/// - `reduce_mask`:由 LSB 生成的 0x00/0xFF 掩码,替代 `if lsb == 1` +/// +/// Reason: GHASH 密钥 H 来自 SM4_K(0^128),属秘密值;原条件分支泄露 H 的汉明重量, +/// 是 cache-timing 和 branch-timing 攻击的经典目标(参见 Bricout 等 2016)。 fn gf128_mul(x: &[u8; 16], y: &[u8; 16]) -> [u8; 16] { let mut z = [0u8; 16]; let mut v = *y; for byte_xi in x.iter() { for bit_idx in (0..8).rev() { - if (byte_xi >> bit_idx) & 1 == 1 { - for j in 0..16 { - z[j] ^= v[j]; - } + // Reason: 0u8.wrapping_sub(1) = 0xFF,wrapping_sub(0) = 0x00 + // 用掩码代替 if,确保两条路径执行时间完全相同 + let mask_xi = 0u8.wrapping_sub((byte_xi >> bit_idx) & 1); + for j in 0..16 { + z[j] ^= v[j] & mask_xi; } let lsb = v[15] & 1; for j in (1..16).rev() { v[j] = (v[j] >> 1) | (v[j - 1] << 7); } v[0] >>= 1; - if lsb == 1 { - v[0] ^= 0xE1; - } + // Reason: 同上,掩码替代 if lsb == 1,消除 GF 规约的秘密依赖分支 + let reduce_mask = 0u8.wrapping_sub(lsb); + v[0] ^= 0xE1 & reduce_mask; } } z @@ -357,13 +365,16 @@ pub fn sm4_decrypt_gcm( // ── CCM ────────────────────────────────────────────────────────────────────── /// 构造 CCM CBC-MAC(RFC 3610) +/// +/// # 错误 +/// `aad` 超过 510 字节时返回 `Error::InvalidInputLength`(当前实现仅支持 2 字节长度编码)。 fn ccm_cbc_mac( rk: &[u32; 32], nonce: &[u8; 12], aad: &[u8], message: &[u8], tag_len: usize, -) -> [u8; 16] { +) -> Result<[u8; 16], crate::error::Error> { let q = 3usize; // nonce=12B 时 q=15-12=3 let has_aad = !aad.is_empty(); let flags = ((has_aad as u8) << 6) | (((tag_len - 2) / 2) as u8) << 3 | (q as u8 - 1); @@ -383,17 +394,23 @@ fn ccm_cbc_mac( // Reason: CCM AAD 前缀 2 字节长度 + AAD 数据,补零至 16 字节对齐 let prefix_len = 2 + aad_len; let padded_len = (prefix_len + 15) / 16 * 16; - let mut aad_buf = [0u8; 512]; // 足够大的栈缓冲区 - if prefix_len <= aad_buf.len() { - aad_buf[0..2].copy_from_slice(&(aad_len as u16).to_be_bytes()); - aad_buf[2..2 + aad_len].copy_from_slice(aad); - for chunk in aad_buf[..padded_len].chunks(16) { - let block: [u8; 16] = chunk.try_into().unwrap(); - for i in 0..16 { - x[i] ^= block[i]; - } - x = encrypt_block_raw(rk, &x); + let mut aad_buf = [0u8; 512]; // 足够大的栈缓冲区(支持 AAD ≤ 510 字节) + + // Reason: 超过 510 字节需要 4 字节长度编码(RFC 3610 §2.2), + // 当前实现仅支持 2 字节编码,超限时必须拒绝而非静默跳过 AAD。 + // 静默跳过会导致认证标签不包含 AAD,攻击者可随意篡改 AAD 而不被检测。 + if prefix_len > aad_buf.len() { + return Err(crate::error::Error::InvalidInputLength); + } + + aad_buf[0..2].copy_from_slice(&(aad_len as u16).to_be_bytes()); + aad_buf[2..2 + aad_len].copy_from_slice(aad); + for chunk in aad_buf[..padded_len].chunks(16) { + let block: [u8; 16] = chunk.try_into().unwrap(); + for i in 0..16 { + x[i] ^= block[i]; } + x = encrypt_block_raw(rk, &x); } } @@ -405,7 +422,7 @@ fn ccm_cbc_mac( } x = encrypt_block_raw(rk, &x); } - x + Ok(x) } /// SM4-CCM 加密(AEAD) @@ -416,6 +433,9 @@ fn ccm_cbc_mac( /// /// # 返回 /// 密文 || 认证标签(`tag_len` 字节) +/// +/// # 错误 +/// - `aad` 超过 510 字节时返回 `Error::InvalidInputLength` #[cfg(feature = "alloc")] pub fn sm4_encrypt_ccm( key: &[u8; 16], @@ -423,7 +443,7 @@ pub fn sm4_encrypt_ccm( aad: &[u8], plaintext: &[u8], tag_len: usize, -) -> Vec { +) -> Result, crate::error::Error> { assert!( (4..=16).contains(&tag_len) && tag_len % 2 == 0, "CCM tag_len 须为 4~16 的偶数" @@ -432,7 +452,7 @@ pub fn sm4_encrypt_ccm( let sm4 = Sm4Key::new(key); let rk = sm4.round_keys(); - let t = ccm_cbc_mac(rk, nonce, aad, plaintext, tag_len); + let t = ccm_cbc_mac(rk, nonce, aad, plaintext, tag_len)?; let mut a0 = [0u8; 16]; a0[0] = 2u8; // q-1 = 3-1 = 2 @@ -457,7 +477,7 @@ pub fn sm4_encrypt_ccm( } } out.extend_from_slice(&enc_tag[..tag_len]); - out + Ok(out) } /// SM4-CCM 解密(AEAD) @@ -500,7 +520,7 @@ pub fn sm4_decrypt_ccm( } // Step 2: 对候选明文重新计算 CBC-MAC - let t = ccm_cbc_mac(rk, nonce, aad, &plaintext, tag_len); + let t = ccm_cbc_mac(rk, nonce, aad, &plaintext, tag_len)?; let mut expected_tag = [0u8; 16]; for i in 0..tag_len { expected_tag[i] = t[i] ^ s0[i]; @@ -536,14 +556,27 @@ fn xts_mul_alpha(tweak: &mut [u8; 16]) { /// - `key1`: 数据加密密钥(16 字节) /// - `key2`: tweak 加密密钥(16 字节) /// - `tweak_sector`: 扇区号(16 字节,通常为扇区编号的小端表示) -/// - `data`: 明文(须为 16 字节整倍数) +/// - `data`: 明文(须为 16 字节整倍数,不支持非对齐输入) +/// +/// # 错误 +/// `data` 为空或长度不是 16 的整倍数时返回 `Error::InvalidInputLength`。 +/// +/// # 注意 +/// XTS 的 ciphertext stealing(非对齐末尾块处理)超出本实现范围, +/// 调用方须保证输入对齐;非对齐时须先在应用层填充后再调用。 #[cfg(feature = "alloc")] pub fn sm4_encrypt_xts( key1: &[u8; 16], key2: &[u8; 16], tweak_sector: &[u8; 16], data: &[u8], -) -> Vec { +) -> Result, crate::error::Error> { + // Reason: 非对齐输入在旧实现中被静默丢弃(最后不足 16 字节块跳过), + // 导致密文比明文短而调用方无感知。拒绝非对齐输入防止数据静默丢失。 + if data.is_empty() || data.len() % 16 != 0 { + return Err(crate::error::Error::InvalidInputLength); + } + let sm4_1 = Sm4Key::new(key1); let sm4_2 = Sm4Key::new(key2); let mut tweak = *tweak_sector; @@ -551,29 +584,35 @@ pub fn sm4_encrypt_xts( let mut out = Vec::with_capacity(data.len()); for chunk in data.chunks(16) { - if chunk.len() == 16 { - let mut block = [0u8; 16]; - for i in 0..16 { - block[i] = chunk[i] ^ tweak[i]; - } - sm4_1.encrypt_block(&mut block); - for i in 0..16 { - out.push(block[i] ^ tweak[i]); - } - xts_mul_alpha(&mut tweak); + let mut block = [0u8; 16]; + for i in 0..16 { + block[i] = chunk[i] ^ tweak[i]; } + sm4_1.encrypt_block(&mut block); + for i in 0..16 { + out.push(block[i] ^ tweak[i]); + } + xts_mul_alpha(&mut tweak); } - out + Ok(out) } -/// SM4-XTS 解密(磁盘加密模式) +/// SM4-XTS 解密(磁盘加密模式,GB/T 17964-2021) +/// +/// # 错误 +/// `data` 为空或长度不是 16 的整倍数时返回 `Error::InvalidInputLength`。 #[cfg(feature = "alloc")] pub fn sm4_decrypt_xts( key1: &[u8; 16], key2: &[u8; 16], tweak_sector: &[u8; 16], data: &[u8], -) -> Vec { +) -> Result, crate::error::Error> { + // Reason: 同 sm4_encrypt_xts,拒绝非对齐输入防止数据静默丢失。 + if data.is_empty() || data.len() % 16 != 0 { + return Err(crate::error::Error::InvalidInputLength); + } + let sm4_1 = Sm4Key::new(key1); let sm4_2 = Sm4Key::new(key2); let mut tweak = *tweak_sector; @@ -581,19 +620,17 @@ pub fn sm4_decrypt_xts( let mut out = Vec::with_capacity(data.len()); for chunk in data.chunks(16) { - if chunk.len() == 16 { - let mut block = [0u8; 16]; - for i in 0..16 { - block[i] = chunk[i] ^ tweak[i]; - } - sm4_1.decrypt_block(&mut block); - for i in 0..16 { - out.push(block[i] ^ tweak[i]); - } - xts_mul_alpha(&mut tweak); + let mut block = [0u8; 16]; + for i in 0..16 { + block[i] = chunk[i] ^ tweak[i]; } + sm4_1.decrypt_block(&mut block); + for i in 0..16 { + out.push(block[i] ^ tweak[i]); + } + xts_mul_alpha(&mut tweak); } - out + Ok(out) } // ── 测试 ────────────────────────────────────────────────────────────────────── @@ -656,7 +693,7 @@ mod tests { let aad = b"ccm aad"; let plain = b"ccm plaintext!!!"; - let ct = sm4_encrypt_ccm(&key, &nonce, aad, plain, 16); + let ct = sm4_encrypt_ccm(&key, &nonce, aad, plain, 16).unwrap(); let pt = sm4_decrypt_ccm(&key, &nonce, aad, &ct, 16).unwrap(); assert_eq!(pt, plain, "CCM 往返解密失败"); } @@ -666,7 +703,7 @@ mod tests { fn test_ccm_tag_tamper() { let key = [0u8; 16]; let nonce = [0u8; 12]; - let mut ct = sm4_encrypt_ccm(&key, &nonce, b"", b"secret data here", 16); + let mut ct = sm4_encrypt_ccm(&key, &nonce, b"", b"secret data here", 16).unwrap(); // 篡改 tag(最后 16 字节) let last = ct.len() - 1; ct[last] ^= 1; @@ -676,6 +713,58 @@ mod tests { ); } + /// CCM AAD 超限应返回错误(而非静默跳过) + #[test] + fn test_ccm_aad_too_long() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let big_aad = [0u8; 511]; // 超过 510 字节限制 + assert!( + sm4_encrypt_ccm(&key, &nonce, &big_aad, b"data", 16).is_err(), + "AAD 超过 510 字节时应返回 InvalidInputLength" + ); + } + + /// XTS 加解密往返测试 + #[test] + fn test_xts_roundtrip() { + let key1 = [0x11u8; 16]; + let key2 = [0x22u8; 16]; + let tweak = [0u8; 16]; + let plain = [0x42u8; 32]; // 2 个 16 字节块 + + let ct = sm4_encrypt_xts(&key1, &key2, &tweak, &plain).unwrap(); + let pt = sm4_decrypt_xts(&key1, &key2, &tweak, &ct).unwrap(); + assert_eq!(pt, plain, "XTS 往返解密失败"); + } + + /// XTS 非对齐数据应返回错误 + #[test] + fn test_xts_non_aligned_rejected() { + let key1 = [0u8; 16]; + let key2 = [0u8; 16]; + let tweak = [0u8; 16]; + + // 空输入 + assert!( + sm4_encrypt_xts(&key1, &key2, &tweak, b"").is_err(), + "空输入应返回 InvalidInputLength" + ); + // 非 16 倍数 + assert!( + sm4_encrypt_xts(&key1, &key2, &tweak, b"not-aligned-data").is_err() == false, + "正好 16 字节不应返回错误" + ); + assert!( + sm4_encrypt_xts(&key1, &key2, &tweak, &[0u8; 17]).is_err(), + "17 字节应返回 InvalidInputLength" + ); + assert!( + sm4_decrypt_xts(&key1, &key2, &tweak, &[0u8; 15]).is_err(), + "15 字节应返回 InvalidInputLength" + ); + } + /// OFB 自反性验证 #[test] fn test_ofb_self_inverse() { diff --git a/src/sm9/fields/fp12.rs b/src/sm9/fields/fp12.rs index ebb4ff6..9506cf0 100644 --- a/src/sm9/fields/fp12.rs +++ b/src/sm9/fields/fp12.rs @@ -489,7 +489,8 @@ pub fn fp12_to_bytes(a: &Fp12) -> [u8; 384] { /// - a: yP 系数 -> c0.c0(1 slot,在 eval_line_at_p 中乘以 yP) /// - b: 常数项 -> c0.c1(v slot) /// - c: xP 系数 -> c1.c0(w slot,在 eval_line_at_p 中乘以 xP) -/// Reason: 经双线性性测试验证,此约定对应 D-type twist BN256 配对正确系数。 +/// +/// Reason: 经双线性性测试验证,此约定对应 D-type twist BN256 配对正确系数。 /// double step: a=Z₁²·u, b=-2Y₁Z₁, c=3X₁² /// add step: a=r·x2, b=-(r·x1+h·y1), c=h·y2 #[derive(Clone, Copy, Debug)] @@ -509,7 +510,7 @@ pub struct LineEval { /// - a 系数(yP 项)→ c0.c0 (1 slot) /// - b 系数(常数项)→ c1.c1 (vw slot) /// - c 系数(xP 项)→ c1.c2 (v²w slot) -/// a、c 已经在 eval_line_at_p 中分别乘以 yP 和 xP。 +/// a、c 已经在 eval_line_at_p 中分别乘以 yP 和 xP。 pub fn fp12_mul_by_line(f: &Fp12, l: &LineEval) -> Fp12 { let line_fp12 = Fp12 { c0: Fp6 { diff --git a/src/sm9/utils.rs b/src/sm9/utils.rs index 239b884..04de776 100644 --- a/src/sm9/utils.rs +++ b/src/sm9/utils.rs @@ -70,12 +70,13 @@ fn hash_to_range(z: &[u8], hid: u8, n: &U256) -> U256 { let h_raw = U256::from_be_slice(&ha[..32]); // h = h_raw mod (n-1) + 1,确保 h ∈ [1, n-1] - // 由于 h_raw 可能 ≥ n-1,使用模运算 - // crypto_bigint 无直接 mod,改用减法循环(n 是 256 位素数,循环次数最多 1 次) - let mut h = h_raw; - while h >= n_minus_1 { - h = h.wrapping_sub(&n_minus_1); - } + // Reason: 原 while 循环的执行次数取决于 h_raw 是否 ≥ n-1,泄露 1 bit 信息。 + // 改用无条件减法 + 掩码选择(conditional_select),执行时间与 h_raw 值无关。 + // crypto_bigint::Uint 实现了 subtle::ConstantTimeLess,ct_lt 为常量时间比较。 + use subtle::{ConditionallySelectable, ConstantTimeLess}; + let need_reduce = !h_raw.ct_lt(&n_minus_1); // h_raw >= n_minus_1 + let reduced = h_raw.wrapping_sub(&n_minus_1); + let h = U256::conditional_select(&h_raw, &reduced, need_reduce); h.wrapping_add(&U256::ONE) }