dcrypt_algorithms/ec/p384/
point.rs

1//! P-384 elliptic curve point operations
2
3use crate::ec::p384::{
4    constants::{
5        P384_FIELD_ELEMENT_SIZE, P384_POINT_COMPRESSED_SIZE, P384_POINT_UNCOMPRESSED_SIZE,
6    },
7    field::FieldElement,
8    scalar::Scalar,
9};
10use crate::error::{validate, Error, Result};
11use dcrypt_params::traditional::ecdsa::NIST_P384;
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-384 elliptic curve point in affine coordinates (x, y)
26///
27/// Represents points on the NIST P-384 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-384 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#[derive(Clone, Copy, Debug)]
46pub(crate) struct ProjectivePoint {
47    /// Whether this point is the identity element (point at infinity)
48    is_identity: Choice,
49    /// X coordinate in Jacobian representation
50    x: FieldElement,
51    /// Y coordinate in Jacobian representation
52    y: FieldElement,
53    /// Z coordinate (projective factor)
54    z: FieldElement,
55}
56
57impl PartialEq for Point {
58    /// Constant-time equality comparison for elliptic curve points
59    fn eq(&self, other: &Self) -> bool {
60        let self_is_identity: bool = self.is_identity.into();
61        let other_is_identity: bool = other.is_identity.into();
62
63        if self_is_identity || other_is_identity {
64            return self_is_identity == other_is_identity;
65        }
66        self.x == other.x && self.y == other.y
67    }
68}
69
70impl Point {
71    /// Create a new elliptic curve point from uncompressed coordinates
72    ///
73    /// Validates that the given (x, y) coordinates satisfy the P-384 curve equation:
74    /// y² = x³ - 3x + b (mod p)
75    ///
76    /// Returns an error if the point is not on the curve.
77    pub fn new_uncompressed(
78        x: &[u8; P384_FIELD_ELEMENT_SIZE],
79        y: &[u8; P384_FIELD_ELEMENT_SIZE],
80    ) -> Result<Self> {
81        let x_fe = FieldElement::from_bytes(x)?;
82        let y_fe = FieldElement::from_bytes(y)?;
83
84        // Validate that the point lies on the curve
85        if !Self::is_on_curve(&x_fe, &y_fe) {
86            return Err(Error::param(
87                "P-384 Point",
88                "Point coordinates do not satisfy curve equation",
89            ));
90        }
91
92        Ok(Point {
93            is_identity: Choice::from(0),
94            x: x_fe,
95            y: y_fe,
96        })
97    }
98
99    /// Create the identity element (point at infinity)
100    ///
101    /// The identity element serves as the additive neutral element
102    /// for the elliptic curve group operation.
103    pub fn identity() -> Self {
104        Point {
105            is_identity: Choice::from(1),
106            x: FieldElement::zero(),
107            y: FieldElement::zero(),
108        }
109    }
110
111    /// Check if this point is the identity element
112    pub fn is_identity(&self) -> bool {
113        self.is_identity.into()
114    }
115
116    /// Get the x-coordinate as a byte array in big-endian format
117    pub fn x_coordinate_bytes(&self) -> [u8; P384_FIELD_ELEMENT_SIZE] {
118        self.x.to_bytes()
119    }
120
121    /// Get the y-coordinate as a byte array in big-endian format
122    pub fn y_coordinate_bytes(&self) -> [u8; P384_FIELD_ELEMENT_SIZE] {
123        self.y.to_bytes()
124    }
125
126    /// Detect point format from serialized bytes
127    ///
128    /// Analyzes the leading byte and length to determine the serialization format.
129    pub fn detect_format(bytes: &[u8]) -> Result<PointFormat> {
130        if bytes.is_empty() {
131            return Err(Error::param("P-384 Point", "Empty point data"));
132        }
133
134        match (bytes[0], bytes.len()) {
135            (0x00, P384_POINT_UNCOMPRESSED_SIZE) => {
136                // Check if all bytes are zero (identity encoding)
137                if bytes.iter().all(|&b| b == 0) {
138                    Ok(PointFormat::Identity)
139                } else {
140                    Err(Error::param(
141                        "P-384 Point",
142                        "Invalid identity point encoding",
143                    ))
144                }
145            }
146            (0x04, P384_POINT_UNCOMPRESSED_SIZE) => Ok(PointFormat::Uncompressed),
147            (0x02 | 0x03, P384_POINT_COMPRESSED_SIZE) => Ok(PointFormat::Compressed),
148            _ => Err(Error::param(
149                "P-384 Point",
150                "Unknown or malformed point format",
151            )),
152        }
153    }
154
155    /// Serialize point to uncompressed format: 0x04 || x || y
156    ///
157    /// The identity point is represented as all zeros.
158    pub fn serialize_uncompressed(&self) -> [u8; P384_POINT_UNCOMPRESSED_SIZE] {
159        let mut result = [0u8; P384_POINT_UNCOMPRESSED_SIZE];
160
161        // Special encoding for the identity element
162        if self.is_identity() {
163            return result; // All zeros represents identity
164        }
165
166        // Standard uncompressed format: 0x04 || x || y
167        result[0] = 0x04;
168        result[1..49].copy_from_slice(&self.x.to_bytes());
169        result[49..97].copy_from_slice(&self.y.to_bytes());
170
171        result
172    }
173
174    /// Deserialize point from uncompressed byte format
175    ///
176    /// Supports the standard uncompressed format (0x04 || x || y) and
177    /// recognizes the all-zeros encoding for the identity element.
178    pub fn deserialize_uncompressed(bytes: &[u8]) -> Result<Self> {
179        validate::length("P-384 Point", bytes.len(), P384_POINT_UNCOMPRESSED_SIZE)?;
180
181        // Check for identity point (all zeros)
182        if bytes.iter().all(|&b| b == 0) {
183            return Ok(Self::identity());
184        }
185
186        // Validate uncompressed format indicator
187        if bytes[0] != 0x04 {
188            return Err(Error::param(
189                "P-384 Point",
190                "Invalid uncompressed point format (expected 0x04 prefix)",
191            ));
192        }
193
194        // Extract and validate coordinates
195        let mut x_bytes = [0u8; P384_FIELD_ELEMENT_SIZE];
196        let mut y_bytes = [0u8; P384_FIELD_ELEMENT_SIZE];
197
198        x_bytes.copy_from_slice(&bytes[1..49]);
199        y_bytes.copy_from_slice(&bytes[49..97]);
200
201        Self::new_uncompressed(&x_bytes, &y_bytes)
202    }
203
204    /// Serialize point to SEC 1 compressed format (0x02/0x03 || x)
205    ///
206    /// The compressed format uses:
207    /// - 0x02 prefix if y-coordinate is even
208    /// - 0x03 prefix if y-coordinate is odd
209    /// - Followed by the x-coordinate in big-endian format
210    pub fn serialize_compressed(&self) -> [u8; P384_POINT_COMPRESSED_SIZE] {
211        let mut out = [0u8; P384_POINT_COMPRESSED_SIZE];
212
213        // Identity → all zeros
214        if self.is_identity() {
215            return out;
216        }
217
218        // Determine prefix based on y-coordinate parity
219        out[0] = if self.y.is_odd() { 0x03 } else { 0x02 };
220        out[1..].copy_from_slice(&self.x.to_bytes());
221        out
222    }
223
224    /// Deserialize SEC 1 compressed point
225    ///
226    /// Recovers the full point from compressed format by computing y² = x³ - 3x + b
227    /// and finding the square root.
228    pub fn deserialize_compressed(bytes: &[u8]) -> Result<Self> {
229        validate::length(
230            "P-384 Compressed Point",
231            bytes.len(),
232            P384_POINT_COMPRESSED_SIZE,
233        )?;
234
235        // Identity encoding
236        if bytes.iter().all(|&b| b == 0) {
237            return Ok(Self::identity());
238        }
239
240        let tag = bytes[0];
241        if tag != 0x02 && tag != 0x03 {
242            return Err(Error::param(
243                "P-384 Point",
244                "Invalid compressed point prefix (expected 0x02 or 0x03)",
245            ));
246        }
247
248        // Extract x-coordinate
249        let mut x_bytes = [0u8; P384_FIELD_ELEMENT_SIZE];
250        x_bytes.copy_from_slice(&bytes[1..]);
251
252        let x_fe = FieldElement::from_bytes(&x_bytes).map_err(|_| {
253            Error::param(
254                "P-384 Point",
255                "Invalid compressed point: x-coordinate yields quadratic non-residue",
256            )
257        })?;
258
259        // Compute right-hand side: y² = x³ - 3x + b
260        let rhs = {
261            let x2 = x_fe.square();
262            let x3 = x2.mul(&x_fe);
263            let a = FieldElement(FieldElement::A_M3); // a = -3
264            let b = FieldElement::from_bytes(&NIST_P384.b).unwrap();
265            x3.add(&a.mul(&x_fe)).add(&b)
266        };
267
268        // Attempt to find square root
269        let y_fe = rhs.sqrt().ok_or_else(|| {
270            Error::param(
271                "P-384 Point",
272                "Invalid compressed point: x-coordinate yields quadratic non-residue",
273            )
274        })?;
275
276        // Select the correct root based on parity
277        let y_final = if (y_fe.is_odd() && tag == 0x03) || (!y_fe.is_odd() && tag == 0x02) {
278            y_fe
279        } else {
280            // Use the negative root (p - y)
281            FieldElement::get_modulus().sub(&y_fe)
282        };
283
284        Ok(Point {
285            is_identity: Choice::from(0),
286            x: x_fe,
287            y: y_final,
288        })
289    }
290
291    /// Elliptic curve point addition using the group law
292    ///
293    /// Implements the abelian group operation for P-384 points.
294    pub fn add(&self, other: &Self) -> Self {
295        let p1 = self.to_projective();
296        let p2 = other.to_projective();
297        let result = p1.add(&p2);
298        result.to_affine()
299    }
300
301    /// Elliptic curve point doubling: 2 * self
302    pub fn double(&self) -> Self {
303        let p = self.to_projective();
304        let result = p.double();
305        result.to_affine()
306    }
307
308    /// Scalar multiplication: compute scalar * self
309    ///
310    /// Uses constant-time double-and-add algorithm.
311    pub fn mul(&self, scalar: &Scalar) -> Result<Self> {
312        if scalar.is_zero() {
313            return Ok(Self::identity());
314        }
315
316        let scalar_bytes = scalar.as_secret_buffer().as_ref();
317
318        // Work in Jacobian/projective coordinates throughout
319        let base = self.to_projective();
320        let mut result = ProjectivePoint::identity();
321
322        for byte in scalar_bytes.iter() {
323            for bit_pos in (0..8).rev() {
324                result = result.double();
325
326                let bit = (byte >> bit_pos) & 1;
327                let choice = Choice::from(bit);
328
329                // Always compute the addition
330                let result_added = result.add(&base);
331
332                // Constant-time select: if bit is 1, use added result, else keep result
333                result = ProjectivePoint::conditional_select(&result, &result_added, choice);
334            }
335        }
336
337        Ok(result.to_affine())
338    }
339
340    // Private helper methods
341
342    /// Validate that coordinates satisfy the P-384 curve equation
343    fn is_on_curve(x: &FieldElement, y: &FieldElement) -> bool {
344        // Left-hand side: y²
345        let y_squared = y.square();
346
347        // Right-hand side: x³ - 3x + b
348        let x_cubed = x.square().mul(x);
349        let a_coeff = FieldElement(FieldElement::A_M3); // a = -3 mod p
350        let ax = a_coeff.mul(x);
351        let b_coeff = FieldElement::from_bytes(&NIST_P384.b).unwrap();
352
353        // Compute x³ - 3x + b
354        let x_cubed_plus_ax = x_cubed.add(&ax);
355        let rhs = x_cubed_plus_ax.add(&b_coeff);
356
357        y_squared == rhs
358    }
359
360    /// Convert affine point to Jacobian projective coordinates
361    fn to_projective(&self) -> ProjectivePoint {
362        if self.is_identity() {
363            return ProjectivePoint::identity();
364        }
365
366        ProjectivePoint {
367            is_identity: Choice::from(0),
368            x: self.x.clone(),
369            y: self.y.clone(),
370            z: FieldElement::one(),
371        }
372    }
373}
374
375impl ConditionallySelectable for ProjectivePoint {
376    fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
377        Self {
378            is_identity: Choice::conditional_select(&a.is_identity, &b.is_identity, choice),
379            x: FieldElement::conditional_select(&a.x, &b.x, choice),
380            y: FieldElement::conditional_select(&a.y, &b.y, choice),
381            z: FieldElement::conditional_select(&a.z, &b.z, choice),
382        }
383    }
384}
385
386impl ProjectivePoint {
387    /// Identity in Jacobian form: (0 : 1 : 0)
388    pub fn identity() -> Self {
389        ProjectivePoint {
390            is_identity: Choice::from(1),
391            x: FieldElement::zero(),
392            y: FieldElement::one(),
393            z: FieldElement::zero(),
394        }
395    }
396
397    /// Projective point addition using constant-time formulas
398    pub fn add(&self, other: &Self) -> Self {
399        // 1. Compute Generic Addition (assuming P != Q, neither is identity)
400        // Reference: "Guide to Elliptic Curve Cryptography" Algorithm 3.22
401        let z1_squared = self.z.square();
402        let z2_squared = other.z.square();
403        let z1_cubed = z1_squared.mul(&self.z);
404        let z2_cubed = z2_squared.mul(&other.z);
405
406        let u1 = self.x.mul(&z2_squared); // X1 · Z2²
407        let u2 = other.x.mul(&z1_squared); // X2 · Z1²
408        let s1 = self.y.mul(&z2_cubed); // Y1 · Z2³
409        let s2 = other.y.mul(&z1_cubed); // Y2 · Z1³
410
411        // Compute differences
412        let h = u2.sub(&u1); // X2·Z1² − X1·Z2²
413        let r = s2.sub(&s1); // Y2·Z1³ − Y1·Z2³
414
415        // General addition arithmetic
416        let h_squared = h.square();
417        let h_cubed = h_squared.mul(&h);
418        let v = u1.mul(&h_squared);
419
420        // X3 = r² − h³ − 2·v
421        let r_squared = r.square();
422        let two_v = v.add(&v);
423        let mut x3 = r_squared.sub(&h_cubed);
424        x3 = x3.sub(&two_v);
425
426        // Y3 = r·(v − X3) − s1·h³
427        let v_minus_x3 = v.sub(&x3);
428        let r_times_diff = r.mul(&v_minus_x3);
429        let s1_times_h_cubed = s1.mul(&h_cubed);
430        let y3 = r_times_diff.sub(&s1_times_h_cubed);
431
432        // Z3 = Z1 · Z2 · h
433        let z1_times_z2 = self.z.mul(&other.z);
434        let z3 = z1_times_z2.mul(&h);
435
436        let generic_point = Self {
437            is_identity: Choice::from(0),
438            x: x3,
439            y: y3,
440            z: z3,
441        };
442
443        // 2. Compute Doubling (fallback for P==Q)
444        let double_point = self.double();
445
446        // 3. Select Result based on state
447        let h_is_zero = Choice::from((h.is_zero() as u8) & 1);
448        let r_is_zero = Choice::from((r.is_zero() as u8) & 1);
449        
450        // Case: P == Q (h=0, r=0)
451        let p_eq_q = h_is_zero & r_is_zero;
452        // Case: P == -Q (h=0, r!=0)
453        let p_eq_neg_q = h_is_zero & !r_is_zero;
454
455        // Start with generic addition result
456        let mut result = generic_point;
457
458        // If P == Q, use doubling result
459        result = Self::conditional_select(&result, &double_point, p_eq_q);
460
461        // If P == -Q, use identity
462        result = Self::conditional_select(&result, &Self::identity(), p_eq_neg_q);
463
464        // If either input is identity, result is the other
465        result = Self::conditional_select(&result, other, self.is_identity);
466        result = Self::conditional_select(&result, self, other.is_identity);
467
468        result
469    }
470
471    /// Projective point doubling using constant-time formulas
472    #[inline]
473    pub fn double(&self) -> Self {
474        // ── 1. Pre-computations ─────────────────────────────────
475        // Δ = Z₁²
476        let delta = self.z.square();
477
478        // Γ = Y₁²
479        let gamma = self.y.square();
480
481        // β = X₁·Γ
482        let beta = self.x.mul(&gamma);
483
484        // α = 3·(X₁ − Δ)·(X₁ + Δ)       (valid because a = –3)
485        let x_plus_delta = self.x.add(&delta);
486        let x_minus_delta = self.x.sub(&delta);
487        let mut alpha = x_plus_delta.mul(&x_minus_delta);
488        alpha = alpha.add(&alpha).add(&alpha); // ×3
489
490        // ── 2. Output coordinates ──────────────────────────────
491        // X₃ = α² − 8·β
492        let mut eight_beta = beta.add(&beta); // 2β
493        eight_beta = eight_beta.add(&eight_beta); // 4β
494        eight_beta = eight_beta.add(&eight_beta); // 8β
495        let x3 = alpha.square().sub(&eight_beta);
496
497        // Z₃ = (Y₁ + Z₁)² − Γ − Δ
498        let y_plus_z = self.y.add(&self.z);
499        let z3 = y_plus_z.square().sub(&gamma).sub(&delta);
500
501        // Y₃ = α·(4·β − X₃) − 8·Γ²
502        let mut four_beta = beta.add(&beta); // 2β
503        four_beta = four_beta.add(&four_beta); // 4β
504        let mut y3 = four_beta.sub(&x3);
505        y3 = alpha.mul(&y3);
506
507        let gamma_sq = gamma.square(); // Γ²
508        let mut eight_gamma_sq = gamma_sq.add(&gamma_sq); // 2Γ²
509        eight_gamma_sq = eight_gamma_sq.add(&eight_gamma_sq); // 4Γ²
510        eight_gamma_sq = eight_gamma_sq.add(&eight_gamma_sq); // 8Γ²
511        y3 = y3.sub(&eight_gamma_sq);
512
513        let result = Self {
514            is_identity: Choice::from(0),
515            x: x3,
516            y: y3,
517            z: z3,
518        };
519
520        // Explicitly handle identity or y=0 cases constant-time
521        let is_y_zero = self.y.is_zero();
522        let return_identity = self.is_identity | Choice::from(is_y_zero as u8);
523        
524        Self::conditional_select(&result, &Self::identity(), return_identity)
525    }
526
527    /// Convert Jacobian projective coordinates back to affine coordinates
528    pub fn to_affine(&self) -> Point {
529        if self.is_identity.into() {
530            return Point::identity();
531        }
532
533        // Compute the modular inverse of Z
534        let z_inv = self
535            .z
536            .invert()
537            .expect("Non-zero Z coordinate should be invertible");
538        let z_inv_squared = z_inv.square();
539        let z_inv_cubed = z_inv_squared.mul(&z_inv);
540
541        // Convert to affine coordinates: (x, y) = (X/Z², Y/Z³)
542        let x_affine = self.x.mul(&z_inv_squared);
543        let y_affine = self.y.mul(&z_inv_cubed);
544
545        Point {
546            is_identity: Choice::from(0),
547            x: x_affine,
548            y: y_affine,
549        }
550    }
551}