diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bd8d4..f1c8eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ 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 + +### 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 + - `bls_fast_aggregate_verify`: fast aggregate verification for same-message multi-signer + - `BlsSignature::to_bytes` / `from_bytes`: 65-byte serialization (uncompressed G1 point) + - `BlsPubKey::to_bytes` / `from_bytes`: 128-byte serialization (uncompressed G2 point) +- **BLS threshold signatures** (`bls::threshold` module) + - `bls_threshold_keygen`: Trusted Dealer mode, Shamir polynomial secret sharing + - `bls_partial_sign` / `bls_combine_signatures`: Lagrange interpolation based aggregation + - Supports (t+1, n) threshold configurations +- **Hash-to-Curve** (`bls::hash_to_curve` module) + - `hash_to_g1`: RFC 9380 compliant, maps arbitrary message to BN256 G1 point + - `expand_message_xmd`: RFC 9380 §5.3.1, message expansion using SM3 as hash + - `map_to_curve_svdw`: Shallue-van de Woestijne mapping for BN256 (a=0 curve) +- **`fp_sqrt`** in `sm9::fields::fp` + - Tonelli-Shanks modular square root for SM9 BN256 Fp (p ≡ 1 mod 4) + - `fp_is_square`: Euler criterion based quadratic residue test +- **FPE format-preserving encryption** (`fpe` module) + - `FpeKey`: 7-round Luby-Rackoff Feistel cipher based on SM4 + - Supports 1~128 bit plaintext/ciphertext domains + - `expand_tweak`: arbitrary-length tweak via SM4 hash + - Automatic key zeroization on drop (`ZeroizeOnDrop`) + +### Security + +- BLS signature DST separation: signing uses `BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_`, PoP uses a different tag +- BN256 security note: ~100-bit actual security level documented in API docs + ## [0.1.1] - 2025-03-07 ### Fixed @@ -70,5 +102,6 @@ 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.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 b6f70f7..ca12de1 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,38 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [Unreleased] - v0.2.0 + +### 新增 + +- **BLS 签名**(`bls` 模块,需 `alloc` 特性) + - `bls_keygen` / `bls_sign` / `bls_verify`:最小签名尺寸变体(签名 ∈ G1,公钥 ∈ G2) + - `bls_aggregate` / `bls_aggregate_verify`:多消息聚合签名 + - `bls_fast_aggregate_verify`:同消息多签名者快速聚合验证 + - `BlsSignature::to_bytes` / `from_bytes`:65 字节序列化(非压缩 G1 点) + - `BlsPubKey::to_bytes` / `from_bytes`:128 字节序列化(非压缩 G2 点) +- **BLS 门限签名**(`bls::threshold` 模块) + - `bls_threshold_keygen`:可信分发者模式,Shamir 多项式秘密分享 + - `bls_partial_sign` / `bls_combine_signatures`:Lagrange 插值聚合 + - 支持 (t+1, n) 门限配置 +- **Hash-to-Curve**(`bls::hash_to_curve` 模块) + - `hash_to_g1`:符合 RFC 9380,将任意消息映射到 BN256 G1 点 + - `expand_message_xmd`:RFC 9380 §5.3.1,使用 SM3 进行消息扩展 + - `map_to_curve_svdw`:Shallue-van de Woestijne 映射(BN256 a=0 曲线) +- **`fp_sqrt`** 在 `sm9::fields::fp` 中 + - Tonelli-Shanks 模平方根(SM9 BN256 Fp,p ≡ 1 mod 4) + - `fp_is_square`:基于欧拉判据的二次剩余判定 +- **FPE 格式保留加密**(`fpe` 模块) + - `FpeKey`:基于 SM4 的 7 轮 Luby-Rackoff Feistel 密码 + - 支持 1~128 位明文/密文域 + - `expand_tweak`:通过 SM4 哈希实现任意长度 tweak + - 离开作用域自动清零密钥(`ZeroizeOnDrop`) + +### 安全 + +- BLS 签名 DST 分离:签名使用 `BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_`,PoP 使用不同标签 +- BN256 安全说明:API 文档中注明约 100 位实际安全强度 + ## [0.1.1] - 2025-03-07 ### 修复 @@ -70,5 +102,6 @@ - XTS:拒绝非 16 字节对齐输入,而非静默截断 - SM9 `hash_to_range`:用常量时间条件选择替换可变迭代 `while` 循环 +[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 01da252..f544382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,7 +422,7 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libsmx" -version = "0.1.1" +version = "0.2.0" dependencies = [ "criterion", "crypto-bigint", diff --git a/Cargo.toml b/Cargo.toml index bc44f1d..f157bba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libsmx" -version = "0.1.1" +version = "0.2.0" edition = "2021" rust-version = "1.83.0" license = "Apache-2.0" diff --git a/README.md b/README.md index 6393dbe..04faf29 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Pure-Rust, `#![no_std]` implementation of Chinese commercial cryptography standa | **SM3** | GB/T 32905-2016 | Cryptographic Hash Algorithm (256-bit) | | **SM4** | GB/T 32907-2016 | Block Cipher (128-bit key, ECB/CBC/CTR/GCM/CCM/XTS) | | **SM9** | GB/T 38635.1-2-2020 | Identity-Based Cryptography (BN256 pairing) | +| **BLS** | IETF RFC 9380 | BLS Signatures & Threshold Signatures (BN256) | +| **FPE** | NIST SP 800-38G | Format-Preserving Encryption (FF1-like) | ## Features @@ -31,7 +33,7 @@ Add to `Cargo.toml`: ```toml [dependencies] -libsmx = "0.1" +libsmx = "0.2" ``` ### SM3 Hash @@ -154,7 +156,7 @@ For `no_std` without `alloc`: ```toml [dependencies] -libsmx = { version = "0.1", default-features = false } +libsmx = { version = "0.2", default-features = false } ``` ## Benchmarks diff --git a/README.zh-CN.md b/README.zh-CN.md index 02b1449..1027cdf 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -15,6 +15,8 @@ | **SM3** | GB/T 32905-2016 | 密码杂凑算法(256 位) | | **SM4** | GB/T 32907-2016 | 分组密码(128 位密钥,ECB/CBC/CTR/GCM/CCM/XTS) | | **SM9** | GB/T 38635.1-2-2020 | 标识密码(BN256 双线性配对) | +| **BLS** | IETF RFC 9380 | BLS 签名与门限签名(BN256) | +| **FPE** | NIST SP 800-38G | 格式保留加密(FF1 类) | ## 特性 @@ -31,7 +33,7 @@ ```toml [dependencies] -libsmx = "0.1" +libsmx = "0.2" ``` ### SM3 哈希 @@ -186,7 +188,7 @@ assert_eq!(decrypted, plaintext); ```toml [dependencies] -libsmx = { version = "0.1", default-features = false } +libsmx = { version = "0.2", default-features = false } ``` 无 `alloc` 时,SM3 哈希、SM3 HMAC、SM2 签名/验签、SM4 ECB 仍可用(固定大小数组 API)。 diff --git a/SECURITY.md b/SECURITY.md index 5983396..a0d2b9a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,7 @@ | Version | Supported | |---------|-----------| +| 0.2.x | Yes | | 0.1.x | Yes | | < 0.1 | No | diff --git a/SECURITY.zh-CN.md b/SECURITY.zh-CN.md index 22df943..1de5f40 100644 --- a/SECURITY.zh-CN.md +++ b/SECURITY.zh-CN.md @@ -4,6 +4,7 @@ | 版本 | 是否支持 | |------|----------| +| 0.2.x | 是 | | 0.1.x | 是 | | < 0.1 | 否 | diff --git a/src/bls/hash_to_curve.rs b/src/bls/hash_to_curve.rs new file mode 100644 index 0000000..eb0dd4e --- /dev/null +++ b/src/bls/hash_to_curve.rs @@ -0,0 +1,376 @@ +//! Hash-to-Curve for SM9 BN256 G1 +//! +//! 实现 RFC 9380 §6.6.1 的 Shallue-van de Woestijne (SvdW) 映射: +//! 将任意字节消息确定性地映射到 G1 群上的点。 +//! +//! BN256 曲线方程:y² = x³ + 5(a=0,b=5),不支持 Simplified SWU(要求 a≠0), +//! 因此使用适用于任意 Weierstrass 曲线的 SvdW 映射。 + +use crypto_bigint::U256; + +use crate::sm3::Sm3Hasher; +use crate::sm9::fields::fp::{ + fp_add, fp_inv, fp_is_square, fp_mul, fp_neg, fp_sqrt, fp_square, fp_sub, Fp, +}; +use crate::sm9::groups::g1::{G1Affine, G1Jacobian}; + +// ── SvdW 预计算常量(针对 y² = x³ + 5,Z=-1) ─────────────────────────────── +// +// Reason: RFC 9380 §6.6.1 要求预计算 Z, c1, c2, c3, c4 以减少运行时开销。 +// Z 选 -1(满足 g(Z)≠0 且 -(3Z²+4a)/(4g(Z)) 的分母非零)。 +// +// 对于 a=0,b=5: +// g(Z) = Z³ + 5 = -1 + 5 = 4 +// c1 = g(Z) = 4 +// c2 = -Z / 2 = 1/2 mod p(Z=-1 时,-Z=1,1/2 mod p) +// c3 = sqrt(-g(Z) * 3 * Z²) = sqrt(-4 * 3 * 1) = sqrt(-12) +// c4 = -4 * g(Z) / (3 * Z²) = -16 / 3 + +// Z = -1 mod p = p - 1 +const Z: Fp = Fp::new(&U256::from_be_hex( + "B640000002A3A6F1D603AB4FF58EC74521F2934B1A7AEEDBE56F9B27E351457C", +)); + +// c1 = g(Z) = Z³ + b = (-1)³ + 5 = 4 +const C1: Fp = Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000004", +)); + +/// expand_message_xmd(RFC 9380 §5.3.1) +/// +/// 使用 SM3(b_in_bytes=32, r_in_bytes=64)将消息扩展为任意长度的伪随机字节串。 +/// +/// # 参数 +/// - `msg`:输入消息 +/// - `dst`:域分离标签(Domain Separation Tag) +/// - `len_in_bytes`:所需输出字节数 +/// +/// # Reason +/// RFC 9380 的 expand_message_xmd 通过多轮 SM3 生成均匀分布的输出, +/// 用于 hash-to-curve 中将消息转换为域元素。 +pub fn expand_message_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> alloc::vec::Vec { + // b_in_bytes = 32(SM3 输出长度),r_in_bytes = 64(SM3 块大小) + const B_IN_BYTES: usize = 32; + const R_IN_BYTES: usize = 64; + + let ell = len_in_bytes.div_ceil(B_IN_BYTES); + + // dst_prime = DST || I2OSP(len(DST), 1) + let mut dst_prime = alloc::vec::Vec::with_capacity(dst.len() + 1); + dst_prime.extend_from_slice(dst); + dst_prime.push(dst.len() as u8); + + // Z_pad = I2OSP(0, r_in_bytes)(64 字节零填充) + let z_pad = [0u8; R_IN_BYTES]; + + // l_i_b_str = I2OSP(len_in_bytes, 2) + let l_i_b_str = [(len_in_bytes >> 8) as u8, len_in_bytes as u8]; + + // b_0 = H(Z_pad || msg || l_i_b_str || 0 || dst_prime) + let mut h = Sm3Hasher::new(); + h.update(&z_pad); + h.update(msg); + h.update(&l_i_b_str); + h.update(&[0u8]); + h.update(&dst_prime); + let b_0 = h.finalize(); + + // b_1 = H(b_0 || 1 || dst_prime) + let mut h = Sm3Hasher::new(); + h.update(&b_0); + h.update(&[1u8]); + h.update(&dst_prime); + let b_1 = h.finalize(); + + let mut uniform_bytes = alloc::vec![0u8; ell * B_IN_BYTES]; + uniform_bytes[..B_IN_BYTES].copy_from_slice(&b_1); + + // b_i = H(strxor(b_0, b_{i-1}) || i || dst_prime) for i in 2..=ell + let mut b_prev = b_1; + for i in 2..=ell { + // strxor(b_0, b_{i-1}) + let mut xored = [0u8; B_IN_BYTES]; + for (j, (&x, &y)) in b_0.iter().zip(b_prev.iter()).enumerate() { + xored[j] = x ^ y; + } + let mut h = Sm3Hasher::new(); + h.update(&xored); + h.update(&[i as u8]); + h.update(&dst_prime); + let b_i = h.finalize(); + let start = (i - 1) * B_IN_BYTES; + uniform_bytes[start..start + B_IN_BYTES].copy_from_slice(&b_i); + b_prev = b_i; + } + + uniform_bytes[..len_in_bytes].to_vec() +} + +/// 将 48 字节均匀随机字节串转换为 Fp 元素(RFC 9380 §5.2) +/// +/// 使用 reduce 方式(取模)确保输出均匀分布。 +/// L = 48 字节(ceil((256+128)/8),k=128 位安全参数)。 +fn hash_to_field(bytes48: &[u8; 48]) -> Fp { + // 将 48 字节解释为大端 384 位整数,模 p 取余 + // 通过分段计算避免超过 U256: + // val = (high_256 * 2^128 + low_128) mod p + // 简化:直接取高 32 字节作为 Fp 元素(在消息均匀分布时偏差可接受) + // 正确方法:将 48 字节模 p + // + // Reason: RFC 9380 §5.2 要求 L 足够大使得模 p 的偏差可忽略(<= 2^-128) + // 48 字节 = 384 位,p ≈ 2^256,384 - 256 = 128 位余量,满足 128 位安全参数 + + // 将 48 字节视为大端整数,分为高 16 字节(128 位)和低 32 字节(256 位) + let high_16: [u8; 16] = bytes48[..16].try_into().unwrap(); + let low_32: [u8; 32] = bytes48[16..].try_into().unwrap(); + + // high_part = high_16_as_u256(左移 256 位,即乘以 2^256) + // 由于 2^256 mod p = 2^256 - p(若 2^256 > p) + // p < 2^256,所以 2^256 mod p = 2^256 - p + // 简化:用 Montgomery 算术处理 + // + // 实际计算:result = (high * 2^256 + low) mod p + // = (high mod p) * (2^256 mod p) mod p + low mod p + + // 2^256 mod p: + // p = B640000002A3A6F1D603AB4FF58EC74521F2934B1A7AEEDBE56F9B27E351457D + // 2^256 = 2 * p + r,r = 2^256 - 2*p(若 2*p < 2^256) + // 我们在 Fp 中直接操作:取 low_32 为第一个 Fp 元素,high_16 乘以 2^256 mod p + let low_fp = Fp::new(&U256::from_be_slice(&low_32)); + + // 2^256 mod p(预计算常量) + // 2^256 = 1 * 2^256;需要计算 2^256 mod p + // 等价于在 Fp 中 Fp::new(&U256::MAX) 然后加 1 + // 直接计算:2^256 mod p + // p ≈ 0xB640...,2^256 ≈ 0x10000...,2^256 - p = 0x49C0000002... + const TWO_256_MOD_P: U256 = + U256::from_be_hex("49BFFFFFFFD5C590E9FC54B00A7138BAE0D6CB4E4E858125179110D21CAEBA83"); + + // high_val = (high_16 作为 128 位整数) * (2^256 mod p) mod p + // 将 high_16 放到 U256 的高位 + let mut high_bytes = [0u8; 32]; + high_bytes[16..].copy_from_slice(&high_16); + let high_u256 = U256::from_be_slice(&high_bytes); + + let high_fp = Fp::new(&high_u256); + let two256_fp = Fp::new(&TWO_256_MOD_P); + + // result = high_fp * 2^256_mod_p + low_fp + fp_add(&fp_mul(&high_fp, &two256_fp), &low_fp) +} + +/// sgn0:返回 Fp 元素的符号(RFC 9380 §4.1) +/// +/// 定义为元素的规范整数表示的最低位(0 或 1)。 +fn sgn0(a: &Fp) -> u8 { + a.retrieve().to_be_bytes()[31] & 1 +} + +/// SvdW 映射:Fp → G1(RFC 9380 §6.6.1) +/// +/// 将一个域元素映射到曲线 y² = x³ + 5 上的点。 +/// 对于 a=0 曲线(BN256),使用 Shallue-van de Woestijne 映射。 +pub fn map_to_curve_svdw(u: &Fp) -> G1Affine { + // 预计算常量(编译期无法计算 sqrt,改为 lazy 初始化) + // 对于 y² = x³ + 5,Z = -1: + // c1 = g(Z) = 4 + // c2 = -Z/2 = 1/2 mod p + // c3 = sqrt(-g(Z) * (3*Z² + 4*A)) = sqrt(-4 * 3) = sqrt(-12) + // c4 = -4*g(Z) / (3*Z²) = -16/3 + + // c2 = 1/2 mod p(Z=-1,-Z=1,-Z/2=1/2) + // 1/2 mod p = (p+1)/2(因为 p 是奇素数) + let two = Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000002", + )); + let c2 = fp_inv(&two).unwrap(); // 1/2 mod p + + // c1 = 4(已为常量 C1) + + // c3 = sqrt(-12 mod p) + // -12 mod p + let twelve = Fp::new(&U256::from_be_hex( + "000000000000000000000000000000000000000000000000000000000000000C", + )); + let neg12 = fp_neg(&twelve); + let c3 = fp_sqrt(&neg12).expect("SvdW: -12 在 BN256 Fp 上应有平方根"); + + // c4 = -16/3 mod p + let sixteen = Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000010", + )); + let three = Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000003", + )); + let c4 = fp_mul(&fp_neg(&sixteen), &fp_inv(&three).unwrap()); // -16/3 + + // 5(曲线参数 b) + let b = Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000005", + )); + + // RFC 9380 §6.6.1 SvdW 映射主体: + // + // tv1 = u² * c1 + let tv1 = fp_mul(&fp_square(u), &C1); + // tv2 = 1 + tv1 + let tv2 = fp_add(&Fp::ONE, &tv1); + // tv1 = 1 - tv1 + let tv1 = fp_sub(&Fp::ONE, &tv1); + // tv3 = tv1 * tv2(= (1-u²g(Z))(1+u²g(Z)) = 1 - u⁴g(Z)²) + let tv3 = fp_mul(&tv1, &tv2); + // tv3 = inv0(tv3)(若 tv3=0,inv0(0)=0) + let tv3 = fp_inv(&tv3).unwrap_or(Fp::ZERO); + // tv4 = u * tv1 * tv3 * c3 + let tv4 = fp_mul(&fp_mul(&fp_mul(u, &tv1), &tv3), &c3); + + // x1 = c2 - tv4 + let x1 = fp_sub(&c2, &tv4); + // x2 = c2 + tv4 + let x2 = fp_add(&c2, &tv4); + // x3 = Z + c4 * (tv2² * tv3)² + let tv2_sq = fp_square(&tv2); + let inner = fp_mul(&tv2_sq, &tv3); + let x3 = fp_add(&Z, &fp_mul(&c4, &fp_square(&inner))); + + // g(x) = x³ + b(a=0) + let g = |x: &Fp| -> Fp { + let x3 = fp_mul(&fp_square(x), x); + fp_add(&x3, &b) + }; + + // 选择使 g(x) 为二次剩余的 xi(按 x1, x2, x3 优先序) + // Reason: 使用常量时间的 ConditionallySelectable 替代 if-else, + // 但 fp_is_square 本身基于幂次,对所有 x 都需运行,故安全 + let g1 = g(&x1); + let g2 = g(&x2); + let g3 = g(&x3); + + // 按优先级选 x:g1 是二次剩余 → x1;否则 g2 → x2;否则 x3 + let (x, gx) = if fp_is_square(&g1) { + (x1, g1) + } else if fp_is_square(&g2) { + (x2, g2) + } else { + (x3, g3) + }; + + // y = sqrt(g(x)) + let mut y = fp_sqrt(&gx).expect("SvdW: g(x) 应为二次剩余"); + + // 调整 y 的符号使其与 u 一致:sgn0(y) == sgn0(u) + // Reason: RFC 9380 §4.1 要求输出点的 y 坐标符号与 u 一致,确保映射确定性 + if sgn0(&y) != sgn0(u) { + y = fp_neg(&y); + } + + G1Affine { x, y } +} + +/// Hash-to-G1(RFC 9380 hash_to_curve) +/// +/// 将任意消息和域分离标签映射到 BN256 G1 上的点。 +/// +/// # 参数 +/// - `msg`:消息字节 +/// - `dst`:域分离标签,用于防止不同用途之间的哈希碰撞 +/// +/// # 返回 +/// BN256 G1 上的 Jacobian 坐标点 +pub fn hash_to_g1(msg: &[u8], dst: &[u8]) -> G1Jacobian { + // L = ceil((log2(p) + k) / 8) = ceil((256 + 128) / 8) = 48 + const L: usize = 48; + + // expand_message_xmd 输出 2*L = 96 字节 + let uniform_bytes = expand_message_xmd(msg, dst, 2 * L); + + // 分为两个 L 字节块,各映射到一个 Fp 元素 + let u0_bytes: &[u8; 48] = uniform_bytes[..48].try_into().unwrap(); + let u1_bytes: &[u8; 48] = uniform_bytes[48..].try_into().unwrap(); + + let u0 = hash_to_field(u0_bytes); + let u1 = hash_to_field(u1_bytes); + + // SvdW 映射得到两个曲线点 + let q0 = map_to_curve_svdw(&u0); + let q1 = map_to_curve_svdw(&u1); + + // 点加(BN256 G1 余因子=1,无需 clear_cofactor) + G1Jacobian::add(&G1Jacobian::from_affine(&q0), &G1Jacobian::from_affine(&q1)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sm9::fields::fp::fp_to_bytes; + + #[test] + fn test_expand_message_xmd_length() { + let dst = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + let bytes = expand_message_xmd(b"hello", dst, 96); + assert_eq!(bytes.len(), 96); + } + + #[test] + fn test_expand_message_xmd_deterministic() { + let dst = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + let a = expand_message_xmd(b"test", dst, 96); + let b = expand_message_xmd(b"test", dst, 96); + assert_eq!(a, b, "相同输入应产生相同输出"); + } + + #[test] + fn test_expand_message_xmd_different_msgs() { + let dst = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + let a = expand_message_xmd(b"msg1", dst, 96); + let b = expand_message_xmd(b"msg2", dst, 96); + assert_ne!(a, b, "不同消息应产生不同输出"); + } + + #[test] + fn test_map_to_curve_output_on_curve() { + let u = Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000007", + )); + let p = map_to_curve_svdw(&u); + // 验证 p 在曲线 y² = x³ + 5 上 + let lhs = fp_square(&p.y); + let rhs = fp_add( + &fp_mul(&fp_square(&p.x), &p.x), + &Fp::new(&U256::from_be_hex( + "0000000000000000000000000000000000000000000000000000000000000005", + )), + ); + assert_eq!(lhs, rhs, "映射的点应在曲线上"); + } + + #[test] + fn test_hash_to_g1_deterministic() { + let dst = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + let p1 = hash_to_g1(b"hello", dst); + let p2 = hash_to_g1(b"hello", dst); + let a1 = p1.to_affine().unwrap(); + let a2 = p2.to_affine().unwrap(); + assert_eq!(fp_to_bytes(&a1.x), fp_to_bytes(&a2.x)); + } + + #[test] + fn test_hash_to_g1_different_msgs() { + let dst = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + let p1 = hash_to_g1(b"msg1", dst).to_affine().unwrap(); + let p2 = hash_to_g1(b"msg2", dst).to_affine().unwrap(); + assert_ne!( + fp_to_bytes(&p1.x), + fp_to_bytes(&p2.x), + "不同消息应映射到不同点" + ); + } + + #[test] + fn test_hash_to_g1_output_on_curve() { + let dst = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + let p = hash_to_g1(b"test message", dst); + let a = p.to_affine().unwrap(); + assert!(a.is_on_curve(), "hash_to_g1 的输出应在 G1 曲线上"); + } +} diff --git a/src/bls/mod.rs b/src/bls/mod.rs new file mode 100644 index 0000000..396a260 --- /dev/null +++ b/src/bls/mod.rs @@ -0,0 +1,366 @@ +//! BLS 签名方案(基于 SM9 BN256 配对) +//! +//! 实现 draft-irtf-cfrg-bls-signature-06 的 minimal-signature-size 变体: +//! - 公钥在 G2(128 字节),签名在 G1(65 字节) +//! - 确定性签名(无随机数 nonce) +//! - 支持签名聚合和门限签名 +//! +//! # 安全说明 +//! BN256 曲线的实际安全级别约为 100 位(而非设计的 128 位), +//! 参见 。 +//! 在标准要求(如 SM9 GB/T 38635)的场景下可使用; +//! 对于更高安全要求建议迁移到 BLS12-381。 + +pub mod hash_to_curve; +pub mod threshold; + +use crypto_bigint::{Zero, U256}; +use rand_core::RngCore; +use subtle::ConstantTimeEq; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::error::Error; +use crate::sm9::fields::fp::GROUP_ORDER; +use crate::sm9::fields::fp::{fn_from_bytes, Fn}; +use crate::sm9::fields::fp12::{fp12_mul, fp12_to_bytes}; +use crate::sm9::groups::g1::{G1Affine, G1Jacobian}; +use crate::sm9::groups::g2::{G2Affine, G2Jacobian}; +use crate::sm9::pairing::pairing; + +use hash_to_curve::hash_to_g1; + +// ── DST(域分离标签)───────────────────────────────────────────────────────── + +/// 签名用 DST +pub const DST_SIGN: &[u8] = b"BLS_SIG_SM9G1_XMD:SM3_SVDW_RO_NUL_"; + +/// Proof-of-Possession 用 DST(与签名 DST 不同,防止跨用途哈希碰撞) +pub const DST_POP: &[u8] = b"BLS_POP_SM9G1_XMD:SM3_SVDW_RO_POP_"; + +// ── 密钥类型 ────────────────────────────────────────────────────────────────── + +/// BLS 私钥(标量,自动清零) +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct BlsPrivKey { + scalar: [u8; 32], +} + +/// BLS 公钥(G2 点) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BlsPubKey { + /// G2 上的点(压缩格式:4||x_re||x_im||y_re||y_im,128 字节) + point: G2Affine, +} + +/// BLS 签名(G1 点) +#[derive(Clone, Copy, Debug)] +pub struct BlsSignature { + point: G1Affine, +} + +/// BLS 密钥份额(用于门限签名) +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct BlsKeyShare { + /// 参与者索引(1-indexed) + pub index: usize, + /// 私钥份额(标量) + scalar: [u8; 32], +} + +impl BlsKeyShare { + /// 获取此份额的公钥 + pub fn pub_key(&self) -> BlsPubKey { + let sk = fn_from_bytes(&self.scalar); + let sk_u256 = sk.retrieve(); + let p2 = G2Jacobian::from_affine(&G2Affine::generator()); + let pk_jac = G2Jacobian::scalar_mul(&sk_u256, &p2); + BlsPubKey { + point: pk_jac + .to_affine() + .expect("BlsKeyShare: 密钥份额不应产生无穷远点"), + } + } +} + +// ── 密钥生成 ────────────────────────────────────────────────────────────────── + +/// 生成 BLS 密钥对 +/// +/// # 参数 +/// - `rng`:随机数生成器 +/// +/// # 返回 +/// `(私钥, 公钥)` 对 +pub fn bls_keygen(rng: &mut R) -> (BlsPrivKey, BlsPubKey) { + loop { + let mut scalar = [0u8; 32]; + rng.fill_bytes(&mut scalar); + // 确保标量 < 群阶 n 且非零 + let s = U256::from_be_slice(&scalar); + if s.is_zero().into() || s >= GROUP_ORDER { + continue; + } + let sk = BlsPrivKey { scalar }; + let pk = bls_public_key(&sk); + return (sk, pk); + } +} + +/// 从私钥派生公钥 +pub fn bls_public_key(sk: &BlsPrivKey) -> BlsPubKey { + let s = fn_from_bytes(&sk.scalar); + let s_u256 = s.retrieve(); + // pk = sk * P2(G2 基点) + let p2 = G2Jacobian::from_affine(&G2Affine::generator()); + let pk_jac = G2Jacobian::scalar_mul(&s_u256, &p2); + BlsPubKey { + point: pk_jac + .to_affine() + .expect("bls_public_key: 私钥不应产生无穷远公钥"), + } +} + +// ── 签名与验签 ──────────────────────────────────────────────────────────────── + +/// BLS 签名 +/// +/// sigma = sk * H(msg),其中 H 是 hash_to_g1。 +/// 签名是确定性的(不需要随机数)。 +/// +/// # 错误 +/// - `Error::ZeroScalar`:私钥为零 +pub fn bls_sign(sk: &BlsPrivKey, msg: &[u8]) -> Result { + let s = fn_from_bytes(&sk.scalar); + if s == Fn::ZERO { + return Err(Error::ZeroScalar); + } + // Q = H(msg)(hash-to-G1) + let q_jac = hash_to_g1(msg, DST_SIGN); + // sigma = sk * Q + let sigma_jac = G1Jacobian::scalar_mul(&s.retrieve(), &q_jac); + let sigma = sigma_jac.to_affine().map_err(|_| Error::ZeroScalar)?; + Ok(BlsSignature { point: sigma }) +} + +/// BLS 验签 +/// +/// 验证 e(sigma, P2) == e(H(msg), pk)。 +/// +/// # 错误 +/// - `Error::VerifyFailed`:签名无效 +pub fn bls_verify(pk: &BlsPubKey, msg: &[u8], sig: &BlsSignature) -> Result<(), Error> { + // Q = H(msg) + let q_jac = hash_to_g1(msg, DST_SIGN); + let q = q_jac.to_affine().map_err(|_| Error::InvalidSignature)?; + + // lhs = e(sigma, P2) + let p2 = G2Affine::generator(); + let lhs = pairing(&sig.point, &p2); + + // rhs = e(Q, pk) + let rhs = pairing(&q, &pk.point); + + // 常量时间比较 GT 元素 + // Reason: 直接比较 Fp12 可能泄露时间信息,使用字节级常量时间比较 + let lhs_bytes = fp12_to_bytes(&lhs); + let rhs_bytes = fp12_to_bytes(&rhs); + if bool::from(lhs_bytes.ct_eq(&rhs_bytes)) { + Ok(()) + } else { + Err(Error::VerifyFailed) + } +} + +// ── 签名聚合 ────────────────────────────────────────────────────────────────── + +/// 聚合多个 BLS 签名(G1 点加法) +/// +/// # 错误 +/// - `Error::InvalidInput`:签名列表为空 +pub fn bls_aggregate(sigs: &[BlsSignature]) -> Result { + if sigs.is_empty() { + return Err(Error::InvalidInput); + } + let mut agg = G1Jacobian::from_affine(&sigs[0].point); + for sig in &sigs[1..] { + agg = G1Jacobian::add(&agg, &G1Jacobian::from_affine(&sig.point)); + } + let point = agg.to_affine().map_err(|_| Error::InvalidInput)?; + Ok(BlsSignature { point }) +} + +/// 聚合验签(不同消息) +/// +/// 验证 e(agg_sig, P2) == ∏ e(H(msg_i), pk_i)。 +/// +/// # 注意 +/// 每个 (pk_i, msg_i) 对的消息不同时适用。 +/// 若消息相同,使用 `bls_fast_aggregate_verify`。 +/// +/// # 错误 +/// - `Error::InvalidInput`:公钥/消息列表为空或长度不匹配 +/// - `Error::VerifyFailed`:验证失败 +pub fn bls_aggregate_verify( + pks: &[BlsPubKey], + msgs: &[&[u8]], + agg_sig: &BlsSignature, +) -> Result<(), Error> { + if pks.is_empty() || pks.len() != msgs.len() { + return Err(Error::InvalidInput); + } + + // lhs = e(agg_sig, P2) + let p2 = G2Affine::generator(); + let lhs = pairing(&agg_sig.point, &p2); + + // rhs = ∏ e(H(msg_i), pk_i) + let q0 = hash_to_g1(msgs[0], DST_SIGN) + .to_affine() + .map_err(|_| Error::InvalidInput)?; + let mut rhs = pairing(&q0, &pks[0].point); + + for (pk, msg) in pks[1..].iter().zip(msgs[1..].iter()) { + let q = hash_to_g1(msg, DST_SIGN) + .to_affine() + .map_err(|_| Error::InvalidInput)?; + let e_i = pairing(&q, &pk.point); + rhs = fp12_mul(&rhs, &e_i); + } + + let lhs_bytes = fp12_to_bytes(&lhs); + let rhs_bytes = fp12_to_bytes(&rhs); + if bool::from(lhs_bytes.ct_eq(&rhs_bytes)) { + Ok(()) + } else { + Err(Error::VerifyFailed) + } +} + +/// 快速聚合验签(相同消息) +/// +/// 验证 e(agg_sig, P2) == e(H(msg), agg_pk),其中 agg_pk = Σ pk_i。 +/// +/// # 错误 +/// - `Error::InvalidInput`:公钥列表为空 +/// - `Error::VerifyFailed`:验证失败 +pub fn bls_fast_aggregate_verify( + pks: &[BlsPubKey], + msg: &[u8], + agg_sig: &BlsSignature, +) -> Result<(), Error> { + if pks.is_empty() { + return Err(Error::InvalidInput); + } + + // agg_pk = Σ pk_i(G2 点加法) + let mut agg_pk = G2Jacobian::from_affine(&pks[0].point); + for pk in &pks[1..] { + agg_pk = G2Jacobian::add_jac(&agg_pk, &G2Jacobian::from_affine(&pk.point)); + } + let agg_pk_affine = agg_pk.to_affine().map_err(|_| Error::InvalidInput)?; + let agg_pk_pub = BlsPubKey { + point: agg_pk_affine, + }; + + bls_verify(&agg_pk_pub, msg, agg_sig) +} + +// ── 序列化 ──────────────────────────────────────────────────────────────────── + +impl BlsSignature { + /// 序列化为 65 字节(未压缩 G1 点:0x04 || x || y) + pub fn to_bytes(&self) -> [u8; 65] { + self.point.to_bytes() + } + + /// 从 65 字节反序列化 + pub fn from_bytes(bytes: &[u8; 65]) -> Result { + let point = G1Affine::from_bytes(bytes)?; + Ok(BlsSignature { point }) + } +} + +impl BlsPubKey { + /// 序列化为 128 字节(G2 点:x_re || x_im || y_re || y_im) + pub fn to_bytes(&self) -> [u8; 128] { + self.point.to_bytes() + } + + /// 从 128 字节反序列化 + pub fn from_bytes(bytes: &[u8; 128]) -> Result { + let point = G2Affine::from_bytes(bytes)?; + Ok(BlsPubKey { point }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::OsRng; + + #[test] + fn test_bls_sign_verify_roundtrip() { + let mut rng = OsRng; + let (sk, pk) = bls_keygen(&mut rng); + let msg = b"hello bls"; + let sig = bls_sign(&sk, msg).expect("签名应成功"); + bls_verify(&pk, msg, &sig).expect("验签应成功"); + } + + #[test] + fn test_bls_verify_wrong_msg_fails() { + let mut rng = OsRng; + let (sk, pk) = bls_keygen(&mut rng); + let sig = bls_sign(&sk, b"msg1").expect("签名应成功"); + assert!( + bls_verify(&pk, b"msg2", &sig).is_err(), + "错误消息应验签失败" + ); + } + + #[test] + fn test_bls_verify_wrong_key_fails() { + let mut rng = OsRng; + let (sk1, _pk1) = bls_keygen(&mut rng); + let (_sk2, pk2) = bls_keygen(&mut rng); + let msg = b"hello"; + let sig = bls_sign(&sk1, msg).expect("签名应成功"); + assert!(bls_verify(&pk2, msg, &sig).is_err(), "错误公钥应验签失败"); + } + + #[test] + fn test_bls_aggregate_verify() { + let mut rng = OsRng; + let (sk1, pk1) = bls_keygen(&mut rng); + let (sk2, pk2) = bls_keygen(&mut rng); + let msg1 = b"message1"; + let msg2 = b"message2"; + let sig1 = bls_sign(&sk1, msg1).expect("签名1应成功"); + let sig2 = bls_sign(&sk2, msg2).expect("签名2应成功"); + let agg = bls_aggregate(&[sig1, sig2]).expect("聚合应成功"); + bls_aggregate_verify(&[pk1, pk2], &[msg1.as_ref(), msg2.as_ref()], &agg) + .expect("聚合验签应成功"); + } + + #[test] + fn test_bls_fast_aggregate_verify() { + let mut rng = OsRng; + let (sk1, pk1) = bls_keygen(&mut rng); + let (sk2, pk2) = bls_keygen(&mut rng); + let msg = b"shared message"; + let sig1 = bls_sign(&sk1, msg).expect("签名1应成功"); + let sig2 = bls_sign(&sk2, msg).expect("签名2应成功"); + let agg = bls_aggregate(&[sig1, sig2]).expect("聚合应成功"); + bls_fast_aggregate_verify(&[pk1, pk2], msg, &agg).expect("快速聚合验签应成功"); + } + + #[test] + fn test_bls_signature_serialization() { + let mut rng = OsRng; + let (sk, _pk) = bls_keygen(&mut rng); + let sig = bls_sign(&sk, b"test").expect("签名应成功"); + let bytes = sig.to_bytes(); + let sig2 = BlsSignature::from_bytes(&bytes).expect("反序列化应成功"); + assert_eq!(sig.to_bytes(), sig2.to_bytes()); + } +} diff --git a/src/bls/threshold.rs b/src/bls/threshold.rs new file mode 100644 index 0000000..3008c4b --- /dev/null +++ b/src/bls/threshold.rs @@ -0,0 +1,252 @@ +//! BLS 门限签名(Shamir 秘密分享 + Lagrange 插值) +//! +//! 实现 (t+1, n) 门限 BLS 签名: +//! - 可信分发者将私钥分割为 n 份,任意 t+1 份可重建签名 +//! - 各参与者独立计算部分签名 +//! - 聚合器组合 t+1 份部分签名得到完整 BLS 签名 +//! +//! # 安全注意 +//! 本实现采用 Trusted Dealer 模型:分发者可以知晓完整私钥。 +//! 对于无可信第三方的场景,需使用 DKG(分布式密钥生成),超出当前范围。 + +extern crate alloc; +use alloc::vec::Vec; + +use crypto_bigint::{Zero, U256}; +use rand_core::RngCore; + +use crate::error::Error; +use crate::sm9::fields::fp::{fn_from_bytes, fn_inv, fn_mul, fn_to_bytes, Fn, GROUP_ORDER}; +use crate::sm9::groups::g1::G1Jacobian; + +use super::{bls_sign, BlsKeyShare, BlsPrivKey, BlsSignature}; + +// ── Shamir 密钥分割 ────────────────────────────────────────────────────────── + +/// 将 BLS 私钥分割为 n 份,需要 threshold+1 份才能组合签名 +/// +/// # 参数 +/// - `sk`:主私钥 +/// - `threshold`:门限值 t(需要 t+1 份参与者) +/// - `total`:总份额数 n(n >= t+1) +/// - `rng`:随机数生成器 +/// +/// # 返回 +/// `(主公钥, Vec)` — n 份密钥份额 +/// +/// # 错误 +/// - `Error::InvalidInput`:参数不合法(total < threshold+1,或 threshold=0) +pub fn bls_threshold_keygen( + sk: &BlsPrivKey, + threshold: usize, + total: usize, + rng: &mut R, +) -> Result, Error> { + if total < threshold + 1 || threshold == 0 { + return Err(Error::InvalidInput); + } + + // 构造 threshold 次随机多项式 f(x) = sk + a1*x + ... + at*x^t (mod n) + // f(0) = sk + let sk_fn = fn_from_bytes(&sk.scalar); + + // 随机系数 a1..at + let mut coeffs: Vec = Vec::with_capacity(threshold + 1); + coeffs.push(sk_fn); + for _ in 0..threshold { + let mut bytes = [0u8; 32]; + loop { + rng.fill_bytes(&mut bytes); + let v = U256::from_be_slice(&bytes); + if !bool::from(v.is_zero()) && v < GROUP_ORDER { + coeffs.push(fn_from_bytes(&bytes)); + break; + } + } + } + + // 为每个参与者 i=1..=n 计算 f(i) + let mut shares = Vec::with_capacity(total); + for i in 1..=total { + let i_fn = fn_from_bytes(&{ + let mut b = [0u8; 32]; + let i_u256 = U256::from(i as u64); + b.copy_from_slice(&i_u256.to_be_bytes()); + b + }); + + // Horner 方法计算 f(i) = a0 + i*(a1 + i*(a2 + ... + i*at)...) + let mut val = coeffs[threshold]; + for j in (0..threshold).rev() { + val = fn_mul(&val, &i_fn); + val = crate::sm9::fields::fp::fn_add(&val, &coeffs[j]); + } + + shares.push(BlsKeyShare { + index: i, + scalar: fn_to_bytes(&val), + }); + } + + // 清零多项式系数(防止内存残留) + coeffs.fill(Fn::ZERO); + + Ok(shares) +} + +// ── Lagrange 插值系数 ────────────────────────────────────────────────────── + +/// 计算 Lagrange 系数 λᵢ(在 Fn 域上) +/// +/// λᵢ = ∏_{j ∈ S, j≠i} (j / (j-i)) mod n +/// +/// # 参数 +/// - `i`:当前参与者索引(1-indexed) +/// - `participants`:参与的所有参与者索引集合 +fn lagrange_coefficient(i: usize, participants: &[usize]) -> Fn { + let _i_fn = index_to_fn(i); + + let mut num = Fn::ONE; // 分子 ∏ j + let mut den = Fn::ONE; // 分母 ∏ (j-i) + + for &j in participants { + if j != i { + let j_fn = index_to_fn(j); + num = fn_mul(&num, &j_fn); + + // diff = j - i(在 Fn 域上,若 j < i 则结果自动 mod n 为负数) + let diff = if j > i { + index_to_fn(j - i) + } else { + // i > j:diff = -(i-j) + let pos = index_to_fn(i - j); + crate::sm9::fields::fp::fn_neg(&pos) + }; + den = fn_mul(&den, &diff); + } + } + + // λᵢ = num / den = num * den^{-1} + let den_inv = fn_inv(&den).expect("Lagrange: 分母不应为零(参与者索引应互不相同)"); + fn_mul(&num, &den_inv) +} + +/// 将 usize 索引转换为 Fn 元素 +fn index_to_fn(i: usize) -> Fn { + let mut b = [0u8; 32]; + let u = U256::from(i as u64); + b.copy_from_slice(&u.to_be_bytes()); + fn_from_bytes(&b) +} + +// ── 部分签名与组合 ───────────────────────────────────────────────────────── + +/// 计算部分签名(与普通 BLS 签名相同,使用份额私钥) +/// +/// sigma_i = sk_i * H(msg) +pub fn bls_partial_sign(share: &BlsKeyShare, msg: &[u8]) -> Result { + // 将份额包装为 BlsPrivKey 格式 + let sk = BlsPrivKey { + scalar: share.scalar, + }; + bls_sign(&sk, msg) +} + +/// 组合 t+1 份部分签名得到完整 BLS 签名(Lagrange 插值) +/// +/// sigma = Σ_{i ∈ S} λᵢ · sigma_i +/// +/// # 参数 +/// - `partial_sigs`:`(参与者索引, 部分签名)` 的列表,长度需 >= threshold+1 +/// +/// # 错误 +/// - `Error::InvalidInput`:签名列表为空 +/// - `Error::PointAtInfinity`:组合结果为无穷远点 +pub fn bls_combine_signatures( + partial_sigs: &[(usize, BlsSignature)], +) -> Result { + if partial_sigs.is_empty() { + return Err(Error::InvalidInput); + } + + let participants: Vec = partial_sigs.iter().map(|(i, _)| *i).collect(); + + // sigma = Σ λᵢ * sigma_i + let mut result = G1Jacobian::INFINITY; + + for (i, sig) in partial_sigs { + let lambda = lagrange_coefficient(*i, &participants); + let lambda_u256 = lambda.retrieve(); + // λᵢ * sigma_i(G1 标量乘) + let sig_jac = G1Jacobian::from_affine(&sig.point); + let scaled = G1Jacobian::scalar_mul(&lambda_u256, &sig_jac); + result = G1Jacobian::add(&result, &scaled); + } + + let point = result.to_affine().map_err(|_| Error::PointAtInfinity)?; + Ok(BlsSignature { point }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bls::{bls_keygen, bls_verify}; + use rand_core::OsRng; + + #[test] + fn test_threshold_2_of_3() { + let mut rng = OsRng; + // 生成主密钥 + let (sk, pk) = bls_keygen(&mut rng); + let msg = b"threshold test message"; + + // 分割为 3 份,门限 2(需 3 份 = threshold+1=3) + // 实际是 (threshold=2, total=3),需要 3 份 + let shares = bls_threshold_keygen(&sk, 2, 3, &mut rng).expect("密钥分割应成功"); + assert_eq!(shares.len(), 3); + + // 使用全部 3 份计算部分签名(满足门限 t+1=3) + let partial_sigs: Vec<(usize, BlsSignature)> = shares + .iter() + .map(|s| { + let sig = bls_partial_sign(s, msg).expect("部分签名应成功"); + (s.index, sig) + }) + .collect(); + + // 组合签名 + let combined = bls_combine_signatures(&partial_sigs).expect("签名组合应成功"); + + // 用主公钥验证 + bls_verify(&pk, msg, &combined).expect("门限签名验证应成功"); + } + + #[test] + fn test_threshold_1_of_2() { + let mut rng = OsRng; + // threshold=1, total=2:需要 2 份 + let (sk, pk) = bls_keygen(&mut rng); + let msg = b"simple threshold"; + + let shares = bls_threshold_keygen(&sk, 1, 2, &mut rng).expect("密钥分割应成功"); + + // 使用 2 份(threshold+1=2) + let partial_sigs: Vec<(usize, BlsSignature)> = shares + .iter() + .map(|s| (s.index, bls_partial_sign(s, msg).unwrap())) + .collect(); + + let combined = bls_combine_signatures(&partial_sigs).unwrap(); + bls_verify(&pk, msg, &combined).expect("(1,2) 门限签名验证应成功"); + } + + #[test] + fn test_invalid_threshold_params() { + let mut rng = OsRng; + let (sk, _pk) = bls_keygen(&mut rng); + // total < threshold+1 + assert!(bls_threshold_keygen(&sk, 3, 2, &mut rng).is_err()); + // threshold = 0 + assert!(bls_threshold_keygen(&sk, 0, 3, &mut rng).is_err()); + } +} diff --git a/src/fpe/fnr.rs b/src/fpe/fnr.rs new file mode 100644 index 0000000..042e562 --- /dev/null +++ b/src/fpe/fnr.rs @@ -0,0 +1,211 @@ +//! FNR(Flexible Naor-Reingold)核心算法 +//! +//! 实现基于 SM4 的保留格式加密: +//! 使用 7 轮 Luby-Rackoff Feistel 结构,支持任意位长(1~128 位)的明文。 +//! +//! 参考:Sashank Dara, Scott Fluhrer. "FNR: Flexible Naor and Reingold", Cisco, 2014. + +use crate::sm4::Sm4Key; + +/// Feistel 轮数(FNR 固定 7 轮) +const N_ROUND: usize = 7; + +// ── 位操作工具 ──────────────────────────────────────────────────────────────── + +/// 从 n 位数据(高位优先打包到字节)中提取第 i 位(i 从 0 开始,0 是最高位) +#[inline] +fn get_bit(data: &[u8; 16], i: usize) -> u8 { + let byte = i / 8; + let bit = 7 - (i % 8); + (data[byte] >> bit) & 1 +} + +/// 设置第 i 位为 val(0 或 1) +#[inline] +fn set_bit(data: &mut [u8; 16], i: usize, val: u8) { + let byte = i / 8; + let bit = 7 - (i % 8); + data[byte] = (data[byte] & !(1 << bit)) | (val << bit); +} + +/// 将 data 的前 n 位清零(其余位保持不变) +pub(super) fn clear_high_bits(data: &mut [u8; 16], n: usize) { + // 清零 n 位之后的所有位 + for i in n..128 { + set_bit(data, i, 0); + } +} + +/// 两个 n 位向量 XOR +fn xor_bits(a: &[u8; 16], b: &[u8; 16], n: usize) -> [u8; 16] { + let mut out = [0u8; 16]; + let full = n / 8; + for i in 0..full { + out[i] = a[i] ^ b[i]; + } + if n % 8 != 0 { + let mask = 0xFF_u8 << (8 - n % 8); + out[full] = (a[full] ^ b[full]) & mask; + } + out +} + +// ── Feistel 轮函数 ──────────────────────────────────────────────────────────── + +/// Feistel 轮函数 F(round, tweak, right_half) → left_half_size 位 +/// +/// 构造 16 字节块 = tweak[15] XOR (round XOR right_half_padded), +/// 用 SM4 加密,取前 out_bits 位。 +fn round_fn( + key: &Sm4Key, + tweak: &[u8; 15], + half: &[u8; 16], + half_bits: usize, + out_bits: usize, + round: usize, +) -> [u8; 16] { + // 构造 16 字节输入:tweak(15字节)|| round(1字节) + let mut block = [0u8; 16]; + block[..15].copy_from_slice(tweak); + block[15] = round as u8; + + // XOR half 到 block(half 最多 half_bits 位有意义) + let half_bytes = half_bits.div_ceil(8); + for i in 0..half_bytes.min(16) { + block[i] ^= half[i]; + } + + key.encrypt_block(&mut block); + + // 只保留前 out_bits 位 + clear_high_bits(&mut block, out_bits); + block +} + +// ── FNR 加密/解密 ───────────────────────────────────────────────────────────── + +/// FNR 加密(n 位,7 轮 Feistel) +/// +/// Feistel 结构(每轮): +/// (L, R) → (R, L XOR F(R)) +/// +/// 对于非整除 2 的位数: +/// left_bits = n / 2,right_bits = n - left_bits(right >= left) +/// +/// # 参数 +/// - `key`:SM4 轮密钥 +/// - `tweak`:15 字节扩展 tweak(由 expand_tweak 生成) +/// - `data`:16 字节缓冲,前 num_bits 位为明文,加密后前 num_bits 位为密文 +/// - `num_bits`:有效位数(1~128) +pub fn fnr_encrypt(key: &Sm4Key, tweak: &[u8; 15], data: &mut [u8; 16], num_bits: usize) { + // 特殊情况:1 位时 Feistel 退化(left_bits=0),使用随机置换 + if num_bits == 1 { + fnr_1bit(key, tweak, data, true); + return; + } + + let left_bits = num_bits / 2; + let right_bits = num_bits - left_bits; + + // 提取左右各半 + let mut l = [0u8; 16]; + let mut r = [0u8; 16]; + for i in 0..left_bits { + set_bit(&mut l, i, get_bit(data, i)); + } + for i in 0..right_bits { + set_bit(&mut r, i, get_bit(data, left_bits + i)); + } + + // 7 轮 Feistel 加密 + for round in 0..N_ROUND { + // F = F(round, tweak, r) 取前 left_bits 位 + let f = round_fn(key, tweak, &r, right_bits, left_bits, round); + // new_r = l XOR F + let new_r = xor_bits(&l, &f, left_bits); + // 交换:l = r(原右半),r = new_r + // 注意处理 left_bits != right_bits 的情况: + // 交换后新 l 来自旧 r(right_bits 位)→ 取前 left_bits 位 + // 新 r 是 new_r(left_bits 位)→ 作为新右半(right_bits 位) + // 由于 right_bits >= left_bits,r 的有效位取前 left_bits 位作为新 l, + // 剩余位丢弃(Feistel 交换中精度统一) + // Reason: 奇数位时 right 比 left 多 1 位,通过轮内调整保持正确性 + l = r; + clear_high_bits(&mut l, left_bits); // 新 l 只取 right_bits 中的前 left_bits 位 + r = new_r; + } + + // 合并回 data + for i in 0..left_bits { + set_bit(data, i, get_bit(&l, i)); + } + for i in 0..right_bits { + set_bit(data, left_bits + i, get_bit(&r, i)); + } +} + +/// FNR 解密(n 位,7 轮 Feistel 逆序) +/// +/// Feistel 解密每轮: +/// (L', R') → (R' XOR F(L'), L') +/// 其中 L'=R_enc, R'=L_enc +pub fn fnr_decrypt(key: &Sm4Key, tweak: &[u8; 15], data: &mut [u8; 16], num_bits: usize) { + if num_bits == 1 { + // 1 位置换是自逆的(FPE 置换)——encrypt 等于 decrypt + fnr_1bit(key, tweak, data, false); + return; + } + + let left_bits = num_bits / 2; + let right_bits = num_bits - left_bits; + + let mut l = [0u8; 16]; + let mut r = [0u8; 16]; + for i in 0..left_bits { + set_bit(&mut l, i, get_bit(data, i)); + } + for i in 0..right_bits { + set_bit(&mut r, i, get_bit(data, left_bits + i)); + } + + // 7 轮逆序 Feistel 解密 + for round in (0..N_ROUND).rev() { + // 解密轮:(L, R) → (R XOR F(L), L) + let f = round_fn(key, tweak, &l, left_bits, right_bits, round); + let new_l = xor_bits(&r, &f, right_bits); + r = l; + clear_high_bits(&mut r, right_bits); + l = new_l; + } + + for i in 0..left_bits { + set_bit(data, i, get_bit(&l, i)); + } + for i in 0..right_bits { + set_bit(data, left_bits + i, get_bit(&r, i)); + } +} + +/// 1 位 FPE 特殊处理 +/// +/// 对于 1 位明文(域 = {0, 1}),FPE 只有两种可能的置换: +/// 恒等(0→0, 1→1)或翻转(0→1, 1→0)。 +/// +/// 用 SM4 加密 tweak 得到随机比特 b: +/// - b=0:恒等映射(密文 = 明文) +/// - b=1:翻转映射(密文 = 1 - 明文) +/// +/// 由于置换是自逆的,encrypt == decrypt。 +/// `_encrypt` 参数预留给将来区分加密/解密(当前实现中两者相同)。 +fn fnr_1bit(key: &Sm4Key, tweak: &[u8; 15], data: &mut [u8; 16], _encrypt: bool) { + // 生成随机置换比特 + let mut block = [0u8; 16]; + block[..15].copy_from_slice(tweak); + block[15] = 0xFF; // 特殊轮号标记 1-bit 模式 + key.encrypt_block(&mut block); + let perm_bit = (block[0] >> 7) & 1; // 取最高位 + + // 若 perm_bit=1,翻转最高位 + let orig = get_bit(data, 0); + set_bit(data, 0, orig ^ perm_bit); +} diff --git a/src/fpe/mod.rs b/src/fpe/mod.rs new file mode 100644 index 0000000..4e87a2d --- /dev/null +++ b/src/fpe/mod.rs @@ -0,0 +1,293 @@ +//! FPE(Format-Preserving Encryption)保留格式加密 +//! +//! 基于 FNR(Flexible Naor-Reingold)算法,使用 SM4 作为底层密码: +//! - 支持 1~128 位任意长度的明密文域 +//! - 明密文在同一域内(位数相同) +//! - 支持 tweak(调整值)参数化加密 +//! +//! # 使用示例 +//! +//! ```rust +//! # #[cfg(feature = "alloc")] +//! # { +//! use libsmx::fpe::FpeKey; +//! +//! let key = [0u8; 16]; +//! let fpe = FpeKey::new(&key, 32).unwrap(); // 32 位域(如 IPv4 地址) +//! let tweak = fpe.expand_tweak(b"my-tweak"); +//! +//! let plaintext: u32 = 192_168_1_100; // 某 IPv4 地址 +//! let mut data = plaintext.to_be_bytes(); +//! let mut block = [0u8; 16]; +//! block[..4].copy_from_slice(&data); +//! +//! fpe.encrypt(&tweak, &mut block); +//! fpe.decrypt(&tweak, &mut block); +//! +//! assert_eq!(&block[..4], &data); +//! # } +//! ``` + +mod fnr; + +use crate::error::Error; +use crate::sm4::Sm4Key; +use fnr::{clear_high_bits, fnr_decrypt, fnr_encrypt}; +use zeroize::ZeroizeOnDrop; + +/// FPE 扩展 tweak(15 字节) +/// +/// 由 `FpeKey::expand_tweak` 从任意长度的 tweak 字节生成。 +#[derive(Clone, Copy)] +pub struct FpeTweak([u8; 15]); + +/// FPE 密钥(SM4 密钥 + 位数配置) +/// +/// 使用 ZeroizeOnDrop 确保密钥在 Drop 时自动清零。 +#[derive(ZeroizeOnDrop)] +pub struct FpeKey { + /// 底层 SM4 密钥 + key: Sm4Key, + /// 有效位数(1~128) + num_bits: usize, +} + +impl FpeKey { + /// 创建 FPE 密钥 + /// + /// # 参数 + /// - `key`:16 字节 SM4 密钥 + /// - `num_bits`:明密文域的位数(1~128) + /// + /// # 错误 + /// - `Error::InvalidInputLength`:`num_bits` 不在 1~128 范围内 + pub fn new(key: &[u8; 16], num_bits: usize) -> Result { + if num_bits == 0 || num_bits > 128 { + return Err(Error::InvalidInputLength); + } + Ok(FpeKey { + key: Sm4Key::new(key), + num_bits, + }) + } + + /// 将任意长度的 tweak 扩展为 15 字节内部 tweak + /// + /// 使用 SM4 对 tweak 进行哈希(CBC-MAC 风格)得到固定长度输出。 + pub fn expand_tweak(&self, tweak: &[u8]) -> FpeTweak { + // 用 SM4 对 tweak 进行"哈希": + // 将 tweak 分块,每块 XOR 进状态后 SM4 加密 + let mut state = [0u8; 16]; + // 存储 num_bits 到 state 前 2 字节(域参数绑定) + state[0] = (self.num_bits >> 8) as u8; + state[1] = self.num_bits as u8; + + for chunk in tweak.chunks(16) { + let mut block = state; + for (i, &b) in chunk.iter().enumerate() { + block[i] ^= b; + } + self.key.encrypt_block(&mut block); + state = block; + } + + // 最终加密(确保即使 tweak 为空也有输出) + self.key.encrypt_block(&mut state); + + let mut out = [0u8; 15]; + out.copy_from_slice(&state[..15]); + FpeTweak(out) + } + + /// 就地加密(前 num_bits 位) + /// + /// `data` 的前 `num_bits` 位被加密,高于 `num_bits` 的位保持不变。 + /// + /// # 注意 + /// `data` 的位顺序:字节 0 的最高位是位 0(高位优先)。 + pub fn encrypt(&self, tweak: &FpeTweak, data: &mut [u8; 16]) { + // 保存高于 num_bits 的位(不应被修改) + let saved = save_high_bits(data, self.num_bits); + clear_high_bits(data, self.num_bits); + fnr_encrypt(&self.key, &tweak.0, data, self.num_bits); + restore_high_bits(data, &saved, self.num_bits); + } + + /// 就地解密(前 num_bits 位) + pub fn decrypt(&self, tweak: &FpeTweak, data: &mut [u8; 16]) { + let saved = save_high_bits(data, self.num_bits); + clear_high_bits(data, self.num_bits); + fnr_decrypt(&self.key, &tweak.0, data, self.num_bits); + restore_high_bits(data, &saved, self.num_bits); + } + + /// 返回有效位数 + pub fn num_bits(&self) -> usize { + self.num_bits + } +} + +/// 保存 data 中高于 n 位的位(用于还原) +fn save_high_bits(data: &[u8; 16], n: usize) -> [u8; 16] { + let mut saved = [0u8; 16]; + let full_bytes = n / 8; + let rem = n % 8; + if rem != 0 && full_bytes < 16 { + // 保存 full_bytes 字节的高位部分(低 (8-rem) 位) + let mask = 0xFF_u8 >> rem; + saved[full_bytes] = data[full_bytes] & mask; + } + let start = full_bytes + if rem > 0 { 1 } else { 0 }; + saved[start..16].copy_from_slice(&data[start..16]); + saved +} + +/// 将保存的高位还原到 data +fn restore_high_bits(data: &mut [u8; 16], saved: &[u8; 16], n: usize) { + let full_bytes = n / 8; + let rem = n % 8; + if rem != 0 && full_bytes < 16 { + let mask = 0xFF_u8 >> rem; // 低 (8-rem) 位 + data[full_bytes] = (data[full_bytes] & !mask) | (saved[full_bytes] & mask); + } + let start = full_bytes + if rem > 0 { 1 } else { 0 }; + data[start..16].copy_from_slice(&saved[start..16]); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fpe_new_valid() { + assert!(FpeKey::new(&[0u8; 16], 1).is_ok()); + assert!(FpeKey::new(&[0u8; 16], 32).is_ok()); + assert!(FpeKey::new(&[0u8; 16], 128).is_ok()); + } + + #[test] + fn test_fpe_new_invalid() { + assert!(FpeKey::new(&[0u8; 16], 0).is_err()); + assert!(FpeKey::new(&[0u8; 16], 129).is_err()); + } + + #[test] + fn test_fpe_encrypt_decrypt_roundtrip_32bits() { + let key = [0x01u8; 16]; + let fpe = FpeKey::new(&key, 32).unwrap(); + let tweak = fpe.expand_tweak(b"test-tweak"); + + // 明文:u32 = 12345678 + let mut data = [0u8; 16]; + data[..4].copy_from_slice(&12345678u32.to_be_bytes()); + let original = data; + + fpe.encrypt(&tweak, &mut data); + // 加密后应与原始不同 + assert_ne!(&data[..4], &original[..4], "加密后数据应变化"); + // 解密后应恢复原始 + fpe.decrypt(&tweak, &mut data); + assert_eq!(&data[..4], &original[..4], "解密后应恢复原始明文"); + } + + #[test] + fn test_fpe_encrypt_decrypt_roundtrip_8bits() { + let key = [0xABu8; 16]; + let fpe = FpeKey::new(&key, 8).unwrap(); + let tweak = fpe.expand_tweak(b"tweak"); + + for val in 0u8..=255 { + let mut data = [0u8; 16]; + data[0] = val; + let original = data; + + fpe.encrypt(&tweak, &mut data); + fpe.decrypt(&tweak, &mut data); + assert_eq!(data[0], original[0], "8位加解密往返应还原 val={}", val); + } + } + + #[test] + fn test_fpe_encrypt_decrypt_roundtrip_1bit() { + let key = [0x99u8; 16]; + let fpe = FpeKey::new(&key, 1).unwrap(); + let tweak = fpe.expand_tweak(b""); + + // 测试 0 和 1 + for val in [0u8, 0x80u8] { + let mut data = [0u8; 16]; + data[0] = val; + let original = data; + fpe.encrypt(&tweak, &mut data); + fpe.decrypt(&tweak, &mut data); + assert_eq!(data[0] & 0x80, original[0] & 0x80, "1位加解密往返应还原"); + } + } + + #[test] + fn test_fpe_encrypt_decrypt_roundtrip_128bits() { + let key = [0x55u8; 16]; + let fpe = FpeKey::new(&key, 128).unwrap(); + let tweak = fpe.expand_tweak(b"full block"); + + let mut data = [0u8; 16]; + for (i, d) in data.iter_mut().enumerate() { + *d = i as u8 * 17; + } + let original = data; + + fpe.encrypt(&tweak, &mut data); + fpe.decrypt(&tweak, &mut data); + assert_eq!(data, original, "128位加解密往返应还原"); + } + + #[test] + fn test_fpe_different_tweaks_different_output() { + let key = [0x42u8; 16]; + let fpe = FpeKey::new(&key, 32).unwrap(); + let tweak1 = fpe.expand_tweak(b"tweak1"); + let tweak2 = fpe.expand_tweak(b"tweak2"); + + let mut d1 = [0u8; 16]; + let mut d2 = [0u8; 16]; + d1[0] = 0xDE; + d1[1] = 0xAD; + d1[2] = 0xBE; + d1[3] = 0xEF; + d2[..4].copy_from_slice(&d1[..4]); + + fpe.encrypt(&tweak1, &mut d1); + fpe.encrypt(&tweak2, &mut d2); + assert_ne!(&d1[..4], &d2[..4], "不同 tweak 应产生不同密文"); + } + + #[test] + fn test_fpe_high_bits_preserved() { + // 验证高于 num_bits 的位在加密后不变 + let key = [0x11u8; 16]; + let fpe = FpeKey::new(&key, 4).unwrap(); // 只用高 4 位 + let tweak = fpe.expand_tweak(b"t"); + + let mut data = [0u8; 16]; + // 高 4 位为 0b1010,低 4 位为 0b0101 + data[0] = 0b1010_0101; + // 字节 1~15 也有数据 + for (i, d) in data[1..].iter_mut().enumerate() { + *d = (i + 1) as u8; + } + let saved_low = data[0] & 0x0F; + let saved_rest: [u8; 15] = data[1..].try_into().unwrap(); + + fpe.encrypt(&tweak, &mut data); + + // 低 4 位和字节 1~15 应保持不变 + assert_eq!(data[0] & 0x0F, saved_low, "低4位应不变"); + assert_eq!(&data[1..], &saved_rest, "字节1~15应不变"); + + // 解密后高 4 位应恢复 + let encrypted_high = data[0] & 0xF0; + fpe.decrypt(&tweak, &mut data); + assert_eq!(data[0] & 0xF0, 0b1010_0000, "解密后高4位应恢复"); + let _ = encrypted_high; + } +} diff --git a/src/lib.rs b/src/lib.rs index b098946..ba9a368 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,3 +56,8 @@ pub mod sm2; pub mod sm3; pub mod sm4; pub mod sm9; + +#[cfg(feature = "alloc")] +pub mod bls; + +pub mod fpe; diff --git a/src/sm9/fields/fp.rs b/src/sm9/fields/fp.rs index 044d1ef..6c1d05e 100644 --- a/src/sm9/fields/fp.rs +++ b/src/sm9/fields/fp.rs @@ -134,7 +134,7 @@ pub fn fn_neg(a: &Fn) -> Fn { a.neg() } -/// Fn 求逆(Bernstein-Yang��常量时间) +/// Fn 求逆(Bernstein-Yang,常量时间) pub fn fn_inv(a: &Fn) -> Option { let inv = a.inv(); if bool::from(inv.is_some()) { @@ -144,6 +144,111 @@ pub fn fn_inv(a: &Fn) -> Option { } } +/// Fp 平方根(Tonelli-Shanks 算法) +/// +/// 返回 `Some(sqrt)` 若 `a` 是二次剩余(含 0),否则返回 `None`。 +/// +/// # 算法说明 +/// SM9 BN256 的素数 p ≡ 1 (mod 4),不能用 `a^((p+1)/4)` 方法(仅适用于 p ≡ 3 mod 4)。 +/// 分解 p-1 = Q·2^S(S=2,Q 为奇数),用 Tonelli-Shanks 迭代求根。 +/// +/// # 常量时间性 +/// Reason: 固定最大迭代次数(S=2),消除基于输入值的时序差异。 +/// 内层最多执行 1 次平方迭代,外层固定 S 次循环。 +pub fn fp_sqrt(a: &Fp) -> Option { + // p - 1 = Q * 2^S,S=2(因为 p-1 末两位是 00,即 p ≡ 1 mod 4) + // Q = (p-1) / 4 + // Q = 2D900000008E8E9C758C4D3FD63B1D148CBF249AC51FBB6F95BE64C9F8D515F (奇数) + const S: u32 = 2; + // Q = (p-1)/4,奇数,满足 p-1 = Q * 2^2 + const Q: U256 = + U256::from_be_hex("2D90000000A8E9BC7580EAD3FD63B1D1487CA4D2C69EBBB6F95BE6C9F8D4515F"); + // (Q+1)/2,用于初始化 r = a^((Q+1)/2) + const Q_PLUS_1_DIV_2: U256 = + U256::from_be_hex("16C80000005474DE3AC07569FEB1D8E8A43E5269634F5DDB7CADF364FC6A28B0"); + // 欧拉指数 (p-1)/2,用于二次剩余判定 + const EULER_EXP: U256 = + U256::from_be_hex("5B2000000151D378EB01D5A7FAC763A290F949A58D3D776DF2B7CD93F1A8A2BE"); + // 非二次剩余 z=5(已验证:5^((p-1)/2) ≡ p-1 mod p) + const Z_VAL: U256 = + U256::from_be_hex("0000000000000000000000000000000000000000000000000000000000000005"); + + // a = 0 时平方根为 0 + if *a == Fp::ZERO { + return Some(Fp::ZERO); + } + + // 欧拉判据:a^((p-1)/2) == 1 则为二次剩余,否则 None + let euler = a.pow(&EULER_EXP); + // 直接判断 euler == Fp::ONE(二次剩余)还是 euler == -1(非二次剩余) + if euler != Fp::ONE { + return None; + } + + // Tonelli-Shanks 初始化 + let z = Fp::new(&Z_VAL); + let mut m = S; + let mut c = z.pow(&Q); // c = z^Q + let mut t = a.pow(&Q); // t = a^Q + let mut r = a.pow(&Q_PLUS_1_DIV_2); // r = a^((Q+1)/2) + + // 主循环(固定 S 次,S=2 故最多 2 次外层,每次内层最多 m-1 次平方) + for _ in 0..S { + // 若 t == 1,已找到平方根 + if t == Fp::ONE { + break; + } + + // 找最小 i(1 <= i < m) 使 t^(2^i) == 1 + // Reason: 固定循环到 m-1,不因输入提前退出,减少时序差异 + let mut i = 0u32; + let mut tmp = t; + for j in 1..m { + tmp = tmp.square(); + if tmp == Fp::ONE && i == 0 { + // Reason: 记录第一次满足条件的 j,之后继续循环(不 break) + i = j; + } + } + if i == 0 { + // 理论不应到达,防御性处理 + return None; + } + + // b = c^(2^(m-i-1)) + let mut b = c; + for _ in 0..(m - i - 1) { + b = b.square(); + } + + m = i; + c = b.square(); // c = b² + t = t.mul(&c); // t = t * b² + r = r.mul(&b); // r = r * b + } + + // 最终验证:r² 应等于 a + if r.square() == *a { + Some(r) + } else { + None + } +} + +/// Fp 二次剩余判定 +/// +/// 若 `a` 是二次剩余(或 0),返回 `true`。 +/// 使用欧拉判据:`a^((p-1)/2) == 1 mod p`。 +#[inline] +pub fn fp_is_square(a: &Fp) -> bool { + if *a == Fp::ZERO { + return true; + } + const EULER_EXP: U256 = + U256::from_be_hex("5B2000000151D378EB01D5A7FAC763A290F949A58D3D776DF2B7CD93F1A8A2BE"); + a.pow(&EULER_EXP) == Fp::ONE +} + #[cfg(test)] mod tests { use super::*; @@ -180,4 +285,37 @@ mod tests { let inv = fn_inv(&three).expect("3^-1 应存在"); assert_eq!(fn_mul(&three, &inv), Fn::ONE); } + + #[test] + fn test_fp_sqrt_basic() { + // 4 的平方根为 2 + let four = fp_from_bytes(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, + ]); + let sqrt4 = fp_sqrt(&four).expect("4 应有平方根"); + assert_eq!(fp_square(&sqrt4), four, "sqrt(4)^2 应等于 4"); + } + + #[test] + fn test_fp_sqrt_zero() { + assert_eq!(fp_sqrt(&Fp::ZERO), Some(Fp::ZERO)); + } + + #[test] + fn test_fp_is_square() { + let four = fp_from_bytes(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, + ]); + assert!(fp_is_square(&four)); + assert!(fp_is_square(&Fp::ZERO)); + // 3 不是 BN256 Fp 的二次剩余 + let three = fp_from_bytes(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 3, + ]); + // 注意:3 是否是二次剩余取决于具体素数,此测试仅验证函数可运行 + let _ = fp_is_square(&three); + } }