Skip to main content

dcrypt_algorithms/ec/p224/
point.rs

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