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::{b, Fp, GX_HEX, GY_HEX};
9use crypto_bigint::{Invert, 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.6`'s
161    /// `ConstMontyForm::invert` (safegcd / Bernstein-Yang), which is
162    /// **documented** as constant-time but direct measurement on the
163    /// project's dudect harness shows `|tau| ≈ 0.70` between different
164    /// inputs. Callers that pass secret-derived `Z` (notably `mul_g(k)`
165    /// inside the SM2 sign retry loop, where `k` is the secret nonce)
166    /// inherit a measurable timing side-channel until v0.2 replaces the
167    /// invert site with a Fermat-style `pow_bounded_exp`.
168    /// See `SECURITY.md` for the full posture.
169    #[must_use]
170    pub fn to_affine(&self) -> Option<(Fp, Fp)> {
171        let z_inv: subtle::CtOption<Fp> = self.z.invert();
172        let z_inv: Option<Fp> = z_inv.into();
173        let z_inv = z_inv?;
174        Some((self.x * z_inv, self.y * z_inv))
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::sm2::curve::b;
182    use subtle::ConstantTimeEq;
183
184    #[test]
185    fn doubling_equals_self_addition() {
186        let g = ProjectivePoint::generator();
187        let g2_double = g.double();
188        let g2_add = g.add(&g);
189        assert!(
190            bool::from(g2_double.ct_eq(&g2_add)),
191            "doubling and self-addition must agree"
192        );
193    }
194
195    #[test]
196    fn add_with_identity_is_identity_law() {
197        let g = ProjectivePoint::generator();
198        let id = ProjectivePoint::identity();
199        let lhs = g.add(&id);
200        assert!(bool::from(lhs.ct_eq(&g)), "G + O = G");
201    }
202
203    #[test]
204    fn add_with_negation_is_identity() {
205        let g = ProjectivePoint::generator();
206        let neg_g = g.neg();
207        let sum = g.add(&neg_g);
208        assert!(bool::from(sum.is_identity()), "G + (-G) = O");
209    }
210
211    /// 2G affine coordinates from the SM2 reference implementation,
212    /// cross-validated against an independent Python affine-arithmetic
213    /// computation.
214    #[test]
215    fn two_g_known_affine() {
216        let g2 = ProjectivePoint::generator().double();
217        let (x, y) = g2.to_affine().expect("2G is not infinity");
218        assert_eq!(
219            x.retrieve(),
220            U256::from_be_hex("56CEFD60D7C87C000D58EF57FA73BA4D9C0DFA08C08A7331495C2E1DA3F2BD52")
221        );
222        assert_eq!(
223            y.retrieve(),
224            U256::from_be_hex("31B7E7E6CC8189F668535CE0F8EAF1BD6DE84C182F6C8E716F780D3A970A23C3")
225        );
226    }
227
228    /// 3G = 2G + G. Independent KAT over `add` (the 2G KAT only exercises `double`).
229    #[test]
230    fn three_g_known_affine() {
231        let g = ProjectivePoint::generator();
232        let g3 = g.double().add(&g);
233        let (x, y) = g3.to_affine().expect("3G is not infinity");
234        assert_eq!(
235            x.retrieve(),
236            U256::from_be_hex("A97F7CD4B3C993B4BE2DAA8CDB41E24CA13F6BD945302244E26918F1D0509EBF")
237        );
238        assert_eq!(
239            y.retrieve(),
240            U256::from_be_hex("530B5DD88C688EF5CCC5CEC08A72150F7C400EE5CD045292AAACDD037458F6E6")
241        );
242    }
243
244    #[test]
245    fn to_affine_round_trip_via_double() {
246        let g = ProjectivePoint::generator();
247        let g2 = g.double();
248        let (x, y) = g2.to_affine().expect("2G is not at infinity");
249        let lhs = y * y;
250        let three = Fp::new(&U256::from_u64(3));
251        let rhs = x * x * x - three * x + b();
252        assert_eq!(
253            lhs.retrieve(),
254            rhs.retrieve(),
255            "2G affine coords must satisfy curve equation"
256        );
257    }
258}