From 4baa567d8b6069309fbce3715058caac067edd69 Mon Sep 17 00:00:00 2001 From: maurges Date: Thu, 11 Jun 2026 14:39:58 +0200 Subject: [PATCH 01/12] Add Zeroize impl for encoded point and scalar Signed-off-by: maurges --- generic-ec/src/encoded.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/generic-ec/src/encoded.rs b/generic-ec/src/encoded.rs index 8725157..deabec4 100644 --- a/generic-ec/src/encoded.rs +++ b/generic-ec/src/encoded.rs @@ -1,8 +1,11 @@ use core::{fmt, ops}; +use zeroize::Zeroize; + use crate::{as_raw::AsRaw, core::ByteArray, Curve}; /// Bytes representation of an elliptic point +#[derive(Zeroize)] pub struct EncodedPoint(EncodedPointInner); impl EncodedPoint { @@ -67,6 +70,24 @@ impl AsRef<[u8]> for EncodedPoint { } } +impl AsMut<[u8]> for EncodedPoint { + fn as_mut(&mut self) -> &mut [u8] { + match &mut self.0 { + EncodedPointInner::Compressed(a) => a.as_mut(), + EncodedPointInner::Uncompressed(a) => a.as_mut(), + } + } +} + +impl Zeroize for EncodedPointInner { + fn zeroize(&mut self) { + match self { + EncodedPointInner::Compressed(a) => a.as_mut().zeroize(), + EncodedPointInner::Uncompressed(a) => a.as_mut().zeroize(), + } + } +} + /// Bytes representation of a scalar (either in big-endian or in little-endian) #[derive(Clone)] pub struct EncodedScalar(E::ScalarArray); @@ -133,3 +154,9 @@ impl AsRaw for EncodedScalar { &self.0 } } + +impl Zeroize for EncodedScalar { + fn zeroize(&mut self) { + self.as_mut().zeroize() + } +} From 3059d17d8d5c3e90c7197aef61f9fdcca8cbf8b2 Mon Sep 17 00:00:00 2001 From: maurges Date: Wed, 17 Jun 2026 10:19:07 +0200 Subject: [PATCH 02/12] Add SecretPoint. Add to_bytes method for SecretScalar Signed-off-by: maurges --- generic-ec/src/arithmetic.rs | 56 ++++++++---- generic-ec/src/lib.rs | 2 + generic-ec/src/secret_point/definition.rs | 85 +++++++++++++++++++ generic-ec/src/secret_point/mod.rs | 61 +++++++++++++ .../src/secret_scalar/docs-constructor.md | 6 +- generic-ec/src/secret_scalar/mod.rs | 16 ++++ 6 files changed, 206 insertions(+), 20 deletions(-) create mode 100644 generic-ec/src/secret_point/definition.rs create mode 100644 generic-ec/src/secret_point/mod.rs diff --git a/generic-ec/src/arithmetic.rs b/generic-ec/src/arithmetic.rs index 262f190..2b0735c 100644 --- a/generic-ec/src/arithmetic.rs +++ b/generic-ec/src/arithmetic.rs @@ -1,6 +1,6 @@ use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use crate::{Curve, Generator, NonZero, Point, Scalar, SecretScalar}; +use crate::{Curve, Generator, NonZero, Point, Scalar, SecretPoint, SecretScalar}; mod laws { use crate::{ @@ -38,8 +38,11 @@ mod laws { /// /// Therefore, (2) holds. #[inline] - pub fn sum_of_points_is_valid_point(a: &Point, b: &Point) -> Point { - let sum = Additive::add(a.as_raw(), b.as_raw()); + pub fn sum_of_points_is_valid_point( + a: impl AsRef>, + b: impl AsRef>, + ) -> Point { + let sum = Additive::add(a.as_ref().as_raw(), b.as_ref().as_raw()); // Correctness: refer to doc comment of the function Point::from_raw_unchecked(sum) } @@ -48,8 +51,11 @@ mod laws { /// /// Please, refer to [`sum_of_points_is_valid_point`], as the proof is pretty much the same. #[inline] - pub fn sub_of_points_is_valid_point(a: &Point, b: &Point) -> Point { - let result = Additive::sub(a.as_raw(), b.as_raw()); + pub fn sub_of_points_is_valid_point( + a: impl AsRef>, + b: impl AsRef>, + ) -> Point { + let result = Additive::sub(a.as_ref().as_raw(), b.as_ref().as_raw()); // Correctness: refer to doc comment of the function Point::from_raw_unchecked(result) } @@ -67,33 +73,33 @@ mod laws { /// If $A$ is valid `Point`, then $A + G$ is valid `Point` #[inline] pub fn sum_of_point_and_generator_is_valid_point( - a: &Point, + a: impl AsRef>, g: &Generator, ) -> Point { - sum_of_points_is_valid_point(a, &g.to_point()) + sum_of_points_is_valid_point(a.as_ref(), &g.to_point()) } /// If $A$ is valid `Point`, then $G + A$ is valid `Point` #[inline] pub fn sum_of_generator_and_point_is_valid_point( g: &Generator, - a: &Point, + a: impl AsRef>, ) -> Point { - sum_of_points_is_valid_point(&g.to_point(), a) + sum_of_points_is_valid_point(&g.to_point(), a.as_ref()) } /// If $A$ is valid `Point`, then $A - G$ is valid `Point` pub fn sub_of_point_and_generator_is_valid_point( - a: &Point, + a: impl AsRef>, g: &Generator, ) -> Point { - sub_of_points_is_valid_point(a, &g.to_point()) + sub_of_points_is_valid_point(a.as_ref(), &g.to_point()) } /// If $A$ is valid `Point`, then $G - A$ is valid `Point` pub fn sub_of_generator_and_point_is_valid_point( g: &Generator, - a: &Point, + a: impl AsRef>, ) -> Point { - sub_of_points_is_valid_point(&g.to_point(), a) + sub_of_points_is_valid_point(&g.to_point(), a.as_ref()) } /// If $A$ is a valid `Point`, then $-A$ is a valid `Point` @@ -135,9 +141,9 @@ mod laws { #[inline] pub fn mul_of_scalar_at_point_is_valid_point( n: impl AsRef>, - a: &Point, + a: impl AsRef>, ) -> Point { - let prod = Multiplicative::mul(n.as_ref().as_raw(), a.as_raw()); + let prod = Multiplicative::mul(n.as_ref().as_raw(), a.as_ref().as_raw()); // Correctness: refer to doc comment of the function Point::from_raw_unchecked(prod) } @@ -145,10 +151,10 @@ mod laws { /// Same as [`mul_of_scalar_at_point_is_valid_point`] but flipped arguments #[inline] pub fn mul_of_point_at_scalar_is_valid_point( - a: &Point, + a: impl AsRef>, b: impl AsRef>, ) -> Point { - mul_of_scalar_at_point_is_valid_point(b, a) + mul_of_scalar_at_point_is_valid_point(b, a.as_ref()) } /// If $n$ is valid `Scalar`, then $n \G$ is valid `Point` @@ -377,19 +383,35 @@ macro_rules! impl_op_assign { // Point <> Point, Point <> Scalar, Scalar <> Scalar arithmetic ops impl_binary_ops! { Add (Point, add, Point = Point) laws::sum_of_points_is_valid_point, + Add (Point, add, SecretPoint = Point) laws::sum_of_points_is_valid_point, + Add (SecretPoint, add, Point = Point) laws::sum_of_points_is_valid_point, + Sub (Point, sub, Point = Point) laws::sub_of_points_is_valid_point, + Sub (Point, sub, SecretPoint = Point) laws::sub_of_points_is_valid_point, + Sub (SecretPoint, sub, Point = Point) laws::sub_of_points_is_valid_point, Add (Point, add, Generator = Point) laws::sum_of_point_and_generator_is_valid_point, Add (Generator, add, Point = Point) laws::sum_of_generator_and_point_is_valid_point, Sub (Point, sub, Generator = Point) laws::sub_of_point_and_generator_is_valid_point, Sub (Generator, sub, Point = Point) laws::sub_of_generator_and_point_is_valid_point, + Add (SecretPoint, add, Generator = Point) + laws::sum_of_point_and_generator_is_valid_point, + Add (Generator, add, SecretPoint = Point) + laws::sum_of_generator_and_point_is_valid_point, + Sub (SecretPoint, sub, Generator = Point) + laws::sub_of_point_and_generator_is_valid_point, + Sub (Generator, sub, SecretPoint = Point) + laws::sub_of_generator_and_point_is_valid_point, + Add (Scalar, add, Scalar = Scalar) scalar::add, Sub (Scalar, sub, Scalar = Scalar) scalar::sub, Mul (Scalar, mul, Scalar = Scalar) scalar::mul, Mul (Point, mul, Scalar = Point) laws::mul_of_point_at_scalar_is_valid_point, + Mul (SecretPoint, mul, Scalar = Point) laws::mul_of_point_at_scalar_is_valid_point, Mul (Scalar, mul, Point = Point) laws::mul_of_scalar_at_point_is_valid_point, + Mul (Scalar, mul, SecretPoint = Point) laws::mul_of_scalar_at_point_is_valid_point, Mul (Generator, mul, Scalar = Point) laws::mul_of_generator_at_scalar_is_valid_point, Mul (Scalar, mul, Generator = Point) laws::mul_of_scalar_at_generator_is_valid_point, } diff --git a/generic-ec/src/lib.rs b/generic-ec/src/lib.rs index c38e7c1..714eade 100644 --- a/generic-ec/src/lib.rs +++ b/generic-ec/src/lib.rs @@ -207,6 +207,7 @@ pub mod multiscalar; mod non_zero; mod point; mod scalar; +mod secret_point; mod secret_scalar; mod _unused_deps { @@ -266,6 +267,7 @@ pub use self::{ non_zero::definition::NonZero, point::definition::Point, scalar::{Radix16Iter, Scalar}, + secret_point::definition::SecretPoint, secret_scalar::definition::SecretScalar, }; diff --git a/generic-ec/src/secret_point/definition.rs b/generic-ec/src/secret_point/definition.rs new file mode 100644 index 0000000..8ded0aa --- /dev/null +++ b/generic-ec/src/secret_point/definition.rs @@ -0,0 +1,85 @@ +use crate::{Curve, Point}; + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all())))] +mod with_alloc { + use alloc::sync::Arc; + use zeroize::Zeroize as _; + + use crate::{Curve, Point}; + + pub(super) type Internal = Arc>>; + + #[inline(always)] + pub(super) fn new(point: &mut Point) -> Internal { + let mut point_on_heap = Arc::>>::default(); + let point_mut = Arc::make_mut(&mut point_on_heap); + core::mem::swap(&mut **point_mut, point); + point.zeroize(); + point_on_heap + } +} + +#[cfg(not(feature = "alloc"))] +#[cfg_attr(docsrs, doc(cfg(all())))] +mod without_alloc { + pub(super) type Internal = zeroize::Zeroizing>; + + #[inline(always)] + pub fn new(point: &mut crate::Point) -> Internal { + let point_new = zeroize::Zeroizing::new(*point); + point.zeroize(); + point_new + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all())))] +use with_alloc as imp; +#[cfg(not(feature = "alloc"))] +#[cfg_attr(docsrs, doc(cfg(all())))] +use without_alloc as imp; + +/// Point representing sensitive information (like a derived secret) +/// +/// Secret point should be treated with an extra care. You shouldn't do any +/// branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel +/// attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). +/// +/// Also, when `alloc` feature is enabled, we enforce extra measures: +/// +/// * Secret point leaves no trace in RAM after it's dropped \ +/// Memory is zeroized after use +/// * All clones of a secret point refer to the same region in the memory \ +/// I.e. there will always be only one instance of the point in the memory +/// no matter how many clones you make +/// +/// All these guarantees can be bypassed by calling `.as_ref()` and obtaining +/// `&Point` that is not protected from timing attacks, leaving traces in +/// the memory, etc. +/// +/// [`ConstantTimeEq`]: subtle::ConstantTimeEq +pub struct SecretPoint(imp::Internal); + +impl SecretPoint { + /// Constructs a new secret point + /// + /// Takes the original point by mutable reference instead of taking by value + /// to avoid leaving copies of the point on stack. Point behind the + /// reference will be zeroized after the function has returned. + pub fn new(point: &mut Point) -> Self { + Self(imp::new(point)) + } +} + +impl AsRef> for SecretPoint { + fn as_ref(&self) -> &Point { + &self.0 + } +} + +impl Clone for SecretPoint { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} diff --git a/generic-ec/src/secret_point/mod.rs b/generic-ec/src/secret_point/mod.rs new file mode 100644 index 0000000..db1e412 --- /dev/null +++ b/generic-ec/src/secret_point/mod.rs @@ -0,0 +1,61 @@ +use core::fmt; +use core::iter::Sum; + +use crate::{errors::InvalidPoint, Curve, EncodedPoint, Point}; +use subtle::{Choice, ConstantTimeEq}; + +use self::definition::SecretPoint; + +pub mod definition; + +impl SecretPoint { + /// Returns the generator defined in the curve specs + pub fn generator() -> Self { + Self::new(&mut Point::generator().into()) + } + + /// Returns identity point $\O$ + pub fn zero() -> Self { + Self::new(&mut Point::zero()) + } + + /// Encodes a point as bytes + #[cfg(feature = "alloc")] + pub fn to_bytes( + &self, + compressed: bool, + ) -> alloc::boxed::Box>> { + let bytes = zeroize::Zeroizing::new(self.as_ref().to_bytes(compressed)); + alloc::boxed::Box::new(bytes) + } + + /// Decodes a point from bytes + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut point = Point::from_bytes(bytes)?; + Ok(Self::new(&mut point)) + } +} + +impl ConstantTimeEq for SecretPoint { + fn ct_eq(&self, other: &Self) -> Choice { + self.as_ref().ct_eq(other.as_ref()) + } +} + +impl Sum> for Point { + fn sum>>(iter: I) -> Self { + iter.fold(Point::::zero(), |acc, i| acc + &i) + } +} + +impl<'s, E: Curve> Sum<&'s SecretPoint> for Point { + fn sum>>(iter: I) -> Self { + iter.fold(Point::::zero(), |acc, i| acc + i) + } +} + +impl fmt::Debug for SecretPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SecretPoint") + } +} diff --git a/generic-ec/src/secret_scalar/docs-constructor.md b/generic-ec/src/secret_scalar/docs-constructor.md index d55eef1..c55b02c 100644 --- a/generic-ec/src/secret_scalar/docs-constructor.md +++ b/generic-ec/src/secret_scalar/docs-constructor.md @@ -1,5 +1,5 @@ Constructs a new secret scalar -It takes original scalar by mutable reference instead of taking by value to avoid leaving -copies of scalar on stack. Scalar behind the reference will be zeroized after function -returned. +Takes the original scalar by mutable reference instead of taking by value to +avoid leaving copies of the scalar on stack. Scalar behind the reference will +be zeroized after the function has returned. diff --git a/generic-ec/src/secret_scalar/mod.rs b/generic-ec/src/secret_scalar/mod.rs index a718cbe..babf707 100644 --- a/generic-ec/src/secret_scalar/mod.rs +++ b/generic-ec/src/secret_scalar/mod.rs @@ -4,6 +4,8 @@ use core::iter::{Product, Sum}; use rand_core::{CryptoRng, RngCore}; use subtle::{Choice, ConstantTimeEq}; +#[cfg(feature = "alloc")] +use crate::EncodedScalar; use crate::{errors::InvalidScalar, Curve, Scalar}; use self::definition::SecretScalar; @@ -61,6 +63,20 @@ impl SecretScalar { Self::random(&mut rng) } + /// Encodes scalar as bytes in big-endian order + #[cfg(feature = "alloc")] + pub fn to_be_bytes(&self) -> alloc::boxed::Box>> { + let bytes = zeroize::Zeroizing::new(self.as_ref().to_be_bytes()); + alloc::boxed::Box::new(bytes) + } + + /// Encodes scalar as bytes in little-endian order + #[cfg(feature = "alloc")] + pub fn to_le_bytes(&self) -> alloc::boxed::Box>> { + let bytes = zeroize::Zeroizing::new(self.as_ref().to_le_bytes()); + alloc::boxed::Box::new(bytes) + } + /// Decodes scalar from its bytes representation in big-endian order pub fn from_be_bytes(bytes: &[u8]) -> Result { let mut scalar = Scalar::from_be_bytes(bytes)?; From 328e62c3786a6d95c9af111f44b9acf77ffde03e Mon Sep 17 00:00:00 2001 From: maurges Date: Wed, 17 Jun 2026 14:20:57 +0200 Subject: [PATCH 03/12] Simplify the definition of SecretScalar Signed-off-by: maurges --- generic-ec/src/secret_scalar/definition.rs | 116 ++++++++++-------- .../src/secret_scalar/docs-constructor.md | 5 - generic-ec/src/secret_scalar/docs.md | 19 --- 3 files changed, 66 insertions(+), 74 deletions(-) delete mode 100644 generic-ec/src/secret_scalar/docs-constructor.md delete mode 100644 generic-ec/src/secret_scalar/docs.md diff --git a/generic-ec/src/secret_scalar/definition.rs b/generic-ec/src/secret_scalar/definition.rs index 4b972e8..b3971a4 100644 --- a/generic-ec/src/secret_scalar/definition.rs +++ b/generic-ec/src/secret_scalar/definition.rs @@ -1,77 +1,93 @@ +use crate::{Curve, Scalar}; + #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(all())))] mod with_alloc { use alloc::sync::Arc; - use zeroize::{Zeroize, Zeroizing}; + use zeroize::{Zeroize as _, Zeroizing}; use crate::{Curve, Scalar}; - #[doc = include_str!("docs.md")] - pub struct SecretScalar(Arc>>); - - impl SecretScalar { - #[doc = include_str!("docs-constructor.md")] - pub fn new(scalar: &mut Scalar) -> Self { - let mut scalar_on_heap = Arc::>>::default(); - let scalar_mut = Arc::make_mut(&mut scalar_on_heap); - core::mem::swap(&mut **scalar_mut, scalar); - scalar.zeroize(); - Self(scalar_on_heap) - } - } - - impl AsRef> for SecretScalar { - fn as_ref(&self) -> &Scalar { - &self.0 - } - } + pub(super) type Inner = Arc>>; - impl Clone for SecretScalar { - fn clone(&self) -> Self { - Self(self.0.clone()) - } + #[inline(always)] + pub(super) fn new(scalar: &mut Scalar) -> Inner { + let mut scalar_on_heap = Arc::>>::default(); + let scalar_mut = Arc::make_mut(&mut scalar_on_heap); + core::mem::swap(&mut **scalar_mut, scalar); + scalar.zeroize(); + scalar_on_heap } } #[cfg(not(feature = "alloc"))] #[cfg_attr(docsrs, doc(cfg(all())))] mod without_alloc { - use zeroize::{Zeroize, Zeroizing}; + use zeroize::{Zeroize as _, Zeroizing}; use crate::{Curve, Scalar}; + pub(super) type Inner = Zeroizing>; + + #[inline(always)] + pub(super) fn new(scalar: &mut Scalar) -> Self { + let scalar_new = Zeroizing::new(*scalar); + scalar.zeroize(); + scalar_new + } + #[doc = include_str!("docs.md")] pub struct SecretScalar(Zeroizing>); +} - impl SecretScalar { - #[doc = include_str!("docs-constructor.md")] - pub fn new(scalar: &mut Scalar) -> Self { - let scalar_new = Self(Zeroizing::new(*scalar)); - scalar.zeroize(); - scalar_new - } - } - impl AsRef> for SecretScalar { - fn as_ref(&self) -> &Scalar { - &self.0 - } - } +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all())))] +use with_alloc as imp; +#[cfg(not(feature = "alloc"))] +#[cfg_attr(docsrs, doc(cfg(all())))] +use without_alloc as imp; + +/// Scalar representing sensitive information (like secret key) +/// +/// Secret scalar should be treated with an extra care. You shouldn't do any +/// branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel +/// attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). +/// +/// Also, when `alloc` feature is enabled, we enforce extra measures: +/// +/// * Secret scalar leaves no trace in RAM after it's dropped \ +/// Memory is zeroized after use +/// * All clones of secret scalar refer to the same region in the memory \ +/// I.e. there will always be only one instance of the scalar in the memory +/// no matter how many clones you make +/// +/// All these guarantees can be bypassed by calling `.as_ref()` and obtaining +/// `&Scalar` that is not protected from timing attacks, leaving traces in +/// the memory, etc. +/// +/// [`ConstantTimeEq`]: subtle::ConstantTimeEq +pub struct SecretScalar(imp::Inner); - impl Clone for SecretScalar { - fn clone(&self) -> Self { - Self(self.0.clone()) - } +impl SecretScalar { + /// Constructs a new secret scalar + /// + /// Takes the original scalar by mutable reference instead of taking by value to + /// avoid leaving copies of the scalar on stack. Scalar behind the reference will + /// be zeroized after the function has returned. + pub fn new(scalar: &mut Scalar) -> Self { + Self(imp::new(scalar)) } } -mod secret_scalar { - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(all())))] - pub use super::with_alloc::SecretScalar; - #[cfg(not(feature = "alloc"))] - #[cfg_attr(docsrs, doc(cfg(all())))] - pub use super::without_alloc::SecretScalar; +impl AsRef> for SecretScalar { + fn as_ref(&self) -> &Scalar { + &self.0 + } } -pub use self::secret_scalar::SecretScalar; +impl Clone for SecretScalar { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} diff --git a/generic-ec/src/secret_scalar/docs-constructor.md b/generic-ec/src/secret_scalar/docs-constructor.md deleted file mode 100644 index c55b02c..0000000 --- a/generic-ec/src/secret_scalar/docs-constructor.md +++ /dev/null @@ -1,5 +0,0 @@ -Constructs a new secret scalar - -Takes the original scalar by mutable reference instead of taking by value to -avoid leaving copies of the scalar on stack. Scalar behind the reference will -be zeroized after the function has returned. diff --git a/generic-ec/src/secret_scalar/docs.md b/generic-ec/src/secret_scalar/docs.md deleted file mode 100644 index 6e0bcec..0000000 --- a/generic-ec/src/secret_scalar/docs.md +++ /dev/null @@ -1,19 +0,0 @@ -Scalar representing sensitive information (like secret key) - -Secret scalar should be treated with an extra care. You shouldn't do any -branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel -attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). - -Also, when `alloc` feature is enabled, we enforce extra measures: - -* Secret scalar leaves no trace in RAM after it's dropped \ - Memory is zeroized after use -* All clones of secret scalar refer to the same region in the memory \ - I.e. there will always be only one instance of the scalar in the memory - no matter how many clones you make - -All these guarantees can be bypassed by calling `.as_ref()` and obtaining -`&Scalar` that is not protected from timing attacks, leaving traces in -the memory, etc. - -[`ConstantTimeEq`]: subtle::ConstantTimeEq From 414a661c975b36d0a4d368416d4a87741864847b Mon Sep 17 00:00:00 2001 From: maurges Date: Wed, 17 Jun 2026 15:37:07 +0200 Subject: [PATCH 04/12] Add non-zero SecretPoint and other missing impls Signed-off-by: maurges --- generic-ec/src/arithmetic.rs | 32 +++++++-- generic-ec/src/lib.rs | 9 ++- generic-ec/src/multiscalar.rs | 2 +- generic-ec/src/non_zero/mod.rs | 81 +++++++++++++++++++++- generic-ec/src/secret_point/mod.rs | 9 +++ generic-ec/src/secret_scalar/definition.rs | 1 - generic-ec/src/serde.rs | 42 ++++++++++- 7 files changed, 162 insertions(+), 14 deletions(-) diff --git a/generic-ec/src/arithmetic.rs b/generic-ec/src/arithmetic.rs index 2b0735c..a24b973 100644 --- a/generic-ec/src/arithmetic.rs +++ b/generic-ec/src/arithmetic.rs @@ -76,7 +76,7 @@ mod laws { a: impl AsRef>, g: &Generator, ) -> Point { - sum_of_points_is_valid_point(a.as_ref(), &g.to_point()) + sum_of_points_is_valid_point(a.as_ref(), g.to_point()) } /// If $A$ is valid `Point`, then $G + A$ is valid `Point` #[inline] @@ -84,7 +84,7 @@ mod laws { g: &Generator, a: impl AsRef>, ) -> Point { - sum_of_points_is_valid_point(&g.to_point(), a.as_ref()) + sum_of_points_is_valid_point(g.to_point(), a.as_ref()) } /// If $A$ is valid `Point`, then $A - G$ is valid `Point` @@ -92,14 +92,14 @@ mod laws { a: impl AsRef>, g: &Generator, ) -> Point { - sub_of_points_is_valid_point(a.as_ref(), &g.to_point()) + sub_of_points_is_valid_point(a.as_ref(), g.to_point()) } /// If $A$ is valid `Point`, then $G - A$ is valid `Point` pub fn sub_of_generator_and_point_is_valid_point( g: &Generator, a: impl AsRef>, ) -> Point { - sub_of_points_is_valid_point(&g.to_point(), a.as_ref()) + sub_of_points_is_valid_point(g.to_point(), a.as_ref()) } /// If $A$ is a valid `Point`, then $-A$ is a valid `Point` @@ -192,7 +192,7 @@ mod laws { #[inline] pub fn mul_of_nonzero_scalar_at_nonzero_point_is_valid_nonzero_point( n: &(impl AsRef> + AlwaysNonZero), - a: &NonZero>, + a: &(impl AsRef> + AlwaysNonZero), ) -> NonZero> { let prod = mul_of_scalar_at_point_is_valid_point(n, a); // Correctness: refer to doc comment of the function @@ -202,7 +202,7 @@ mod laws { /// Same as [`mul_of_nonzero_scalar_at_nonzero_point_is_valid_nonzero_point`] but flipped arguments #[inline] pub fn mul_of_nonzero_point_at_nonzero_scalar_is_valid_nonzero_point( - a: &NonZero>, + a: &(impl AsRef> + AlwaysNonZero), n: &(impl AsRef> + AlwaysNonZero), ) -> NonZero> { mul_of_nonzero_scalar_at_nonzero_point_is_valid_nonzero_point(n, a) @@ -465,6 +465,9 @@ impl_binary_ops! { Mul (Generator, mul, NonZero> = NonZero>) laws::mul_of_generator_at_nonzero_scalar_is_valid_nonzero_point, Mul (NonZero>, mul, Generator = NonZero>) laws::mul_of_nonzero_scalar_at_generator_is_valid_nonzero_point, + Mul (NonZero>, mul, NonZero> = NonZero>) laws::mul_of_nonzero_point_at_nonzero_scalar_is_valid_nonzero_point, + Mul (NonZero>, mul, NonZero> = NonZero>) laws::mul_of_nonzero_scalar_at_nonzero_point_is_valid_nonzero_point, + Mul (NonZero>, mul, NonZero> = NonZero>) laws::mul_of_nonzero_point_at_nonzero_scalar_is_valid_nonzero_point, Mul (NonZero>, mul, NonZero> = NonZero>) laws::mul_of_nonzero_scalar_at_nonzero_point_is_valid_nonzero_point, Mul (Generator, mul, NonZero> = NonZero>) laws::mul_of_generator_at_nonzero_scalar_is_valid_nonzero_point, @@ -477,6 +480,14 @@ impl_nonzero_ops! { Add (Point, add, Point = Point) laws::sum_of_points_is_valid_point, Sub (Point, sub, Point = Point) laws::sub_of_points_is_valid_point, + Add (Point, add, SecretPoint = Point) laws::sum_of_points_is_valid_point, + Add (SecretPoint, add, Point = Point) laws::sum_of_points_is_valid_point, + Sub (Point, sub, SecretPoint = Point) laws::sub_of_points_is_valid_point, + Sub (SecretPoint, sub, Point = Point) laws::sub_of_points_is_valid_point, + + Add (SecretPoint, add, SecretPoint = Point) laws::sum_of_points_is_valid_point, + Sub (SecretPoint, sub, SecretPoint = Point) laws::sub_of_points_is_valid_point, + Add (Scalar, add, Scalar = Scalar) scalar::add, Sub (Scalar, sub, Scalar = Scalar) scalar::sub, @@ -501,6 +512,11 @@ impl_binary_ops! { Mul (NonZero>, mul, Point = Point) laws::mul_of_scalar_at_point_is_valid_point, Mul (NonZero>, mul, Scalar = Point) laws::mul_of_point_at_scalar_is_valid_point, Mul (Scalar, mul, NonZero> = Point) laws::mul_of_scalar_at_point_is_valid_point, + + Mul (SecretPoint, mul, NonZero> = Point) laws::mul_of_point_at_scalar_is_valid_point, + Mul (NonZero>, mul, SecretPoint = Point) laws::mul_of_scalar_at_point_is_valid_point, + Mul (NonZero>, mul, Scalar = Point) laws::mul_of_point_at_scalar_is_valid_point, + Mul (Scalar, mul, NonZero> = Point) laws::mul_of_scalar_at_point_is_valid_point, } // -Point, -Scalar, -NonZero, -NonZero @@ -515,10 +531,14 @@ impl_unary_ops! { impl_op_assign! { Point, AddAssign, Point, add_assign, +, Point, AddAssign, NonZero>, add_assign, +, + Point, AddAssign, SecretPoint, add_assign, +, + Point, AddAssign, NonZero>, add_assign, +, Point, AddAssign, Generator, add_assign, +, Point, SubAssign, Point, sub_assign, -, Point, SubAssign, NonZero>, sub_assign, -, + Point, SubAssign, SecretPoint, sub_assign, -, + Point, SubAssign, NonZero>, sub_assign, -, Point, SubAssign, Generator, sub_assign, -, Point, MulAssign, Scalar, mul_assign, *, diff --git a/generic-ec/src/lib.rs b/generic-ec/src/lib.rs index 714eade..71e92a8 100644 --- a/generic-ec/src/lib.rs +++ b/generic-ec/src/lib.rs @@ -40,12 +40,13 @@ //! always enforced. E.g. if you're deserializing a sequence of bytes that represents an invalid point, //! deserialization will result into error. //! -//! ### `SecretScalar` +//! ### `SecretScalar` and `SecretPoint` //! //! Sometimes your scalar represents some sensitive value like secret key, and you want to keep it safer. //! `SecretScalar` is in-place replacement of `Scalar` that enforces additional security by storing //! the scalar value on the heap, and erasing it on drop. Its advantage is that it doesn't leave any trace -//! in memory dump after it's dropped (which is not guaranteed by regular `Scalar`). +//! in memory dump after it's dropped (which is not guaranteed by regular `Scalar`). `SecretPoint` +//! is a similar structure, except it acts as a replacement for `Point`. //! //! But keep in mind that we can't control the OS which could potentially load RAM page containing sensitive value //! to the swap disk (i.e. on your HDD/SSD) if you're running low on memory. Or it could do any other fancy stuff. @@ -326,6 +327,10 @@ pub mod curves { #[doc = stringify!($curve)] /// `](E) curve pub type Point = crate::Point; + /// Secret point on [` + #[doc = stringify!($curve)] + /// `](E) curve + pub type SecretPoint = crate::SecretPoint; /// Scalar in [` #[doc = stringify!($curve)] /// `](E) curve large prime subgroup diff --git a/generic-ec/src/multiscalar.rs b/generic-ec/src/multiscalar.rs index 2e018e0..3dc847d 100644 --- a/generic-ec/src/multiscalar.rs +++ b/generic-ec/src/multiscalar.rs @@ -44,7 +44,7 @@ pub trait MultiscalarMul { /// algorithm for multiscalar multiplication, preallocate memory, etc. If iterator size is not /// correct, it may worsen performance or lead to runtime panic. /// - /// Note that the multiscalar algorithm is not necessarily constant-time, thus is should not be + /// Note that the multiscalar algorithm is not necessarily constant-time, thus it should not be /// used with [`SecretScalar`](crate::SecretScalar). fn multiscalar_mul(scalar_points: impl ExactSizeIterator) -> Point where diff --git a/generic-ec/src/non_zero/mod.rs b/generic-ec/src/non_zero/mod.rs index c921e7e..5dbbb7c 100644 --- a/generic-ec/src/non_zero/mod.rs +++ b/generic-ec/src/non_zero/mod.rs @@ -10,7 +10,7 @@ use crate::{ as_raw::FromRaw, core::{ByteArray, FromUniformBytes}, errors::{ZeroPoint, ZeroScalar}, - Curve, Point, Scalar, SecretScalar, + Curve, Point, Scalar, SecretPoint, SecretScalar, }; use self::definition::NonZero; @@ -38,6 +38,43 @@ impl NonZero> { // zero value is not accessible by anyone CtOption::new(Self::new_unchecked(point), is_non_zero) } + + /// Convert this value into a `NonZero>`. You should do this at the end + /// of computations that produce a secret, like a key exchange + pub fn into_secret(self) -> NonZero> { + let mut point = self.into_inner(); + let secret_point = SecretPoint::new(&mut point); + // Correctness: `point` was checked to be nonzero + NonZero::new_unchecked(secret_point) + } +} + +impl NonZero> { + /// Returns the generator defined in the curve specs + pub fn generator() -> Self { + // Correctness: generator of a non-degenerate group is not zero + Self::new_unchecked(SecretPoint::generator()) + } + + /// Constructs non-zero point + /// + /// Returns `None` if point is zero + pub fn from_secret_point(point: SecretPoint) -> Option { + Self::ct_from_secret_point(point).into() + } + + /// Constructs non-zero point (constant time) + /// + /// Returns `None` if point is zero + pub fn ct_from_secret_point(secret_point: SecretPoint) -> CtOption { + let zero = Point::zero(); + let is_non_zero = !secret_point.as_ref().ct_eq(&zero); + + // Correctness: although we technically construct `NonZero` regardless if + // it's actually non-zero, `CtOption` never exposes it, so `NonZero` with + // zero value is not accessible by anyone + CtOption::new(Self::new_unchecked(secret_point), is_non_zero) + } } impl NonZero> { @@ -204,6 +241,12 @@ impl From>> for Point { } } +impl From>> for SecretPoint { + fn from(secret_point: NonZero>) -> Self { + secret_point.into_inner() + } +} + impl From>> for Scalar { fn from(scalar: NonZero>) -> Self { scalar.into_inner() @@ -224,6 +267,14 @@ impl TryFrom> for NonZero> { } } +impl TryFrom> for NonZero> { + type Error = ZeroPoint; + + fn try_from(secret_point: SecretPoint) -> Result { + Self::from_secret_point(secret_point).ok_or(ZeroPoint) + } +} + impl TryFrom> for NonZero> { type Error = ZeroScalar; @@ -307,6 +358,21 @@ impl<'s, E: Curve> Sum<&'s NonZero>> for Point { } } +impl Sum>> for SecretPoint { + fn sum>>>(iter: I) -> Self { + let mut out = Point::zero(); + iter.for_each(|x| out += x); + SecretPoint::new(&mut out) + } +} +impl<'s, E: Curve> Sum<&'s NonZero>> for SecretPoint { + fn sum>>>(iter: I) -> Self { + let mut out = Point::zero(); + iter.for_each(|x| out += x); + SecretPoint::new(&mut out) + } +} + impl crate::traits::Samplable for NonZero> { fn random(rng: &mut R) -> Self { Self::random(rng) @@ -345,6 +411,13 @@ impl crate::traits::One for NonZero> { } } +impl AsRef> for NonZero> { + fn as_ref(&self) -> &Point { + let secret_point: &SecretPoint = self.as_ref(); + secret_point.as_ref() + } +} + impl AsRef> for NonZero> { fn as_ref(&self) -> &Scalar { let secret_scalar: &SecretScalar = self.as_ref(); @@ -394,7 +467,8 @@ macro_rules! impl_reverse_partial_eq_cmp { )*}; } -// Note: not implemented for SecretScalar as it doesn't implement `PartialEq` for security reasons. +// Note: not implemented for SecretScalar and SecretPoint as they don't +// implement `PartialEq` for security reasons. impl_reverse_partial_eq_cmp!(Point, Scalar); impl ConstantTimeEq for NonZero { @@ -405,7 +479,7 @@ impl ConstantTimeEq for NonZero { #[cfg(all(test, feature = "serde"))] mod non_zero_is_serializable { - use crate::{Curve, NonZero, Point, Scalar, SecretScalar}; + use crate::{Curve, NonZero, Point, Scalar, SecretPoint, SecretScalar}; fn impls_serde() where @@ -416,6 +490,7 @@ mod non_zero_is_serializable { #[allow(dead_code)] fn ensure_non_zero_is_serde() { impls_serde::>>(); + impls_serde::>>(); impls_serde::>>(); impls_serde::>>(); } diff --git a/generic-ec/src/secret_point/mod.rs b/generic-ec/src/secret_point/mod.rs index db1e412..7baa4d4 100644 --- a/generic-ec/src/secret_point/mod.rs +++ b/generic-ec/src/secret_point/mod.rs @@ -8,6 +8,15 @@ use self::definition::SecretPoint; pub mod definition; +impl Point { + /// Convert this value into a [`SecretPoint`]. You should do this at the end + /// of computations that produce a secret, like a key exchange + #[inline(always)] // Prevent a byte copy in most cases + pub fn into_secret(mut self) -> SecretPoint { + SecretPoint::new(&mut self) + } +} + impl SecretPoint { /// Returns the generator defined in the curve specs pub fn generator() -> Self { diff --git a/generic-ec/src/secret_scalar/definition.rs b/generic-ec/src/secret_scalar/definition.rs index b3971a4..64879c3 100644 --- a/generic-ec/src/secret_scalar/definition.rs +++ b/generic-ec/src/secret_scalar/definition.rs @@ -40,7 +40,6 @@ mod without_alloc { pub struct SecretScalar(Zeroizing>); } - #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(all())))] use with_alloc as imp; diff --git a/generic-ec/src/serde.rs b/generic-ec/src/serde.rs index d68f711..ac1b0e9 100644 --- a/generic-ec/src/serde.rs +++ b/generic-ec/src/serde.rs @@ -166,7 +166,7 @@ impl<'de, E: Curve> serde::Deserialize<'de> for CurveName { pub use optional::*; #[cfg(feature = "serde")] mod optional { - use crate::{core::Curve, Point, Scalar, SecretScalar}; + use crate::{core::Curve, Point, Scalar, SecretPoint, SecretScalar}; use super::CurveName; @@ -190,6 +190,24 @@ mod optional { } } + impl serde::Serialize for SecretPoint { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_ref().serialize(serializer) + } + } + + impl<'de, E: Curve> serde::Deserialize<'de> for SecretPoint { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(SecretPoint::new(&mut Point::deserialize(deserializer)?)) + } + } + impl serde::Serialize for Scalar { fn serialize(&self, serializer: S) -> Result where @@ -253,6 +271,28 @@ mod optional { } } + impl serde_with::SerializeAs> for Compact { + fn serialize_as(source: &SecretPoint, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::Serialize; + models::PointCompact::from(source.as_ref()).serialize(serializer) + } + } + + impl<'de, E: Curve> serde_with::DeserializeAs<'de, SecretPoint> for Compact { + fn deserialize_as(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let mut point = >>::deserialize_as( + deserializer, + )?; + Ok(SecretPoint::new(&mut point)) + } + } + impl serde_with::SerializeAs> for Compact { fn serialize_as(source: &Scalar, serializer: S) -> Result where From 19909f38b47da380fcb6171673cd956297b18dfe Mon Sep 17 00:00:00 2001 From: maurges Date: Wed, 17 Jun 2026 16:17:07 +0200 Subject: [PATCH 05/12] Remove unnecessary docsrs cfg Signed-off-by: maurges --- generic-ec/src/secret_point/definition.rs | 4 ---- generic-ec/src/secret_scalar/definition.rs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/generic-ec/src/secret_point/definition.rs b/generic-ec/src/secret_point/definition.rs index 8ded0aa..35d0e0d 100644 --- a/generic-ec/src/secret_point/definition.rs +++ b/generic-ec/src/secret_point/definition.rs @@ -1,7 +1,6 @@ use crate::{Curve, Point}; #[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(all())))] mod with_alloc { use alloc::sync::Arc; use zeroize::Zeroize as _; @@ -21,7 +20,6 @@ mod with_alloc { } #[cfg(not(feature = "alloc"))] -#[cfg_attr(docsrs, doc(cfg(all())))] mod without_alloc { pub(super) type Internal = zeroize::Zeroizing>; @@ -34,10 +32,8 @@ mod without_alloc { } #[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(all())))] use with_alloc as imp; #[cfg(not(feature = "alloc"))] -#[cfg_attr(docsrs, doc(cfg(all())))] use without_alloc as imp; /// Point representing sensitive information (like a derived secret) diff --git a/generic-ec/src/secret_scalar/definition.rs b/generic-ec/src/secret_scalar/definition.rs index 64879c3..c8c97e5 100644 --- a/generic-ec/src/secret_scalar/definition.rs +++ b/generic-ec/src/secret_scalar/definition.rs @@ -1,7 +1,6 @@ use crate::{Curve, Scalar}; #[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(all())))] mod with_alloc { use alloc::sync::Arc; use zeroize::{Zeroize as _, Zeroizing}; @@ -21,7 +20,6 @@ mod with_alloc { } #[cfg(not(feature = "alloc"))] -#[cfg_attr(docsrs, doc(cfg(all())))] mod without_alloc { use zeroize::{Zeroize as _, Zeroizing}; @@ -41,10 +39,8 @@ mod without_alloc { } #[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(all())))] use with_alloc as imp; #[cfg(not(feature = "alloc"))] -#[cfg_attr(docsrs, doc(cfg(all())))] use without_alloc as imp; /// Scalar representing sensitive information (like secret key) From d2b2917c375770c782ef7ae347b5b665ecc48ac0 Mon Sep 17 00:00:00 2001 From: maurges Date: Wed, 17 Jun 2026 16:28:45 +0200 Subject: [PATCH 06/12] Fix readme and no-std build For some reason cargo build --no-default-features didn't fully check every definition? Signed-off-by: maurges --- README.md | 5 +++-- generic-ec/src/secret_point/definition.rs | 6 +++++- generic-ec/src/secret_point/mod.rs | 4 ++-- generic-ec/src/secret_scalar/definition.rs | 5 +---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e38bf9d..77339a9 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,13 @@ Point or scalar not meeting above requirements cannot be constructed (in safe Ru always enforced. E.g. if you're deserializing a sequence of bytes that represents an invalid point, deserialization will result into error. -### `SecretScalar` +### `SecretScalar` and `SecretPoint` Sometimes your scalar represents some sensitive value like secret key, and you want to keep it safer. `SecretScalar` is in-place replacement of `Scalar` that enforces additional security by storing the scalar value on the heap, and erasing it on drop. Its advantage is that it doesn't leave any trace -in memory dump after it's dropped (which is not guaranteed by regular `Scalar`). +in memory dump after it's dropped (which is not guaranteed by regular `Scalar`). `SecretPoint` +is a similar structure, except it acts as a replacement for `Point`. But keep in mind that we can't control the OS which could potentially load RAM page containing sensitive value to the swap disk (i.e. on your HDD/SSD) if you're running low on memory. Or it could do any other fancy stuff. diff --git a/generic-ec/src/secret_point/definition.rs b/generic-ec/src/secret_point/definition.rs index 35d0e0d..8088141 100644 --- a/generic-ec/src/secret_point/definition.rs +++ b/generic-ec/src/secret_point/definition.rs @@ -21,10 +21,14 @@ mod with_alloc { #[cfg(not(feature = "alloc"))] mod without_alloc { + use zeroize::Zeroize as _; + + use crate::{Curve, Point}; + pub(super) type Internal = zeroize::Zeroizing>; #[inline(always)] - pub fn new(point: &mut crate::Point) -> Internal { + pub fn new(point: &mut Point) -> Internal { let point_new = zeroize::Zeroizing::new(*point); point.zeroize(); point_new diff --git a/generic-ec/src/secret_point/mod.rs b/generic-ec/src/secret_point/mod.rs index 7baa4d4..41f6951 100644 --- a/generic-ec/src/secret_point/mod.rs +++ b/generic-ec/src/secret_point/mod.rs @@ -1,7 +1,7 @@ use core::fmt; use core::iter::Sum; -use crate::{errors::InvalidPoint, Curve, EncodedPoint, Point}; +use crate::{errors::InvalidPoint, Curve, Point}; use subtle::{Choice, ConstantTimeEq}; use self::definition::SecretPoint; @@ -33,7 +33,7 @@ impl SecretPoint { pub fn to_bytes( &self, compressed: bool, - ) -> alloc::boxed::Box>> { + ) -> alloc::boxed::Box>> { let bytes = zeroize::Zeroizing::new(self.as_ref().to_bytes(compressed)); alloc::boxed::Box::new(bytes) } diff --git a/generic-ec/src/secret_scalar/definition.rs b/generic-ec/src/secret_scalar/definition.rs index c8c97e5..eb2f66a 100644 --- a/generic-ec/src/secret_scalar/definition.rs +++ b/generic-ec/src/secret_scalar/definition.rs @@ -28,14 +28,11 @@ mod without_alloc { pub(super) type Inner = Zeroizing>; #[inline(always)] - pub(super) fn new(scalar: &mut Scalar) -> Self { + pub(super) fn new(scalar: &mut Scalar) -> Inner { let scalar_new = Zeroizing::new(*scalar); scalar.zeroize(); scalar_new } - - #[doc = include_str!("docs.md")] - pub struct SecretScalar(Zeroizing>); } #[cfg(feature = "alloc")] From 108a855b4537cb9fc3b2124f35ade38c795222e2 Mon Sep 17 00:00:00 2001 From: maurges Date: Mon, 22 Jun 2026 17:52:26 +0200 Subject: [PATCH 07/12] Add EncodedSecret*. More symmetry in secret point and scalar Signed-off-by: maurges --- generic-ec/src/encoded.rs | 167 ++++++++++++++++++++++++++++ generic-ec/src/lib.rs | 2 +- generic-ec/src/non_zero/mod.rs | 2 + generic-ec/src/secret_point/mod.rs | 12 +- generic-ec/src/secret_scalar/mod.rs | 26 +++-- 5 files changed, 190 insertions(+), 19 deletions(-) diff --git a/generic-ec/src/encoded.rs b/generic-ec/src/encoded.rs index deabec4..c1a0522 100644 --- a/generic-ec/src/encoded.rs +++ b/generic-ec/src/encoded.rs @@ -1,5 +1,6 @@ use core::{fmt, ops}; +use subtle::{Choice, ConstantTimeEq}; use zeroize::Zeroize; use crate::{as_raw::AsRaw, core::ByteArray, Curve}; @@ -32,6 +33,14 @@ impl Clone for EncodedPoint { } } +impl Default for EncodedPoint { + fn default() -> Self { + Self(EncodedPointInner::Uncompressed( + E::UncompressedPointArray::zeroes(), + )) + } +} + impl PartialEq for EncodedPoint { fn eq(&self, other: &Self) -> bool { self.as_bytes() == other.as_bytes() @@ -160,3 +169,161 @@ impl Zeroize for EncodedScalar { self.as_mut().zeroize() } } + +// Internals for both SecretEncodedPoint and SecretEncodingScalar depending on +// presence of alloc +#[cfg(feature = "alloc")] +mod imp { + use alloc::sync::Arc; + + pub(super) type Secret = Arc>; + + #[inline(always)] + pub(super) fn new(x: &mut T) -> Secret + where + T: zeroize::Zeroize + Default + Clone, + { + let mut value_on_heap = Arc::>::default(); + let value_mut = Arc::make_mut(&mut value_on_heap); + core::mem::swap(&mut **value_mut, x); + x.zeroize(); + value_on_heap + } + + pub(super) fn inner(x: Secret) -> T + where + T: zeroize::Zeroize + Clone, + { + (**x).clone() + } +} +#[cfg(not(feature = "alloc"))] +mod imp { + pub(super) type Secret = zeroize::Zeroizing; + + #[inline(always)] + pub(super) fn new(x: &mut T) -> Secret + where + T: zeroize::Zeroize + Clone, + { + let value_new = zeroize::Zeroizing::new(x.clone()); + x.zeroize(); + value_new + } + + pub(super) fn inner(x: Secret) -> T + where + T: zeroize::Zeroize + Clone, + { + (*x).clone() + } +} + +/// Bytes representation of a secret elliptic point. See [`SecretPoint`] for +/// more information +/// +/// This representation is automatically zeroed on drop, and doesn't implement +/// some vartime methods. You can still access the underlying bytes by calling +/// `.as_ref()`. This bypasses all the secrecy guarantees given by this struct. +/// +/// [`SecretPoint`]: crate::SecretPoint +#[derive(Clone, Default)] +pub struct EncodedSecretPoint(imp::Secret>); + +impl EncodedSecretPoint { + /// Wrap a non-secret representation + #[inline(always)] + pub fn new(mut point: EncodedPoint) -> Self { + Self(imp::new(&mut point)) + } + + /// Obtain the reference to the encoded bytes. This bypasses all the secrecy + /// guarantees, so be careful when handling them + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Convert the representation into not-secret, dropping all guarantees + pub fn into_not_secret(self) -> EncodedPoint { + imp::inner(self.0) + } +} + +impl fmt::Debug for EncodedSecretPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SecretEncodedPoint") + } +} + +impl ConstantTimeEq for EncodedSecretPoint { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl ops::Deref for EncodedSecretPoint { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl AsRef<[u8]> for EncodedSecretPoint { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +/// Bytes representation of a secret elliptic scalar. See [`SecretScalar`] for +/// more information +/// +/// This representation is automatically zeroed on drop, and doesn't implement +/// some vartime methods. You can still access the underlying bytes by calling +/// `.as_ref()`. This bypasses all the secrecy guarantees given by this struct. +/// +/// [`SecretScalar`]: crate::SecretScalar +#[derive(Clone, Default)] +pub struct EncodedSecretScalar(imp::Secret>); + +impl EncodedSecretScalar { + /// Wrap a non-secret representation + pub fn new(mut scalar: EncodedScalar) -> Self { + Self(imp::new(&mut scalar)) + } + + /// Obtain the reference to the encoded bytes. This bypasses all the secrecy + /// guarantees, so be careful when handling them + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Convert the representation into not-secret, dropping all guarantees + pub fn into_not_secret(self) -> EncodedScalar { + imp::inner(self.0) + } +} + +impl fmt::Debug for EncodedSecretScalar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SecretEncodedScalar") + } +} + +impl ConstantTimeEq for EncodedSecretScalar { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl ops::Deref for EncodedSecretScalar { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl AsRef<[u8]> for EncodedSecretScalar { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} diff --git a/generic-ec/src/lib.rs b/generic-ec/src/lib.rs index 71e92a8..bf85690 100644 --- a/generic-ec/src/lib.rs +++ b/generic-ec/src/lib.rs @@ -263,7 +263,7 @@ pub mod serde; pub use self::{ core::Curve, - encoded::{EncodedPoint, EncodedScalar}, + encoded::{EncodedPoint, EncodedScalar, EncodedSecretPoint, EncodedSecretScalar}, generator::Generator, non_zero::definition::NonZero, point::definition::Point, diff --git a/generic-ec/src/non_zero/mod.rs b/generic-ec/src/non_zero/mod.rs index 5dbbb7c..244f456 100644 --- a/generic-ec/src/non_zero/mod.rs +++ b/generic-ec/src/non_zero/mod.rs @@ -41,6 +41,7 @@ impl NonZero> { /// Convert this value into a `NonZero>`. You should do this at the end /// of computations that produce a secret, like a key exchange + #[inline(always)] // Prevent a byte copy in most cases pub fn into_secret(self) -> NonZero> { let mut point = self.into_inner(); let secret_point = SecretPoint::new(&mut point); @@ -176,6 +177,7 @@ impl NonZero> { } /// Upgrades the non-zero scalar into non-zero [`SecretScalar`] + #[inline(always)] // Prevent a byte copy in most cases pub fn into_secret(self) -> NonZero> { let mut scalar = self.into_inner(); let secret_scalar = SecretScalar::new(&mut scalar); diff --git a/generic-ec/src/secret_point/mod.rs b/generic-ec/src/secret_point/mod.rs index 41f6951..c6022b5 100644 --- a/generic-ec/src/secret_point/mod.rs +++ b/generic-ec/src/secret_point/mod.rs @@ -1,7 +1,7 @@ use core::fmt; use core::iter::Sum; -use crate::{errors::InvalidPoint, Curve, Point}; +use crate::{errors::InvalidPoint, Curve, EncodedSecretPoint, Point}; use subtle::{Choice, ConstantTimeEq}; use self::definition::SecretPoint; @@ -29,13 +29,9 @@ impl SecretPoint { } /// Encodes a point as bytes - #[cfg(feature = "alloc")] - pub fn to_bytes( - &self, - compressed: bool, - ) -> alloc::boxed::Box>> { - let bytes = zeroize::Zeroizing::new(self.as_ref().to_bytes(compressed)); - alloc::boxed::Box::new(bytes) + pub fn to_bytes(&self, compressed: bool) -> EncodedSecretPoint { + let bytes = self.as_ref().to_bytes(compressed); + EncodedSecretPoint::new(bytes) } /// Decodes a point from bytes diff --git a/generic-ec/src/secret_scalar/mod.rs b/generic-ec/src/secret_scalar/mod.rs index babf707..c2efaec 100644 --- a/generic-ec/src/secret_scalar/mod.rs +++ b/generic-ec/src/secret_scalar/mod.rs @@ -4,14 +4,22 @@ use core::iter::{Product, Sum}; use rand_core::{CryptoRng, RngCore}; use subtle::{Choice, ConstantTimeEq}; -#[cfg(feature = "alloc")] -use crate::EncodedScalar; +use crate::EncodedSecretScalar; use crate::{errors::InvalidScalar, Curve, Scalar}; use self::definition::SecretScalar; pub mod definition; +impl Scalar { + /// Convert this value into a [`SecretScalar`]. You should do this at the end + /// of computations that produce a secret, like a key exchange + #[inline(always)] // Prevent a byte copy in most cases + pub fn into_secret(mut self) -> SecretScalar { + SecretScalar::new(&mut self) + } +} + impl SecretScalar { /// Returns scalar $S = 0$ pub fn zero() -> Self { @@ -64,17 +72,15 @@ impl SecretScalar { } /// Encodes scalar as bytes in big-endian order - #[cfg(feature = "alloc")] - pub fn to_be_bytes(&self) -> alloc::boxed::Box>> { - let bytes = zeroize::Zeroizing::new(self.as_ref().to_be_bytes()); - alloc::boxed::Box::new(bytes) + pub fn to_be_bytes(&self) -> EncodedSecretScalar { + let bytes = self.as_ref().to_be_bytes(); + EncodedSecretScalar::new(bytes) } /// Encodes scalar as bytes in little-endian order - #[cfg(feature = "alloc")] - pub fn to_le_bytes(&self) -> alloc::boxed::Box>> { - let bytes = zeroize::Zeroizing::new(self.as_ref().to_le_bytes()); - alloc::boxed::Box::new(bytes) + pub fn to_le_bytes(&self) -> EncodedSecretScalar { + let bytes = self.as_ref().to_le_bytes(); + EncodedSecretScalar::new(bytes) } /// Decodes scalar from its bytes representation in big-endian order From bdeb16d774f99b6bd87cf40414c0821b0f5ba921 Mon Sep 17 00:00:00 2001 From: maurges Date: Tue, 23 Jun 2026 17:46:53 +0200 Subject: [PATCH 08/12] Remove nonsecret-leaking trait impls Signed-off-by: maurges --- generic-ec/src/encoded.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/generic-ec/src/encoded.rs b/generic-ec/src/encoded.rs index c1a0522..05d0d6e 100644 --- a/generic-ec/src/encoded.rs +++ b/generic-ec/src/encoded.rs @@ -261,19 +261,6 @@ impl ConstantTimeEq for EncodedSecretPoint { } } -impl ops::Deref for EncodedSecretPoint { - type Target = [u8]; - fn deref(&self) -> &[u8] { - self.as_bytes() - } -} - -impl AsRef<[u8]> for EncodedSecretPoint { - fn as_ref(&self) -> &[u8] { - self.as_bytes() - } -} - /// Bytes representation of a secret elliptic scalar. See [`SecretScalar`] for /// more information /// @@ -314,16 +301,3 @@ impl ConstantTimeEq for EncodedSecretScalar { self.0.ct_eq(&other.0) } } - -impl ops::Deref for EncodedSecretScalar { - type Target = [u8]; - fn deref(&self) -> &[u8] { - self.as_bytes() - } -} - -impl AsRef<[u8]> for EncodedSecretScalar { - fn as_ref(&self) -> &[u8] { - self.as_bytes() - } -} From 33495a7c7532af3598496f02eb663b14893d6b92 Mon Sep 17 00:00:00 2001 From: maurges Date: Tue, 23 Jun 2026 18:00:46 +0200 Subject: [PATCH 09/12] Unify the definitions of all secret structs Signed-off-by: maurges --- generic-ec/src/encoded.rs | 63 ++------------ generic-ec/src/lib.rs | 5 +- generic-ec/src/secret.rs | 55 ++++++++++++ .../{secret_point/mod.rs => secret_point.rs} | 46 +++++++++- generic-ec/src/secret_point/definition.rs | 85 ------------------- .../mod.rs => secret_scalar.rs} | 48 +++++++++-- generic-ec/src/secret_scalar/definition.rs | 85 ------------------- 7 files changed, 150 insertions(+), 237 deletions(-) create mode 100644 generic-ec/src/secret.rs rename generic-ec/src/{secret_point/mod.rs => secret_point.rs} (52%) delete mode 100644 generic-ec/src/secret_point/definition.rs rename generic-ec/src/{secret_scalar/mod.rs => secret_scalar.rs} (72%) delete mode 100644 generic-ec/src/secret_scalar/definition.rs diff --git a/generic-ec/src/encoded.rs b/generic-ec/src/encoded.rs index 05d0d6e..7218919 100644 --- a/generic-ec/src/encoded.rs +++ b/generic-ec/src/encoded.rs @@ -3,7 +3,7 @@ use core::{fmt, ops}; use subtle::{Choice, ConstantTimeEq}; use zeroize::Zeroize; -use crate::{as_raw::AsRaw, core::ByteArray, Curve}; +use crate::{as_raw::AsRaw, core::ByteArray, secret, Curve}; /// Bytes representation of an elliptic point #[derive(Zeroize)] @@ -170,55 +170,6 @@ impl Zeroize for EncodedScalar { } } -// Internals for both SecretEncodedPoint and SecretEncodingScalar depending on -// presence of alloc -#[cfg(feature = "alloc")] -mod imp { - use alloc::sync::Arc; - - pub(super) type Secret = Arc>; - - #[inline(always)] - pub(super) fn new(x: &mut T) -> Secret - where - T: zeroize::Zeroize + Default + Clone, - { - let mut value_on_heap = Arc::>::default(); - let value_mut = Arc::make_mut(&mut value_on_heap); - core::mem::swap(&mut **value_mut, x); - x.zeroize(); - value_on_heap - } - - pub(super) fn inner(x: Secret) -> T - where - T: zeroize::Zeroize + Clone, - { - (**x).clone() - } -} -#[cfg(not(feature = "alloc"))] -mod imp { - pub(super) type Secret = zeroize::Zeroizing; - - #[inline(always)] - pub(super) fn new(x: &mut T) -> Secret - where - T: zeroize::Zeroize + Clone, - { - let value_new = zeroize::Zeroizing::new(x.clone()); - x.zeroize(); - value_new - } - - pub(super) fn inner(x: Secret) -> T - where - T: zeroize::Zeroize + Clone, - { - (*x).clone() - } -} - /// Bytes representation of a secret elliptic point. See [`SecretPoint`] for /// more information /// @@ -228,13 +179,13 @@ mod imp { /// /// [`SecretPoint`]: crate::SecretPoint #[derive(Clone, Default)] -pub struct EncodedSecretPoint(imp::Secret>); +pub struct EncodedSecretPoint(secret::Secret>); impl EncodedSecretPoint { /// Wrap a non-secret representation #[inline(always)] pub fn new(mut point: EncodedPoint) -> Self { - Self(imp::new(&mut point)) + Self(secret::new(&mut point)) } /// Obtain the reference to the encoded bytes. This bypasses all the secrecy @@ -245,7 +196,7 @@ impl EncodedSecretPoint { /// Convert the representation into not-secret, dropping all guarantees pub fn into_not_secret(self) -> EncodedPoint { - imp::inner(self.0) + secret::inner(self.0) } } @@ -270,12 +221,12 @@ impl ConstantTimeEq for EncodedSecretPoint { /// /// [`SecretScalar`]: crate::SecretScalar #[derive(Clone, Default)] -pub struct EncodedSecretScalar(imp::Secret>); +pub struct EncodedSecretScalar(secret::Secret>); impl EncodedSecretScalar { /// Wrap a non-secret representation pub fn new(mut scalar: EncodedScalar) -> Self { - Self(imp::new(&mut scalar)) + Self(secret::new(&mut scalar)) } /// Obtain the reference to the encoded bytes. This bypasses all the secrecy @@ -286,7 +237,7 @@ impl EncodedSecretScalar { /// Convert the representation into not-secret, dropping all guarantees pub fn into_not_secret(self) -> EncodedScalar { - imp::inner(self.0) + secret::inner(self.0) } } diff --git a/generic-ec/src/lib.rs b/generic-ec/src/lib.rs index bf85690..5cb6df2 100644 --- a/generic-ec/src/lib.rs +++ b/generic-ec/src/lib.rs @@ -208,6 +208,7 @@ pub mod multiscalar; mod non_zero; mod point; mod scalar; +mod secret; mod secret_point; mod secret_scalar; @@ -268,8 +269,8 @@ pub use self::{ non_zero::definition::NonZero, point::definition::Point, scalar::{Radix16Iter, Scalar}, - secret_point::definition::SecretPoint, - secret_scalar::definition::SecretScalar, + secret_point::SecretPoint, + secret_scalar::SecretScalar, }; /// Curves supported out of the box diff --git a/generic-ec/src/secret.rs b/generic-ec/src/secret.rs new file mode 100644 index 0000000..a696fc0 --- /dev/null +++ b/generic-ec/src/secret.rs @@ -0,0 +1,55 @@ +//! Internals for both Secret structs, like `SecretScalar` and `SecretEncodedPoint`. They have a different representation based on the presence of `alloc`: either a reference-counted nonmoving one, or a moving value + +#[cfg(feature = "alloc")] +mod imp { + use alloc::sync::Arc; + + pub type Secret = Arc>; + + #[inline(always)] + pub fn new(x: &mut T) -> Secret + where + T: zeroize::Zeroize + Default + Clone, + { + let mut value_on_heap = Arc::>::default(); + let value_mut = Arc::make_mut(&mut value_on_heap); + core::mem::swap(&mut **value_mut, x); + x.zeroize(); + value_on_heap + } + + pub fn inner(x: Secret) -> T + where + T: zeroize::Zeroize + Clone, + { + (**x).clone() + } +} +#[cfg(not(feature = "alloc"))] +mod imp { + pub type Secret = zeroize::Zeroizing; + + #[inline(always)] + pub fn new(x: &mut T) -> Secret + where + T: zeroize::Zeroize + Clone, + { + let value_new = zeroize::Zeroizing::new(x.clone()); + x.zeroize(); + value_new + } + + pub fn inner(x: Secret) -> T + where + T: zeroize::Zeroize + Clone, + { + (*x).clone() + } +} + +/// Unwrap the Secret into the underlying type +pub(crate) use imp::inner; +/// Wrap the underlying type into the Secret repr +pub(crate) use imp::new; +/// Type used as the representation +pub(crate) use imp::Secret; diff --git a/generic-ec/src/secret_point/mod.rs b/generic-ec/src/secret_point.rs similarity index 52% rename from generic-ec/src/secret_point/mod.rs rename to generic-ec/src/secret_point.rs index c6022b5..40b1def 100644 --- a/generic-ec/src/secret_point/mod.rs +++ b/generic-ec/src/secret_point.rs @@ -1,12 +1,29 @@ use core::fmt; use core::iter::Sum; -use crate::{errors::InvalidPoint, Curve, EncodedSecretPoint, Point}; +use crate::{errors::InvalidPoint, secret, Curve, EncodedSecretPoint, Point}; use subtle::{Choice, ConstantTimeEq}; -use self::definition::SecretPoint; - -pub mod definition; +/// Point representing sensitive information (like a derived secret) +/// +/// Secret point should be treated with an extra care. You shouldn't do any +/// branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel +/// attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). +/// +/// Also, when `alloc` feature is enabled, we enforce extra measures: +/// +/// * Secret point leaves no trace in RAM after it's dropped \ +/// Memory is zeroized after use +/// * All clones of a secret point refer to the same region in the memory \ +/// I.e. there will always be only one instance of the point in the memory +/// no matter how many clones you make +/// +/// All these guarantees can be bypassed by calling `.as_ref()` and obtaining +/// `&Point` that is not protected from timing attacks, leaving traces in +/// the memory, etc. +/// +/// [`ConstantTimeEq`]: subtle::ConstantTimeEq +pub struct SecretPoint(secret::Secret>); impl Point { /// Convert this value into a [`SecretPoint`]. You should do this at the end @@ -18,6 +35,15 @@ impl Point { } impl SecretPoint { + /// Constructs a new secret point + /// + /// Takes the original point by mutable reference instead of taking by value + /// to avoid leaving copies of the point on stack. Point behind the + /// reference will be zeroized after the function has returned. + pub fn new(point: &mut Point) -> Self { + Self(secret::new(point)) + } + /// Returns the generator defined in the curve specs pub fn generator() -> Self { Self::new(&mut Point::generator().into()) @@ -41,6 +67,18 @@ impl SecretPoint { } } +impl AsRef> for SecretPoint { + fn as_ref(&self) -> &Point { + &self.0 + } +} + +impl Clone for SecretPoint { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + impl ConstantTimeEq for SecretPoint { fn ct_eq(&self, other: &Self) -> Choice { self.as_ref().ct_eq(other.as_ref()) diff --git a/generic-ec/src/secret_point/definition.rs b/generic-ec/src/secret_point/definition.rs deleted file mode 100644 index 8088141..0000000 --- a/generic-ec/src/secret_point/definition.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::{Curve, Point}; - -#[cfg(feature = "alloc")] -mod with_alloc { - use alloc::sync::Arc; - use zeroize::Zeroize as _; - - use crate::{Curve, Point}; - - pub(super) type Internal = Arc>>; - - #[inline(always)] - pub(super) fn new(point: &mut Point) -> Internal { - let mut point_on_heap = Arc::>>::default(); - let point_mut = Arc::make_mut(&mut point_on_heap); - core::mem::swap(&mut **point_mut, point); - point.zeroize(); - point_on_heap - } -} - -#[cfg(not(feature = "alloc"))] -mod without_alloc { - use zeroize::Zeroize as _; - - use crate::{Curve, Point}; - - pub(super) type Internal = zeroize::Zeroizing>; - - #[inline(always)] - pub fn new(point: &mut Point) -> Internal { - let point_new = zeroize::Zeroizing::new(*point); - point.zeroize(); - point_new - } -} - -#[cfg(feature = "alloc")] -use with_alloc as imp; -#[cfg(not(feature = "alloc"))] -use without_alloc as imp; - -/// Point representing sensitive information (like a derived secret) -/// -/// Secret point should be treated with an extra care. You shouldn't do any -/// branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel -/// attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). -/// -/// Also, when `alloc` feature is enabled, we enforce extra measures: -/// -/// * Secret point leaves no trace in RAM after it's dropped \ -/// Memory is zeroized after use -/// * All clones of a secret point refer to the same region in the memory \ -/// I.e. there will always be only one instance of the point in the memory -/// no matter how many clones you make -/// -/// All these guarantees can be bypassed by calling `.as_ref()` and obtaining -/// `&Point` that is not protected from timing attacks, leaving traces in -/// the memory, etc. -/// -/// [`ConstantTimeEq`]: subtle::ConstantTimeEq -pub struct SecretPoint(imp::Internal); - -impl SecretPoint { - /// Constructs a new secret point - /// - /// Takes the original point by mutable reference instead of taking by value - /// to avoid leaving copies of the point on stack. Point behind the - /// reference will be zeroized after the function has returned. - pub fn new(point: &mut Point) -> Self { - Self(imp::new(point)) - } -} - -impl AsRef> for SecretPoint { - fn as_ref(&self) -> &Point { - &self.0 - } -} - -impl Clone for SecretPoint { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} diff --git a/generic-ec/src/secret_scalar/mod.rs b/generic-ec/src/secret_scalar.rs similarity index 72% rename from generic-ec/src/secret_scalar/mod.rs rename to generic-ec/src/secret_scalar.rs index c2efaec..60a20ea 100644 --- a/generic-ec/src/secret_scalar/mod.rs +++ b/generic-ec/src/secret_scalar.rs @@ -5,11 +5,28 @@ use rand_core::{CryptoRng, RngCore}; use subtle::{Choice, ConstantTimeEq}; use crate::EncodedSecretScalar; -use crate::{errors::InvalidScalar, Curve, Scalar}; - -use self::definition::SecretScalar; - -pub mod definition; +use crate::{errors::InvalidScalar, secret, Curve, Scalar}; + +/// Scalar representing sensitive information (like secret key) +/// +/// Secret scalar should be treated with an extra care. You shouldn't do any +/// branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel +/// attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). +/// +/// Also, when `alloc` feature is enabled, we enforce extra measures: +/// +/// * Secret scalar leaves no trace in RAM after it's dropped \ +/// Memory is zeroized after use +/// * All clones of secret scalar refer to the same region in the memory \ +/// I.e. there will always be only one instance of the scalar in the memory +/// no matter how many clones you make +/// +/// All these guarantees can be bypassed by calling `.as_ref()` and obtaining +/// `&Scalar` that is not protected from timing attacks, leaving traces in +/// the memory, etc. +/// +/// [`ConstantTimeEq`]: subtle::ConstantTimeEq +pub struct SecretScalar(secret::Secret>); impl Scalar { /// Convert this value into a [`SecretScalar`]. You should do this at the end @@ -21,6 +38,15 @@ impl Scalar { } impl SecretScalar { + /// Constructs a new secret scalar + /// + /// Takes the original scalar by mutable reference instead of taking by value to + /// avoid leaving copies of the scalar on stack. Scalar behind the reference will + /// be zeroized after the function has returned. + pub fn new(scalar: &mut Scalar) -> Self { + Self(secret::new(scalar)) + } + /// Returns scalar $S = 0$ pub fn zero() -> Self { Self::new(&mut Scalar::zero()) @@ -96,6 +122,18 @@ impl SecretScalar { } } +impl AsRef> for SecretScalar { + fn as_ref(&self) -> &Scalar { + &self.0 + } +} + +impl Clone for SecretScalar { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + impl ConstantTimeEq for SecretScalar { fn ct_eq(&self, other: &Self) -> Choice { self.as_ref().ct_eq(other.as_ref()) diff --git a/generic-ec/src/secret_scalar/definition.rs b/generic-ec/src/secret_scalar/definition.rs deleted file mode 100644 index eb2f66a..0000000 --- a/generic-ec/src/secret_scalar/definition.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::{Curve, Scalar}; - -#[cfg(feature = "alloc")] -mod with_alloc { - use alloc::sync::Arc; - use zeroize::{Zeroize as _, Zeroizing}; - - use crate::{Curve, Scalar}; - - pub(super) type Inner = Arc>>; - - #[inline(always)] - pub(super) fn new(scalar: &mut Scalar) -> Inner { - let mut scalar_on_heap = Arc::>>::default(); - let scalar_mut = Arc::make_mut(&mut scalar_on_heap); - core::mem::swap(&mut **scalar_mut, scalar); - scalar.zeroize(); - scalar_on_heap - } -} - -#[cfg(not(feature = "alloc"))] -mod without_alloc { - use zeroize::{Zeroize as _, Zeroizing}; - - use crate::{Curve, Scalar}; - - pub(super) type Inner = Zeroizing>; - - #[inline(always)] - pub(super) fn new(scalar: &mut Scalar) -> Inner { - let scalar_new = Zeroizing::new(*scalar); - scalar.zeroize(); - scalar_new - } -} - -#[cfg(feature = "alloc")] -use with_alloc as imp; -#[cfg(not(feature = "alloc"))] -use without_alloc as imp; - -/// Scalar representing sensitive information (like secret key) -/// -/// Secret scalar should be treated with an extra care. You shouldn't do any -/// branching (e.g. `Eq`, `Ord`) on the secret to avoid timing side-channel -/// attacks, so it implements only constant time traits (like [`ConstantTimeEq`]). -/// -/// Also, when `alloc` feature is enabled, we enforce extra measures: -/// -/// * Secret scalar leaves no trace in RAM after it's dropped \ -/// Memory is zeroized after use -/// * All clones of secret scalar refer to the same region in the memory \ -/// I.e. there will always be only one instance of the scalar in the memory -/// no matter how many clones you make -/// -/// All these guarantees can be bypassed by calling `.as_ref()` and obtaining -/// `&Scalar` that is not protected from timing attacks, leaving traces in -/// the memory, etc. -/// -/// [`ConstantTimeEq`]: subtle::ConstantTimeEq -pub struct SecretScalar(imp::Inner); - -impl SecretScalar { - /// Constructs a new secret scalar - /// - /// Takes the original scalar by mutable reference instead of taking by value to - /// avoid leaving copies of the scalar on stack. Scalar behind the reference will - /// be zeroized after the function has returned. - pub fn new(scalar: &mut Scalar) -> Self { - Self(imp::new(scalar)) - } -} - -impl AsRef> for SecretScalar { - fn as_ref(&self) -> &Scalar { - &self.0 - } -} - -impl Clone for SecretScalar { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} From b4fb648c798620219d46965b000f1eb99c33227a Mon Sep 17 00:00:00 2001 From: maurges Date: Mon, 6 Jul 2026 14:49:50 +0200 Subject: [PATCH 10/12] Rethink converting secret to inner Signed-off-by: maurges --- generic-ec/src/encoded.rs | 28 ++++++++++++++++++---------- generic-ec/src/secret.rs | 18 ++++++++++-------- generic-ec/src/secret_point.rs | 13 ++++++++++++- generic-ec/src/secret_scalar.rs | 13 ++++++++++++- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/generic-ec/src/encoded.rs b/generic-ec/src/encoded.rs index 7218919..1256f80 100644 --- a/generic-ec/src/encoded.rs +++ b/generic-ec/src/encoded.rs @@ -189,14 +189,18 @@ impl EncodedSecretPoint { } /// Obtain the reference to the encoded bytes. This bypasses all the secrecy - /// guarantees, so be careful when handling them - pub fn as_bytes(&self) -> &[u8] { + /// guarantees, so be careful when handling them; ideally this reference should not be + /// held for longer than one expression + pub fn as_nonsecret_bytes(&self) -> &[u8] { self.0.as_bytes() } - /// Convert the representation into not-secret, dropping all guarantees - pub fn into_not_secret(self) -> EncodedPoint { - secret::inner(self.0) + /// Obtain the reference to the encoded point, which you can use as bytes + /// when necessary. This bypasses all the secrecy guarantees, so be careful + /// when handling the resulting value; ideally this reference should not be + /// held for longer than one expression + pub fn as_nonsecret(&self) -> &EncodedPoint { + secret::inner_ref(&self.0) } } @@ -230,14 +234,18 @@ impl EncodedSecretScalar { } /// Obtain the reference to the encoded bytes. This bypasses all the secrecy - /// guarantees, so be careful when handling them - pub fn as_bytes(&self) -> &[u8] { + /// guarantees, so be careful when handling them; ideally this reference should not be + /// held for longer than one expression + pub fn as_nonsecret_bytes(&self) -> &[u8] { self.0.as_bytes() } - /// Convert the representation into not-secret, dropping all guarantees - pub fn into_not_secret(self) -> EncodedScalar { - secret::inner(self.0) + /// Obtain the reference to the encoded scalar, which you can use as bytes + /// when necessary. This bypasses all the secrecy guarantees, so be careful + /// when handling the resulting value; ideally this reference should not be + /// held for longer than one expression + pub fn as_nonsecret(&self) -> &EncodedScalar { + secret::inner_ref(&self.0) } } diff --git a/generic-ec/src/secret.rs b/generic-ec/src/secret.rs index a696fc0..6068302 100644 --- a/generic-ec/src/secret.rs +++ b/generic-ec/src/secret.rs @@ -1,4 +1,6 @@ -//! Internals for both Secret structs, like `SecretScalar` and `SecretEncodedPoint`. They have a different representation based on the presence of `alloc`: either a reference-counted nonmoving one, or a moving value +//! Internals for Secret structs, like `SecretScalar` and `SecretEncodedPoint`. +//! They have a different representation based on the presence of `alloc`: +//! either a reference-counted pinned one, or an unpinned value #[cfg(feature = "alloc")] mod imp { @@ -18,11 +20,11 @@ mod imp { value_on_heap } - pub fn inner(x: Secret) -> T + pub fn inner_ref(x: &Secret) -> &T where - T: zeroize::Zeroize + Clone, + T: zeroize::Zeroize, { - (**x).clone() + x.as_ref() } } #[cfg(not(feature = "alloc"))] @@ -39,16 +41,16 @@ mod imp { value_new } - pub fn inner(x: Secret) -> T + pub fn inner_ref(x: &Secret) -> &T where - T: zeroize::Zeroize + Clone, + T: zeroize::Zeroize, { - (*x).clone() + x.as_ref() } } /// Unwrap the Secret into the underlying type -pub(crate) use imp::inner; +pub(crate) use imp::inner_ref; /// Wrap the underlying type into the Secret repr pub(crate) use imp::new; /// Type used as the representation diff --git a/generic-ec/src/secret_point.rs b/generic-ec/src/secret_point.rs index 40b1def..511f200 100644 --- a/generic-ec/src/secret_point.rs +++ b/generic-ec/src/secret_point.rs @@ -54,6 +54,17 @@ impl SecretPoint { Self::new(&mut Point::zero()) } + /// Obtain the reference to the point, which you can use for any operations + /// necessary. This bypasses all the secrecy guarantees, so be careful when + /// handling the resulting value; ideally this reference should not be held + /// for longer than one expression + /// + /// The [`AsRef`] impl uses this method under the hood, and is provided as a + /// less explicit but very convenient alternative + pub fn as_nonsecret(&self) -> &Point { + secret::inner_ref(&self.0) + } + /// Encodes a point as bytes pub fn to_bytes(&self, compressed: bool) -> EncodedSecretPoint { let bytes = self.as_ref().to_bytes(compressed); @@ -69,7 +80,7 @@ impl SecretPoint { impl AsRef> for SecretPoint { fn as_ref(&self) -> &Point { - &self.0 + self.as_nonsecret() } } diff --git a/generic-ec/src/secret_scalar.rs b/generic-ec/src/secret_scalar.rs index 60a20ea..ab37424 100644 --- a/generic-ec/src/secret_scalar.rs +++ b/generic-ec/src/secret_scalar.rs @@ -97,6 +97,17 @@ impl SecretScalar { Self::random(&mut rng) } + /// Obtain the reference to the scalar, which you can use for any operations + /// necessary. This bypasses all the secrecy guarantees, so be careful when + /// handling the resulting value; ideally this reference should not be held + /// for longer than one expression + /// + /// The [`AsRef`] impl uses this method under the hood, and is provided as a + /// less explicit but very convenient alternative + pub fn as_nonsecret(&self) -> &Scalar { + secret::inner_ref(&self.0) + } + /// Encodes scalar as bytes in big-endian order pub fn to_be_bytes(&self) -> EncodedSecretScalar { let bytes = self.as_ref().to_be_bytes(); @@ -124,7 +135,7 @@ impl SecretScalar { impl AsRef> for SecretScalar { fn as_ref(&self) -> &Scalar { - &self.0 + self.as_nonsecret() } } From f276ca155efffc912aef21e3c3d76da4ab898191 Mon Sep 17 00:00:00 2001 From: maurges Date: Mon, 6 Jul 2026 15:57:44 +0200 Subject: [PATCH 11/12] Fix nostd build again Signed-off-by: maurges --- generic-ec/src/secret.rs | 2 +- wasm/nostd/Cargo.lock | 45 +++++++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/generic-ec/src/secret.rs b/generic-ec/src/secret.rs index 6068302..634e1ee 100644 --- a/generic-ec/src/secret.rs +++ b/generic-ec/src/secret.rs @@ -45,7 +45,7 @@ mod imp { where T: zeroize::Zeroize, { - x.as_ref() + core::ops::Deref::deref(x) } } diff --git a/wasm/nostd/Cargo.lock b/wasm/nostd/Cargo.lock index d69310f..5a9bced 100644 --- a/wasm/nostd/Cargo.lock +++ b/wasm/nostd/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addchain" @@ -222,12 +222,15 @@ dependencies = [ [[package]] name = "generic-ec" -version = "0.1.4" +version = "0.5.0" dependencies = [ + "curve25519-dalek", + "digest", "generic-ec-core", "generic-ec-curves", "phantom-type", "rand_core", + "rand_hash", "subtle", "udigest", "zeroize", @@ -235,7 +238,7 @@ dependencies = [ [[package]] name = "generic-ec-core" -version = "0.1.2" +version = "0.3.0" dependencies = [ "generic-array", "rand_core", @@ -245,15 +248,15 @@ dependencies = [ [[package]] name = "generic-ec-curves" -version = "0.1.3" +version = "0.3.0" dependencies = [ - "crypto-bigint", "curve25519-dalek", "elliptic-curve", "generic-ec-core", "group", "k256", "p256", + "p384", "rand_core", "sha2", "stark-curve", @@ -263,8 +266,9 @@ dependencies = [ [[package]] name = "generic-ec-zkp" -version = "0.1.2" +version = "0.5.0" dependencies = [ + "digest", "generic-array", "generic-ec", "rand_core", @@ -364,6 +368,16 @@ dependencies = [ "primeorder", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "elliptic-curve", + "primeorder", +] + [[package]] name = "phantom-type" version = "0.4.2" @@ -412,6 +426,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bc1dd921383c6564eb0b8252f5b3f6622b84d40c6e35f5e6790e1fd7abb7a9" +dependencies = [ + "digest", + "rand_core", + "udigest", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -500,9 +525,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "udigest" -version = "0.1.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e4a5cb9599a5dc3d47f21ad605a5a14594e0b212c7627d68dd5ca9776ace2e" +checksum = "19ff079a60bd5dc98b364ce7b5a633a8937bf558f5d19c9a390f5ae1973cf07e" dependencies = [ "digest", "udigest-derive", @@ -510,9 +535,9 @@ dependencies = [ [[package]] name = "udigest-derive" -version = "0.1.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b29f121da05aa0857e7b96cf2f8782bd4140911506518486d4a125b97d7d609" +checksum = "25fd5248861b973cd5d1da5604b0ce22a35fa77f015d9f7ed9ab57078205bb86" dependencies = [ "proc-macro2", "quote", From 9d5c76f0599915c1c6fafcb1139d436a3a725416 Mon Sep 17 00:00:00 2001 From: maurges Date: Mon, 13 Jul 2026 16:26:09 +0200 Subject: [PATCH 12/12] Bump generic-ec version Signed-off-by: maurges --- generic-ec/CHANGELOG.md | 7 +++++++ generic-ec/Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/generic-ec/CHANGELOG.md b/generic-ec/CHANGELOG.md index f34a90c..204eb78 100644 --- a/generic-ec/CHANGELOG.md +++ b/generic-ec/CHANGELOG.md @@ -1,3 +1,10 @@ +## v0.5.1 +* Add `SecretPoint`, `EncodedSecretPoint`, `EncodedSecretScalar`; add more + methods to `SecretScalar` [#92] +* `EncodedPoint` and `EncodedScalar` are now `Zeroize` [#92] + +[#92]: https://github.com/LFDT-Lockness/generic-ec/pull/92 + ## v0.5.0 * Hash to scalar: guarantee reproducibility on all platforms [#55] * Use reproducible & constant-time algorithm for random scalar diff --git a/generic-ec/Cargo.toml b/generic-ec/Cargo.toml index c54f9f2..2c5ef25 100644 --- a/generic-ec/Cargo.toml +++ b/generic-ec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "generic-ec" -version = "0.5.0" +version = "0.5.1" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/LFDT-Lockness/generic-ec"