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 /// Constant-time conditional swap of two projective points
434 ///
435 /// Swaps the two points if choice is 1, leaves them unchanged if choice is 0.
436 /// This operation is performed in constant time to prevent timing attacks.
437 #[inline(always)]
438 fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) {
439 FieldElement::conditional_swap(&mut a.x, &mut b.x, choice);
440 FieldElement::conditional_swap(&mut a.y, &mut b.y, choice);
441 FieldElement::conditional_swap(&mut a.z, &mut b.z, choice);
442
443 // swap the identity flag as well
444 let ai = a.is_identity;
445 let bi = b.is_identity;
446 a.is_identity = Choice::conditional_select(&ai, &bi, choice);
447 b.is_identity = Choice::conditional_select(&bi, &ai, choice);
448 }
449
450 /// Projective point addition using complete addition formulas
451 ///
452 /// Implements the addition law for Jacobian coordinates that works
453 /// for all input combinations, including point doubling and identity cases.
454 ///
455 /// Uses optimized formulas that avoid expensive field inversions
456 /// until the final conversion back to affine coordinates.
457 pub fn add(&self, other: &Self) -> Self {
458 // Handle identity element cases
459 if self.is_identity.into() {
460 return other.clone();
461 }
462 if other.is_identity.into() {
463 return self.clone();
464 }
465
466 // Compute addition using Jacobian coordinate formulas
467 // Reference: "Guide to Elliptic Curve Cryptography" Algorithm 3.22
468
469 // Pre-compute commonly used values
470 let z1_squared = self.z.square();
471 let z2_squared = other.z.square();
472 let z1_cubed = z1_squared.mul(&self.z);
473 let z2_cubed = z2_squared.mul(&other.z);
474
475 // Project coordinates to common denominator
476 let u1 = self.x.mul(&z2_squared); // X1 · Z2²
477 let u2 = other.x.mul(&z1_squared); // X2 · Z1²
478 let s1 = self.y.mul(&z2_cubed); // Y1 · Z2³
479 let s2 = other.y.mul(&z1_cubed); // Y2 · Z1³
480
481 // Compute differences
482 let h = u2.sub(&u1); // X2·Z1² − X1·Z2²
483 let r = s2.sub(&s1); // Y2·Z1³ − Y1·Z2³
484
485 // Handle special cases: point doubling or inverse points
486 if h.is_zero() {
487 if r.is_zero() {
488 // Points are equal: use doubling formula
489 return self.double();
490 } else {
491 // Points are inverses: return identity
492 return Self {
493 is_identity: Choice::from(1),
494 x: FieldElement::zero(),
495 y: FieldElement::one(), // (0 : 1 : 0)
496 z: FieldElement::zero(),
497 };
498 }
499 }
500
501 // General addition case
502 let h_squared = h.square();
503 let h_cubed = h_squared.mul(&h);
504 let v = u1.mul(&h_squared);
505
506 // X3 = r² − h³ − 2·v
507 let r_squared = r.square();
508 let two_v = v.add(&v);
509 let mut x3 = r_squared.sub(&h_cubed);
510 x3 = x3.sub(&two_v);
511
512 // Y3 = r·(v − X3) − s1·h³
513 let v_minus_x3 = v.sub(&x3);
514 let r_times_diff = r.mul(&v_minus_x3);
515 let s1_times_h_cubed = s1.mul(&h_cubed);
516 let y3 = r_times_diff.sub(&s1_times_h_cubed);
517
518 // Z3 = Z1 · Z2 · h
519 let z1_times_z2 = self.z.mul(&other.z);
520 let z3 = z1_times_z2.mul(&h);
521
522 // if Z3 == 0 we actually computed the point at infinity
523 if z3.is_zero() {
524 return Self {
525 is_identity: Choice::from(1),
526 x: FieldElement::zero(),
527 y: FieldElement::one(), // canonical projective infinity
528 z: FieldElement::zero(),
529 };
530 }
531
532 // Normal return path
533 Self {
534 is_identity: Choice::from(0),
535 x: x3,
536 y: y3,
537 z: z3,
538 }
539 }
540
541 /// Projective point doubling using efficient doubling formulas
542 ///
543 /// Implements optimized point doubling in Jacobian coordinates.
544 /// More efficient than general addition when both operands are the same.
545 /// Jacobian doubling for short-Weierstrass curves with *a = –3*
546 /// (SEC 1, Algorithm 3.2.1 — Δ / Γ / β / α form)
547 #[inline]
548 pub fn double(&self) -> Self {
549 // ── 0. Easy outs ────────────────────────────────────────
550 if self.is_identity.into() {
551 return self.clone();
552 }
553 if self.y.is_zero() {
554 // (x,0) is its own negative ⇒ 2·P = ∞
555 return Self {
556 is_identity: Choice::from(1),
557 x: FieldElement::zero(),
558 y: FieldElement::one(),
559 z: FieldElement::zero(),
560 };
561 }
562
563 // ── 1. Pre-computations ─────────────────────────────────
564 // Δ = Z₁²
565 let delta = self.z.square();
566
567 // Γ = Y₁²
568 let gamma = self.y.square();
569
570 // β = X₁·Γ
571 let beta = self.x.mul(&gamma);
572
573 // α = 3·(X₁ − Δ)·(X₁ + Δ) (valid because a = –3)
574 let x_plus_delta = self.x.add(&delta);
575 let x_minus_delta = self.x.sub(&delta);
576 let mut alpha = x_plus_delta.mul(&x_minus_delta);
577 alpha = alpha.add(&alpha).add(&alpha); // ×3
578
579 // ── 2. Output coordinates ──────────────────────────────
580 // X₃ = α² − 8·β
581 let mut eight_beta = beta.add(&beta); // 2β
582 eight_beta = eight_beta.add(&eight_beta); // 4β
583 eight_beta = eight_beta.add(&eight_beta); // 8β
584 let x3 = alpha.square().sub(&eight_beta);
585
586 // Z₃ = (Y₁ + Z₁)² − Γ − Δ
587 let y_plus_z = self.y.add(&self.z);
588 let z3 = y_plus_z.square().sub(&gamma).sub(&delta);
589
590 // Y₃ = α·(4·β − X₃) − 8·Γ²
591 let mut four_beta = beta.add(&beta); // 2β
592 four_beta = four_beta.add(&four_beta); // 4β
593 let mut y3 = four_beta.sub(&x3);
594 y3 = alpha.mul(&y3);
595
596 let gamma_sq = gamma.square(); // Γ²
597 let mut eight_gamma_sq = gamma_sq.add(&gamma_sq); // 2Γ²
598 eight_gamma_sq = eight_gamma_sq.add(&eight_gamma_sq); // 4Γ²
599 eight_gamma_sq = eight_gamma_sq.add(&eight_gamma_sq); // 8Γ²
600 y3 = y3.sub(&eight_gamma_sq);
601
602 Self {
603 is_identity: Choice::from(0),
604 x: x3,
605 y: y3,
606 z: z3,
607 }
608 }
609
610 /// Convert Jacobian projective coordinates back to affine coordinates
611 ///
612 /// Performs the conversion (X:Y:Z) → (X/Z², Y/Z³) using field inversion.
613 /// This is the most expensive operation but only needed for final results.
614 pub fn to_affine(&self) -> Point {
615 if self.is_identity.into() {
616 return Point::identity();
617 }
618
619 // Compute the modular inverse of Z
620 let z_inv = self
621 .z
622 .invert()
623 .expect("Non-zero Z coordinate should be invertible");
624 let z_inv_squared = z_inv.square();
625 let z_inv_cubed = z_inv_squared.mul(&z_inv);
626
627 // Convert to affine coordinates: (x, y) = (X/Z², Y/Z³)
628 let x_affine = self.x.mul(&z_inv_squared);
629 let y_affine = self.y.mul(&z_inv_cubed);
630
631 Point {
632 is_identity: Choice::from(0),
633 x: x_affine,
634 y: y_affine,
635 }
636 }
637}