Skip to main content

dcrypt_algorithms/ec/p256/
point.rs

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