From f95de3a01166c354296d4e44c540f2b7d08303c8 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 12 Dec 2022 14:54:29 +1300 Subject: [PATCH] Implement `ff` traits for `Scalar` These are placed behind the new `ff` feature flag. --- .github/workflows/rust.yml | 1 + CHANGELOG.md | 1 + Cargo.toml | 2 + src/scalar.rs | 176 +++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 48896192a..2eb912d50 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,6 +29,7 @@ jobs: - run: ${{ matrix.deps }} - run: cargo test --target ${{ matrix.target }} --no-default-features - run: cargo test --target ${{ matrix.target }} + - run: cargo test --target ${{ matrix.target }} --features ff - run: cargo test --target ${{ matrix.target }} --features serde - env: RUSTFLAGS: '--cfg curve25519_dalek_backend="fiat"' diff --git a/CHANGELOG.md b/CHANGELOG.md index a05e4f99c..d17769303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ major series. version. * Relax the `zeroize` dependency to `^1` * Update the edition from 2015 to 2021 +* Add implementations of the `ff` traits, behind the `ff` feature flag. ## 3.x series diff --git a/Cargo.toml b/Cargo.toml index b1b49ff04..4527868b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ required-features = ["rand_core"] [dependencies] cfg-if = "1" +ff_crate = { version = "0.13", default-features = false, optional = true, package = "ff" } rand_core = { version = "0.6.4", default-features = false, optional = true } digest = { version = "0.10", default-features = false, optional = true } subtle = { version = "2.3.0", default-features = false } @@ -65,6 +66,7 @@ packed_simd = { version = "0.3.4", package = "packed_simd_2", features = ["into_ [features] default = ["alloc"] alloc = ["zeroize/alloc"] +ff = ["ff_crate", "rand_core"] [profile.dev] opt-level = 2 diff --git a/src/scalar.rs b/src/scalar.rs index d14a79627..1167eb9ea 100644 --- a/src/scalar.rs +++ b/src/scalar.rs @@ -152,6 +152,12 @@ use core::ops::{Sub, SubAssign}; use cfg_if::cfg_if; +#[cfg(feature = "ff")] +use { + ff_crate::{Field, FromUniformBytes, PrimeField}, + rand_core::RngCore, +}; + #[cfg(any(test, feature = "rand_core"))] use rand_core::CryptoRngCore; @@ -1214,6 +1220,126 @@ impl UnpackedScalar { } } +#[cfg(feature = "ff")] +impl Field for Scalar { + const ZERO: Self = Self::ZERO; + const ONE: Self = Self::ONE; + + fn random(mut rng: impl RngCore) -> Self { + // NOTE: this is duplicated due to different `rng` bounds + let mut scalar_bytes = [0u8; 64]; + rng.fill_bytes(&mut scalar_bytes); + Self::from_bytes_mod_order_wide(&scalar_bytes) + } + + fn square(&self) -> Self { + self * self + } + + fn double(&self) -> Self { + self + self + } + + fn invert(&self) -> CtOption { + CtOption::new(self.invert(), !self.is_zero()) + } + + fn sqrt_ratio(num: &Self, div: &Self) -> (Choice, Self) { + ff_crate::helpers::sqrt_ratio_generic(num, div) + } + + fn sqrt(&self) -> CtOption { + ff_crate::helpers::sqrt_tonelli_shanks( + self, + [ + 0xcb02_4c63_4b9e_ba7d, + 0x029b_df3b_d45e_f39a, + 0x0000_0000_0000_0000, + 0x0200_0000_0000_0000, + ], + ) + } +} + +#[cfg(feature = "ff")] +impl PrimeField for Scalar { + type Repr = [u8; 32]; + + fn from_repr(repr: Self::Repr) -> CtOption { + Self::from_canonical_bytes(repr) + } + + fn from_repr_vartime(repr: Self::Repr) -> Option { + // Check that the high bit is not set + if (repr[31] >> 7) != 0u8 { + return None; + } + + let candidate = Scalar::from_bits(repr); + + if candidate == candidate.reduce() { + Some(candidate) + } else { + None + } + } + + fn to_repr(&self) -> Self::Repr { + self.to_bytes() + } + + fn is_odd(&self) -> Choice { + Choice::from(self.as_bytes()[0] & 1) + } + + const MODULUS: &'static str = + "0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed"; + const NUM_BITS: u32 = 253; + const CAPACITY: u32 = 252; + + const TWO_INV: Self = Self { + bytes: [ + 0xf7, 0xe9, 0x7a, 0x2e, 0x8d, 0x31, 0x09, 0x2c, 0x6b, 0xce, 0x7b, 0x51, 0xef, 0x7c, + 0x6f, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x08, + ], + }; + const MULTIPLICATIVE_GENERATOR: Self = Self { + bytes: [ + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ], + }; + const S: u32 = 2; + const ROOT_OF_UNITY: Self = Self { + bytes: [ + 0xd4, 0x07, 0xbe, 0xeb, 0xdf, 0x75, 0x87, 0xbe, 0xfe, 0x83, 0xce, 0x42, 0x53, 0x56, + 0xf0, 0x0e, 0x7a, 0xc2, 0xc1, 0xab, 0x60, 0x6d, 0x3d, 0x7d, 0xe7, 0x81, 0x79, 0xe0, + 0x10, 0x73, 0x4a, 0x09, + ], + }; + const ROOT_OF_UNITY_INV: Self = Self { + bytes: [ + 0x19, 0xcc, 0x37, 0x71, 0x3a, 0xed, 0x8a, 0x99, 0xd7, 0x18, 0x29, 0x60, 0x8b, 0xa3, + 0xee, 0x05, 0x86, 0x3d, 0x3e, 0x54, 0x9f, 0x92, 0xc2, 0x82, 0x18, 0x7e, 0x86, 0x1f, + 0xef, 0x8c, 0xb5, 0x06, + ], + }; + const DELTA: Self = Self { + bytes: [ + 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ], + }; +} + +#[cfg(feature = "ff")] +impl FromUniformBytes<64> for Scalar { + fn from_uniform_bytes(bytes: &[u8; 64]) -> Self { + Scalar::from_bytes_mod_order_wide(bytes) + } +} + /// Read one or more u64s stored as little endian bytes. /// /// ## Panics @@ -1857,6 +1983,56 @@ mod test { assert_eq!(sx + s1, Scalar::from(x + 1)); } + #[cfg(feature = "ff")] + #[test] + fn ff_constants() { + assert_eq!(Scalar::from(2u64) * Scalar::TWO_INV, Scalar::ONE); + + assert_eq!( + Scalar::ROOT_OF_UNITY * Scalar::ROOT_OF_UNITY_INV, + Scalar::ONE, + ); + + // ROOT_OF_UNITY^{2^s} mod m == 1 + assert_eq!( + Scalar::ROOT_OF_UNITY.pow(&[1u64 << Scalar::S, 0, 0, 0]), + Scalar::ONE, + ); + + // DELTA^{t} mod m == 1 + assert_eq!( + Scalar::DELTA.pow(&[ + 0x9604_98c6_973d_74fb, + 0x0537_be77_a8bd_e735, + 0x0000_0000_0000_0000, + 0x0400_0000_0000_0000, + ]), + Scalar::ONE, + ); + } + + #[cfg(feature = "ff")] + #[test] + fn ff_impls() { + assert!(bool::from(Scalar::ZERO.is_even())); + assert!(bool::from(Scalar::ONE.is_odd())); + assert!(bool::from(Scalar::from(2u64).is_even())); + assert!(bool::from(Scalar::DELTA.is_even())); + + assert!(bool::from(Field::invert(&Scalar::ZERO).is_none())); + assert_eq!(Field::invert(&X).unwrap(), XINV); + + let x_sq = X.square(); + // We should get back either the positive or negative root. + assert!([X, -X].contains(&x_sq.sqrt().unwrap())); + + assert_eq!(Scalar::from_repr_vartime(X.to_repr()), Some(X)); + assert_eq!(Scalar::from_repr_vartime([0xff; 32]), None); + + assert_eq!(Scalar::from_repr(X.to_repr()).unwrap(), X); + assert!(bool::from(Scalar::from_repr([0xff; 32]).is_none())); + } + #[test] #[should_panic] fn test_read_le_u64_into_should_panic_on_bad_input() {