dcrypt_algorithms/ec/p256/field.rs
1//! P-256 field arithmetic implementation
2
3use crate::ec::p256::constants::P256_FIELD_ELEMENT_SIZE;
4use crate::error::{Error, Result};
5use dcrypt_internal::{
6 constant_time::{Choice, ConditionallySelectable},
7 Zeroize, Zeroizing,
8};
9
10/// P-256 field element representing values in F_p
11///
12/// Internally stored as 8 little-endian 32-bit limbs for efficient arithmetic.
13/// All operations maintain the invariant that values are reduced modulo p.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct FieldElement(pub(crate) [u32; 8]);
16
17// `ConditionallySelectable` currently requires `Copy`. Safe Rust therefore
18// cannot promise erasure of compiler- or register-created copies. Every
19// explicit source-owned arithmetic buffer below is nevertheless held by a
20// `Zeroizing` owner and cleared on all normal and error paths.
21impl Default for FieldElement {
22 fn default() -> Self {
23 Self::zero()
24 }
25}
26
27impl Zeroize for FieldElement {
28 fn zeroize(&mut self) {
29 self.0.zeroize();
30 }
31}
32
33impl ConditionallySelectable for FieldElement {
34 fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
35 let mut out = Zeroizing::new([0u32; 8]);
36 for i in 0..8 {
37 out[i] = u32::conditional_select(&a.0[i], &b.0[i], choice);
38 }
39 FieldElement(out.into_inner())
40 }
41}
42
43impl FieldElement {
44 /* -------------------------------------------------------------------- */
45 /* NIST P-256 Field Constants (stored as little-endian 32-bit limbs) */
46 /* -------------------------------------------------------------------- */
47
48 /// The NIST P-256 prime modulus: p = 2^256 - 2^224 + 2^192 + 2^96 - 1
49 /// Stored as 8 little-endian 32-bit limbs where limbs[0] is least significant
50 pub(crate) const MOD_LIMBS: [u32; 8] = [
51 0xFFFF_FFFF, // 2⁰ … 2³¹
52 0xFFFF_FFFF, // 2³² … 2⁶³
53 0xFFFF_FFFF, // 2⁶⁴ … 2⁹⁵
54 0x0000_0000, // 2⁹⁶ … 2¹²⁷
55 0x0000_0000, // 2¹²⁸ … 2¹⁵⁹
56 0x0000_0000, // 2¹⁶⁰ … 2¹⁹¹
57 0x0000_0001, // 2¹⁹² … 2²²³
58 0xFFFF_FFFF, // 2²²⁴ … 2²⁵⁵
59 ];
60
61 /// The curve parameter a = -3 mod p, used in the curve equation y² = x³ + ax + b
62 /// For P-256: a = p - 3
63 pub(crate) const A_M3: [u32; 8] = [
64 0xFFFF_FFFC, // (2³² - 1) - 3 = 2³² - 4
65 0xFFFF_FFFF,
66 0xFFFF_FFFF,
67 0x0000_0000,
68 0x0000_0000,
69 0x0000_0000,
70 0x0000_0001,
71 0xFFFF_FFFF, // Most significant limb
72 ];
73
74 /// The additive identity element: 0
75 pub fn zero() -> Self {
76 FieldElement([0, 0, 0, 0, 0, 0, 0, 0])
77 }
78
79 /// The multiplicative identity element: 1
80 pub fn one() -> Self {
81 FieldElement([1, 0, 0, 0, 0, 0, 0, 0])
82 }
83
84 /// Create a field element from big-endian byte representation
85 ///
86 /// Validates that the input represents a value less than the field modulus p.
87 /// Returns an error if the value is >= p.
88 pub fn from_bytes(bytes: &[u8; P256_FIELD_ELEMENT_SIZE]) -> Result<Self> {
89 let mut limbs = Zeroizing::new([0u32; 8]);
90
91 // Convert from big-endian bytes to little-endian limbs
92 // limbs[0] = least-significant 4 bytes (bytes[28..32])
93 // limbs[7] = most-significant 4 bytes (bytes[0..4])
94 #[allow(clippy::needless_range_loop)] // Index used for offset calculation
95 for i in 0..8 {
96 let offset = (7 - i) * 4; // Byte offset: 28, 24, 20, ..., 0
97 limbs[i] = u32::from_be_bytes([
98 bytes[offset],
99 bytes[offset + 1],
100 bytes[offset + 2],
101 bytes[offset + 3],
102 ]);
103 }
104
105 // Validate that the value is in the field (< p)
106 let fe = Zeroizing::new(FieldElement(limbs.into_inner()));
107 if !fe.is_valid() {
108 return Err(Error::param(
109 "FieldElement",
110 "Value must be less than the field modulus",
111 ));
112 }
113
114 Ok(fe.into_inner())
115 }
116
117 /// Convert field element to big-endian byte representation
118 pub fn to_bytes(&self) -> [u8; P256_FIELD_ELEMENT_SIZE] {
119 let mut bytes = Zeroizing::new([0u8; P256_FIELD_ELEMENT_SIZE]);
120
121 // Convert from little-endian limbs to big-endian bytes
122 for i in 0..8 {
123 let limb_bytes = Zeroizing::new(self.0[i].to_be_bytes());
124 let offset = (7 - i) * 4; // Byte offset: 28, 24, 20, ..., 0
125 bytes[offset..offset + 4].copy_from_slice(&limb_bytes[..]);
126 }
127 bytes.into_inner()
128 }
129
130 /// Constant-time validation that the field element is in canonical form (< p)
131 ///
132 /// Uses constant-time subtraction to check if self < p without branching.
133 /// Returns true if the element is valid (< p), false otherwise.
134 #[inline(always)]
135 pub fn is_valid(&self) -> bool {
136 // Attempt to subtract p from self
137 // If subtraction requires a borrow, then self < p (valid)
138 let (_difference, borrow) = Self::sbb8(&self.0, &Self::MOD_LIMBS);
139 borrow == 1
140 }
141
142 /// Constant-time field addition: (self + other) mod p
143 ///
144 /// Algorithm:
145 /// 1. Perform full 256-bit addition with carry detection
146 /// 2. Conditionally subtract p if result >= p
147 /// 3. Ensure result is in canonical form
148 #[inline(always)]
149 pub fn add(&self, other: &Self) -> Self {
150 // Step 1: Full 256-bit addition
151 let (sum, carry) = Self::adc8(&self.0, &other.0);
152
153 // Step 2: Attempt conditional reduction by subtracting p
154 let (sum_minus_p, borrow) = Self::sbb8(&sum, &Self::MOD_LIMBS);
155
156 // Step 3: Choose reduced value if:
157 // - Addition overflowed (carry == 1), OR
158 // - Subtraction didn't borrow (borrow == 0), meaning sum >= p
159 let need_reduce = (carry | (borrow ^ 1)) & 1;
160 let reduced = Zeroizing::new(Self::conditional_select(
161 &sum,
162 &sum_minus_p,
163 Choice::from(need_reduce as u8),
164 ));
165
166 // Step 4: Final canonical reduction
167 let result = Zeroizing::new(reduced.conditional_sub_p());
168 result.into_inner()
169 }
170
171 /// Constant-time field subtraction: (self - other) mod p
172 ///
173 /// Algorithm:
174 /// 1. Perform limb-wise subtraction
175 /// 2. If subtraction borrows, add p to get the correct positive result
176 pub fn sub(&self, other: &Self) -> Self {
177 // Step 1: Raw subtraction
178 let (diff, borrow) = Self::sbb8(&self.0, &other.0);
179
180 // Step 2: If we borrowed, add p to get the correct positive result
181 let (candidate, _carry) = Self::adc8(&diff, &Self::MOD_LIMBS);
182
183 // Step 3: Constant-time select based on borrow flag
184 let result = Zeroizing::new(Self::conditional_select(
185 &diff,
186 &candidate,
187 Choice::from(borrow as u8),
188 ));
189 result.into_inner()
190 }
191
192 /// Field multiplication: (self * other) mod p
193 ///
194 /// Algorithm:
195 /// 1. Compute the full 512-bit product using schoolbook multiplication
196 /// 2. Perform carry propagation to get proper limb representation
197 /// 3. Apply NIST P-256 specific fast reduction (Solinas method)
198 ///
199 /// The multiplication is performed in three phases to maintain clarity
200 /// and correctness while achieving good performance.
201 pub fn mul(&self, other: &Self) -> Self {
202 // Phase 1: Accumulate partial products in 128-bit temporaries
203 // This prevents overflow during the schoolbook multiplication
204 let mut t = Zeroizing::new([0u128; 16]);
205 for i in 0..8 {
206 for j in 0..8 {
207 t[i + j] += (self.0[i] as u128) * (other.0[j] as u128);
208 }
209 }
210
211 // Phase 2: Carry propagation to convert to 32-bit limb representation
212 let mut prod = Zeroizing::new([0u32; 16]);
213 let mut carry: u128 = 0;
214 for i in 0..16 {
215 let v = t[i] + carry;
216 prod[i] = (v & 0xffff_ffff) as u32;
217 carry = v >> 32;
218 }
219
220 // Phase 3: Apply NIST P-256 fast reduction
221 Self::reduce_wide(prod)
222 }
223
224 /// Field squaring: self² mod p
225 ///
226 /// Optimized version of multiplication for the case where both operands
227 /// are the same. Currently implemented as self.mul(self) but could be
228 /// optimized further with dedicated squaring algorithms.
229 #[inline(always)]
230 pub fn square(&self) -> Self {
231 self.mul(self)
232 }
233
234 /// Compute the modular multiplicative inverse using Fermat's Little Theorem
235 ///
236 /// For prime fields, a^(p-1) ≡ 1 (mod p), so a^(p-2) ≡ a^(-1) (mod p).
237 /// Uses binary exponentiation (square-and-multiply) for efficiency.
238 ///
239 /// Returns an error if attempting to invert zero (which has no inverse).
240 pub fn invert(&self) -> Result<Self> {
241 if self.is_zero() {
242 return Err(Error::param(
243 "FieldElement",
244 "Inversion of zero is undefined",
245 ));
246 }
247
248 // The exponent p-2 for NIST P-256 in big-endian byte format
249 const P_MINUS_2: [u8; 32] = [
250 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
251 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
252 0xFF, 0xFF, 0xFF, 0xFD,
253 ];
254
255 // Binary exponentiation: compute self^(p-2) mod p
256 let mut result = Zeroizing::new(FieldElement::one());
257 let mut base = Zeroizing::new(*self);
258
259 // Process each bit of the exponent from least to most significant
260 for &byte in P_MINUS_2.iter().rev() {
261 for bit in 0..8 {
262 if (byte >> bit) & 1 == 1 {
263 let product = Zeroizing::new(result.mul(&base));
264 result.zeroize();
265 *result = product.into_inner();
266 }
267 let squared = Zeroizing::new(base.square());
268 base.zeroize();
269 *base = squared.into_inner();
270 }
271 }
272
273 Ok(result.into_inner())
274 }
275
276 /// Check if the field element represents zero
277 ///
278 /// Constant-time check across all limbs to determine if the
279 /// field element is the additive identity.
280 pub fn is_zero(&self) -> bool {
281 let mut any = Zeroizing::new(0u32);
282 for &limb in &self.0 {
283 *any |= limb;
284 }
285 *any == 0
286 }
287
288 /// Return `true` if the field element is odd (least-significant bit set)
289 ///
290 /// Used for point compression to determine the sign of the y-coordinate.
291 /// The parity is determined by the least significant bit of the canonical
292 /// representation.
293 pub fn is_odd(&self) -> bool {
294 (self.0[0] & 1) == 1
295 }
296
297 /// Compute modular square root using exponentiation.
298 ///
299 /// Because the P-256 prime satisfies p ≡ 3 (mod 4), we can compute
300 /// sqrt(a) = a^((p+1)/4) mod p. This is more efficient than the
301 /// general Tonelli-Shanks algorithm.
302 ///
303 /// Returns `None` when the input is a quadratic non-residue (i.e.,
304 /// when no square root exists in the field).
305 ///
306 /// # Algorithm
307 /// For p ≡ 3 (mod 4), if a has a square root, then:
308 /// - sqrt(a) = ±a^((p+1)/4) mod p
309 /// - We return the principal square root (the smaller of the two)
310 pub fn sqrt(&self) -> Option<Self> {
311 if self.is_zero() {
312 return Some(Self::zero());
313 }
314
315 // (p + 1) / 4 for P-256 as big-endian bytes
316 // p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
317 // (p + 1) / 4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000
318 const EXP: [u8; 32] = [
319 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
320 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
321 0x00, 0x00, 0x00, 0x00,
322 ];
323
324 let mut result = Zeroizing::new(FieldElement::one());
325 let mut base = Zeroizing::new(*self);
326
327 // Binary exponentiation from LSB to MSB
328 for &byte in EXP.iter().rev() {
329 for bit in 0..8 {
330 if ((byte >> bit) & 1) == 1 {
331 let product = Zeroizing::new(result.mul(&base));
332 result.zeroize();
333 *result = product.into_inner();
334 }
335 let squared = Zeroizing::new(base.square());
336 base.zeroize();
337 *base = squared.into_inner();
338 }
339 }
340
341 // Verify that result^2 = self (constant-time check)
342 let squared = Zeroizing::new(result.square());
343 if *squared == *self {
344 Some(result.into_inner())
345 } else {
346 None
347 }
348 }
349
350 // Private helper methods
351
352 /// Constant-time conditional selection between two limb arrays
353 ///
354 /// Returns a if flag == 0, returns b if flag == 1
355 /// Used to avoid source-level branches in reviewed conditional selections.
356 fn conditional_select(a: &[u32; 8], b: &[u32; 8], flag: Choice) -> Self {
357 let mut out = Zeroizing::new([0u32; 8]);
358 for i in 0..8 {
359 out[i] = u32::conditional_select(&a[i], &b[i], flag);
360 }
361 FieldElement(out.into_inner())
362 }
363
364 /// 8-limb addition with carry propagation
365 ///
366 /// Performs full-width addition across all limbs, returning both
367 /// the sum and the final carry bit for overflow detection.
368 #[inline(always)]
369 fn adc8(a: &[u32; 8], b: &[u32; 8]) -> (Zeroizing<[u32; 8]>, u32) {
370 let mut r = Zeroizing::new([0u32; 8]);
371 let mut carry = 0;
372
373 #[allow(clippy::needless_range_loop)] // Index used for multiple arrays
374 for i in 0..8 {
375 // Add corresponding limbs plus carry from previous iteration
376 let (sum1, carry1) = a[i].overflowing_add(b[i]);
377 let (sum2, carry2) = sum1.overflowing_add(carry);
378
379 r[i] = sum2;
380 carry = (carry1 as u32) | (carry2 as u32);
381 }
382
383 (r, carry)
384 }
385
386 /// 8-limb subtraction with borrow propagation
387 ///
388 /// Performs full-width subtraction across all limbs, returning both
389 /// the difference and the final borrow bit for underflow detection.
390 #[inline(always)]
391 fn sbb8(a: &[u32; 8], b: &[u32; 8]) -> (Zeroizing<[u32; 8]>, u32) {
392 let mut r = Zeroizing::new([0u32; 8]);
393 let mut borrow = 0;
394
395 #[allow(clippy::needless_range_loop)] // Index used for multiple arrays
396 for i in 0..8 {
397 // Subtract corresponding limbs minus borrow from previous iteration
398 let (diff1, borrow1) = a[i].overflowing_sub(b[i]);
399 let (diff2, borrow2) = diff1.overflowing_sub(borrow);
400
401 r[i] = diff2;
402 borrow = (borrow1 as u32) | (borrow2 as u32);
403 }
404 (r, borrow)
405 }
406
407 /// Conditionally subtract p if the current value is >= p
408 ///
409 /// Ensures the field element is in canonical reduced form.
410 /// Used as a final step in arithmetic operations.
411 fn conditional_sub_p(&self) -> Self {
412 let needs_sub = Choice::from((!self.is_valid() as u8) & 1);
413 Self::conditional_sub(&self.0, needs_sub)
414 }
415
416 /// Conditionally subtract the field modulus p based on a boolean condition
417 ///
418 /// Uses constant-time selection to avoid branching while maintaining
419 /// the option to perform the subtraction.
420 fn conditional_sub(limbs: &[u32; 8], condition: Choice) -> Self {
421 let mut result = Zeroizing::new([0u32; 8]);
422 let (diff, _borrow) = Self::sbb8(limbs, &Self::MOD_LIMBS);
423
424 // Constant-time select between original limbs and difference
425 for i in 0..8 {
426 result[i] = u32::conditional_select(&limbs[i], &diff[i], condition);
427 }
428
429 Self(result.into_inner())
430 }
431
432 /// NIST P-256 specific reduction for 512-bit values using Solinas method
433 /// Fully constant-time Solinas reduction with two carry-folds.
434 pub(crate) fn reduce_wide(t: Zeroizing<[u32; 16]>) -> FieldElement {
435 // 1) load into signed 128-bit
436 let mut s = Zeroizing::new([0i128; 16]);
437 for (i, &val) in t.iter().enumerate() {
438 s[i] = val as i128;
439 }
440
441 // 2) fold high limbs 8..15 into 0..7 via
442 // 2^256 ≡ 2^224 − 2^192 − 2^96 + 1
443 for i in (8..16).rev() {
444 let v = s[i];
445 s[i] = 0;
446 s[i - 8] = s[i - 8].wrapping_add(v); // +2^0
447 s[i - 5] = s[i - 5].wrapping_sub(v); // -2^96
448 s[i - 2] = s[i - 2].wrapping_sub(v); // -2^192
449 s[i - 1] = s[i - 1].wrapping_add(v); // +2^224
450 }
451
452 // 3) first signed carry-propagate
453 let mut carry1: i128 = 0;
454 for val in s.iter_mut().take(8) {
455 let tmp = *val + carry1;
456 *val = tmp & 0xffff_ffff;
457 carry1 = tmp >> 32; // arithmetic shift
458 }
459
460 // 4) fold carry1 back down (correct indices: 3 & 6)
461 let c1 = carry1;
462 s[0] = s[0].wrapping_add(c1); // +2^0
463 s[3] = s[3].wrapping_sub(c1); // -2^96
464 s[6] = s[6].wrapping_sub(c1); // -2^192
465 s[7] = s[7].wrapping_add(c1); // +2^224
466
467 // 5) second signed carry-propagate
468 let mut carry2: i128 = 0;
469 for val in s.iter_mut().take(8) {
470 let tmp = *val + carry2;
471 *val = tmp & 0xffff_ffff;
472 carry2 = tmp >> 32;
473 }
474
475 // 6) fold carry2 back down (correct indices: 3 & 6)
476 let c2 = carry2;
477 s[0] = s[0].wrapping_add(c2);
478 s[3] = s[3].wrapping_sub(c2);
479 s[6] = s[6].wrapping_sub(c2);
480 s[7] = s[7].wrapping_add(c2);
481
482 // 7) final signed carry-propagate into 32-bit limbs
483 let mut out = Zeroizing::new([0u32; 8]);
484 let mut carry3: i128 = 0;
485 for (i, val) in s.iter().take(8).enumerate() {
486 let tmp = *val + carry3;
487 out[i] = (tmp & 0xffff_ffff) as u32;
488 carry3 = tmp >> 32;
489 }
490
491 // 8) one last constant-time subtract if ≥ p
492 let (subbed, borrow) = Self::sbb8(&out, &Self::MOD_LIMBS);
493 let need_sub = Choice::from((borrow ^ 1) as u8); // borrow==0 ⇒ out>=p
494 let result = Zeroizing::new(Self::conditional_select(&out, &subbed, need_sub));
495 result.into_inner()
496 }
497
498 /// Get the field modulus p as a FieldElement
499 ///
500 /// Returns the NIST P-256 prime modulus for use in reduction operations.
501 pub(crate) fn get_modulus() -> Self {
502 FieldElement(Self::MOD_LIMBS)
503 }
504}
505
506#[cfg(test)]
507mod field_constants_tests {
508 use super::*;
509
510 #[test]
511 fn test_modulus_is_correct() {
512 // The correct secp256k1 prime in hex:
513 // p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
514
515 // Convert MOD_LIMBS to bytes for comparison
516 let mut mod_bytes = [0u8; 32];
517 for (i, &limb) in FieldElement::MOD_LIMBS.iter().enumerate() {
518 let limb_bytes = limb.to_be_bytes();
519 let offset = (7 - i) * 4;
520 mod_bytes[offset..offset + 4].copy_from_slice(&limb_bytes);
521 }
522
523 // Expected prime as bytes
524 let expected_bytes: [u8; 32] = [
525 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
526 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
527 0xFF, 0xFF, 0xFF, 0xFF,
528 ];
529
530 assert_eq!(
531 mod_bytes, expected_bytes,
532 "MOD_LIMBS does not encode the correct NIST P-256 prime"
533 );
534 }
535
536 #[test]
537 fn zeroize_is_owner_local_under_required_copy_semantics() {
538 // `ConditionallySelectable` requires `Copy`: this tests the precise
539 // owner-local guarantee and documents that an earlier copy remains.
540 let original = FieldElement::one();
541 let mut owned_copy = original;
542
543 owned_copy.zeroize();
544
545 assert!(owned_copy.is_zero());
546 assert!(!original.is_zero());
547 }
548}