From 4c0b1ade2a09d3da2c321fbb5a82519204b3c9f9 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sun, 8 Jun 2025 09:38:07 +0200 Subject: [PATCH 1/8] Implement `BatchNormalize` for `NonIdentity` --- elliptic-curve/src/lib.rs | 2 +- elliptic-curve/src/point.rs | 4 +- elliptic-curve/src/point/non_identity.rs | 67 +++++++++++++++++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/elliptic-curve/src/lib.rs b/elliptic-curve/src/lib.rs index cca9515ed..471988a8c 100644 --- a/elliptic-curve/src/lib.rs +++ b/elliptic-curve/src/lib.rs @@ -5,7 +5,7 @@ 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)] +#![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..f29a6d4f6 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -1,16 +1,20 @@ //! Non-identity point type. +use core::mem::{self, ManuallyDrop}; use core::ops::{Deref, Mul}; 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 +23,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 +108,66 @@ 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] { + debug_assert_eq!(size_of::

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

(), align_of::>()); + debug_assert_eq!( + size_of::(), + size_of::>() + ); + debug_assert_eq!( + align_of::(), + align_of::>() + ); + + #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + let points: &[P; N] = unsafe { mem::transmute(points) }; + let affine_points =

>::batch_normalize(points); + + #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + unsafe { + mem::transmute_copy(&ManuallyDrop::new(affine_points)) + } + } +} + +#[cfg(feature = "alloc")] +impl

BatchNormalize<[Self]> for NonIdentity

