Skip to main content

gmcrypto_core/sm2/
point.rs

1//! SM2 curve points in projective (X:Y:Z) coordinates with
2//! Renes-Costello-Batina complete addition formulas (eprint 2015/1060).
3//!
4//! See `add` and `double` for the algorithms transcribed from the paper.
5//! No early-out branches; the point at infinity is represented as `Z = 0`
6//! and is folded into the formulas via projective representation.
7
8use crate::sm2::curve::{Fp, GX_HEX, GY_HEX, b};
9use crypto_bigint::U256;
10use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
11
12/// A point on the SM2 curve in projective coordinates (X:Y:Z).
13///
14/// The point at infinity is represented as (0:1:0).
15#[derive(Clone, Copy, Debug)]
16pub struct ProjectivePoint {
17    pub(crate) x: Fp,
18    pub(crate) y: Fp,
19    pub(crate) z: Fp,
20}
21
22impl ConstantTimeEq for ProjectivePoint {
23    fn ct_eq(&self, other: &Self) -> Choice {
24        let lhs_x = self.x * other.z;
25        let rhs_x = other.x * self.z;
26        let lhs_y = self.y * other.z;
27        let rhs_y = other.y * self.z;
28        lhs_x.retrieve().ct_eq(&rhs_x.retrieve()) & lhs_y.retrieve().ct_eq(&rhs_y.retrieve())
29    }
30}
31
32impl ConditionallySelectable for ProjectivePoint {
33    fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
34        Self {
35            x: Fp::conditional_select(&a.x, &b.x, choice),
36            y: Fp::conditional_select(&a.y, &b.y, choice),
37            z: Fp::conditional_select(&a.z, &b.z, choice),
38        }
39    }
40}
41
42impl ProjectivePoint {
43    /// The point at infinity (0 : 1 : 0).
44    #[must_use]
45    pub const fn identity() -> Self {
46        Self {
47            x: Fp::new(&U256::ZERO),
48            y: Fp::new(&U256::ONE),
49            z: Fp::new(&U256::ZERO),
50        }
51    }
52
53    /// The curve generator G.
54    #[must_use]
55    pub const fn generator() -> Self {
56        Self {
57            x: Fp::new(&U256::from_be_hex(GX_HEX)),
58            y: Fp::new(&U256::from_be_hex(GY_HEX)),
59            z: Fp::new(&U256::ONE),
60        }
61    }
62
63    /// Whether this is the point at infinity (Z = 0). Constant-time.
64    #[must_use]
65    pub fn is_identity(&self) -> Choice {
66        self.z.retrieve().ct_eq(&U256::ZERO)
67    }
68
69    /// Add two points using RCB Algorithm 4 (a=-3 specialized, complete).
70    ///
71    /// Transcribed from eprint 2015/1060 Algorithm 4.
72    #[must_use]
73    #[allow(clippy::similar_names)]
74    pub fn add(&self, other: &Self) -> Self {
75        let b = b();
76        let (x1, y1, z1) = (self.x, self.y, self.z);
77        let (x2, y2, z2) = (other.x, other.y, other.z);
78
79        let xx = x1 * x2; // 1
80        let yy = y1 * y2; // 2
81        let zz = z1 * z2; // 3
82        let xy_pairs = ((x1 + y1) * (x2 + y2)) - (xx + yy); // 4,5,6,7,8
83        let yz_pairs = ((y1 + z1) * (y2 + z2)) - (yy + zz); // 9,10,11,12,13
84        let xz_pairs = ((x1 + z1) * (x2 + z2)) - (xx + zz); // 14,15,16,17,18
85
86        let bzz_part = xz_pairs - b * zz; // 19,20
87        let bzz3_part = bzz_part + bzz_part + bzz_part; // 21,22
88        let yy_m_bzz3 = yy - bzz3_part; // 23
89        let yy_p_bzz3 = yy + bzz3_part; // 24
90
91        let zz3 = zz + zz + zz; // 26,27
92        let bxz_part = b * xz_pairs - (zz3 + xx); // 25,28,29
93        let bxz3_part = bxz_part + bxz_part + bxz_part; // 30,31
94        let xx3_m_zz3 = xx + xx + xx - zz3; // 32,33,34
95
96        Self {
97            x: (yy_p_bzz3 * xy_pairs) - (yz_pairs * bxz3_part), // 35,39,40
98            y: (yy_p_bzz3 * yy_m_bzz3) + (xx3_m_zz3 * bxz3_part), // 36,37,38
99            z: (yy_m_bzz3 * yz_pairs) + (xy_pairs * xx3_m_zz3), // 41,42,43
100        }
101    }
102
103    /// Double a point using RCB Algorithm 6 (a=-3 specialized).
104    /// Cost: 3S + 5M + a few additions.
105    ///
106    /// Transcribed from eprint 2015/1060 Algorithm 6.
107    #[must_use]
108    #[allow(clippy::similar_names)]
109    pub fn double(&self) -> Self {
110        let b = b();
111        let (x, y, z) = (self.x, self.y, self.z);
112
113        let xx = x * x; // 1
114        let yy = y * y; // 2
115        let zz = z * z; // 3
116        let xy2 = (x * y) + (x * y); // 4, 5
117        let xz2 = (x * z) + (x * z); // 6, 7
118
119        let bzz_part = b * zz - xz2; // 8, 9
120        let bzz3_part = bzz_part + bzz_part + bzz_part; // 10, 11
121        let yy_m_bzz3 = yy - bzz3_part; // 12
122        let yy_p_bzz3 = yy + bzz3_part; // 13
123        let y_frag = yy_p_bzz3 * yy_m_bzz3; // 14
124        let x_frag = yy_m_bzz3 * xy2; // 15
125
126        let zz3 = zz + zz + zz; // 16, 17
127        let bxz2_part = b * xz2 - (zz3 + xx); // 18, 19, 20
128        let bxz6_part = bxz2_part + bxz2_part + bxz2_part; // 21, 22
129        let xx3_m_zz3 = xx + xx + xx - zz3; // 23, 24, 25
130
131        let y3 = y_frag + xx3_m_zz3 * bxz6_part; // 26, 27
132        let yz2 = (y * z) + (y * z); // 28, 29
133        let x3 = x_frag - bxz6_part * yz2; // 30, 31
134        let z3_tmp = yz2 * yy; // 32
135        let z3_tmp2 = z3_tmp + z3_tmp; // 33
136        let z3 = z3_tmp2 + z3_tmp2; // 34
137
138        Self {
139            x: x3,
140            y: y3,
141            z: z3,
142        }
143    }
144
145    /// Negate a point: (X:Y:Z) -> (X:-Y:Z).
146    #[must_use]
147    pub fn neg(&self) -> Self {
148        Self {
149            x: self.x,
150            y: -self.y,
151            z: self.z,
152        }
153    }
154
155    /// Convert to affine (x, y) coordinates. Returns `None` for the identity
156    /// point (where Z = 0).
157    ///
158    /// # Constant-time caveat
159    ///
160    /// The Z-inverse goes through `crypto-bigint = 0.7.3`'s
161    /// `ConstMontyForm::invert` (safegcd / Bernstein-Yang). v0.1.0 shipped
162    /// on `crypto-bigint = 0.6` where direct measurement on the dudect
163    /// harness showed `|tau| ≈ 0.70` between different inputs — a
164    /// nonce-dependent timing side-channel for callers passing
165    /// secret-derived `Z`. Main (post-publish, on 0.7.3) measures
166    /// `|tau| ≈ 0.006` directly via the W0 `ct_fp_invert` target at 100K
167    /// samples — two orders of magnitude under the 0.20 gate. The v0.2
168    /// Fermat-invert workstream is dropped; `pow_bounded_exp` remains a
169    /// fallback if a future `crypto-bigint` release regresses.
170    /// See `SECURITY.md` for the full posture.
171    #[must_use]
172    pub fn to_affine(&self) -> Option<(Fp, Fp)> {
173        let z_inv: subtle::CtOption<Fp> = self.z.invert().into();
174        let z_inv: Option<Fp> = z_inv.into();
175        let z_inv = z_inv?;
176        Some((self.x * z_inv, self.y * z_inv))
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::sm2::curve::b;
184    use subtle::ConstantTimeEq;
185
186    #[test]
187    fn doubling_equals_self_addition() {
188        let g = ProjectivePoint::generator();
189        let g2_double = g.double();
190        let g2_add = g.add(&g);
191        assert!(
192            bool::from(g2_double.ct_eq(&g2_add)),
193            "doubling and self-addition must agree"
194        );
195    }
196
197    #[test]
198    fn add_with_identity_is_identity_law() {
199        let g = ProjectivePoint::generator();
200        let id = ProjectivePoint::identity();
201        let lhs = g.add(&id);
202        assert!(bool::from(lhs.ct_eq(&g)), "G + O = G");
203    }
204
205    #[test]
206    fn add_with_negation_is_identity() {
207        let g = ProjectivePoint::generator();
208        let neg_g = g.neg();
209        let sum = g.add(&neg_g);
210        assert!(bool::from(sum.is_identity()), "G + (-G) = O");
211    }
212
213    /// 2G affine coordinates from the SM2 reference implementation,
214    /// cross-validated against an independent Python affine-arithmetic
215    /// computation.
216    #[test]
217    fn two_g_known_affine() {
218        let g2 = ProjectivePoint::generator().double();
219        let (x, y) = g2.to_affine().expect("2G is not infinity");
220        assert_eq!(
221            x.retrieve(),
222            U256::from_be_hex("56CEFD60D7C87C000D58EF57FA73BA4D9C0DFA08C08A7331495C2E1DA3F2BD52")
223        );
224        assert_eq!(
225            y.retrieve(),
226            U256::from_be_hex("31B7E7E6CC8189F668535CE0F8EAF1BD6DE84C182F6C8E716F780D3A970A23C3")
227        );
228    }
229
230    /// 3G = 2G + G. Independent KAT over `add` (the 2G KAT only exercises `double`).
231    #[test]
232    fn three_g_known_affine() {
233        let g = ProjectivePoint::generator();
234        let g3 = g.double().add(&g);
235        let (x, y) = g3.to_affine().expect("3G is not infinity");
236        assert_eq!(
237            x.retrieve(),
238            U256::from_be_hex("A97F7CD4B3C993B4BE2DAA8CDB41E24CA13F6BD945302244E26918F1D0509EBF")
239        );
240        assert_eq!(
241            y.retrieve(),
242            U256::from_be_hex("530B5DD88C688EF5CCC5CEC08A72150F7C400EE5CD045292AAACDD037458F6E6")
243        );
244    }
245
246    #[test]
247    fn to_affine_round_trip_via_double() {
248        let g = ProjectivePoint::generator();
249        let g2 = g.double();
250        let (x, y) = g2.to_affine().expect("2G is not at infinity");
251        let lhs = y * y;
252        let three = Fp::new(&U256::from_u64(3));
253        let rhs = x * x * x - three * x + b();
254        assert_eq!(
255            lhs.retrieve(),
256            rhs.retrieve(),
257            "2G affine coords must satisfy curve equation"
258        );
259    }
260}