diff --git a/.github/workflows/elliptic-curve.yml b/.github/workflows/elliptic-curve.yml index 57d4d4c9a..8ab5154ce 100644 --- a/.github/workflows/elliptic-curve.yml +++ b/.github/workflows/elliptic-curve.yml @@ -88,3 +88,21 @@ jobs: - run: cargo test --no-default-features - run: cargo test - run: cargo test --all-features + + test-careful: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: cargo install cargo-careful + - run: cargo careful test --all-features + + test-miri: + runs-on: ubuntu-latest + env: + MIRIFLAGS: "-Zmiri-symbolic-alignment-check -Zmiri-strict-provenance" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: rustup component add miri && cargo miri setup + - run: cargo miri test --all-features diff --git a/elliptic-curve/src/dev.rs b/elliptic-curve/src/dev.rs index 5d1eb8887..4d1f6c080 100644 --- a/elliptic-curve/src/dev.rs +++ b/elliptic-curve/src/dev.rs @@ -4,7 +4,7 @@ //! the traits in this crate. use crate::{ - Curve, CurveArithmetic, FieldBytesEncoding, PrimeCurve, + BatchNormalize, Curve, CurveArithmetic, FieldBytesEncoding, PrimeCurve, array::typenum::U32, bigint::{Limb, U256}, error::{Error, Result}, @@ -17,6 +17,7 @@ use crate::{ zeroize::DefaultIsZeroes, }; use core::{ + array, iter::{Product, Sum}, ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; @@ -24,6 +25,9 @@ use ff::{Field, PrimeField}; use hex_literal::hex; use pkcs8::AssociatedOid; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + #[cfg(feature = "bits")] use ff::PrimeFieldBits; @@ -584,6 +588,23 @@ pub enum ProjectivePoint { Other(AffinePoint), } +impl BatchNormalize<[ProjectivePoint; N]> for ProjectivePoint { + type Output = [AffinePoint; N]; + + fn batch_normalize(points: &[ProjectivePoint; N]) -> [AffinePoint; N] { + array::from_fn(|index| points[index].into()) + } +} + +#[cfg(feature = "alloc")] +impl BatchNormalize<[ProjectivePoint]> for ProjectivePoint { + type Output = Vec; + + fn batch_normalize(points: &[ProjectivePoint]) -> Vec { + points.iter().copied().map(AffinePoint::from).collect() + } +} + impl ConstantTimeEq for ProjectivePoint { fn ct_eq(&self, other: &Self) -> Choice { match (self, other) { diff --git a/elliptic-curve/src/lib.rs b/elliptic-curve/src/lib.rs index cca9515ed..79f444d3b 100644 --- a/elliptic-curve/src/lib.rs +++ b/elliptic-curve/src/lib.rs @@ -5,7 +5,8 @@ html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg" )] -#![forbid(unsafe_code)] +// Only allowed for newtype casts. +#![deny(unsafe_code)] #![warn( clippy::cast_lossless, clippy::cast_possible_truncation, diff --git a/elliptic-curve/src/point.rs b/elliptic-curve/src/point.rs index 2eec19245..b82da0551 100644 --- a/elliptic-curve/src/point.rs +++ b/elliptic-curve/src/point.rs @@ -40,9 +40,9 @@ pub trait AffineCoordinates { /// Normalize point(s) in projective representation by converting them to their affine ones. #[cfg(feature = "arithmetic")] -pub trait BatchNormalize: group::Curve { +pub trait BatchNormalize { /// The output of the batch normalization; a container of affine points. - type Output: AsRef<[Self::AffineRepr]>; + type Output; /// Perform a batched conversion to affine representation on a sequence of projective points /// at an amortized cost that should be practically as efficient as a single conversion. diff --git a/elliptic-curve/src/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index 91217827b..7bc99b3d7 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -6,11 +6,14 @@ use group::{Curve, Group, GroupEncoding, prime::PrimeCurveAffine}; use rand_core::CryptoRng; use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + #[cfg(feature = "serde")] use serdect::serde::{Deserialize, Serialize, de, ser}; use zeroize::Zeroize; -use crate::{CurveArithmetic, NonZeroScalar, Scalar}; +use crate::{BatchNormalize, CurveArithmetic, NonZeroScalar, Scalar}; /// Non-identity point type. /// @@ -19,6 +22,7 @@ use crate::{CurveArithmetic, NonZeroScalar, Scalar}; /// In the context of ECC, it's useful for ensuring that certain arithmetic /// cannot result in the identity point. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(transparent)] pub struct NonIdentity

