Skip to main content

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}