threshold_pairing/into_fr.rs
1use super::Scalar;
2use crate::error::{Error, Result};
3use ff::Field;
4
5/// A conversion into an element of the scalar field.
6pub trait IntoScalar: Copy {
7 /// Converts `self` to a field element.
8 fn into_scalar(self) -> Scalar;
9}
10
11impl IntoScalar for Scalar {
12 fn into_scalar(self) -> Scalar {
13 self
14 }
15}
16
17impl IntoScalar for u64 {
18 fn into_scalar(self) -> Scalar {
19 Scalar::from(self)
20 }
21}
22
23impl IntoScalar for usize {
24 fn into_scalar(self) -> Scalar {
25 (self as u64).into_scalar()
26 }
27}
28
29impl IntoScalar for i32 {
30 fn into_scalar(self) -> Scalar {
31 if self >= 0 {
32 (self as u64).into_scalar()
33 } else {
34 // Use wrapping_neg to correctly handle i32::MIN
35 // i32::MIN.wrapping_neg() as u64 gives the correct absolute value
36 let abs_val = (self as i64).wrapping_neg() as u64;
37 abs_val.into_scalar().neg()
38 }
39 }
40}
41
42impl IntoScalar for i64 {
43 fn into_scalar(self) -> Scalar {
44 if self >= 0 {
45 (self as u64).into_scalar()
46 } else {
47 // Use wrapping_neg to correctly handle i64::MIN
48 // i64::MIN.wrapping_neg() as u64 gives 2^63, the correct absolute value
49 let abs_val = (self as u64).wrapping_neg();
50 abs_val.into_scalar().neg()
51 }
52 }
53}
54
55impl<T: IntoScalar> IntoScalar for &T {
56 fn into_scalar(self) -> Scalar {
57 (*self).into_scalar()
58 }
59}
60
61/// A safe conversion into a *non-zero* scalar field element, suitable for use as a share index.
62///
63/// This trait is the required bound for all share-index parameters (e.g.
64/// [`SecretKeySet::secret_key_share`](crate::SecretKeySet::secret_key_share) and
65/// [`PublicKeySet::public_key_share`](crate::PublicKeySet::public_key_share)).
66///
67/// # Security
68///
69/// Share indices are mapped to scalar field elements via `index + 1`. If the resulting
70/// scalar is zero — which happens when a signed index value such as `-1i32` is used —
71/// the evaluation point equals `poly(0)`, i.e. the **master secret key**. This trait
72/// enforces that the post-mapping scalar is non-zero, preventing that attack.
73///
74/// For all unsigned types (`u64`, `usize`, `Scalar`) the check is statically guaranteed
75/// to succeed and the blanket implementation returns `Ok` without runtime overhead.
76pub trait TryIntoScalarSafe: Copy {
77 /// Converts `self` to a scalar, returning `Err(Error::IndexMapsToZero)` if the
78 /// conversion would produce the zero scalar after the `+ 1` offset applied
79 /// by the share-index machinery.
80 ///
81 /// The check is performed *after* adding 1, so the predicate is:
82 /// `self.into_scalar() + 1 != Scalar::zero()`.
83 fn try_into_scalar_safe(self) -> Result<Scalar>;
84}
85
86/// Blanket implementation for all types that already implement [`IntoScalar`].
87///
88/// For unsigned types the `+ 1` offset can never produce zero (since the only way
89/// to get zero is `x + 1 ≡ 0 (mod p)`, i.e. `x = p - 1`, which is representable
90/// only as a `Scalar` value, not as a `u64`/`usize`). We still perform the runtime
91/// check so that callers who provide a raw `Scalar` equal to `p - 1` are caught.
92impl<T: IntoScalar> TryIntoScalarSafe for T {
93 fn try_into_scalar_safe(self) -> Result<Scalar> {
94 let s = self.into_scalar();
95 // Compute index + 1 (what the share machinery will use as the evaluation point)
96 let eval_point = s + Scalar::one();
97 if bool::from(eval_point.is_zero()) {
98 return Err(Error::IndexMapsToZero);
99 }
100 Ok(s)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use core::ops::Neg;
108
109 #[test]
110 fn test_i32_min_does_not_overflow() {
111 let scalar = i32::MIN.into_scalar();
112 // i32::MIN is -2147483648, so the scalar should be -2147483648 mod p
113 // which equals (p - 2147483648)
114 let expected = (2147483648u64).into_scalar().neg();
115 assert_eq!(scalar, expected);
116 }
117
118 #[test]
119 fn test_i64_min_does_not_overflow() {
120 let scalar = i64::MIN.into_scalar();
121 // i64::MIN is -9223372036854775808, so the scalar should be the negation
122 // of 2^63 in the field
123 let expected = (9223372036854775808u64).into_scalar().neg();
124 assert_eq!(scalar, expected);
125 }
126
127 #[test]
128 fn test_negative_values_correct() {
129 // Test that negative values are correctly converted
130 let neg_one_i32 = (-1i32).into_scalar();
131 let neg_one_i64 = (-1i64).into_scalar();
132 let one = 1u64.into_scalar();
133
134 assert_eq!(neg_one_i32, one.neg());
135 assert_eq!(neg_one_i64, one.neg());
136 }
137
138 // --- TryIntoScalarSafe tests ---
139
140 #[test]
141 fn test_safe_negative_one_i32_rejected() {
142 // -1i32 maps to p-1, and (p-1) + 1 = p = 0 in the field => must be rejected
143 assert_eq!((-1i32).try_into_scalar_safe(), Err(Error::IndexMapsToZero));
144 }
145
146 #[test]
147 fn test_safe_negative_one_i64_rejected() {
148 assert_eq!((-1i64).try_into_scalar_safe(), Err(Error::IndexMapsToZero));
149 }
150
151 #[test]
152 fn test_safe_unsigned_zero_accepted() {
153 // 0u64 + 1 = 1 != 0, must be accepted
154 assert!(0u64.try_into_scalar_safe().is_ok());
155 }
156
157 #[test]
158 fn test_safe_usize_zero_accepted() {
159 assert!(0usize.try_into_scalar_safe().is_ok());
160 }
161
162 #[test]
163 fn test_safe_positive_i32_accepted() {
164 assert!(0i32.try_into_scalar_safe().is_ok());
165 assert!(1i32.try_into_scalar_safe().is_ok());
166 assert!(42i32.try_into_scalar_safe().is_ok());
167 }
168
169 #[test]
170 fn test_safe_large_negative_i32_accepted() {
171 // -2i32 maps to p-2, and (p-2) + 1 = p-1 != 0 => accepted
172 assert!((-2i32).try_into_scalar_safe().is_ok());
173 }
174
175 #[test]
176 fn test_safe_scalar_p_minus_1_rejected() {
177 // Scalar equal to p-1 should also be rejected since (p-1)+1 = 0
178 let p_minus_1 = Scalar::zero() - Scalar::one();
179 assert_eq!(
180 p_minus_1.try_into_scalar_safe(),
181 Err(Error::IndexMapsToZero)
182 );
183 }
184}