Skip to main content

dcrypt_algorithms/ec/p521/
point.rs

1//! P-521 elliptic curve point operations
2
3use crate::ec::p521::{
4    constants::{
5        P521_FIELD_ELEMENT_SIZE, P521_POINT_COMPRESSED_SIZE, P521_POINT_UNCOMPRESSED_SIZE,
6    },
7    field::FieldElement,
8    scalar::Scalar,
9};
10use crate::error::{validate, Error, Result};
11use dcrypt_internal::constant_time::{Choice, ConditionallySelectable};
12use dcrypt_internal::zeroing::{Zeroize, ZeroizeOnDrop, Zeroizing};
13use dcrypt_params::traditional::ecdsa::NIST_P521;
14
15/// Format of a serialized elliptic curve point
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PointFormat {
18    /// SEC 1 identity point (`0x00`)
19    Identity,
20    /// Uncompressed format: 0x04 || x || y
21    Uncompressed,
22    /// Compressed format: 0x02/0x03 || x
23    Compressed,
24}
25
26/// P-521 elliptic curve point in affine coordinates (x, y)
27///
28/// Represents points on the NIST P-521 curve. The special point at infinity
29/// (identity element) is represented with is_identity = true.
30#[derive(Clone, Debug)]
31pub struct Point {
32    /// Whether this point is the identity element (point at infinity)
33    pub(crate) is_identity: Choice,
34    /// X coordinate in affine representation
35    pub(crate) x: FieldElement,
36    /// Y coordinate in affine representation  
37    pub(crate) y: FieldElement,
38}
39
40impl Zeroize for Point {
41    fn zeroize(&mut self) {
42        self.is_identity.zeroize();
43        self.x.zeroize();
44        self.y.zeroize();
45    }
46}
47
48impl Drop for Point {
49    fn drop(&mut self) {
50        self.zeroize();
51    }
52}
53
54impl ZeroizeOnDrop for Point {}
55
56/// P-521 point in Jacobian projective coordinates (X:Y:Z) for efficient arithmetic
57///
58/// Jacobian coordinates represent affine point (x,y) as (X:Y:Z) where:
59/// - x = X/Z²
60/// - y = Y/Z³  
61/// - Point at infinity has Z = 0
62///
63/// This representation allows for efficient point addition and doubling
64/// without expensive field inversions during intermediate calculations.
65#[derive(Clone, Debug)]
66pub(crate) struct ProjectivePoint {
67    /// Whether this point is the identity element (point at infinity)
68    is_identity: Choice,
69    /// X coordinate in Jacobian representation
70    x: FieldElement,
71    /// Y coordinate in Jacobian representation
72    y: FieldElement,
73    /// Z coordinate (projective factor)
74    z: FieldElement,
75}
76
77impl Default for ProjectivePoint {
78    fn default() -> Self {
79        Self::identity()
80    }
81}
82
83impl Zeroize for ProjectivePoint {
84    fn zeroize(&mut self) {
85        self.is_identity.zeroize();
86        self.x.zeroize();
87        self.y.zeroize();
88        self.z.zeroize();
89    }
90}
91
92impl PartialEq for Point {
93    /// Constant-time equality comparison for elliptic curve points
94    ///
95    /// Handles the special case where either point is the identity element.
96    /// For regular points, compares both x and y coordinates.
97    fn eq(&self, other: &Self) -> bool {
98        // If either is identity, both must be identity to be equal
99        let self_is_identity: bool = self.is_identity.into();
100        let other_is_identity: bool = other.is_identity.into();
101
102        if self_is_identity || other_is_identity {
103            return self_is_identity == other_is_identity;
104        }
105
106        // Otherwise compare coordinates
107        self.x == other.x && self.y == other.y
108    }
109}
110
111impl Point {
112    /// Create a new elliptic curve point from uncompressed coordinates
113    ///
114    /// Validates that the given (x, y) coordinates satisfy the P-521 curve equation:
115    /// y² = x³ - 3x + b (mod p)
116    ///
117    /// Returns an error if the point is not on the curve.
118    pub fn new_uncompressed(
119        x: &[u8; P521_FIELD_ELEMENT_SIZE],
120        y: &[u8; P521_FIELD_ELEMENT_SIZE],
121    ) -> Result<Self> {
122        let x_fe = Zeroizing::new(FieldElement::from_bytes(x)?);
123        let y_fe = Zeroizing::new(FieldElement::from_bytes(y)?);
124
125        // Validate that the point lies on the curve
126        if !Self::is_on_curve(&x_fe, &y_fe) {
127            return Err(Error::param(
128                "P-521 Point",
129                "Point coordinates do not satisfy curve equation",
130            ));
131        }
132
133        Ok(Point {
134            is_identity: Choice::from(0),
135            x: x_fe.into_inner(),
136            y: y_fe.into_inner(),
137        })
138    }
139
140    /// Create the identity element (point at infinity)
141    ///
142    /// The identity element serves as the additive neutral element
143    /// for the elliptic curve group operation.
144    pub fn identity() -> Self {
145        Point {
146            is_identity: Choice::from(1),
147            x: FieldElement::zero(),
148            y: FieldElement::zero(),
149        }
150    }
151
152    /// Check if this point is the identity element
153    pub fn is_identity(&self) -> bool {
154        self.is_identity.into()
155    }
156
157    /// Get the x-coordinate as a byte array in big-endian format
158    pub fn x_coordinate_bytes(&self) -> [u8; P521_FIELD_ELEMENT_SIZE] {
159        self.x.to_bytes()
160    }
161
162    /// Get the y-coordinate as a byte array in big-endian format
163    pub fn y_coordinate_bytes(&self) -> [u8; P521_FIELD_ELEMENT_SIZE] {
164        self.y.to_bytes()
165    }
166
167    /// Detect point format from serialized bytes
168    ///
169    /// Analyzes the leading byte and length to determine the serialization format.
170    /// Useful for handling points that could be in either compressed or uncompressed form.
171    ///
172    /// # Returns
173    /// - `Ok(PointFormat)` indicating the detected format
174    /// - `Err` if the format is invalid or unrecognized
175    pub fn detect_format(bytes: &[u8]) -> Result<PointFormat> {
176        if bytes.is_empty() {
177            return Err(Error::param("P-521 Point", "Empty point data"));
178        }
179
180        match (bytes[0], bytes.len()) {
181            (0x00, 1) => Ok(PointFormat::Identity),
182            (0x04, P521_POINT_UNCOMPRESSED_SIZE) => Ok(PointFormat::Uncompressed),
183            (0x02 | 0x03, P521_POINT_COMPRESSED_SIZE) => Ok(PointFormat::Compressed),
184            _ => Err(Error::param(
185                "P-521 Point",
186                "Unknown or malformed point format",
187            )),
188        }
189    }
190
191    /// Serialize point to uncompressed format: 0x04 || x || y
192    ///
193    /// The uncompressed point format is:
194    /// - 1 byte: 0x04 (uncompressed indicator)
195    /// - 66 bytes: x-coordinate (big-endian)
196    /// - 66 bytes: y-coordinate (big-endian)
197    ///
198    /// For the identity this fixed-width API returns an all-zero internal
199    /// sentinel. SEC 1 encodes identity as the single byte `0x00`, so the
200    /// sentinel is deliberately rejected by the deserializer and must not be
201    /// placed on the wire.
202    pub fn serialize_uncompressed(&self) -> [u8; P521_POINT_UNCOMPRESSED_SIZE] {
203        let mut result = [0u8; P521_POINT_UNCOMPRESSED_SIZE];
204
205        // Special encoding for the identity element
206        if self.is_identity() {
207            return result; // All zeros represents identity
208        }
209
210        // Standard uncompressed format: 0x04 || x || y
211        result[0] = 0x04;
212        self.x.write_bytes(&mut result[1..67]);
213        self.y.write_bytes(&mut result[67..133]);
214
215        result
216    }
217
218    /// Deserialize point from uncompressed byte format
219    ///
220    /// Supports the standard uncompressed format (0x04 || x || y) and
221    /// rejects non-canonical fixed-width encodings of the identity element.
222    pub fn deserialize_uncompressed(bytes: &[u8]) -> Result<Self> {
223        validate::length("P-521 Point", bytes.len(), P521_POINT_UNCOMPRESSED_SIZE)?;
224
225        // Validate uncompressed format indicator
226        if bytes[0] != 0x04 {
227            return Err(Error::param(
228                "P-521 Point",
229                "Invalid uncompressed point format (expected 0x04 prefix)",
230            ));
231        }
232
233        // Extract and validate coordinates
234        let mut x_bytes = Zeroizing::new([0u8; P521_FIELD_ELEMENT_SIZE]);
235        let mut y_bytes = Zeroizing::new([0u8; P521_FIELD_ELEMENT_SIZE]);
236
237        x_bytes.copy_from_slice(&bytes[1..67]);
238        y_bytes.copy_from_slice(&bytes[67..133]);
239
240        Self::new_uncompressed(&x_bytes, &y_bytes)
241    }
242
243    /// Serialize point to SEC 1 compressed format (0x02/0x03 || x)
244    ///
245    /// The compressed format uses:
246    /// - 0x02 prefix if y-coordinate is even
247    /// - 0x03 prefix if y-coordinate is odd
248    /// - Followed by the x-coordinate in big-endian format
249    ///
250    /// For the identity this fixed-width API returns an all-zero internal
251    /// sentinel, which is not a canonical SEC 1 encoding and is rejected by
252    /// the deserializer.
253    ///
254    /// This format reduces storage/transmission size by ~50% compared to
255    /// uncompressed points while maintaining full recoverability.
256    pub fn serialize_compressed(&self) -> [u8; P521_POINT_COMPRESSED_SIZE] {
257        let mut out = [0u8; P521_POINT_COMPRESSED_SIZE];
258
259        // Identity → all zeros
260        if self.is_identity() {
261            return out;
262        }
263
264        // Determine prefix based on y-coordinate parity
265        out[0] = if self.y.is_odd() { 0x03 } else { 0x02 };
266        self.x.write_bytes(&mut out[1..]);
267        out
268    }
269
270    /// Deserialize SEC 1 compressed point
271    ///
272    /// Recovers the full point from compressed format by:
273    /// 1. Extracting the x-coordinate
274    /// 2. Computing y² = x³ - 3x + b
275    /// 3. Finding the square root of y²
276    /// 4. Selecting the root with correct parity based on the prefix
277    ///
278    /// # Errors
279    /// Returns an error if:
280    /// - The prefix is not 0x02 or 0x03
281    /// - The x-coordinate is not in the valid field range
282    /// - The x-coordinate corresponds to a non-residue (not on curve)
283    pub fn deserialize_compressed(bytes: &[u8]) -> Result<Self> {
284        validate::length(
285            "P-521 Compressed Point",
286            bytes.len(),
287            P521_POINT_COMPRESSED_SIZE,
288        )?;
289
290        let tag = bytes[0];
291        if tag != 0x02 && tag != 0x03 {
292            return Err(Error::param(
293                "P-521 Point",
294                "Invalid compressed point prefix (expected 0x02 or 0x03)",
295            ));
296        }
297
298        // Extract x-coordinate
299        let mut x_bytes = Zeroizing::new([0u8; P521_FIELD_ELEMENT_SIZE]);
300        x_bytes.copy_from_slice(&bytes[1..]);
301
302        let x_fe = Zeroizing::new(FieldElement::from_bytes(&x_bytes).map_err(|_| {
303            Error::param(
304                "P-521 Point",
305                "Invalid compressed point: x-coordinate yields quadratic non-residue",
306            )
307        })?);
308
309        // Compute right-hand side: y² = x³ - 3x + b
310        let x2 = Zeroizing::new(x_fe.square());
311        let x3 = Zeroizing::new(x2.mul(&x_fe));
312        let a = Zeroizing::new(FieldElement(FieldElement::A_M3)); // a = -3
313        let b = Zeroizing::new(FieldElement::from_bytes(&NIST_P521.b).unwrap());
314        let ax = Zeroizing::new(a.mul(&x_fe));
315        let x3_plus_ax = Zeroizing::new(x3.add(&ax));
316        let rhs = Zeroizing::new(x3_plus_ax.add(&b));
317
318        // Attempt to find square root
319        let y_fe = Zeroizing::new(rhs.sqrt().ok_or_else(|| {
320            Error::param(
321                "P-521 Point",
322                "Invalid compressed point: x-coordinate yields quadratic non-residue",
323            )
324        })?);
325
326        // Select the correct root based on parity
327        let y_matches = (y_fe.is_odd() && tag == 0x03) || (!y_fe.is_odd() && tag == 0x02);
328        let y_final = if y_matches {
329            Zeroizing::new(y_fe.into_inner())
330        } else {
331            // Use the negative root (p - y)
332            let modulus = Zeroizing::new(FieldElement::get_modulus());
333            Zeroizing::new(modulus.sub(&y_fe))
334        };
335
336        Ok(Point {
337            is_identity: Choice::from(0),
338            x: x_fe.into_inner(),
339            y: y_final.into_inner(),
340        })
341    }
342
343    /// Elliptic curve point addition using the group law
344    ///
345    /// Implements the abelian group operation for P-521 points.
346    /// Converts to projective coordinates for efficient computation,
347    /// then converts back to affine form.
348    pub fn add(&self, other: &Self) -> Self {
349        let p1 = Zeroizing::new(self.to_projective());
350        let p2 = Zeroizing::new(other.to_projective());
351        let result = Zeroizing::new(p1.add(&p2));
352        result.to_affine()
353    }
354
355    /// Elliptic curve point doubling: 2 * self
356    ///
357    /// Computes the sum of a point with itself, which has a more
358    /// efficient formula than general point addition.
359    pub fn double(&self) -> Self {
360        let p = Zeroizing::new(self.to_projective());
361        let result = Zeroizing::new(p.double());
362        result.to_affine()
363    }
364
365    /// Scalar multiplication: compute scalar * self
366    ///
367    /// Uses constant-time Montgomery ladder to prevent timing attacks.
368    /// Execution time is independent of the scalar's bit pattern.
369    ///
370    /// Returns the identity element if scalar is zero.
371    pub fn mul(&self, scalar: &Scalar) -> Result<Self> {
372        let mut r0 = Zeroizing::new(ProjectivePoint::identity());
373        let mut r1 = Zeroizing::new(self.to_projective()); // P
374
375        // Montgomery ladder — MSB→LSB
376        for byte in scalar.as_secret_buffer().as_ref() {
377            for bit in (0..8).rev() {
378                let mut choice = Choice::from((byte >> bit) & 1);
379
380                ProjectivePoint::conditional_swap(&mut r0, &mut r1, choice);
381                // "ladder step"
382                let t0 = Zeroizing::new(r0.add(&r1)); // R0 + R1
383                let t1 = Zeroizing::new(r0.double()); // 2·R0
384                r1.zeroize();
385                *r1 = t0.into_inner();
386                r0.zeroize();
387                *r0 = t1.into_inner();
388                ProjectivePoint::conditional_swap(&mut r0, &mut r1, choice);
389                choice.zeroize();
390            }
391        }
392
393        Ok(r0.to_affine())
394    }
395
396    // Private helper methods
397
398    /// Validate that coordinates satisfy the P-521 curve equation
399    ///
400    /// Verifies: y² = x³ - 3x + b (mod p)
401    /// where b is the curve parameter from NIST P-521 specification.
402    ///
403    /// This is a critical security check to prevent invalid curve attacks.
404    fn is_on_curve(x: &FieldElement, y: &FieldElement) -> bool {
405        // Left-hand side: y²
406        let y_squared = Zeroizing::new(y.square());
407
408        // Right-hand side: x³ - 3x + b
409        let x_squared = Zeroizing::new(x.square());
410        let x_cubed = Zeroizing::new(x_squared.mul(x));
411        let a_coeff = Zeroizing::new(FieldElement(FieldElement::A_M3)); // a = -3 mod p
412        let ax = Zeroizing::new(a_coeff.mul(x));
413        let b_coeff = Zeroizing::new(FieldElement::from_bytes(&NIST_P521.b).unwrap());
414
415        // Compute x³ - 3x + b
416        let x_cubed_plus_ax = Zeroizing::new(x_cubed.add(&ax));
417        let rhs = Zeroizing::new(x_cubed_plus_ax.add(&b_coeff));
418
419        *y_squared == *rhs
420    }
421
422    /// Convert affine point to Jacobian projective coordinates
423    ///
424    /// Affine (x, y) → Jacobian (X:Y:Z) where X=x, Y=y, Z=1
425    /// Identity point maps to (0:1:0) following standard conventions.
426    fn to_projective(&self) -> ProjectivePoint {
427        let regular = Zeroizing::new(ProjectivePoint {
428            is_identity: Choice::from(0),
429            x: self.x.clone(),
430            y: self.y.clone(),
431            z: FieldElement::one(),
432        });
433        let identity = Zeroizing::new(ProjectivePoint::identity());
434        let selected = Zeroizing::new(ProjectivePoint::conditional_select(
435            &regular,
436            &identity,
437            self.is_identity,
438        ));
439        selected.into_inner()
440    }
441}
442
443impl ProjectivePoint {
444    fn identity() -> Self {
445        Self {
446            is_identity: Choice::from(1),
447            x: FieldElement::zero(),
448            y: FieldElement::one(),
449            z: FieldElement::zero(),
450        }
451    }
452
453    /// Constant-time conditional swap of two projective points
454    ///
455    /// Swaps the two points if choice is 1, leaves them unchanged if choice is 0.
456    /// This operation is performed in constant time to prevent timing attacks.
457    #[inline(always)]
458    fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) {
459        FieldElement::conditional_swap(&mut a.x, &mut b.x, choice);
460        FieldElement::conditional_swap(&mut a.y, &mut b.y, choice);
461        FieldElement::conditional_swap(&mut a.z, &mut b.z, choice);
462
463        // swap the identity flag as well
464        let mut ai = a.is_identity;
465        let mut bi = b.is_identity;
466        a.is_identity = Choice::conditional_select(&ai, &bi, choice);
467        b.is_identity = Choice::conditional_select(&bi, &ai, choice);
468        ai.zeroize();
469        bi.zeroize();
470    }
471
472    /// Projective point addition using complete addition formulas
473    ///
474    /// Implements the addition law for Jacobian coordinates that works
475    /// for all input combinations, including point doubling and identity cases.
476    ///
477    /// Uses optimized formulas that avoid expensive field inversions
478    /// until the final conversion back to affine coordinates.
479    pub fn add(&self, other: &Self) -> Self {
480        // Compute addition using Jacobian coordinate formulas
481        // Reference: "Guide to Elliptic Curve Cryptography" Algorithm 3.22
482
483        // Pre-compute commonly used values
484        let z1_squared = Zeroizing::new(self.z.square());
485        let z2_squared = Zeroizing::new(other.z.square());
486        let z1_cubed = Zeroizing::new(z1_squared.mul(&self.z));
487        let z2_cubed = Zeroizing::new(z2_squared.mul(&other.z));
488
489        // Project coordinates to common denominator
490        let u1 = Zeroizing::new(self.x.mul(&z2_squared)); // X1 · Z2²
491        let u2 = Zeroizing::new(other.x.mul(&z1_squared)); // X2 · Z1²
492        let s1 = Zeroizing::new(self.y.mul(&z2_cubed)); // Y1 · Z2³
493        let s2 = Zeroizing::new(other.y.mul(&z1_cubed)); // Y2 · Z1³
494
495        // Compute differences
496        let h = Zeroizing::new(u2.sub(&u1)); // X2·Z1² − X1·Z2²
497        let r = Zeroizing::new(s2.sub(&s1)); // Y2·Z1³ − Y1·Z2³
498
499        // General addition case
500        let h_squared = Zeroizing::new(h.square());
501        let h_cubed = Zeroizing::new(h_squared.mul(&h));
502        let v = Zeroizing::new(u1.mul(&h_squared));
503
504        // X3 = r² − h³ − 2·v
505        let r_squared = Zeroizing::new(r.square());
506        let two_v = Zeroizing::new(v.add(&v));
507        let x3_minus_h_cubed = Zeroizing::new(r_squared.sub(&h_cubed));
508        let x3 = Zeroizing::new(x3_minus_h_cubed.sub(&two_v));
509
510        // Y3 = r·(v − X3) − s1·h³
511        let v_minus_x3 = Zeroizing::new(v.sub(&x3));
512        let r_times_diff = Zeroizing::new(r.mul(&v_minus_x3));
513        let s1_times_h_cubed = Zeroizing::new(s1.mul(&h_cubed));
514        let y3 = Zeroizing::new(r_times_diff.sub(&s1_times_h_cubed));
515
516        // Z3 = Z1 · Z2 · h
517        let z1_times_z2 = Zeroizing::new(self.z.mul(&other.z));
518        let z3 = Zeroizing::new(z1_times_z2.mul(&h));
519
520        let generic = Zeroizing::new(Self {
521            is_identity: Choice::from(0),
522            x: x3.into_inner(),
523            y: y3.into_inner(),
524            z: z3.into_inner(),
525        });
526
527        let double_point = Zeroizing::new(self.double());
528        let mut h_is_zero = Choice::from(h.is_zero() as u8);
529        let mut r_is_zero = Choice::from(r.is_zero() as u8);
530        let mut p_eq_q = h_is_zero & r_is_zero;
531        let mut p_eq_neg_q = h_is_zero & !r_is_zero;
532
533        let mut result = Zeroizing::new(Self::conditional_select(&generic, &double_point, p_eq_q));
534        let identity = Zeroizing::new(Self::identity());
535        let selected = Zeroizing::new(Self::conditional_select(&result, &identity, p_eq_neg_q));
536        result.zeroize();
537        *result = selected.into_inner();
538
539        let selected = Zeroizing::new(Self::conditional_select(&result, other, self.is_identity));
540        result.zeroize();
541        *result = selected.into_inner();
542        let selected = Zeroizing::new(Self::conditional_select(&result, self, other.is_identity));
543        result.zeroize();
544        *result = selected.into_inner();
545
546        h_is_zero.zeroize();
547        r_is_zero.zeroize();
548        p_eq_q.zeroize();
549        p_eq_neg_q.zeroize();
550        result.into_inner()
551    }
552
553    /// Projective point doubling using efficient doubling formulas
554    ///
555    /// Implements optimized point doubling in Jacobian coordinates.  
556    /// More efficient than general addition when both operands are the same.
557    /// Jacobian doubling for short-Weierstrass curves with *a = –3*
558    /// (SEC 1, Algorithm 3.2.1  —  Δ / Γ / β / α form)
559    #[inline]
560    pub fn double(&self) -> Self {
561        // ── 1. Pre-computations ─────────────────────────────────
562        // Δ = Z₁²
563        let delta = Zeroizing::new(self.z.square());
564
565        // Γ = Y₁²
566        let gamma = Zeroizing::new(self.y.square());
567
568        // β = X₁·Γ
569        let beta = Zeroizing::new(self.x.mul(&gamma));
570
571        // α = 3·(X₁ − Δ)·(X₁ + Δ)       (valid because a = –3)
572        let x_plus_delta = Zeroizing::new(self.x.add(&delta));
573        let x_minus_delta = Zeroizing::new(self.x.sub(&delta));
574        let alpha_base = Zeroizing::new(x_plus_delta.mul(&x_minus_delta));
575        let two_alpha = Zeroizing::new(alpha_base.add(&alpha_base));
576        let alpha = Zeroizing::new(two_alpha.add(&alpha_base)); // ×3
577
578        // ── 2. Output coordinates ──────────────────────────────
579        // X₃ = α² − 8·β
580        let two_beta = Zeroizing::new(beta.add(&beta));
581        let four_beta = Zeroizing::new(two_beta.add(&two_beta));
582        let eight_beta = Zeroizing::new(four_beta.add(&four_beta));
583        let alpha_squared = Zeroizing::new(alpha.square());
584        let x3 = Zeroizing::new(alpha_squared.sub(&eight_beta));
585
586        // Z₃ = (Y₁ + Z₁)² − Γ − Δ
587        let y_plus_z = Zeroizing::new(self.y.add(&self.z));
588        let y_plus_z_squared = Zeroizing::new(y_plus_z.square());
589        let z3_minus_gamma = Zeroizing::new(y_plus_z_squared.sub(&gamma));
590        let z3 = Zeroizing::new(z3_minus_gamma.sub(&delta));
591
592        // Y₃ = α·(4·β − X₃) − 8·Γ²
593        let four_beta_minus_x3 = Zeroizing::new(four_beta.sub(&x3));
594        let alpha_times_difference = Zeroizing::new(alpha.mul(&four_beta_minus_x3));
595
596        let gamma_sq = Zeroizing::new(gamma.square());
597        let two_gamma_sq = Zeroizing::new(gamma_sq.add(&gamma_sq));
598        let four_gamma_sq = Zeroizing::new(two_gamma_sq.add(&two_gamma_sq));
599        let eight_gamma_sq = Zeroizing::new(four_gamma_sq.add(&four_gamma_sq));
600        let y3 = Zeroizing::new(alpha_times_difference.sub(&eight_gamma_sq));
601
602        let result = Zeroizing::new(Self {
603            is_identity: Choice::from(0),
604            x: x3.into_inner(),
605            y: y3.into_inner(),
606            z: z3.into_inner(),
607        });
608
609        let mut return_identity = self.is_identity | Choice::from(self.y.is_zero() as u8);
610        let identity = Zeroizing::new(Self::identity());
611        let selected = Zeroizing::new(Self::conditional_select(
612            &result,
613            &identity,
614            return_identity,
615        ));
616        return_identity.zeroize();
617        selected.into_inner()
618    }
619
620    /// Convert Jacobian projective coordinates back to affine coordinates
621    ///
622    /// Performs the conversion (X:Y:Z) → (X/Z², Y/Z³) using field inversion.
623    /// This is the most expensive operation but only needed for final results.
624    pub fn to_affine(&self) -> Point {
625        // Select a non-zero denominator for the identity so conversion does
626        // not branch on a scalar-derived result.
627        let one = Zeroizing::new(FieldElement::one());
628        let safe_z = Zeroizing::new(FieldElement::conditional_select(
629            &self.z,
630            &one,
631            self.is_identity,
632        ));
633        let z_inv = Zeroizing::new(
634            safe_z
635                .invert()
636                .expect("Non-zero Z coordinate should be invertible"),
637        );
638        let z_inv_squared = Zeroizing::new(z_inv.square());
639        let z_inv_cubed = Zeroizing::new(z_inv_squared.mul(&z_inv));
640
641        // Convert to affine coordinates: (x, y) = (X/Z², Y/Z³)
642        let x_affine = Zeroizing::new(self.x.mul(&z_inv_squared));
643        let y_affine = Zeroizing::new(self.y.mul(&z_inv_cubed));
644        let zero = Zeroizing::new(FieldElement::zero());
645        let x = Zeroizing::new(FieldElement::conditional_select(
646            &x_affine,
647            &zero,
648            self.is_identity,
649        ));
650        let y = Zeroizing::new(FieldElement::conditional_select(
651            &y_affine,
652            &zero,
653            self.is_identity,
654        ));
655
656        Point {
657            is_identity: self.is_identity,
658            x: x.into_inner(),
659            y: y.into_inner(),
660        }
661    }
662
663    #[inline(never)]
664    fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
665        Self {
666            is_identity: Choice::conditional_select(&a.is_identity, &b.is_identity, choice),
667            x: FieldElement::conditional_select(&a.x, &b.x, choice),
668            y: FieldElement::conditional_select(&a.y, &b.y, choice),
669            z: FieldElement::conditional_select(&a.z, &b.z, choice),
670        }
671    }
672}