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