Skip to content

Filip/secret felt implementation #144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4f3a2b3
Affine coordinate are not correctly exposed from the underlying type.
FilipLaurentiu Jan 29, 2025
b53ca90
Merge branch 'main' into main
FilipLaurentiu Jan 29, 2025
9c0676e
Fix the `Add` implementation
FilipLaurentiu Jan 29, 2025
86e617c
Merge branch 'starknet-io:main' into main
FilipLaurentiu Feb 4, 2025
ad125a3
Merge branch 'main' into main
FilipLaurentiu Feb 12, 2025
f9d898c
Merge branch 'main' into main
FilipLaurentiu Mar 3, 2025
9d49f0b
Merge branch 'starknet-io:main' into main
FilipLaurentiu Apr 11, 2025
733693f
Merge branch 'starknet-io:main' into main
FilipLaurentiu May 28, 2025
4a84f6b
Merge branch 'starknet-io:main' into main
FilipLaurentiu Jul 21, 2025
bd27423
Implement SecretFelt, a secure wrapper for Felt that will avoid leaki…
FilipLaurentiu Jul 26, 2025
05e2f40
Add warning message to raise attention that good cryptographic hygien…
FilipLaurentiu Jul 27, 2025
ddf80c8
Merge branch 'main' into filip/secret_felt_implementation
FilipLaurentiu Jul 27, 2025
161d5a7
Fix fmt
FilipLaurentiu Jul 29, 2025
8d4a2a4
Update code documentation
FilipLaurentiu Jul 29, 2025
cad3f8e
Merge branch 'main' into filip/secret_felt_implementation
FilipLaurentiu Jul 30, 2025
9020969
Fix `--no-default-features` build error
FilipLaurentiu Jul 30, 2025
ce58b8f
Change condition to use imports from `alloc` only when std is not enable
FilipLaurentiu Jul 31, 2025
b7fb59d
Apply `Clippy` suggestions
FilipLaurentiu Aug 4, 2025
46d7af6
Merge branch 'main' into filip/secret_felt_implementation
FilipLaurentiu Aug 4, 2025
f6f22fd
- Implement constant time equality check for `SecretFelt` using `subt…
FilipLaurentiu Aug 7, 2025
042b7b9
Merge branch 'main' into filip/secret_felt_implementation
FilipLaurentiu Aug 7, 2025
13a1854
Merge branch 'main' into filip/secret_felt_implementation
FilipLaurentiu Aug 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions crates/starknet-types-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -49,18 +52,18 @@ serde = ["alloc", "dep:serde"]
prime-bigint = ["dep:lazy_static"]
num-traits = []
papyrus-serialization = ["std"]
zeroize = ["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 }

Expand Down
4 changes: 2 additions & 2 deletions crates/starknet-types-core/src/felt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ mod parity_scale_codec;
#[cfg(feature = "prime-bigint")]
mod prime_bigint;
mod primitive_conversions;
#[cfg(feature = "secret_felt")]
pub mod secret_felt;
#[cfg(feature = "serde")]
mod serde;
#[cfg(feature = "zeroize")]
mod zeroize;

use lambdaworks_math::errors::CreationError;
pub use non_zero::{FeltIsZeroError, NonZeroFelt};
Expand Down
280 changes: 280 additions & 0 deletions crates/starknet-types-core/src/felt/secret_felt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
use crate::felt::{Felt, FromStrError};
use rand::{CryptoRng, RngCore};
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, Zeroizing};

#[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.
///
/// 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<Felt>);

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.
///
/// # 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
///
/// ```
/// 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_felt: &mut Felt) -> Self {
let boxed_copy = Box::new(*secret_felt);
secret_felt.zeroize();
Self(boxed_copy)
}

/// Creates a new [SecretFelt] from a hex String and zeroized the original String.
///
/// # 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);
/// ```
pub fn from_hex_string(hex: &mut String) -> Result<Self, FromStrError> {
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<u8> 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 [u8; 32]) -> Self {
let secret_felt = Self(Box::new(Felt::from_bytes_be(secret)));
secret.zeroize();
secret_felt
}

/// Creates a new [SecretFelt] from its little-endian representation in a Vec<u8> 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 [u8; 32]) -> Self {
let secret_felt = Self(Box::new(Felt::from_bytes_le(secret)));
secret.zeroize();
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<T>(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.
///
/// # Warning
///
/// Be careful not to copy the value elsewhere, as that would defeat
/// the security guarantees of this type.
pub fn inner_value(&self) -> Zeroizing<Felt> {
Zeroizing::new(*self.0.clone())
}
}

/// 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 signing_key = SecretFelt::from_random(ChaCha20Rng::seed_from_u64(1));
signing_key.zeroize();

// Get a pointer to the inner Felt
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::<Felt>()) };

// 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::<Felt>()) };

// 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::<Felt>()) };

assert_eq!(
Felt::from_bytes_be_slice(after_zeroize),
Felt::ZERO,
"Original value was not properly zeroized"
);
}

#[test]
fn test_zeroize_on_drop() {
let mut private_key = Felt::from_hex_unchecked(
"0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
);

// make a copy, the initial Felt will be zeroized
let pk_copy = private_key;

let raw_ptr;
{
let signing_key = SecretFelt::from_felt(&mut private_key);

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::<Felt>()) };
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::<Felt>()) };

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_copy, felt_after_drop);
}

#[test]
fn test_inner_value() {
let mut private_key = Felt::from_hex_unchecked(
"0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
);

// make a copy, the initial Felt will be zeroized
let pk_copy = private_key;

let raw_ptr;
{
let signing_key = SecretFelt::from_felt(&mut private_key);

let inner_felt = signing_key.inner_value();

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

let after_drop = unsafe { std::slice::from_raw_parts(raw_ptr, size_of::<Felt>()) };
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_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));
}
}
17 changes: 0 additions & 17 deletions crates/starknet-types-core/src/felt/zeroize.rs

This file was deleted.