|
| 1 | +//! Extra input validation for the secp256r1 `add` / `mul` syscalls: rejects an operation that |
| 2 | +//! involves a point with x-coordinate 0. |
| 3 | +//! |
| 4 | +//! The functions below mirror the secp256r1 EC routines in |
| 5 | +//! `starkware/cairo/common/secp256r1/ec.cairo` so they visit the same sequence of points; they only |
| 6 | +//! inspect those points and never affect the syscall's computed result. Each `fn` here mirrors the |
| 7 | +//! cairo `func` of the same name — keep them in sync. |
| 8 | +
|
| 9 | +use ark_ec::short_weierstrass::{Affine, Projective, SWCurveConfig}; |
| 10 | +use ark_ff::{PrimeField, Zero}; |
| 11 | +use num_bigint::BigUint; |
| 12 | + |
| 13 | +use crate::execution::secp::{zero_x_point_error, SecpZeroXPolicy}; |
| 14 | +use crate::execution::syscalls::vm_syscall_utils::SyscallExecutorBaseError; |
| 15 | + |
| 16 | +type ShadowResult<T> = Result<T, SyscallExecutorBaseError>; |
| 17 | + |
| 18 | +/// Whether `point` has affine x-coordinate 0 and is not the point at infinity. Tested on the |
| 19 | +/// projective `X` coordinate to avoid an inversion: the affine `x` is `X` times a nonzero factor, |
| 20 | +/// so it is zero iff `X == 0` and the point is not at infinity. |
| 21 | +fn is_zero_x_point<Curve: SWCurveConfig>(point: &Projective<Curve>) -> bool { |
| 22 | + !point.is_zero() && point.x.is_zero() |
| 23 | +} |
| 24 | + |
| 25 | +/// Rejects a non-infinity point with x-coordinate 0. |
| 26 | +fn reject_zero_x_operand<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 27 | + point: &Projective<Curve>, |
| 28 | +) -> ShadowResult<()> |
| 29 | +where |
| 30 | + Curve::BaseField: PrimeField, |
| 31 | +{ |
| 32 | + if is_zero_x_point(point) { |
| 33 | + return Err(zero_x_point_error()); |
| 34 | + } |
| 35 | + Ok(()) |
| 36 | +} |
| 37 | + |
| 38 | +/// Mirror of `ec_double`. |
| 39 | +fn ec_double<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 40 | + point: Projective<Curve>, |
| 41 | +) -> ShadowResult<Projective<Curve>> |
| 42 | +where |
| 43 | + Curve::BaseField: PrimeField, |
| 44 | +{ |
| 45 | + reject_zero_x_operand(&point)?; |
| 46 | + Ok(point + point) |
| 47 | +} |
| 48 | + |
| 49 | +/// Mirror of both `ec_add` and `fast_ec_add`, which are identical here: only the two operands are |
| 50 | +/// inspected. Every `ec_add` branch (`fast_ec_add`, `ec_double`, or the point at infinity) operates |
| 51 | +/// on `point0` / `point1`, and the point at infinity is naturally distinguished in projective form |
| 52 | +/// (`Z == 0` vs `X == 0`). |
| 53 | +fn ec_add<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 54 | + point0: Projective<Curve>, |
| 55 | + point1: Projective<Curve>, |
| 56 | +) -> ShadowResult<Projective<Curve>> |
| 57 | +where |
| 58 | + Curve::BaseField: PrimeField, |
| 59 | +{ |
| 60 | + reject_zero_x_operand(&point0)?; |
| 61 | + reject_zero_x_operand(&point1)?; |
| 62 | + Ok(point0 + point1) |
| 63 | +} |
| 64 | + |
| 65 | +/// Mirror of `build_ec_mul_table`: returns `table[i] = i * point` for `i` in `0..=15`. |
| 66 | +fn build_ec_mul_table<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 67 | + point: Projective<Curve>, |
| 68 | +) -> ShadowResult<[Projective<Curve>; 16]> |
| 69 | +where |
| 70 | + Curve::BaseField: PrimeField, |
| 71 | +{ |
| 72 | + // table[0] is the point at infinity. |
| 73 | + let mut table = [Projective::<Curve>::zero(); 16]; |
| 74 | + table[1] = point; |
| 75 | + table[2] = ec_double(table[1])?; |
| 76 | + // The cairo unrolls table[3..=15]; table[i] = fast_ec_add(table[i - 1], point). |
| 77 | + for i in 3..16 { |
| 78 | + table[i] = ec_add(table[i - 1], point)?; |
| 79 | + } |
| 80 | + Ok(table) |
| 81 | +} |
| 82 | + |
| 83 | +/// Mirror of `fast_ec_mul_inner`: for each nibble (consumed most significant first), multiplies the |
| 84 | +/// accumulator by 16 (four doublings) and adds the nibble's table entry. |
| 85 | +fn fast_ec_mul_inner<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 86 | + table: &[Projective<Curve>; 16], |
| 87 | + mut point: Projective<Curve>, |
| 88 | + nibbles: impl Iterator<Item = usize>, |
| 89 | +) -> ShadowResult<Projective<Curve>> |
| 90 | +where |
| 91 | + Curve::BaseField: PrimeField, |
| 92 | +{ |
| 93 | + for nibble in nibbles { |
| 94 | + for _ in 0..4 { |
| 95 | + point = ec_double(point)?; |
| 96 | + } |
| 97 | + point = ec_add(point, table[nibble])?; |
| 98 | + } |
| 99 | + Ok(point) |
| 100 | +} |
| 101 | + |
| 102 | +/// The 64 nibbles (4 bits each) of the 256-bit scalar, `nibbles[0]` least significant. |
| 103 | +fn scalar_nibbles(scalar: &BigUint) -> [usize; 64] { |
| 104 | + let bytes = scalar.to_bytes_le(); |
| 105 | + core::array::from_fn(|k| { |
| 106 | + let byte = bytes.get(k / 2).copied().unwrap_or(0); |
| 107 | + usize::from(if k.is_multiple_of(2) { byte & 0x0f } else { byte >> 4 }) |
| 108 | + }) |
| 109 | +} |
| 110 | + |
| 111 | +/// Mirror of `ec_mul_by_uint256`: a 16-entry precompute table plus a windowed double-and-add over |
| 112 | +/// the scalar's 64 nibbles, most significant first. |
| 113 | +fn ec_mul_by_uint256<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 114 | + point: &Affine<Curve>, |
| 115 | + scalar: &BigUint, |
| 116 | +) -> ShadowResult<Projective<Curve>> |
| 117 | +where |
| 118 | + Curve::BaseField: PrimeField, |
| 119 | +{ |
| 120 | + let table = build_ec_mul_table(Projective::from(*point))?; |
| 121 | + let nibbles = scalar_nibbles(scalar); |
| 122 | + |
| 123 | + // first_nibble = nibbles[63], last_nibble = nibbles[0]. |
| 124 | + let mut res = table[nibbles[63]]; |
| 125 | + // First inner call (cairo m = 124): nibbles 62..=32, most significant first. |
| 126 | + res = fast_ec_mul_inner(&table, res, (32..=62).rev().map(|k| nibbles[k]))?; |
| 127 | + // Second inner call (cairo m = 124): nibbles 31..=1. |
| 128 | + res = fast_ec_mul_inner(&table, res, (1..=31).rev().map(|k| nibbles[k]))?; |
| 129 | + // Final window for the least-significant nibble: 16 * res, then ec_add with its table entry. |
| 130 | + for _ in 0..4 { |
| 131 | + res = ec_double(res)?; |
| 132 | + } |
| 133 | + ec_add(res, table[nibbles[0]]) |
| 134 | +} |
| 135 | + |
| 136 | +/// Rejects a `secp256r1_add(point0, point1)` syscall that involves a point with x-coordinate 0. |
| 137 | +/// No-op for curves without [`SecpZeroXPolicy::REJECT_ZERO_X_POINT`]. |
| 138 | +pub fn reject_zero_x_in_add<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 139 | + point0: &Affine<Curve>, |
| 140 | + point1: &Affine<Curve>, |
| 141 | +) -> ShadowResult<()> |
| 142 | +where |
| 143 | + Curve::BaseField: PrimeField, |
| 144 | +{ |
| 145 | + if !Curve::REJECT_ZERO_X_POINT { |
| 146 | + return Ok(()); |
| 147 | + } |
| 148 | + // Only the rejection matters; the computed point is discarded. |
| 149 | + ec_add(Projective::from(*point0), Projective::from(*point1)).map(|_| ()) |
| 150 | +} |
| 151 | + |
| 152 | +/// Rejects a `secp256r1_mul(point, scalar)` syscall that involves a point with x-coordinate 0 at |
| 153 | +/// any point in the computation. No-op for curves without [`SecpZeroXPolicy::REJECT_ZERO_X_POINT`]. |
| 154 | +pub fn reject_zero_x_in_mul<Curve: SWCurveConfig + SecpZeroXPolicy>( |
| 155 | + point: &Affine<Curve>, |
| 156 | + scalar: &BigUint, |
| 157 | +) -> ShadowResult<()> |
| 158 | +where |
| 159 | + Curve::BaseField: PrimeField, |
| 160 | +{ |
| 161 | + if !Curve::REJECT_ZERO_X_POINT { |
| 162 | + return Ok(()); |
| 163 | + } |
| 164 | + // Only the rejection matters; the computed point is discarded. |
| 165 | + ec_mul_by_uint256(point, scalar).map(|_| ()) |
| 166 | +} |
| 167 | + |
| 168 | +#[cfg(test)] |
| 169 | +mod tests { |
| 170 | + use ark_ec::short_weierstrass::Affine; |
| 171 | + use num_bigint::BigUint; |
| 172 | + |
| 173 | + use super::{reject_zero_x_in_add, reject_zero_x_in_mul}; |
| 174 | + |
| 175 | + fn secp256r1_point(x_hex: &str, y_hex: &str) -> Affine<ark_secp256r1::Config> { |
| 176 | + let parse = |hex: &str| BigUint::parse_bytes(hex.as_bytes(), 16).unwrap(); |
| 177 | + Affine::new_unchecked(parse(x_hex).into(), parse(y_hex).into()) |
| 178 | + } |
| 179 | + |
| 180 | + fn zero_x_point() -> Affine<ark_secp256r1::Config> { |
| 181 | + secp256r1_point("0", "66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4") |
| 182 | + } |
| 183 | + |
| 184 | + fn generator() -> Affine<ark_secp256r1::Config> { |
| 185 | + secp256r1_point( |
| 186 | + "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", |
| 187 | + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", |
| 188 | + ) |
| 189 | + } |
| 190 | + |
| 191 | + /// mul rejects a point with x-coordinate 0 that is reached early in the computation. |
| 192 | + #[test] |
| 193 | + fn mul_rejects_zero_x_point() { |
| 194 | + let half = secp256r1_point( |
| 195 | + "81bfb55b010b1bdf08b8d9d8590087aa278e28febff3b05632eeff09011c5579", |
| 196 | + "8cd2f199d9815d7585073034eb76c93d50799b354b0fb1e77eb75eba8bff3d58", |
| 197 | + ); |
| 198 | + for scalar in [3_u32, 5, 7] { |
| 199 | + assert!(reject_zero_x_in_mul(&half, &BigUint::from(scalar)).is_err()); |
| 200 | + } |
| 201 | + let quarter = secp256r1_point( |
| 202 | + "02495ac9f43e45aae30d3366e351cc08828cf3e11cc3b7209fbd1730c4a14f4e", |
| 203 | + "b55567231f26b356bbac703086b614bb21448433dd75ab263264c3d12206b9ee", |
| 204 | + ); |
| 205 | + assert!(reject_zero_x_in_mul(&quarter, &BigUint::from(7_u32)).is_err()); |
| 206 | + } |
| 207 | + |
| 208 | + /// mul rejects a point with x-coordinate 0 reached only deep in the computation. |
| 209 | + #[test] |
| 210 | + fn mul_rejects_zero_x_point_large_scalar() { |
| 211 | + let sixteenth = secp256r1_point( |
| 212 | + "776aef1acb82b628e132cc29440988f0a15d4cc2b4f328aecb063c9b86e5018e", |
| 213 | + "6e44dfc60444faa9c4e36bc217451f7ac2956cb3b2e9bbd655eba297163d1f34", |
| 214 | + ); |
| 215 | + let scalar = BigUint::from(1_u8) << 252_u32; |
| 216 | + assert!(reject_zero_x_in_mul(&sixteenth, &scalar).is_err()); |
| 217 | + } |
| 218 | + |
| 219 | + /// mul accepts a multiplication that never involves a point with x-coordinate 0. |
| 220 | + #[test] |
| 221 | + fn mul_accepts_regular_point() { |
| 222 | + assert!(reject_zero_x_in_mul(&generator(), &BigUint::from(5_u32)).is_ok()); |
| 223 | + } |
| 224 | + |
| 225 | + /// add rejects an operand with x-coordinate 0 but not regular points. |
| 226 | + #[test] |
| 227 | + fn add_rejects_zero_x_operand() { |
| 228 | + assert!(reject_zero_x_in_add(&generator(), &generator()).is_ok()); |
| 229 | + assert!(reject_zero_x_in_add(&zero_x_point(), &generator()).is_err()); |
| 230 | + assert!(reject_zero_x_in_add(&generator(), &zero_x_point()).is_err()); |
| 231 | + } |
| 232 | +} |
0 commit comments