+where + P: Curve + BatchNormalize<[P], Output = Vec>, +{ + type Output = Vec>; + + fn batch_normalize(points: &[Self]) -> Vec> { + debug_assert_eq!(size_of::

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

(), align_of::>()); + debug_assert_eq!( + size_of::(), + size_of::>() + ); + debug_assert_eq!( + align_of::(), + align_of::>() + ); + + #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + let points: &[P] = unsafe { mem::transmute(points) }; + let affine_points =

>::batch_normalize(points); + + #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + // `Vec::into_raw_parts()` is not stable yet. + unsafe { + mem::transmute_copy(&ManuallyDrop::new(affine_points)) + } + } +} + impl

ConditionallySelectable for NonIdentity

where P: ConditionallySelectable, From 9e1b79e0608bc33925fda4b4635d923ea250ef5e Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 12 Jun 2025 10:48:55 +0200 Subject: [PATCH 2/8] Add safety docs --- elliptic-curve/src/point/non_identity.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/elliptic-curve/src/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index f29a6d4f6..6a20f5be0 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -126,11 +126,13 @@ where align_of::>() ); - #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + #[allow(unsafe_code)] + // SAFETY: `NonIdentity` is `repr(transparent)`. let points: &[P; N] = unsafe { mem::transmute(points) }; let affine_points =

>::batch_normalize(points); - #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + #[allow(unsafe_code)] + // SAFETY: `NonIdentity` is `repr(transparent)`. unsafe { mem::transmute_copy(&ManuallyDrop::new(affine_points)) } @@ -156,12 +158,14 @@ where align_of::>() ); - #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] + #[allow(unsafe_code)] + // SAFETY: `NonIdentity` is `repr(transparent)`. let points: &[P] = unsafe { mem::transmute(points) }; let affine_points =

>::batch_normalize(points); - #[expect(unsafe_code, reason = "`NonIdentity` is `repr(transparent)`")] // `Vec::into_raw_parts()` is not stable yet. + #[allow(unsafe_code)] + // SAFETY: `NonIdentity` is `repr(transparent)`. unsafe { mem::transmute_copy(&ManuallyDrop::new(affine_points)) } From e2c3a8f9c62c38bf329043ebd56c87e8faee2c43 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 12 Jun 2025 11:06:52 +0200 Subject: [PATCH 3/8] Remove `transmute()` calls entirely --- elliptic-curve/src/point/non_identity.rs | 45 +++++++++++++++--------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/elliptic-curve/src/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index 6a20f5be0..7c66edeb9 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -1,7 +1,7 @@ //! Non-identity point type. -use core::mem::{self, ManuallyDrop}; use core::ops::{Deref, Mul}; +use core::slice; use group::{Curve, Group, GroupEncoding, prime::PrimeCurveAffine}; use rand_core::CryptoRng; @@ -115,8 +115,18 @@ where 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] = unsafe { slice::from_raw_parts(points.as_ptr().cast(), N) }; + let points = points.try_into().expect("slice should be size `N`"); + let affine_points =

>::batch_normalize(points); + + // Ensure `array::map()` can be optimized to a `memcpy`. debug_assert_eq!( size_of::(), size_of::>() @@ -126,16 +136,7 @@ where align_of::>() ); - #[allow(unsafe_code)] - // SAFETY: `NonIdentity` is `repr(transparent)`. - let points: &[P; N] = unsafe { mem::transmute(points) }; - let affine_points =

>::batch_normalize(points); - - #[allow(unsafe_code)] - // SAFETY: `NonIdentity` is `repr(transparent)`. - unsafe { - mem::transmute_copy(&ManuallyDrop::new(affine_points)) - } + affine_points.map(|point| NonIdentity { point }) } } @@ -147,8 +148,18 @@ where 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 { slice::from_raw_parts(points.as_ptr().cast(), points.len()) }; + let mut affine_points =

>::batch_normalize(points); + + // Ensure casting is safe. + // This always succeeds because `NonIdentity` is `repr(transparent)`. debug_assert_eq!( size_of::(), size_of::>() @@ -158,16 +169,16 @@ where align_of::>() ); - #[allow(unsafe_code)] - // SAFETY: `NonIdentity` is `repr(transparent)`. - let points: &[P] = unsafe { mem::transmute(points) }; - let affine_points =

>::batch_normalize(points); - // `Vec::into_raw_parts()` is not stable yet. + let ptr = affine_points.as_mut_ptr(); + let length = affine_points.len(); + let capacity = affine_points.capacity(); + core::mem::forget(affine_points); + #[allow(unsafe_code)] // SAFETY: `NonIdentity` is `repr(transparent)`. unsafe { - mem::transmute_copy(&ManuallyDrop::new(affine_points)) + Vec::from_raw_parts(ptr.cast(), length, capacity) } } } From 6cda165d9542b83b7526604c42dd3a38b87c5bba Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 12 Jun 2025 18:26:48 +0200 Subject: [PATCH 4/8] Leave comment about why `unsafe_code` is not `forbid` --- elliptic-curve/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/elliptic-curve/src/lib.rs b/elliptic-curve/src/lib.rs index 471988a8c..79f444d3b 100644 --- a/elliptic-curve/src/lib.rs +++ b/elliptic-curve/src/lib.rs @@ -5,6 +5,7 @@ html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg" )] +// Only allowed for newtype casts. #![deny(unsafe_code)] #![warn( clippy::cast_lossless, From 0a351c7355ce8101d133b387ccacf063fdd75993 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 12 Jun 2025 18:36:35 +0200 Subject: [PATCH 5/8] Use pointer casting instead of fallible conversion --- elliptic-curve/src/point/non_identity.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/elliptic-curve/src/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index 7c66edeb9..b897b4c95 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -1,7 +1,6 @@ //! Non-identity point type. use core::ops::{Deref, Mul}; -use core::slice; use group::{Curve, Group, GroupEncoding, prime::PrimeCurveAffine}; use rand_core::CryptoRng; @@ -122,8 +121,7 @@ where #[allow(unsafe_code)] // SAFETY: `NonIdentity` is `repr(transparent)`. - let points: &[P] = unsafe { slice::from_raw_parts(points.as_ptr().cast(), N) }; - let points = points.try_into().expect("slice should be size `N`"); + let points: &[P; N] = unsafe { &*points.as_ptr().cast() }; let affine_points =

>::batch_normalize(points); // Ensure `array::map()` can be optimized to a `memcpy`. @@ -155,7 +153,8 @@ where #[allow(unsafe_code)] // SAFETY: `NonIdentity` is `repr(transparent)`. - let points: &[P] = unsafe { slice::from_raw_parts(points.as_ptr().cast(), points.len()) }; + let points: &[P] = + unsafe { core::slice::from_raw_parts(points.as_ptr().cast(), points.len()) }; let mut affine_points =

>::batch_normalize(points); // Ensure casting is safe. From 6edc705acb631662475c86a58ef81bbefe823013 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 13 Jun 2025 02:10:17 +0200 Subject: [PATCH 6/8] Use pointer casting instead of `*::from_raw_parts()` --- elliptic-curve/src/point/non_identity.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/elliptic-curve/src/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index b897b4c95..48cf0c54c 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -153,9 +153,8 @@ where #[allow(unsafe_code)] // SAFETY: `NonIdentity` is `repr(transparent)`. - let points: &[P] = - unsafe { core::slice::from_raw_parts(points.as_ptr().cast(), points.len()) }; - let mut affine_points =

>::batch_normalize(points); + let points: &[P] = unsafe { &*(points as *const [NonIdentity

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

>::batch_normalize(points); // Ensure casting is safe. // This always succeeds because `NonIdentity` is `repr(transparent)`. @@ -168,17 +167,16 @@ where align_of::>() ); - // `Vec::into_raw_parts()` is not stable yet. - let ptr = affine_points.as_mut_ptr(); - let length = affine_points.len(); - let capacity = affine_points.capacity(); - core::mem::forget(affine_points); - #[allow(unsafe_code)] // SAFETY: `NonIdentity` is `repr(transparent)`. - unsafe { - Vec::from_raw_parts(ptr.cast(), length, capacity) - } + let result: Vec> = unsafe { + (&affine_points as *const Vec) + .cast::>>() + .read() + }; + core::mem::forget(affine_points); + + result } } From d9dd7034200aa8fb9f8ab313e6ff0a40d45cb68d Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 13 Jun 2025 20:22:41 +0200 Subject: [PATCH 7/8] Compiler is able to optimize away `into_iter()` + `collect()` --- elliptic-curve/src/point/non_identity.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/elliptic-curve/src/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index 48cf0c54c..76d2d26c1 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -156,8 +156,7 @@ where let points: &[P] = unsafe { &*(points as *const [NonIdentity

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

>::batch_normalize(points); - // Ensure casting is safe. - // This always succeeds because `NonIdentity` is `repr(transparent)`. + // Ensure `into_iter()` + `collect()` can be optimized away. debug_assert_eq!( size_of::(), size_of::>() @@ -167,16 +166,10 @@ where align_of::>() ); - #[allow(unsafe_code)] - // SAFETY: `NonIdentity` is `repr(transparent)`. - let result: Vec> = unsafe { - (&affine_points as *const Vec) - .cast::>>() - .read() - }; - core::mem::forget(affine_points); - - result + affine_points + .into_iter() + .map(|point| NonIdentity { point }) + .collect() } } From 1fd380a1d4c508ec2dbed56a05e33e0ba3c8dd57 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 13 Jun 2025 20:45:42 +0200 Subject: [PATCH 8/8] Add `BatchNormalize for NonIdentity` test --- .github/workflows/elliptic-curve.yml | 18 ++++++++++++ elliptic-curve/src/dev.rs | 23 +++++++++++++++- elliptic-curve/src/point/non_identity.rs | 35 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) 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/point/non_identity.rs b/elliptic-curve/src/point/non_identity.rs index 76d2d26c1..7bc99b3d7 100644 --- a/elliptic-curve/src/point/non_identity.rs +++ b/elliptic-curve/src/point/non_identity.rs @@ -308,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; @@ -373,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); + } + } }