From 4f3a2b3f78d70a8c3d9b37e90ee8d0b80c1dc9f6 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 29 Jan 2025 12:02:32 +0200 Subject: [PATCH 01/15] Affine coordinate are not correctly exposed from the underlying type. ProjectivePoints have 3 coordinates (x,y,z) and the AffinePoint incorrectly expose just the (x, y) coordinate without converting the representation from projective to affine. --- crates/starknet-types-core/src/curve/affine_point.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/starknet-types-core/src/curve/affine_point.rs b/crates/starknet-types-core/src/curve/affine_point.rs index 76a73b92..04f515a6 100644 --- a/crates/starknet-types-core/src/curve/affine_point.rs +++ b/crates/starknet-types-core/src/curve/affine_point.rs @@ -44,12 +44,12 @@ impl AffinePoint { /// Returns the `x` coordinate of the point. pub fn x(&self) -> Felt { - Felt(*self.0.x()) + Felt(*self.0.to_affine().x()) } /// Returns the `y` coordinate of the point. pub fn y(&self) -> Felt { - Felt(*self.0.y()) + Felt(*self.0.to_affine().y()) } // Returns the generator point of the StarkCurve @@ -70,7 +70,7 @@ impl core::ops::Add for AffinePoint { type Output = AffinePoint; fn add(self, rhs: Self) -> Self::Output { - AffinePoint(self.0.operate_with_affine(&rhs.0)) + AffinePoint(self.0.operate_with_affine(&rhs.0.to_affine())) } } From 9c0676eaa8151f36a56b00aa4238768c8f1a589c Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 29 Jan 2025 19:38:56 +0200 Subject: [PATCH 02/15] Fix the `Add` implementation --- crates/starknet-types-core/src/curve/affine_point.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/starknet-types-core/src/curve/affine_point.rs b/crates/starknet-types-core/src/curve/affine_point.rs index 04f515a6..5ee09db2 100644 --- a/crates/starknet-types-core/src/curve/affine_point.rs +++ b/crates/starknet-types-core/src/curve/affine_point.rs @@ -70,7 +70,7 @@ impl core::ops::Add for AffinePoint { type Output = AffinePoint; fn add(self, rhs: Self) -> Self::Output { - AffinePoint(self.0.operate_with_affine(&rhs.0.to_affine())) + AffinePoint(self.0.operate_with(&rhs.0)) } } From bd27423768984521d28c605ee8dfcfc6f0bbde60 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Sat, 26 Jul 2025 18:47:57 +0300 Subject: [PATCH 03/15] Implement SecretFelt, a secure wrapper for Felt that will avoid leaking secrets by zeroizing the memory at the end Remove the zeroized implementation for the Felt --- crates/starknet-types-core/Cargo.toml | 2 +- crates/starknet-types-core/src/felt/mod.rs | 4 +- .../src/felt/secret_felt.rs | 201 ++++++++++++++++++ .../starknet-types-core/src/felt/zeroize.rs | 17 -- 4 files changed, 204 insertions(+), 20 deletions(-) create mode 100644 crates/starknet-types-core/src/felt/secret_felt.rs delete mode 100644 crates/starknet-types-core/src/felt/zeroize.rs diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index f603f359..3476d72a 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -50,7 +50,7 @@ serde = ["alloc", "dep:serde"] prime-bigint = ["dep:lazy_static"] num-traits = [] papyrus-serialization = ["std"] -zeroize = ["dep:zeroize"] +secret_felt = ["dep:zeroize"] [dev-dependencies] proptest = { version = "1.5", default-features = false, features = ["alloc", "proptest-macro"] } diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index fa0b39a5..6577d49d 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -16,8 +16,8 @@ mod prime_bigint; mod primitive_conversions; #[cfg(feature = "serde")] mod serde; -#[cfg(feature = "zeroize")] -mod zeroize; +#[cfg(feature = "secret_felt")] +pub mod secret_felt; use lambdaworks_math::errors::CreationError; pub use non_zero::{FeltIsZeroError, NonZeroFelt}; diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs new file mode 100644 index 00000000..3074d3e6 --- /dev/null +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -0,0 +1,201 @@ +use crate::felt::{Felt, FromStrError}; +use lambdaworks_math::errors::ByteConversionError; +use zeroize::Zeroize; + +/// A wrapper for a [Felt] that ensures the value is securely zeroized when dropped. +/// +/// This type provides secure handling of sensitive [Felt] values (like private keys) +/// by ensuring that the memory is properly cleared when the value is no longer needed. +pub struct SecretFelt(Box); + +impl zeroize::DefaultIsZeroes for Felt {} + +impl Zeroize for SecretFelt { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Drop for SecretFelt { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl SecretFelt { + /// Creates a new [SecretFelt] from a [Felt] value and zeroize the original. + /// + /// It takes a mutable reference to a [Felt] value, creates a copy, + /// and then zeroize the original value to ensure it doesn't remain in memory. + /// + /// # Example + /// + /// ``` + /// use starknet_types_core::felt::{Felt, secret_felt::SecretFelt}; + /// + /// let mut private_key = Felt::from_hex_unchecked("0x123..."); + /// let secret_felt = SecretFelt::from_felt(&mut private_key); + /// // private_key is now zeroized (set to Felt::ZERO) + /// ``` + pub fn from_felt(secret_scalar: &mut Felt) -> Self { + let boxed_copy = Box::new(*secret_scalar); + secret_scalar.zeroize(); + Self(boxed_copy) + } + + /// Creates a new [SecretFelt] from a hex String and zeroized the original String. + /// + /// # Example + /// ``` + /// use starknet_types_core::felt::secret_felt::SecretFelt; + /// use std::str::FromStr; + /// + /// // make sure the String is initialized in a secure way + /// let mut private_key = String::from_utf8(vec![255,255,..]).unwrap(); + /// let secret_felt = SecretFelt::from_hex_string(&mut private_key); + /// ``` + pub fn from_hex_string(hex: &mut String) -> Result { + let secret_felt = Felt::from_hex(&hex)?; + hex.zeroize(); + Ok(Self(Box::new(secret_felt))) + } + + /// Creates a new [SecretFelt] from its big-endian representation in a Vec of length 32. + /// Internally it uses [from_bytes_be](Felt::from_bytes_be). + pub fn from_bytes_be(secret: &mut Vec) -> Result { + let mut value: [u8; 32] = secret + .as_slice() + .try_into() + .map_err(|_| ByteConversionError::InvalidValue)?; + let secret_felt = Self(Box::new(Felt::from_bytes_be(&value))); + secret.zeroize(); + value.zeroize(); + Ok(secret_felt) + } + + /// Creates a new [SecretFelt] from its little-endian representation in a Vec of length 32. + /// Internally it uses [from_bytes_le](Felt::from_bytes_le). + pub fn from_bytes_le(secret: &mut Vec) -> Result { + let mut value: [u8; 32] = secret + .as_slice() + .try_into() + .map_err(|_| ByteConversionError::InvalidValue)?; + let secret_felt = Self(Box::new(Felt::from_bytes_le(&value))); + secret.zeroize(); + value.zeroize(); + Ok(secret_felt) + } + + /// Provides reference access to the secret scalar. + /// + /// # Warning + /// + /// Be careful not to copy the value elsewhere, as that would defeat + /// the security guarantees of this type. + pub fn secret_scalar(&self) -> &Felt { + &self.0 + } +} + +#[cfg(test)] +mod test { + use crate::felt::{secret_felt::SecretFelt, Felt}; + use core::mem::size_of; + use std::str::FromStr; + use zeroize::Zeroize; + + #[test] + fn test_zeroize_secret_felt() { + let mut private_key = Felt::from_hex_unchecked( + "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ); + + let mut signing_key = SecretFelt::from_felt(&mut private_key); + signing_key.zeroize(); + + // Get a pointer to the inner Felt + let ptr = signing_key.secret_scalar() as *const Felt as *const u8; + let after_zeroize = unsafe { std::slice::from_raw_parts(ptr, size_of::()) }; + + // Check that the memory is zeroed + assert_eq!( + Felt::from_bytes_be_slice(after_zeroize), + Felt::ZERO, + "Memory was not properly zeroized" + ); + } + + #[test] + fn test_zeroize_original() { + let mut private_key = Felt::from_hex_unchecked( + "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ); + let mut signing_key = SecretFelt::from_felt(&mut private_key); + signing_key.zeroize(); + + // Get a pointer to the original memory + let ptr = private_key.as_ref() as *const Felt as *const u8; + let after_zeroize = unsafe { std::slice::from_raw_parts(ptr, size_of::()) }; + + // Check that original value was zeroized + assert_eq!( + Felt::from_bytes_be_slice(after_zeroize), + Felt::ZERO, + "Original value was not properly zeroized" + ); + } + + #[test] + fn test_zeroize_hex_string() { + let mut private_key = + String::from_str(&"0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + .unwrap(); + + let mut signing_key = SecretFelt::from_hex_string(&mut private_key).unwrap(); + signing_key.zeroize(); + + let ptr = private_key.as_ptr() as *const Felt as *const u8; + let after_zeroize = unsafe { std::slice::from_raw_parts(ptr, size_of::()) }; + + assert_eq!( + Felt::from_bytes_be_slice(after_zeroize), + Felt::ZERO, + "Original value was not properly zeroized" + ); + } + + #[test] + fn test_zeroize_on_drop() { + // Create a raw pointer to track the memory + let mut private_key = Felt::from_hex_unchecked( + "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ); + + let pk_clone = private_key.clone(); + + // pointer to the memory that will be zeroed + let raw_ptr; + + { + let signing_key = SecretFelt::from_felt(&mut private_key); + + raw_ptr = signing_key.secret_scalar() as *const Felt as *const u8; + + // Verify it's not zero before dropping + let before_drop = unsafe { std::slice::from_raw_parts(raw_ptr, size_of::()) }; + assert!( + !before_drop.iter().all(|&b| b == 0), + "Memory should not be zeroed yet" + ); + } // At this point, signing_key has been dropped and zeroized + + // Check that the memory is zeroed after drop + let after_drop = unsafe { std::slice::from_raw_parts(raw_ptr, size_of::()) }; + + let felt_after_drop = Felt::from_bytes_be_slice(after_drop); + + // Memory is not zero because the compiler reuse that memory slot + // but should not be equal to the initial value + assert_ne!(pk_clone, felt_after_drop); + } +} diff --git a/crates/starknet-types-core/src/felt/zeroize.rs b/crates/starknet-types-core/src/felt/zeroize.rs deleted file mode 100644 index df8f3a73..00000000 --- a/crates/starknet-types-core/src/felt/zeroize.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::felt::Felt; - -impl zeroize::DefaultIsZeroes for Felt {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn zeroing_felt() { - use zeroize::Zeroize; - - let mut felt = Felt::from_hex_unchecked("0x01"); - felt.zeroize(); - assert_eq!(felt, Felt::ZERO); - } -} From 05e2f40f91180f5f62bf6d4658fd382206f2f1f2 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Sun, 27 Jul 2025 16:27:57 +0300 Subject: [PATCH 04/15] Add warning message to raise attention that good cryptographic hygiene is a must. Add `inner_value` function that returnes a safe copy of the inner felt. The value will be zeroized automatically when is out of scope. --- .../src/felt/secret_felt.rs | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index 3074d3e6..58cfa6ee 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -1,6 +1,6 @@ use crate::felt::{Felt, FromStrError}; use lambdaworks_math::errors::ByteConversionError; -use zeroize::Zeroize; +use zeroize::{Zeroize, Zeroizing}; /// A wrapper for a [Felt] that ensures the value is securely zeroized when dropped. /// @@ -28,6 +28,11 @@ impl SecretFelt { /// It takes a mutable reference to a [Felt] value, creates a copy, /// and then zeroize the original value to ensure it doesn't remain in memory. /// + /// # Warning + /// + /// Avoid moving the secret [Felt] in the memory and initialize the [SecretFelt] + /// as soon as possible in order to not let any copy of the value in memory + /// /// # Example /// /// ``` @@ -37,9 +42,9 @@ impl SecretFelt { /// let secret_felt = SecretFelt::from_felt(&mut private_key); /// // private_key is now zeroized (set to Felt::ZERO) /// ``` - pub fn from_felt(secret_scalar: &mut Felt) -> Self { - let boxed_copy = Box::new(*secret_scalar); - secret_scalar.zeroize(); + pub fn from_felt(secret_felt: &mut Felt) -> Self { + let boxed_copy = Box::new(secret_felt.clone()); + secret_felt.zeroize(); Self(boxed_copy) } @@ -86,14 +91,14 @@ impl SecretFelt { Ok(secret_felt) } - /// Provides reference access to the secret scalar. + /// Returns a safe copy of the inner value. /// /// # Warning /// /// Be careful not to copy the value elsewhere, as that would defeat /// the security guarantees of this type. - pub fn secret_scalar(&self) -> &Felt { - &self.0 + pub fn inner_value(&self) -> Zeroizing { + Zeroizing::new(*self.0.clone()) } } @@ -101,7 +106,7 @@ impl SecretFelt { mod test { use crate::felt::{secret_felt::SecretFelt, Felt}; use core::mem::size_of; - use std::str::FromStr; + use std::{ops::Deref, str::FromStr}; use zeroize::Zeroize; #[test] @@ -114,7 +119,7 @@ mod test { signing_key.zeroize(); // Get a pointer to the inner Felt - let ptr = signing_key.secret_scalar() as *const Felt as *const u8; + let ptr = signing_key.inner_value().deref() as *const Felt as *const u8; let after_zeroize = unsafe { std::slice::from_raw_parts(ptr, size_of::()) }; // Check that the memory is zeroed @@ -166,20 +171,19 @@ mod test { #[test] fn test_zeroize_on_drop() { - // Create a raw pointer to track the memory let mut private_key = Felt::from_hex_unchecked( "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ); + // the initial Felt will be zeroized let pk_clone = private_key.clone(); - // pointer to the memory that will be zeroed let raw_ptr; - { let signing_key = SecretFelt::from_felt(&mut private_key); - raw_ptr = signing_key.secret_scalar() as *const Felt as *const u8; + let inner_value = *signing_key.0; + raw_ptr = &inner_value as *const Felt as *const u8; // Verify it's not zero before dropping let before_drop = unsafe { std::slice::from_raw_parts(raw_ptr, size_of::()) }; @@ -198,4 +202,32 @@ mod test { // but should not be equal to the initial value assert_ne!(pk_clone, felt_after_drop); } + + #[test] + fn test_inner_value() { + let mut private_key = Felt::from_hex_unchecked( + "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ); + + // the initial Felt will be zeroized + let pk_clone = private_key.clone(); + + let raw_ptr; + { + let signing_key = SecretFelt::from_felt(&mut private_key); + + let inner_felt = signing_key.inner_value(); + + assert_eq!(*inner_felt, pk_clone); + + raw_ptr = inner_felt.as_ref() as *const Felt as *const u8; + } // inner_value should be zeroized when is out of scope + + let after_drop = unsafe { std::slice::from_raw_parts(raw_ptr, size_of::()) }; + let felt_after_drop = Felt::from_bytes_be_slice(after_drop); + + // Memory is not zero because the compiler reuse that memory slot + // but should not be equal to the initial value + assert_ne!(pk_clone, felt_after_drop); + } } From 161d5a7cb91690be3789828acbf7b497622094c1 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Tue, 29 Jul 2025 17:34:15 +0300 Subject: [PATCH 05/15] Fix fmt --- crates/starknet-types-core/src/felt/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index 7b7674ef..94f6e688 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -14,10 +14,10 @@ mod parity_scale_codec; #[cfg(feature = "prime-bigint")] mod prime_bigint; mod primitive_conversions; -#[cfg(feature = "serde")] -mod serde; #[cfg(feature = "secret_felt")] pub mod secret_felt; +#[cfg(feature = "serde")] +mod serde; use lambdaworks_math::errors::CreationError; pub use non_zero::{FeltIsZeroError, NonZeroFelt}; From 8d4a2a40ff65aa57023bf4eb99e987a3700e6bb9 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Tue, 29 Jul 2025 18:09:44 +0300 Subject: [PATCH 06/15] Update code documentation --- crates/starknet-types-core/src/felt/secret_felt.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index 58cfa6ee..f265666b 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -67,6 +67,7 @@ impl SecretFelt { /// Creates a new [SecretFelt] from its big-endian representation in a Vec of length 32. /// Internally it uses [from_bytes_be](Felt::from_bytes_be). + /// The input will be zeroized after calling this function pub fn from_bytes_be(secret: &mut Vec) -> Result { let mut value: [u8; 32] = secret .as_slice() @@ -80,6 +81,7 @@ impl SecretFelt { /// Creates a new [SecretFelt] from its little-endian representation in a Vec of length 32. /// Internally it uses [from_bytes_le](Felt::from_bytes_le). + /// The input will be zeroized after calling this function pub fn from_bytes_le(secret: &mut Vec) -> Result { let mut value: [u8; 32] = secret .as_slice() From 9020969d1ba65c62bfc00be38c9e3cf0b5a4514c Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 30 Jul 2025 18:44:18 +0300 Subject: [PATCH 07/15] Fix `--no-default-features` build error --- crates/starknet-types-core/Cargo.toml | 2 +- crates/starknet-types-core/src/felt/secret_felt.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 44bfd88b..e0e478cc 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -49,7 +49,7 @@ serde = ["alloc", "dep:serde"] prime-bigint = ["dep:lazy_static"] num-traits = [] papyrus-serialization = ["std"] -secret_felt = ["dep:zeroize"] +secret_felt = ["alloc", "dep:zeroize"] [dev-dependencies] proptest = { version = "1.5", default-features = false, features = ["alloc", "proptest-macro"] } diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index f265666b..413bd9c8 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -2,6 +2,9 @@ use crate::felt::{Felt, FromStrError}; use lambdaworks_math::errors::ByteConversionError; use zeroize::{Zeroize, Zeroizing}; +#[cfg(feature = "alloc")] +use super::alloc::{boxed::Box, string::String, vec::Vec}; + /// A wrapper for a [Felt] that ensures the value is securely zeroized when dropped. /// /// This type provides secure handling of sensitive [Felt] values (like private keys) From ce58b8f5716b41a11a7d72f59f11b13956c0e200 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Thu, 31 Jul 2025 17:48:16 +0300 Subject: [PATCH 08/15] Change condition to use imports from `alloc` only when std is not enable --- crates/starknet-types-core/src/felt/secret_felt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index 413bd9c8..48e63832 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -2,7 +2,7 @@ use crate::felt::{Felt, FromStrError}; use lambdaworks_math::errors::ByteConversionError; use zeroize::{Zeroize, Zeroizing}; -#[cfg(feature = "alloc")] +#[cfg(not(feature = "std"))] use super::alloc::{boxed::Box, string::String, vec::Vec}; /// A wrapper for a [Felt] that ensures the value is securely zeroized when dropped. From b7fb59dec5e43a544399e702c35b934c51fe85ac Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Mon, 4 Aug 2025 15:50:40 +0300 Subject: [PATCH 09/15] Apply `Clippy` suggestions --- crates/starknet-types-core/src/felt/secret_felt.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index 48e63832..8448128d 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -46,7 +46,7 @@ impl SecretFelt { /// // private_key is now zeroized (set to Felt::ZERO) /// ``` pub fn from_felt(secret_felt: &mut Felt) -> Self { - let boxed_copy = Box::new(secret_felt.clone()); + let boxed_copy = Box::new(*secret_felt); secret_felt.zeroize(); Self(boxed_copy) } @@ -63,7 +63,7 @@ impl SecretFelt { /// let secret_felt = SecretFelt::from_hex_string(&mut private_key); /// ``` pub fn from_hex_string(hex: &mut String) -> Result { - let secret_felt = Felt::from_hex(&hex)?; + let secret_felt = Felt::from_hex(hex)?; hex.zeroize(); Ok(Self(Box::new(secret_felt))) } From f6f22fddac68027d7e8a6db52b4846c547604d65 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Thu, 7 Aug 2025 17:09:56 +0300 Subject: [PATCH 10/15] - Implement constant time equality check for `SecretFelt` using `subtle` crate. - Implement `from_random` for `SecretFelt` to generate a secret Felt using a CSPRNG - Improve the `from_hex_string` example - Change `from_bytes_be`, `from_bytes_le` function signature to accept a `[u8; 32]` instead of `Vec`. No conversion needed and the function can't fail anymore. --- crates/starknet-types-core/Cargo.toml | 31 +++--- .../src/felt/secret_felt.rs | 104 ++++++++++++------ 2 files changed, 90 insertions(+), 45 deletions(-) diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 9e99a00a..ae04fa9a 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -21,24 +21,27 @@ arbitrary = { version = "1.3", optional = true } blake2 = { version = "0.10.6", default-features = false, optional = true } digest = { version = "0.10.7", optional = true } serde = { version = "1", optional = true, default-features = false, features = [ - "alloc", "derive" + "alloc", "derive" ] } lambdaworks-crypto = { version = "0.10.0", default-features = false, optional = true } parity-scale-codec = { version = "3.6", default-features = false, optional = true } lazy_static = { version = "1.5", default-features = false, optional = true } zeroize = { version = "1.8.1", default-features = false, optional = true } +subtle = { version = "2.6.1", default-features = false, optional = true } +rand = { version = "0.9.2", default-features = false, optional = true } [features] default = ["std", "serde", "curve", "num-traits"] std = [ - "alloc", - "lambdaworks-math/std", - "num-traits/std", - "num-bigint/std", - "num-integer/std", - "serde?/std", - "lambdaworks-crypto?/std", - "zeroize?/std", + "alloc", + "lambdaworks-math/std", + "num-traits/std", + "num-bigint/std", + "num-integer/std", + "serde?/std", + "lambdaworks-crypto?/std", + "zeroize?/std", + "rand?/std" ] alloc = ["zeroize?/alloc"] curve = [] @@ -49,18 +52,18 @@ serde = ["alloc", "dep:serde"] prime-bigint = ["dep:lazy_static"] num-traits = [] papyrus-serialization = ["std"] -secret_felt = ["alloc", "dep:zeroize"] +secret_felt = ["alloc", "dep:zeroize", "dep:subtle", "subtle/const-generics", "subtle/core_hint_black_box", "dep:rand", "rand/alloc"] [dev-dependencies] proptest = { version = "1.5", default-features = false, features = [ - "alloc", - "proptest-macro", + "alloc", + "proptest-macro", ] } regex = "1.11" serde_test = "1" criterion = "0.5" -rand_chacha = "0.3" -rand = "0.8" +rand_chacha = "0.9" +rand = "0.9.2" rstest = "0.24" lazy_static = { version = "1.5", default-features = false } diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index 8448128d..a6a0a0d1 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -1,5 +1,6 @@ use crate::felt::{Felt, FromStrError}; -use lambdaworks_math::errors::ByteConversionError; +use rand::{CryptoRng, RngCore}; +use subtle::ConstantTimeEq; use zeroize::{Zeroize, Zeroizing}; #[cfg(not(feature = "std"))] @@ -55,11 +56,12 @@ impl SecretFelt { /// /// # Example /// ``` + /// use std::fs; /// use starknet_types_core::felt::secret_felt::SecretFelt; /// use std::str::FromStr; /// /// // make sure the String is initialized in a secure way - /// let mut private_key = String::from_utf8(vec![255,255,..]).unwrap(); + /// let mut private_key = fs::read_to_string("path/to/secret_value").unwrap(); /// let secret_felt = SecretFelt::from_hex_string(&mut private_key); /// ``` pub fn from_hex_string(hex: &mut String) -> Result { @@ -71,29 +73,43 @@ impl SecretFelt { /// Creates a new [SecretFelt] from its big-endian representation in a Vec of length 32. /// Internally it uses [from_bytes_be](Felt::from_bytes_be). /// The input will be zeroized after calling this function - pub fn from_bytes_be(secret: &mut Vec) -> Result { - let mut value: [u8; 32] = secret - .as_slice() - .try_into() - .map_err(|_| ByteConversionError::InvalidValue)?; - let secret_felt = Self(Box::new(Felt::from_bytes_be(&value))); + pub fn from_bytes_be(secret: &mut [u8; 32]) -> Self { + let secret_felt = Self(Box::new(Felt::from_bytes_be(secret))); secret.zeroize(); - value.zeroize(); - Ok(secret_felt) + secret_felt } /// Creates a new [SecretFelt] from its little-endian representation in a Vec of length 32. /// Internally it uses [from_bytes_le](Felt::from_bytes_le). /// The input will be zeroized after calling this function - pub fn from_bytes_le(secret: &mut Vec) -> Result { - let mut value: [u8; 32] = secret - .as_slice() - .try_into() - .map_err(|_| ByteConversionError::InvalidValue)?; - let secret_felt = Self(Box::new(Felt::from_bytes_le(&value))); + pub fn from_bytes_le(secret: &mut [u8; 32]) -> Self { + let secret_felt = Self(Box::new(Felt::from_bytes_le(secret))); secret.zeroize(); - value.zeroize(); - Ok(secret_felt) + secret_felt + } + + /// Create a new [SecretFelt] from cryptographically secure PRNG + /// + /// # Example + /// ``` + /// use starknet_types_core::felt::secret_felt::SecretFelt; + /// use rand_chacha::ChaCha20Rng; + /// use rand::SeedableRng; + /// + /// let rng = ChaCha20Rng::from_os_rng(); + /// let secret_key = SecretFelt::from_random(rng); + /// ``` + pub fn from_random(mut rng: T) -> Self + where + T: RngCore + CryptoRng, + { + let mut buffer = [0u8; 32]; + rng.fill_bytes(&mut buffer); + + let secret_felt = Self(Box::new(Felt::from_bytes_be(&buffer))); + buffer.zeroize(); + + secret_felt } /// Returns a safe copy of the inner value. @@ -107,20 +123,32 @@ impl SecretFelt { } } +/// Constant time equality check for [SecretFelt] +impl PartialEq for SecretFelt { + fn eq(&self, other: &Self) -> bool { + let mut self_limbs = self.0 .0.representative().limbs; + let mut other_limbs = other.0 .0.representative().limbs; + + let is_eq: bool = self_limbs.ct_eq(&other_limbs).into(); + + self_limbs.zeroize(); + other_limbs.zeroize(); + + is_eq + } +} + #[cfg(test)] mod test { use crate::felt::{secret_felt::SecretFelt, Felt}; use core::mem::size_of; + use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use std::{ops::Deref, str::FromStr}; use zeroize::Zeroize; #[test] fn test_zeroize_secret_felt() { - let mut private_key = Felt::from_hex_unchecked( - "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - ); - - let mut signing_key = SecretFelt::from_felt(&mut private_key); + let mut signing_key = SecretFelt::from_random(ChaCha20Rng::seed_from_u64(1)); signing_key.zeroize(); // Get a pointer to the inner Felt @@ -158,7 +186,7 @@ mod test { #[test] fn test_zeroize_hex_string() { let mut private_key = - String::from_str(&"0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + String::from_str("0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") .unwrap(); let mut signing_key = SecretFelt::from_hex_string(&mut private_key).unwrap(); @@ -180,8 +208,8 @@ mod test { "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ); - // the initial Felt will be zeroized - let pk_clone = private_key.clone(); + // make a copy, the initial Felt will be zeroized + let pk_copy = private_key; let raw_ptr; { @@ -205,7 +233,7 @@ mod test { // Memory is not zero because the compiler reuse that memory slot // but should not be equal to the initial value - assert_ne!(pk_clone, felt_after_drop); + assert_ne!(pk_copy, felt_after_drop); } #[test] @@ -214,8 +242,8 @@ mod test { "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ); - // the initial Felt will be zeroized - let pk_clone = private_key.clone(); + // make a copy, the initial Felt will be zeroized + let pk_copy = private_key; let raw_ptr; { @@ -223,7 +251,7 @@ mod test { let inner_felt = signing_key.inner_value(); - assert_eq!(*inner_felt, pk_clone); + assert_eq!(*inner_felt, pk_copy); raw_ptr = inner_felt.as_ref() as *const Felt as *const u8; } // inner_value should be zeroized when is out of scope @@ -233,6 +261,20 @@ mod test { // Memory is not zero because the compiler reuse that memory slot // but should not be equal to the initial value - assert_ne!(pk_clone, felt_after_drop); + assert_ne!(pk_copy, felt_after_drop); + } + + #[test] + fn test_partial_eq() { + let mut private_key1 = [255u8; 32]; + let mut private_key2 = [255u8; 32]; + let mut private_key3 = [254u8; 32]; + + let signing_key1 = SecretFelt::from_bytes_be(&mut private_key1); + let signing_key2 = SecretFelt::from_bytes_be(&mut private_key2); + let signing_key3 = SecretFelt::from_bytes_be(&mut private_key3); + + assert!(signing_key1.eq(&signing_key2)); + assert!(signing_key1.ne(&signing_key3)); } } From c0253a7439928a8d5c98d03ede329414b8df4794 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 20 Aug 2025 08:14:21 +0300 Subject: [PATCH 11/15] Fix doctests. --- crates/starknet-types-core/src/felt/secret_felt.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index a6a0a0d1..bb05e6cf 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -42,7 +42,7 @@ impl SecretFelt { /// ``` /// use starknet_types_core::felt::{Felt, secret_felt::SecretFelt}; /// - /// let mut private_key = Felt::from_hex_unchecked("0x123..."); + /// let mut private_key = Felt::from_hex_unchecked("0x2d39148a92f479fb077389d"); /// let secret_felt = SecretFelt::from_felt(&mut private_key); /// // private_key is now zeroized (set to Felt::ZERO) /// ``` @@ -54,15 +54,19 @@ impl SecretFelt { /// Creates a new [SecretFelt] from a hex String and zeroized the original String. /// + /// + /// # Warning + /// Make sure the String is initialized in a secure way. + /// e.g. read from a file. + /// /// # Example /// ``` /// use std::fs; /// use starknet_types_core::felt::secret_felt::SecretFelt; /// use std::str::FromStr; /// - /// // make sure the String is initialized in a secure way - /// let mut private_key = fs::read_to_string("path/to/secret_value").unwrap(); - /// let secret_felt = SecretFelt::from_hex_string(&mut private_key); + /// let mut private_key = String::from_str("0x2d39148a92f479fb077389d").unwrap(); + /// let secret_felt = SecretFelt::from_hex_string(&mut private_key).unwrap(); /// ``` pub fn from_hex_string(hex: &mut String) -> Result { let secret_felt = Felt::from_hex(hex)?; From 0b8cc9f948164a71f8a095bd465682151bb42f5e Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 20 Aug 2025 08:17:04 +0300 Subject: [PATCH 12/15] Implement `Eq` for `SecretFelt` --- crates/starknet-types-core/src/felt/secret_felt.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index bb05e6cf..9e25e16f 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -142,6 +142,8 @@ impl PartialEq for SecretFelt { } } +impl Eq for SecretFelt {} + #[cfg(test)] mod test { use crate::felt::{secret_felt::SecretFelt, Felt}; From 7a386efbef7de70e72160a10a141748572d77fb2 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 20 Aug 2025 17:57:42 +0300 Subject: [PATCH 13/15] Update doc for `SecretFelt` Co-authored-by: Gabriel Bosio <38794644+gabrielbosio@users.noreply.github.com> --- crates/starknet-types-core/src/felt/secret_felt.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index 9e25e16f..f638996f 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -34,8 +34,9 @@ impl SecretFelt { /// /// # Warning /// - /// Avoid moving the secret [Felt] in the memory and initialize the [SecretFelt] - /// as soon as possible in order to not let any copy of the value in memory + /// Avoid moving the secret [Felt] in memory and avoid intermediate + /// operations between the [Felt] creation and the [SecretFelt] initialization + /// in order to not leave any copies of the value in memory /// /// # Example /// From 1a4571c3d2d9118041ddec2b7f840323f16860fc Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 20 Aug 2025 18:00:33 +0300 Subject: [PATCH 14/15] Derive `Eq` for `SecretFelt` instead of default impl --- crates/starknet-types-core/src/felt/secret_felt.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/starknet-types-core/src/felt/secret_felt.rs b/crates/starknet-types-core/src/felt/secret_felt.rs index f638996f..12198c67 100644 --- a/crates/starknet-types-core/src/felt/secret_felt.rs +++ b/crates/starknet-types-core/src/felt/secret_felt.rs @@ -10,6 +10,7 @@ use super::alloc::{boxed::Box, string::String, vec::Vec}; /// /// This type provides secure handling of sensitive [Felt] values (like private keys) /// by ensuring that the memory is properly cleared when the value is no longer needed. +#[derive(Eq)] pub struct SecretFelt(Box); impl zeroize::DefaultIsZeroes for Felt {} @@ -34,7 +35,7 @@ impl SecretFelt { /// /// # Warning /// - /// Avoid moving the secret [Felt] in memory and avoid intermediate + /// Avoid moving the secret [Felt] in memory and avoid intermediate /// operations between the [Felt] creation and the [SecretFelt] initialization /// in order to not leave any copies of the value in memory /// @@ -143,8 +144,6 @@ impl PartialEq for SecretFelt { } } -impl Eq for SecretFelt {} - #[cfg(test)] mod test { use crate::felt::{secret_felt::SecretFelt, Felt}; From ddb5c476b4f2a66567e709a04d349a0fb11e6fe6 Mon Sep 17 00:00:00 2001 From: Filip Laurentiu Date: Wed, 20 Aug 2025 18:01:29 +0300 Subject: [PATCH 15/15] Rename `secret_felt` feature to `secret-felt` to keep naming consistency --- crates/starknet-types-core/Cargo.toml | 2 +- crates/starknet-types-core/src/felt/mod.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index fcc5205b..35b8217d 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -52,7 +52,7 @@ serde = ["alloc", "dep:serde"] prime-bigint = ["dep:lazy_static"] num-traits = [] papyrus-serialization = ["std"] -secret_felt = ["alloc", "dep:zeroize", "dep:subtle", "subtle/const-generics", "subtle/core_hint_black_box", "dep:rand", "rand/alloc"] +secret-felt = ["alloc", "dep:zeroize", "dep:subtle", "subtle/const-generics", "subtle/core_hint_black_box", "dep:rand", "rand/alloc"] [dev-dependencies] proptest = { version = "1.5", default-features = false, features = [ diff --git a/crates/starknet-types-core/src/felt/mod.rs b/crates/starknet-types-core/src/felt/mod.rs index f8a51ed1..6b3d1388 100644 --- a/crates/starknet-types-core/src/felt/mod.rs +++ b/crates/starknet-types-core/src/felt/mod.rs @@ -1,4 +1,6 @@ #[cfg(feature = "alloc")] +pub extern crate alloc; +#[cfg(feature = "alloc")] mod alloc_impls; #[cfg(feature = "arbitrary")] mod arbitrary; @@ -14,7 +16,7 @@ mod parity_scale_codec; #[cfg(feature = "prime-bigint")] mod prime_bigint; mod primitive_conversions; -#[cfg(feature = "secret_felt")] +#[cfg(feature = "secret-felt")] pub mod secret_felt; #[cfg(feature = "serde")] mod serde; @@ -33,9 +35,6 @@ use num_integer::Integer; use num_traits::{One, Zero}; pub use primitive_conversions::PrimitiveFromFeltError; -#[cfg(feature = "alloc")] -pub extern crate alloc; - use lambdaworks_math::{ field::{ element::FieldElement, fields::fft_friendly::stark_252_prime_field::Stark252PrimeField, @@ -742,7 +741,6 @@ mod arithmetic { } mod formatting { - use core::fmt; use super::*;