{ point: P, } @@ -103,6 +107,72 @@ impl

AsRef

for NonIdentity

{ } } +impl BatchNormalize<[Self; N]> for NonIdentity

+where + P: Curve + BatchNormalize<[P; N], Output = [P::AffineRepr; N]>, +{ + type Output = [NonIdentity; N]; + + fn batch_normalize(points: &[Self; N]) -> [NonIdentity; N] { + // Ensure casting is safe. + // This always succeeds because `NonIdentity` is `repr(transparent)`. + debug_assert_eq!(size_of::

(), size_of::>()); + debug_assert_eq!(align_of::

(), align_of::>()); + + #[allow(unsafe_code)] + // SAFETY: `NonIdentity` is `repr(transparent)`. + let points: &[P; N] = unsafe { &*points.as_ptr().cast() }; + let affine_points =

>::batch_normalize(points); + + // Ensure `array::map()` can be optimized to a `memcpy`. + debug_assert_eq!( + size_of::(), + size_of::>() + ); + debug_assert_eq!( + align_of::(), + align_of::>() + ); + + affine_points.map(|point| NonIdentity { point }) + } +} + +#[cfg(feature = "alloc")] +impl

BatchNormalize<[Self]> for NonIdentity

+where + P: Curve + BatchNormalize<[P], Output = Vec>, +{ + type Output = Vec>; + + fn batch_normalize(points: &[Self]) -> Vec> { + // Ensure casting is safe. + // This always succeeds because `NonIdentity` is `repr(transparent)`. + debug_assert_eq!(size_of::

(), size_of::>()); + debug_assert_eq!(align_of::

(), align_of::>()); + + #[allow(unsafe_code)] + // SAFETY: `NonIdentity` is `repr(transparent)`. + let points: &[P] = unsafe { &*(points as *const [NonIdentity

] as *const [P]) }; + let affine_points =

>::batch_normalize(points); + + // Ensure `into_iter()` + `collect()` can be optimized away. + debug_assert_eq!( + size_of::(), + size_of::>() + ); + debug_assert_eq!( + align_of::(), + align_of::>() + ); + + affine_points + .into_iter() + .map(|point| NonIdentity { point }) + .collect() + } +} + impl

ConditionallySelectable for NonIdentity

where P: ConditionallySelectable, @@ -238,6 +308,7 @@ impl Zeroize for NonIdentity

{ #[cfg(all(test, feature = "dev"))] mod tests { use super::NonIdentity; + use crate::BatchNormalize; use crate::dev::{AffinePoint, NonZeroScalar, ProjectivePoint, SecretKey}; use group::GroupEncoding; use hex_literal::hex; @@ -303,4 +374,38 @@ mod tests { assert_eq!(point.to_point(), pk.to_projective()); } + + #[test] + fn batch_normalize() { + let point = ProjectivePoint::from_bytes( + &hex!("02c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721").into(), + ) + .unwrap(); + let point = NonIdentity::new(point).unwrap(); + let points = [point, point]; + + for (point, affine_point) in points + .into_iter() + .zip(NonIdentity::batch_normalize(&points)) + { + assert_eq!(point.to_affine(), affine_point); + } + } + + #[test] + #[cfg(feature = "alloc")] + fn batch_normalize_alloc() { + let point = ProjectivePoint::from_bytes( + &hex!("02c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721").into(), + ) + .unwrap(); + let point = NonIdentity::new(point).unwrap(); + let points = vec![point, point]; + + let affine_points = NonIdentity::batch_normalize(points.as_slice()); + + for (point, affine_point) in points.into_iter().zip(affine_points) { + assert_eq!(point.to_affine(), affine_point); + } + } }