From cafc77ebb835a7c89184e14979eee9600d68756e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 13:10:00 +0000 Subject: [PATCH 01/65] [cryptography] secrets wrapper --- .../bls12381_primitive_operations.rs | 5 +- cryptography/fuzz/fuzz_targets/common/mod.rs | 5 +- cryptography/src/bls12381/dkg.rs | 171 ++++++++------- cryptography/src/bls12381/primitives/group.rs | 58 +++-- cryptography/src/bls12381/primitives/ops.rs | 14 +- cryptography/src/bls12381/scheme.rs | 42 ++-- cryptography/src/ed25519/scheme.rs | 48 +++-- cryptography/src/lib.rs | 2 + cryptography/src/secp256r1/common.rs | 50 +++-- cryptography/src/secret.rs | 203 ++++++++++++++++++ 10 files changed, 429 insertions(+), 169 deletions(-) create mode 100644 cryptography/src/secret.rs diff --git a/cryptography/fuzz/fuzz_targets/bls12381_primitive_operations.rs b/cryptography/fuzz/fuzz_targets/bls12381_primitive_operations.rs index 9bfedb7a10..68831dea46 100644 --- a/cryptography/fuzz/fuzz_targets/bls12381_primitive_operations.rs +++ b/cryptography/fuzz/fuzz_targets/bls12381_primitive_operations.rs @@ -417,10 +417,7 @@ fn arbitrary_g2(u: &mut Unstructured) -> Result { } fn arbitrary_share(u: &mut Unstructured) -> Result { - Ok(Share { - index: u.int_in_range(1..=100)?, - private: arbitrary_scalar(u)?, - }) + Ok(Share::new(u.int_in_range(1..=100)?, arbitrary_scalar(u)?)) } fn arbitrary_poly_scalar(u: &mut Unstructured) -> Result, arbitrary::Error> { diff --git a/cryptography/fuzz/fuzz_targets/common/mod.rs b/cryptography/fuzz/fuzz_targets/common/mod.rs index a481f3c8c6..9bda62cded 100644 --- a/cryptography/fuzz/fuzz_targets/common/mod.rs +++ b/cryptography/fuzz/fuzz_targets/common/mod.rs @@ -105,10 +105,7 @@ pub fn arbitrary_scalar(u: &mut Unstructured) -> Result Result { - Ok(Share { - index: u.int_in_range(1..=100)?, - private: arbitrary_scalar(u)?, - }) + Ok(Share::new(u.int_in_range(1..=100)?, arbitrary_scalar(u)?)) } #[allow(unused)] diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 517a3837c5..69e58ab39d 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -290,7 +290,7 @@ use crate::{ variant::Variant, }, transcript::{Summary, Transcript}, - PublicKey, Signer, + PublicKey, Secret, Signer, }; use commonware_codec::{Encode, EncodeSize, RangeCfg, Read, ReadExt, Write}; use commonware_math::{ @@ -565,7 +565,7 @@ impl Info { return false; }; let expected = pub_msg.commitment.eval_msm(&scalar); - expected == V::Public::generator() * &priv_msg.share + expected == V::Public::generator() * priv_msg.share.expose() } } @@ -681,24 +681,39 @@ where #[derive(Clone, PartialEq, Eq)] pub struct DealerPrivMsg { - share: Scalar, + share: Secret, } -impl std::fmt::Debug for DealerPrivMsg { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "DealerPrivMsg(REDACTED)") +impl DealerPrivMsg { + /// Creates a new DealerPrivMsg with the given share. + /// + /// The share is wrapped in a `Secret` for secure handling. + pub const fn new(share: Scalar) -> Self { + Self { + share: Secret::new(share), + } + } + + /// Returns a reference to the wrapped share. + pub const fn share(&self) -> &Secret { + &self.share + } + + /// Returns a mutable reference to the wrapped share. + pub const fn share_mut(&mut self) -> &mut Secret { + &mut self.share } } impl EncodeSize for DealerPrivMsg { fn encode_size(&self) -> usize { - self.share.encode_size() + self.share.expose().encode_size() } } impl Write for DealerPrivMsg { fn write(&self, buf: &mut impl bytes::BufMut) { - self.share.write(buf); + self.share.expose().write(buf); } } @@ -709,8 +724,9 @@ impl Read for DealerPrivMsg { buf: &mut impl bytes::Buf, _cfg: &Self::Cfg, ) -> Result { + let share: Scalar = ReadExt::read(buf)?; Ok(Self { - share: ReadExt::read(buf)?, + share: Secret::new(share), }) } } @@ -718,8 +734,10 @@ impl Read for DealerPrivMsg { #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for DealerPrivMsg { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { - let share = u.arbitrary()?; - Ok(Self { share }) + let share: Scalar = u.arbitrary()?; + Ok(Self { + share: Secret::new(share), + }) } } @@ -1172,7 +1190,8 @@ impl Dealer { ) -> Result<(Self, DealerPubMsg, Vec<(S::PublicKey, DealerPrivMsg)>), Error> { // Check that this dealer is defined in the round. info.dealer_index(&me.public_key())?; - let share = info.unwrap_or_random_share(&mut rng, share.map(|x| x.private))?; + let share = + info.unwrap_or_random_share(&mut rng, share.map(|x| x.private().expose().clone()))?; let my_poly = Poly::new_with_constant(&mut rng, info.degree(), share); let priv_msgs = info .players @@ -1180,10 +1199,9 @@ impl Dealer { .map(|pk| { ( pk.clone(), - DealerPrivMsg { - share: my_poly - .eval_msm(&info.player_scalar(pk).expect("player should exist")), - }, + DealerPrivMsg::new( + my_poly.eval_msm(&info.player_scalar(pk).expect("player should exist")), + ), ) }) .collect::>(); @@ -1477,7 +1495,7 @@ impl Player { let share = self .view .get(dealer) - .map(|(_, priv_msg)| priv_msg.share.clone()) + .map(|(_, priv_msg)| priv_msg.share().expose().clone()) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( || { @@ -1485,7 +1503,7 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - |priv_msg| priv_msg.share.clone(), + |priv_msg| priv_msg.share().expose().clone(), ) }); (dealer.clone(), share) @@ -1508,10 +1526,7 @@ impl Player { .expect("select ensures that we can recover") }, ); - let share = Share { - index: self.index, - private, - }; + let share = Share::new(self.index, private); Ok((output, share)) } } @@ -1537,10 +1552,7 @@ pub fn deal( .map(|(i, p)| { let i = i as u32; let eval = private.eval_msm(&mode.scalar(n, i).expect("player index should be valid")); - let share = Share { - index: i, - private: eval, - }; + let share = Share::new(i, eval); (p.clone(), share) }) .try_collect() @@ -1926,68 +1938,65 @@ mod test_plan { let share = match (shares.get(&pk), round.replace_shares.contains(&i_dealer)) { (None, _) => None, (Some(s), false) => Some(s.clone()), - (Some(_), true) => Some(Share { - index: i_dealer, - private: Scalar::random(&mut rng), - }), + (Some(_), true) => Some(Share::new(i_dealer, Scalar::random(&mut rng))), }; // Start dealer (with potential modifications) - let (mut dealer, pub_msg, mut priv_msgs) = if let Some(shift) = - round.shift_degrees.get(&i_dealer) - { - // Create dealer with shifted degree - let degree = - u32::try_from(info.degree() as i32 + shift.get()).unwrap_or_default(); - - // Manually create the dealer with adjusted polynomial - let share = info - .unwrap_or_random_share(&mut rng, share.map(|s| s.private)) - .expect("Failed to generate dealer share"); - - let my_poly = Poly::new_with_constant(&mut rng, degree, share); - let priv_msgs = info - .players - .iter() - .map(|pk| { - ( - pk.clone(), - DealerPrivMsg { - share: my_poly.eval_msm( - &info.player_scalar(pk).expect("player should exist"), - ), - }, + let (mut dealer, pub_msg, mut priv_msgs) = + if let Some(shift) = round.shift_degrees.get(&i_dealer) { + // Create dealer with shifted degree + let degree = u32::try_from(info.degree() as i32 + shift.get()) + .unwrap_or_default(); + + // Manually create the dealer with adjusted polynomial + let share = info + .unwrap_or_random_share( + &mut rng, + share.map(|s| s.private().expose().clone()), ) - }) - .collect::>(); - let results: Map<_, _> = priv_msgs - .iter() - .map(|(pk, pm)| (pk.clone(), AckOrReveal::Reveal(pm.clone()))) - .try_collect() - .unwrap(); - let commitment = Poly::commit(my_poly); - let pub_msg = DealerPubMsg { commitment }; - let transcript = { - let t = transcript_for_round(&info); - transcript_for_ack(&t, &pk, &pub_msg) - }; - let dealer = Dealer { - me: sk.clone(), - info: info.clone(), - pub_msg: pub_msg.clone(), - results, - transcript, + .expect("Failed to generate dealer share"); + + let my_poly = Poly::new_with_constant(&mut rng, degree, share); + let priv_msgs = info + .players + .iter() + .map(|pk| { + ( + pk.clone(), + DealerPrivMsg::new(my_poly.eval_msm( + &info.player_scalar(pk).expect("player should exist"), + )), + ) + }) + .collect::>(); + let results: Map<_, _> = priv_msgs + .iter() + .map(|(pk, pm)| (pk.clone(), AckOrReveal::Reveal(pm.clone()))) + .try_collect() + .unwrap(); + let commitment = Poly::commit(my_poly); + let pub_msg = DealerPubMsg { commitment }; + let transcript = { + let t = transcript_for_round(&info); + transcript_for_ack(&t, &pk, &pub_msg) + }; + let dealer = Dealer { + me: sk.clone(), + info: info.clone(), + pub_msg: pub_msg.clone(), + results, + transcript, + }; + (dealer, pub_msg, priv_msgs) + } else { + Dealer::start(&mut rng, info.clone(), sk.clone(), share)? }; - (dealer, pub_msg, priv_msgs) - } else { - Dealer::start(&mut rng, info.clone(), sk.clone(), share)? - }; // Apply BadShare perturbations for (player, priv_msg) in &mut priv_msgs { let player_key_idx = pk_to_key_idx[player]; if round.bad_shares.contains(&(i_dealer, player_key_idx)) { - priv_msg.share = Scalar::random(&mut rng); + *priv_msg.share_mut().expose_mut() = Scalar::random(&mut rng); } } assert_eq!(priv_msgs.len(), players.len()); @@ -2064,9 +2073,9 @@ mod test_plan { *results .get_value_mut(&player_pk) .ok_or_else(|| anyhow!("unknown player: {:?}", &player_pk))? = - AckOrReveal::Reveal(DealerPrivMsg { - share: Scalar::random(&mut rng), - }); + AckOrReveal::Reveal(DealerPrivMsg::new(Scalar::random( + &mut rng, + ))); } } } diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 0e5783a41f..127dcaafce 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -11,6 +11,7 @@ //! is already taken care of for you if you use the provided `deserialize` function. use super::variant::Variant; +use crate::Secret; #[cfg(not(feature = "std"))] use alloc::{vec, vec::Vec}; use blst::{ @@ -493,33 +494,52 @@ impl Random for Scalar { /// A share of a threshold signing key. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Share { /// The share's index in the polynomial. pub index: u32, /// The scalar corresponding to the share's secret. - pub private: Private, + private: Secret, } impl AsRef for Share { fn as_ref(&self) -> &Private { - &self.private + self.private.expose() } } impl Share { + /// Creates a new Share with the given index and private key. + /// + /// The private key is wrapped in a `Secret` for secure handling. + pub const fn new(index: u32, private: Private) -> Self { + Self { + index, + private: Secret::new(private), + } + } + /// Returns the public key corresponding to the share. /// /// This can be verified against the public polynomial. pub fn public(&self) -> V::Public { - V::Public::generator() * &self.private + V::Public::generator() * self.private.expose() + } + + /// Returns a reference to the wrapped private key. + pub const fn private(&self) -> &Secret { + &self.private + } + + /// Returns a mutable reference to the wrapped private key. + pub const fn private_mut(&mut self) -> &mut Secret { + &mut self.private } } impl Write for Share { fn write(&self, buf: &mut impl BufMut) { UInt(self.index).write(buf); - self.private.write(buf); + self.private.expose().write(buf); } } @@ -529,25 +549,40 @@ impl Read for Share { fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let index = UInt::read(buf)?.into(); let private = Private::read(buf)?; - Ok(Self { index, private }) + Ok(Self { + index, + private: Secret::new(private), + }) } } impl EncodeSize for Share { fn encode_size(&self) -> usize { - UInt(self.index).encode_size() + self.private.encode_size() + UInt(self.index).encode_size() + self.private.expose().encode_size() } } impl Display for Share { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "Share(index={}, private={})", self.index, self.private) + write!(f, "Share(index={}, private=[REDACTED])", self.index) } } impl Debug for Share { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "Share(index={}, private={})", self.index, self.private) + write!(f, "Share(index={}, private=[REDACTED])", self.index) + } +} + +#[cfg(feature = "arbitrary")] +impl arbitrary::Arbitrary<'_> for Share { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + let index = u.arbitrary()?; + let private = Private::arbitrary(u)?; + Ok(Self { + index, + private: Secret::new(private), + }) } } @@ -1448,10 +1483,7 @@ mod tests { let scalar = Scalar::random(&mut rng); let g1 = G1::generator() * &scalar; let g2 = G2::generator() * &scalar; - let share = Share { - index: scalar_set.len() as u32, - private: scalar.clone(), - }; + let share = Share::new(scalar_set.len() as u32, scalar.clone()); scalar_set.insert(scalar); g1_set.insert(g1); diff --git a/cryptography/src/bls12381/primitives/ops.rs b/cryptography/src/bls12381/primitives/ops.rs index c7f569a11a..c4f92b36b7 100644 --- a/cryptography/src/bls12381/primitives/ops.rs +++ b/cryptography/src/bls12381/primitives/ops.rs @@ -143,7 +143,7 @@ pub fn partial_sign_proof_of_possession( ) -> PartialSignature { // Sign the public key let sig = sign::( - &private.private, + private.as_ref(), V::PROOF_OF_POSSESSION, &sharing.public().encode(), ); @@ -176,7 +176,7 @@ pub fn partial_sign_message( namespace: Option<&[u8]>, message: &[u8], ) -> PartialSignature { - let sig = sign_message::(&private.private, namespace, message); + let sig = sign_message::(private.as_ref(), namespace, message); PartialSignature { value: sig, index: private.index, @@ -1445,7 +1445,7 @@ mod tests { // Corrupt a share let share = shares.get_mut(3).unwrap(); - share.private = Private::random(&mut rand::thread_rng()); + *share.private_mut().expose_mut() = Private::random(&mut rand::thread_rng()); // Generate the partial signatures let namespace = Some(&b"test"[..]); @@ -1504,7 +1504,7 @@ mod tests { // Corrupt the second share's private key let corrupted_index = 1; - shares[corrupted_index].private = Private::random(&mut rng); + *shares[corrupted_index].private_mut().expose_mut() = Private::random(&mut rng); // Generate partial signatures let partials: Vec<_> = shares @@ -1544,7 +1544,7 @@ mod tests { // Corrupt shares at indices 1 and 3 let corrupted_indices = vec![1, 3]; for &idx in &corrupted_indices { - shares[idx].private = Private::random(&mut rng); + *shares[idx].private_mut().expose_mut() = Private::random(&mut rng); } // Generate partial signatures @@ -1639,7 +1639,7 @@ mod tests { let namespace = Some(&b"test"[..]); let msg = b"hello"; - shares[0].private = Private::random(&mut rng); + *shares[0].private_mut().expose_mut() = Private::random(&mut rng); let partials: Vec<_> = shares .iter() @@ -1667,7 +1667,7 @@ mod tests { let msg = b"hello"; let corrupted_index = n - 1; - shares[corrupted_index as usize].private = Private::random(&mut rng); + *shares[corrupted_index as usize].private_mut().expose_mut() = Private::random(&mut rng); let partials: Vec<_> = shares .iter() diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index f561f4d166..9836fcc154 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -30,7 +30,7 @@ use super::primitives::{ ops, variant::{MinPk, Variant}, }; -use crate::{Array, BatchVerifier, Signer as _}; +use crate::{Array, BatchVerifier, Secret, Signer as _}; #[cfg(not(feature = "std"))] use alloc::borrow::Cow; #[cfg(not(feature = "std"))] @@ -49,20 +49,19 @@ use core::{ use rand_core::CryptoRngCore; #[cfg(feature = "std")] use std::borrow::Cow; -use zeroize::{Zeroize, ZeroizeOnDrop}; const CURVE_NAME: &str = "bls12381"; /// BLS12-381 private key. -#[derive(Clone, Eq, PartialEq, Zeroize, ZeroizeOnDrop)] +#[derive(Clone, Eq, PartialEq)] pub struct PrivateKey { - raw: [u8; group::PRIVATE_KEY_LENGTH], - key: group::Private, + raw: Secret<[u8; group::PRIVATE_KEY_LENGTH]>, + key: Secret, } impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { - self.raw.write(buf); + self.raw.expose().write(buf); } } @@ -73,7 +72,10 @@ impl Read for PrivateKey { let raw = <[u8; Self::SIZE]>::read(buf)?; let key = group::Private::decode(raw.as_ref()) .map_err(|e| CodecError::Wrapped(CURVE_NAME, e.into()))?; - Ok(Self { raw, key }) + Ok(Self { + raw: Secret::new(raw), + key: Secret::new(key), + }) } } @@ -87,13 +89,13 @@ impl Array for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - self.raw.hash(state); + self.raw.expose().hash(state); } } impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.cmp(&other.raw) + self.raw.expose().cmp(other.raw.expose()) } } @@ -105,33 +107,36 @@ impl PartialOrd for PrivateKey { impl AsRef<[u8]> for PrivateKey { fn as_ref(&self) -> &[u8] { - &self.raw + self.raw.expose() } } impl Deref for PrivateKey { type Target = [u8]; fn deref(&self) -> &[u8] { - &self.raw + self.raw.expose() } } impl From for PrivateKey { fn from(key: Scalar) -> Self { let raw = key.encode_fixed(); - Self { raw, key } + Self { + raw: Secret::new(raw), + key: Secret::new(key), + } } } impl Debug for PrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", hex(&self.raw)) + f.write_str("[REDACTED]") } } impl Display for PrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", hex(&self.raw)) + f.write_str("[REDACTED]") } } @@ -142,7 +147,7 @@ impl crate::Signer for PrivateKey { type PublicKey = PublicKey; fn public_key(&self) -> Self::PublicKey { - PublicKey::from(ops::compute_public::(&self.key)) + PublicKey::from(ops::compute_public::(self.key.expose())) } fn sign(&self, namespace: &[u8], msg: &[u8]) -> Self::Signature { @@ -153,7 +158,7 @@ impl crate::Signer for PrivateKey { impl PrivateKey { #[inline(always)] fn sign_inner(&self, namespace: Option<&[u8]>, message: &[u8]) -> Signature { - ops::sign_message::(&self.key, namespace, message).into() + ops::sign_message::(self.key.expose(), namespace, message).into() } } @@ -161,7 +166,10 @@ impl Random for PrivateKey { fn random(mut rng: impl CryptoRngCore) -> Self { let (private, _) = ops::keypair::<_, MinPk>(&mut rng); let raw = private.encode_fixed(); - Self { raw, key: private } + Self { + raw: Secret::new(raw), + key: Secret::new(private), + } } } diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index c7c48c4c0d..b9c5fbcfb4 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -1,4 +1,4 @@ -use crate::{Array, BatchVerifier}; +use crate::{Array, BatchVerifier, Secret}; #[cfg(not(feature = "std"))] use alloc::{ borrow::{Cow, ToOwned}, @@ -17,7 +17,6 @@ use ed25519_consensus::{self, VerificationKey}; use rand_core::CryptoRngCore; #[cfg(feature = "std")] use std::borrow::{Cow, ToOwned}; -use zeroize::{Zeroize, ZeroizeOnDrop}; const CURVE_NAME: &str = "ed25519"; const PRIVATE_KEY_LENGTH: usize = 32; @@ -25,10 +24,10 @@ const PUBLIC_KEY_LENGTH: usize = 32; const SIGNATURE_LENGTH: usize = 64; /// Ed25519 Private Key. -#[derive(Clone, Zeroize, ZeroizeOnDrop)] +#[derive(Clone)] pub struct PrivateKey { - raw: [u8; PRIVATE_KEY_LENGTH], - key: ed25519_consensus::SigningKey, + raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, + key: Secret, } impl crate::PrivateKey for PrivateKey {} @@ -42,10 +41,10 @@ impl crate::Signer for PrivateKey { } fn public_key(&self) -> Self::PublicKey { - let raw = self.key.verification_key().to_bytes(); + let raw = self.key.expose().verification_key().to_bytes(); Self::PublicKey { raw, - key: self.key.verification_key().to_owned(), + key: self.key.expose().verification_key().to_owned(), } } } @@ -56,7 +55,7 @@ impl PrivateKey { let payload = namespace .map(|namespace| Cow::Owned(union_unique(namespace, msg))) .unwrap_or_else(|| Cow::Borrowed(msg)); - let sig = self.key.sign(&payload); + let sig = self.key.expose().sign(&payload); Signature::from(sig) } } @@ -65,13 +64,16 @@ impl Random for PrivateKey { fn random(rng: impl CryptoRngCore) -> Self { let key = ed25519_consensus::SigningKey::new(rng); let raw = key.to_bytes(); - Self { raw, key } + Self { + raw: Secret::new(raw), + key: Secret::new(key), + } } } impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { - self.raw.write(buf); + self.raw.expose().write(buf); } } @@ -81,7 +83,10 @@ impl Read for PrivateKey { fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let raw = <[u8; Self::SIZE]>::read(buf)?; let key = ed25519_consensus::SigningKey::from(raw); - Ok(Self { raw, key }) + Ok(Self { + raw: Secret::new(raw), + key: Secret::new(key), + }) } } @@ -97,7 +102,7 @@ impl Eq for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - self.raw.hash(state); + self.raw.expose().hash(state); } } @@ -109,7 +114,7 @@ impl PartialEq for PrivateKey { impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.cmp(&other.raw) + self.raw.expose().cmp(other.raw.expose()) } } @@ -121,33 +126,36 @@ impl PartialOrd for PrivateKey { impl AsRef<[u8]> for PrivateKey { fn as_ref(&self) -> &[u8] { - &self.raw + self.raw.expose() } } impl Deref for PrivateKey { type Target = [u8]; fn deref(&self) -> &[u8] { - &self.raw + self.raw.expose() } } impl From for PrivateKey { fn from(key: ed25519_consensus::SigningKey) -> Self { let raw = key.to_bytes(); - Self { raw, key } + Self { + raw: Secret::new(raw), + key: Secret::new(key), + } } } impl Debug for PrivateKey { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", hex(&self.raw)) + f.write_str("[REDACTED]") } } impl Display for PrivateKey { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", hex(&self.raw)) + f.write_str("[REDACTED]") } } @@ -170,10 +178,10 @@ pub struct PublicKey { impl From for PublicKey { fn from(value: PrivateKey) -> Self { - let raw = value.key.verification_key().to_bytes(); + let raw = value.key.expose().verification_key().to_bytes(); Self { raw, - key: value.key.verification_key(), + key: value.key.expose().verification_key(), } } } diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index 5c5694967b..dcf9047280 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -35,6 +35,8 @@ pub mod handshake; pub mod lthash; pub use lthash::LtHash; pub mod secp256r1; +pub mod secret; +pub use secret::Secret; pub mod transcript; /// Produces [Signature]s over messages that can be verified with a corresponding [PublicKey]. diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 8502e649df..93bb37b3a8 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -5,6 +5,7 @@ cfg_if::cfg_if! { use alloc::borrow::ToOwned; } } +use crate::Secret; use bytes::{Buf, BufMut}; use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write}; use commonware_math::algebra::Random; @@ -16,34 +17,34 @@ use core::{ }; use p256::ecdsa::{SigningKey, VerifyingKey}; use rand_core::CryptoRngCore; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::ZeroizeOnDrop; pub const CURVE_NAME: &str = "secp256r1"; pub const PRIVATE_KEY_LENGTH: usize = 32; pub const PUBLIC_KEY_LENGTH: usize = 33; // Y-Parity || X /// Internal Secp256r1 Private Key storage. -#[derive(Clone, Eq, PartialEq, ZeroizeOnDrop)] +/// +/// Note: `SigningKey` implements `ZeroizeOnDrop` (not `Zeroize`), so it cannot be wrapped +/// in `Secret`. The `raw` bytes are wrapped in `Secret` for zeroization, while `key` +/// relies on its own `ZeroizeOnDrop` implementation. +#[derive(Clone, Eq, PartialEq)] pub struct PrivateKeyInner { - raw: [u8; PRIVATE_KEY_LENGTH], + raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, pub key: SigningKey, } -impl Zeroize for PrivateKeyInner { - fn zeroize(&mut self) { - self.raw.zeroize(); - - // skip zeroizing `key` here, `ZeroizeOnDrop` is implemented for `SigningKey` and - // can't be called directly. - // - // Reference: - } -} +// SAFETY: Both `raw` (via Secret) and `key` (via its own ZeroizeOnDrop) are zeroized on drop. +impl ZeroizeOnDrop for PrivateKeyInner {} impl PrivateKeyInner { pub fn new(key: SigningKey) -> Self { - let raw = key.to_bytes().into(); - Self { raw, key } + let bytes = key.to_bytes(); + let raw: [u8; PRIVATE_KEY_LENGTH] = bytes.into(); + Self { + raw: Secret::new(raw), + key, + } } } @@ -55,7 +56,7 @@ impl Random for PrivateKeyInner { impl Write for PrivateKeyInner { fn write(&self, buf: &mut impl BufMut) { - self.raw.write(buf); + self.raw.expose().write(buf); } } @@ -70,7 +71,10 @@ impl Read for PrivateKeyInner { #[cfg(not(feature = "std"))] let key = result .map_err(|e| CodecError::Wrapped(CURVE_NAME, alloc::format!("{:?}", e).into()))?; - Ok(Self { raw, key }) + Ok(Self { + raw: Secret::new(raw), + key, + }) } } @@ -84,13 +88,13 @@ impl Array for PrivateKeyInner {} impl Hash for PrivateKeyInner { fn hash(&self, state: &mut H) { - self.raw.hash(state); + self.raw.expose().hash(state); } } impl Ord for PrivateKeyInner { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.cmp(&other.raw) + self.raw.expose().cmp(other.raw.expose()) } } @@ -102,14 +106,14 @@ impl PartialOrd for PrivateKeyInner { impl AsRef<[u8]> for PrivateKeyInner { fn as_ref(&self) -> &[u8] { - &self.raw + self.raw.expose() } } impl Deref for PrivateKeyInner { type Target = [u8]; fn deref(&self) -> &[u8] { - &self.raw + self.raw.expose() } } @@ -121,13 +125,13 @@ impl From for PrivateKeyInner { impl Debug for PrivateKeyInner { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", hex(&self.raw)) + f.write_str("[REDACTED]") } } impl Display for PrivateKeyInner { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", hex(&self.raw)) + f.write_str("[REDACTED]") } } diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs new file mode 100644 index 0000000000..0a16ba25dc --- /dev/null +++ b/cryptography/src/secret.rs @@ -0,0 +1,203 @@ +//! A wrapper type for secret values that prevents accidental leakage. +//! +//! # Status +//! +//! `Secret` provides the following guarantees: +//! - Debug and Display always show `[REDACTED]` instead of the actual value +//! - The inner value is zeroized on drop +//! - Access to the inner value requires an explicit `expose()` call + +use core::{ + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, +}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +/// A wrapper for secret values that prevents accidental leakage. +/// +/// `Secret` ensures that: +/// - Debug and Display always show `[REDACTED]` instead of the actual value +/// - The inner value is zeroized on drop when `T: Zeroize` +/// - Access to the inner value requires an explicit `expose()` call +/// +/// # Example +/// +/// ``` +/// use commonware_cryptography::Secret; +/// use zeroize::Zeroize; +/// +/// let secret = Secret::new([1u8, 2, 3, 4]); +/// +/// // Debug output is redacted +/// assert_eq!(format!("{:?}", secret), "[REDACTED]"); +/// +/// // Access requires explicit call +/// assert_eq!(secret.expose(), &[1u8, 2, 3, 4]); +/// ``` +pub struct Secret(T); + +impl Secret { + /// Creates a new `Secret` wrapping the given value. + #[inline] + pub const fn new(value: T) -> Self { + Self(value) + } + + /// Exposes the secret value for use. + /// + /// # Warning + /// + /// This method should be used sparingly and only when the secret + /// value is actually needed for cryptographic operations. + #[inline] + pub const fn expose(&self) -> &T { + &self.0 + } + + /// Exposes the secret value mutably. + /// + /// # Warning + /// + /// This method should be used sparingly and only when mutable access + /// to the secret value is actually needed. + #[inline] + pub const fn expose_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl Debug for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") + } +} + +impl Display for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") + } +} + +impl Zeroize for Secret { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Drop for Secret { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +// SAFETY: Secret auto-zeroizes on drop via the Drop impl above. +// This marker trait indicates this behavior to users. +impl ZeroizeOnDrop for Secret {} + +impl Clone for Secret { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl PartialEq for Secret { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for Secret {} + +impl Hash for Secret { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl PartialOrd for Secret { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} + +impl Ord for Secret { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_debug_redacted() { + let secret = Secret::new([1u8, 2, 3, 4]); + assert_eq!(format!("{:?}", secret), "[REDACTED]"); + } + + #[test] + fn test_display_redacted() { + let secret = Secret::new([1u8, 2, 3, 4]); + assert_eq!(format!("{}", secret), "[REDACTED]"); + } + + #[test] + fn test_expose() { + let secret = Secret::new([1u8, 2, 3, 4]); + assert_eq!(secret.expose(), &[1u8, 2, 3, 4]); + } + + #[test] + fn test_expose_mut() { + let mut secret = Secret::new([1u8, 2, 3, 4]); + secret.expose_mut()[0] = 5; + assert_eq!(secret.expose(), &[5u8, 2, 3, 4]); + } + + #[test] + fn test_clone() { + let secret = Secret::new([1u8, 2, 3, 4]); + let cloned = secret.clone(); + assert_eq!(secret.expose(), cloned.expose()); + } + + #[test] + fn test_equality() { + let s1 = Secret::new([1u8, 2, 3, 4]); + let s2 = Secret::new([1u8, 2, 3, 4]); + let s3 = Secret::new([5u8, 6, 7, 8]); + assert_eq!(s1, s2); + assert_ne!(s1, s3); + } + + #[test] + fn test_zeroize() { + let mut secret = Secret::new([1u8, 2, 3, 4]); + secret.zeroize(); + assert_eq!(secret.expose(), &[0u8, 0, 0, 0]); + } + + #[test] + fn test_hash() { + use std::collections::hash_map::DefaultHasher; + + let s1 = Secret::new([1u8, 2, 3, 4]); + let s2 = Secret::new([1u8, 2, 3, 4]); + + let mut hasher1 = DefaultHasher::new(); + let mut hasher2 = DefaultHasher::new(); + s1.hash(&mut hasher1); + s2.hash(&mut hasher2); + + assert_eq!(hasher1.finish(), hasher2.finish()); + } + + #[test] + fn test_ordering() { + let s1 = Secret::new([1u8, 2, 3, 4]); + let s2 = Secret::new([5u8, 6, 7, 8]); + assert!(s1 < s2); + assert!(s2 > s1); + } +} From 1ccaa2b234f0651a67615529cfb76d5c9f63551f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 14:27:29 +0000 Subject: [PATCH 02/65] [cryptography] mlock/mprotect secret memory --- Cargo.lock | 1 + cryptography/Cargo.toml | 3 + .../secp256r1_recoverable_decode.rs | 2 +- .../fuzz_targets/secp256r1_standard_decode.rs | 2 +- cryptography/src/bls12381/dkg.rs | 70 +- cryptography/src/bls12381/primitives/group.rs | 20 +- cryptography/src/bls12381/primitives/ops.rs | 12 +- cryptography/src/bls12381/scheme.rs | 26 +- cryptography/src/ed25519/scheme.rs | 18 +- cryptography/src/lib.rs | 13 +- cryptography/src/secp256r1/common.rs | 65 +- cryptography/src/secp256r1/recoverable.rs | 2 +- cryptography/src/secp256r1/standard.rs | 2 +- cryptography/src/secret.rs | 666 +++++++++++++++--- 14 files changed, 727 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5541df21f..cda4bd6abe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,6 +1211,7 @@ dependencies = [ "ecdsa", "ed25519-consensus", "getrandom 0.2.16", + "libc", "p256", "proptest", "rand 0.8.5", diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 6875c0c5a3..b3b87d9b70 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -41,6 +41,9 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } version = "0.2.15" features = ["js"] +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [dev-dependencies] anyhow.workspace = true commonware-conformance.workspace = true diff --git a/cryptography/fuzz/fuzz_targets/secp256r1_recoverable_decode.rs b/cryptography/fuzz/fuzz_targets/secp256r1_recoverable_decode.rs index c652cbad0d..7d7e654cb6 100644 --- a/cryptography/fuzz/fuzz_targets/secp256r1_recoverable_decode.rs +++ b/cryptography/fuzz/fuzz_targets/secp256r1_recoverable_decode.rs @@ -53,7 +53,7 @@ fn test_private_key(data: &[u8]) { if let (Ok(ref_key), Ok(our_key)) = (ref_result, our_result) { assert_eq!( ref_key.to_bytes().as_slice(), - our_key.as_ref(), + our_key.encode().as_ref(), "32-byte input: keys don't match" ); } diff --git a/cryptography/fuzz/fuzz_targets/secp256r1_standard_decode.rs b/cryptography/fuzz/fuzz_targets/secp256r1_standard_decode.rs index 8e91a70014..cdc7f83bcd 100644 --- a/cryptography/fuzz/fuzz_targets/secp256r1_standard_decode.rs +++ b/cryptography/fuzz/fuzz_targets/secp256r1_standard_decode.rs @@ -53,7 +53,7 @@ fn test_private_key(data: &[u8]) { if let (Ok(ref_key), Ok(our_key)) = (ref_result, our_result) { assert_eq!( ref_key.to_bytes().as_slice(), - our_key.as_ref(), + our_key.encode().as_ref(), "32-byte input: keys don't match" ); } diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 69e58ab39d..c64043cf4a 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -290,7 +290,7 @@ use crate::{ variant::Variant, }, transcript::{Summary, Transcript}, - PublicKey, Secret, Signer, + PublicKey, Secret, SecretGuard, SecretGuardMut, Signer, }; use commonware_codec::{Encode, EncodeSize, RangeCfg, Read, ReadExt, Write}; use commonware_math::{ @@ -565,7 +565,8 @@ impl Info { return false; }; let expected = pub_msg.commitment.eval_msm(&scalar); - expected == V::Public::generator() * priv_msg.share.expose() + let guard = priv_msg.expose_share(); + expected == V::Public::generator() * &*guard } } @@ -679,7 +680,7 @@ where } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct DealerPrivMsg { share: Secret, } @@ -688,20 +689,48 @@ impl DealerPrivMsg { /// Creates a new DealerPrivMsg with the given share. /// /// The share is wrapped in a `Secret` for secure handling. + /// On Unix, this includes OS-level memory protection (mlock + mprotect). + /// + /// Note: This function is const on non-Unix platforms only. + #[cfg(not(unix))] pub const fn new(share: Scalar) -> Self { Self { share: Secret::new(share), } } - /// Returns a reference to the wrapped share. - pub const fn share(&self) -> &Secret { - &self.share + /// Creates a new DealerPrivMsg with the given share. + /// + /// The share is wrapped in a `Secret` for secure handling. + /// On Unix, this includes OS-level memory protection (mlock + mprotect). + #[cfg(unix)] + pub fn new(share: Scalar) -> Self { + Self { + share: Secret::new(share), + } + } + + /// Temporarily exposes the share for reading. + /// + /// The returned guard re-protects the memory when dropped (on supported platforms). + #[cfg(not(unix))] + pub const fn expose_share(&self) -> SecretGuard<'_, Scalar> { + self.share.expose() + } + + /// Temporarily exposes the share for reading. + /// + /// The returned guard re-protects the memory when dropped (on supported platforms). + #[cfg(unix)] + pub fn expose_share(&self) -> SecretGuard<'_, Scalar> { + self.share.expose() } - /// Returns a mutable reference to the wrapped share. - pub const fn share_mut(&mut self) -> &mut Secret { - &mut self.share + /// Temporarily exposes the share for mutation. + /// + /// The returned guard re-protects the memory when dropped (on supported platforms). + pub fn expose_share_mut(&mut self) -> SecretGuardMut<'_, Scalar> { + self.share.expose_mut() } } @@ -725,9 +754,7 @@ impl Read for DealerPrivMsg { _cfg: &Self::Cfg, ) -> Result { let share: Scalar = ReadExt::read(buf)?; - Ok(Self { - share: Secret::new(share), - }) + Ok(Self::new(share)) } } @@ -735,12 +762,19 @@ impl Read for DealerPrivMsg { impl arbitrary::Arbitrary<'_> for DealerPrivMsg { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let share: Scalar = u.arbitrary()?; - Ok(Self { - share: Secret::new(share), - }) + Ok(Self::new(share)) + } +} + +impl PartialEq for DealerPrivMsg { + fn eq(&self, other: &Self) -> bool { + // Use Secret's constant-time comparison + self.share == other.share } } +impl Eq for DealerPrivMsg {} + #[derive(Clone, Debug)] pub struct PlayerAck { sig: P::Signature, @@ -1495,7 +1529,7 @@ impl Player { let share = self .view .get(dealer) - .map(|(_, priv_msg)| priv_msg.share().expose().clone()) + .map(|(_, priv_msg)| (*priv_msg.expose_share()).clone()) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( || { @@ -1503,7 +1537,7 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - |priv_msg| priv_msg.share().expose().clone(), + |priv_msg| (*priv_msg.expose_share()).clone(), ) }); (dealer.clone(), share) @@ -1996,7 +2030,7 @@ mod test_plan { for (player, priv_msg) in &mut priv_msgs { let player_key_idx = pk_to_key_idx[player]; if round.bad_shares.contains(&(i_dealer, player_key_idx)) { - *priv_msg.share_mut().expose_mut() = Scalar::random(&mut rng); + *priv_msg.expose_share_mut() = Scalar::random(&mut rng); } } assert_eq!(priv_msgs.len(), players.len()); diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 127dcaafce..d486ca0fdb 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -501,9 +501,12 @@ pub struct Share { private: Secret, } +// AsRef is only available on non-protected platforms because protected memory +// requires holding a guard to keep the memory accessible. +#[cfg(not(unix))] impl AsRef for Share { fn as_ref(&self) -> &Private { - self.private.expose() + &*self.private.expose() } } @@ -511,6 +514,7 @@ impl Share { /// Creates a new Share with the given index and private key. /// /// The private key is wrapped in a `Secret` for secure handling. + #[cfg(not(unix))] pub const fn new(index: u32, private: Private) -> Self { Self { index, @@ -518,11 +522,23 @@ impl Share { } } + /// Creates a new Share with the given index and private key. + /// + /// The private key is wrapped in a `Secret` for secure handling. + #[cfg(unix)] + pub fn new(index: u32, private: Private) -> Self { + Self { + index, + private: Secret::new(private), + } + } + /// Returns the public key corresponding to the share. /// /// This can be verified against the public polynomial. pub fn public(&self) -> V::Public { - V::Public::generator() * self.private.expose() + let guard = self.private.expose(); + V::Public::generator() * &*guard } /// Returns a reference to the wrapped private key. diff --git a/cryptography/src/bls12381/primitives/ops.rs b/cryptography/src/bls12381/primitives/ops.rs index c4f92b36b7..9448901486 100644 --- a/cryptography/src/bls12381/primitives/ops.rs +++ b/cryptography/src/bls12381/primitives/ops.rs @@ -137,16 +137,14 @@ pub fn verify_message( } /// Generates a proof of possession for the private key share. +#[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected pub fn partial_sign_proof_of_possession( sharing: &Sharing, private: &Share, ) -> PartialSignature { // Sign the public key - let sig = sign::( - private.as_ref(), - V::PROOF_OF_POSSESSION, - &sharing.public().encode(), - ); + let guard = private.private().expose(); + let sig = sign::(&guard, V::PROOF_OF_POSSESSION, &sharing.public().encode()); PartialSignature { value: sig, index: private.index, @@ -171,12 +169,14 @@ pub fn partial_verify_proof_of_possession( } /// Signs the provided message with the key share. +#[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected pub fn partial_sign_message( private: &Share, namespace: Option<&[u8]>, message: &[u8], ) -> PartialSignature { - let sig = sign_message::(private.as_ref(), namespace, message); + let guard = private.private().expose(); + let sig = sign_message::(&guard, namespace, message); PartialSignature { value: sig, index: private.index, diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 9836fcc154..b5305d70b3 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -85,17 +85,23 @@ impl FixedSize for PrivateKey { impl Span for PrivateKey {} +// Array is only available on non-protected platforms because it requires +// AsRef<[u8]> and Deref, which need the memory to stay accessible +// without a guard. +#[cfg(not(unix))] impl Array for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - self.raw.expose().hash(state); + let guard = self.raw.expose(); + (*guard).hash(state); } } impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.expose().cmp(other.raw.expose()) + // Use Secret's constant-time comparison + self.raw.cmp(&other.raw) } } @@ -105,16 +111,20 @@ impl PartialOrd for PrivateKey { } } +// AsRef and Deref are only available on non-protected platforms because +// protected memory requires holding a guard to keep the memory accessible. +#[cfg(not(unix))] impl AsRef<[u8]> for PrivateKey { fn as_ref(&self) -> &[u8] { - self.raw.expose() + &*self.raw.expose() } } +#[cfg(not(unix))] impl Deref for PrivateKey { type Target = [u8]; fn deref(&self) -> &[u8] { - self.raw.expose() + &*self.raw.expose() } } @@ -146,8 +156,10 @@ impl crate::Signer for PrivateKey { type Signature = Signature; type PublicKey = PublicKey; + #[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected fn public_key(&self) -> Self::PublicKey { - PublicKey::from(ops::compute_public::(self.key.expose())) + let guard = self.key.expose(); + PublicKey::from(ops::compute_public::(&guard)) } fn sign(&self, namespace: &[u8], msg: &[u8]) -> Self::Signature { @@ -157,8 +169,10 @@ impl crate::Signer for PrivateKey { impl PrivateKey { #[inline(always)] + #[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected fn sign_inner(&self, namespace: Option<&[u8]>, message: &[u8]) -> Signature { - ops::sign_message::(self.key.expose(), namespace, message).into() + let guard = self.key.expose(); + ops::sign_message::(&guard, namespace, message).into() } } diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index b9c5fbcfb4..9d684a5a07 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -96,13 +96,18 @@ impl FixedSize for PrivateKey { impl Span for PrivateKey {} +// Array is only available on non-protected platforms because it requires +// AsRef<[u8]> and Deref, which need the memory to stay accessible +// without a guard. +#[cfg(not(unix))] impl Array for PrivateKey {} impl Eq for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - self.raw.expose().hash(state); + let guard = self.raw.expose(); + (*guard).hash(state); } } @@ -114,7 +119,8 @@ impl PartialEq for PrivateKey { impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.expose().cmp(other.raw.expose()) + // Use Secret's constant-time comparison + self.raw.cmp(&other.raw) } } @@ -124,16 +130,20 @@ impl PartialOrd for PrivateKey { } } +// AsRef and Deref are only available on non-protected platforms because +// protected memory requires holding a guard to keep the memory accessible. +#[cfg(not(unix))] impl AsRef<[u8]> for PrivateKey { fn as_ref(&self) -> &[u8] { - self.raw.expose() + &*self.raw.expose() } } +#[cfg(not(unix))] impl Deref for PrivateKey { type Target = [u8]; fn deref(&self) -> &[u8] { - self.raw.expose() + &*self.raw.expose() } } diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index dcf9047280..e1f8e7841e 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -36,7 +36,7 @@ pub mod lthash; pub use lthash::LtHash; pub mod secp256r1; pub mod secret; -pub use secret::Secret; +pub use secret::{Secret, SecretGuard, SecretGuardMut}; pub mod transcript; /// Produces [Signature]s over messages that can be verified with a corresponding [PublicKey]. @@ -74,8 +74,19 @@ pub trait Signer: Random + Send + Sync + Clone + 'static { } /// A [Signer] that can be serialized/deserialized. +/// +/// Note: On Unix, `Array` is not a bound because protected secrets cannot +/// implement `AsRef`/`Deref` without holding a guard. +#[cfg(not(unix))] pub trait PrivateKey: Signer + Sized + ReadExt + Encode + PartialEq + Array {} +/// A [Signer] that can be serialized/deserialized. +/// +/// Note: On Unix, `Array` is not a bound because protected secrets cannot +/// implement `AsRef`/`Deref` without holding a guard. +#[cfg(unix)] +pub trait PrivateKey: Signer + Sized + ReadExt + Encode + PartialEq {} + /// Verifies [Signature]s over messages. pub trait Verifier { /// The type of [Signature] that this verifier can verify. diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 93bb37b3a8..356c6b39b1 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -1,10 +1,3 @@ -cfg_if::cfg_if! { - if #[cfg(feature = "std")] { - use std::borrow::ToOwned; - } else { - use alloc::borrow::ToOwned; - } -} use crate::Secret; use bytes::{Buf, BufMut}; use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write}; @@ -25,16 +18,13 @@ pub const PUBLIC_KEY_LENGTH: usize = 33; // Y-Parity || X /// Internal Secp256r1 Private Key storage. /// -/// Note: `SigningKey` implements `ZeroizeOnDrop` (not `Zeroize`), so it cannot be wrapped -/// in `Secret`. The `raw` bytes are wrapped in `Secret` for zeroization, while `key` -/// relies on its own `ZeroizeOnDrop` implementation. +/// Only stores the raw key bytes in protected memory. The `SigningKey` is +/// reconstructed on demand to avoid keeping unprotected copies in memory. #[derive(Clone, Eq, PartialEq)] pub struct PrivateKeyInner { raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, - pub key: SigningKey, } -// SAFETY: Both `raw` (via Secret) and `key` (via its own ZeroizeOnDrop) are zeroized on drop. impl ZeroizeOnDrop for PrivateKeyInner {} impl PrivateKeyInner { @@ -43,9 +33,23 @@ impl PrivateKeyInner { let raw: [u8; PRIVATE_KEY_LENGTH] = bytes.into(); Self { raw: Secret::new(raw), - key, } } + + /// Reconstructs the `SigningKey` from the protected raw bytes. + /// + /// This is called on-demand to avoid keeping an unprotected `SigningKey` + /// in memory. + pub(crate) fn signing_key(&self) -> SigningKey { + let guard = self.raw.expose(); + // This cannot fail since we only store valid keys + SigningKey::from_slice(&*guard).expect("stored key bytes are always valid") + } + + /// Returns the `VerifyingKey` corresponding to this private key. + pub fn verifying_key(&self) -> VerifyingKey { + *self.signing_key().verifying_key() + } } impl Random for PrivateKeyInner { @@ -65,15 +69,14 @@ impl Read for PrivateKeyInner { fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let raw = <[u8; PRIVATE_KEY_LENGTH]>::read(buf)?; + // Validate that the bytes form a valid key let result = SigningKey::from_slice(&raw); #[cfg(feature = "std")] - let key = result.map_err(|e| CodecError::Wrapped(CURVE_NAME, e.into()))?; + result.map_err(|e| CodecError::Wrapped(CURVE_NAME, e.into()))?; #[cfg(not(feature = "std"))] - let key = result - .map_err(|e| CodecError::Wrapped(CURVE_NAME, alloc::format!("{:?}", e).into()))?; + result.map_err(|e| CodecError::Wrapped(CURVE_NAME, alloc::format!("{:?}", e).into()))?; Ok(Self { raw: Secret::new(raw), - key, }) } } @@ -84,17 +87,23 @@ impl FixedSize for PrivateKeyInner { impl Span for PrivateKeyInner {} +// Array is only available on non-protected platforms because it requires +// AsRef<[u8]> and Deref, which need the memory to stay accessible +// without a guard. +#[cfg(not(unix))] impl Array for PrivateKeyInner {} impl Hash for PrivateKeyInner { fn hash(&self, state: &mut H) { - self.raw.expose().hash(state); + let guard = self.raw.expose(); + (*guard).hash(state); } } impl Ord for PrivateKeyInner { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.expose().cmp(other.raw.expose()) + // Use Secret's constant-time comparison + self.raw.cmp(&other.raw) } } @@ -104,16 +113,20 @@ impl PartialOrd for PrivateKeyInner { } } +// AsRef and Deref are only available on non-protected platforms because +// protected memory requires holding a guard to keep the memory accessible. +#[cfg(not(unix))] impl AsRef<[u8]> for PrivateKeyInner { fn as_ref(&self) -> &[u8] { - self.raw.expose() + &*self.raw.expose() } } +#[cfg(not(unix))] impl Deref for PrivateKeyInner { type Target = [u8]; fn deref(&self) -> &[u8] { - self.raw.expose() + &*self.raw.expose() } } @@ -161,7 +174,7 @@ impl PublicKeyInner { } pub fn from_private_key(private_key: &PrivateKeyInner) -> Self { - Self::new(private_key.key.verifying_key().to_owned()) + Self::new(private_key.verifying_key()) } } @@ -273,6 +286,10 @@ macro_rules! impl_private_key_wrapper { impl commonware_utils::Span for $name {} + // Array is only available on non-protected platforms because it requires + // AsRef<[u8]> and Deref, which need the memory to stay accessible + // without a guard. + #[cfg(not(unix))] impl commonware_utils::Array for $name {} impl core::hash::Hash for $name { @@ -293,12 +310,16 @@ macro_rules! impl_private_key_wrapper { } } + // AsRef and Deref are only available on non-protected platforms because + // protected memory requires holding a guard to keep the memory accessible. + #[cfg(not(unix))] impl AsRef<[u8]> for $name { fn as_ref(&self) -> &[u8] { self.0.as_ref() } } + #[cfg(not(unix))] impl core::ops::Deref for $name { type Target = [u8]; fn deref(&self) -> &[u8] { diff --git a/cryptography/src/secp256r1/recoverable.rs b/cryptography/src/secp256r1/recoverable.rs index c333529a12..09a811c08c 100644 --- a/cryptography/src/secp256r1/recoverable.rs +++ b/cryptography/src/secp256r1/recoverable.rs @@ -50,7 +50,7 @@ impl PrivateKey { }); let (mut signature, mut recovery_id) = self .0 - .key + .signing_key() .sign_recoverable(&payload) .expect("signing must succeed"); diff --git a/cryptography/src/secp256r1/standard.rs b/cryptography/src/secp256r1/standard.rs index b0804731ef..2583769e76 100644 --- a/cryptography/src/secp256r1/standard.rs +++ b/cryptography/src/secp256r1/standard.rs @@ -49,7 +49,7 @@ impl PrivateKey { let payload = namespace.map_or(Cow::Borrowed(msg), |namespace| { Cow::Owned(union_unique(namespace, msg)) }); - let signature: p256::ecdsa::Signature = self.0.key.sign(&payload); + let signature: p256::ecdsa::Signature = self.0.signing_key().sign(&payload); let signature = signature.normalize_s().unwrap_or(signature); Signature::from(signature) } diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 0a16ba25dc..979c0bc8ef 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -6,126 +6,560 @@ //! - Debug and Display always show `[REDACTED]` instead of the actual value //! - The inner value is zeroized on drop //! - Access to the inner value requires an explicit `expose()` call +//! - Comparisons use constant-time operations to prevent timing attacks +//! +//! # Platform-Specific Behavior +//! +//! On Unix platforms, `Secret` +//! provides additional OS-level memory protection: +//! - Memory is locked to prevent swapping (mlock) +//! - Memory is marked no-access except during expose() (mprotect) +//! +//! On other platforms, `Secret` provides software-only protection +//! (zeroization and redacted debug output). +//! +//! # Type Constraints +//! +//! When using protected memory, `Secret` only provides full protection for +//! self-contained types (no heap pointers). Types like `Vec` or `String` +//! will only have their metadata protected, not heap data. -use core::{ - fmt::{Debug, Display, Formatter}, - hash::{Hash, Hasher}, -}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -/// A wrapper for secret values that prevents accidental leakage. -/// -/// `Secret` ensures that: -/// - Debug and Display always show `[REDACTED]` instead of the actual value -/// - The inner value is zeroized on drop when `T: Zeroize` -/// - Access to the inner value requires an explicit `expose()` call +/// Constant-time equality comparison for byte slices. /// -/// # Example -/// -/// ``` -/// use commonware_cryptography::Secret; -/// use zeroize::Zeroize; -/// -/// let secret = Secret::new([1u8, 2, 3, 4]); -/// -/// // Debug output is redacted -/// assert_eq!(format!("{:?}", secret), "[REDACTED]"); +/// XORs all bytes together and checks if the result is zero. +/// This prevents timing attacks by always comparing all bytes. +#[inline] +fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +/// Constant-time less-than comparison for byte slices (big-endian). /// -/// // Access requires explicit call -/// assert_eq!(secret.expose(), &[1u8, 2, 3, 4]); -/// ``` -pub struct Secret(T); +/// Returns true if a < b, using constant-time operations. +#[inline] +fn ct_lt_bytes(a: &[u8], b: &[u8]) -> bool { + debug_assert_eq!(a.len(), b.len()); + let mut result = 0u8; // 0 = equal so far, 1 = a < b, 2 = a > b + for (x, y) in a.iter().zip(b.iter()) { + // Only update result if we haven't found a difference yet (result == 0) + let is_equal_so_far = result.wrapping_sub(1) >> 7; // 1 if result == 0, 0 otherwise + let x_lt_y = ((*x as u16).wrapping_sub(*y as u16) >> 8) as u8; // 1 if x < y + let x_gt_y = ((*y as u16).wrapping_sub(*x as u16) >> 8) as u8; // 1 if x > y + result |= is_equal_so_far & ((x_lt_y) | (x_gt_y << 1)); + } + result == 1 +} + +// Use protected implementation on Unix with the feature enabled +#[cfg(unix)] +mod implementation { + use core::{ + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut}, + ptr::NonNull, + }; + use std::alloc::{alloc, dealloc, Layout}; + use zeroize::{Zeroize, ZeroizeOnDrop}; -impl Secret { - /// Creates a new `Secret` wrapping the given value. - #[inline] - pub const fn new(value: T) -> Self { - Self(value) + /// Returns the system page size. + fn page_size() -> usize { + // SAFETY: sysconf is safe to call with _SC_PAGESIZE + let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + if size <= 0 { + 4096 + } else { + size as usize + } } - /// Exposes the secret value for use. + /// A wrapper for secret values with OS-level memory protection. /// - /// # Warning + /// On Unix: + /// - Memory is locked to prevent swapping (mlock) + /// - Memory is marked no-access except during expose() (mprotect) + /// - Zeroized on drop /// - /// This method should be used sparingly and only when the secret - /// value is actually needed for cryptographic operations. - #[inline] - pub const fn expose(&self) -> &T { - &self.0 + /// Access requires explicit `expose()` call which returns a guard. + /// Memory is re-protected when the guard is dropped. + pub struct Secret { + ptr: NonNull, + size: usize, } - /// Exposes the secret value mutably. - /// - /// # Warning - /// - /// This method should be used sparingly and only when mutable access - /// to the secret value is actually needed. - #[inline] - pub const fn expose_mut(&mut self) -> &mut T { - &mut self.0 + // SAFETY: Secret owns its memory and ensures proper synchronization + // through the guard pattern. Access to the protected memory region is + // controlled by mprotect calls that make the memory accessible only + // during the lifetime of guard objects. + unsafe impl Send for Secret {} + // SAFETY: Same reasoning as Send - the guard pattern ensures proper + // synchronization of memory access. + unsafe impl Sync for Secret {} + + impl Secret { + /// Creates a new `Secret` wrapping the given value. + /// + /// # Panics + /// + /// Panics if memory protection fails (allocation, mlock, or mprotect). + #[inline] + pub fn new(value: T) -> Self { + Self::try_new(value).expect("failed to create protected secret") + } + + /// Creates a new `Secret`, returning an error on failure. + /// + /// # Safety Invariants + /// + /// This function performs several unsafe operations to set up protected memory: + /// - Allocates page-aligned memory using the global allocator + /// - Writes the value to the allocated memory + /// - Locks the memory with mlock to prevent swapping + /// - Protects the memory with mprotect to prevent unauthorized access + /// + /// All unsafe operations are properly sequenced and cleaned up on failure. + #[allow(clippy::undocumented_unsafe_blocks)] + pub fn try_new(value: T) -> Result { + let page_size = page_size(); + let type_size = core::mem::size_of::(); + // Round up to page boundary (minimum one page) + let size = type_size.max(1).next_multiple_of(page_size); + + let layout = Layout::from_size_align(size, page_size).map_err(|_| "invalid layout")?; + + // SAFETY: layout is valid (checked above), ptr may be null (checked below) + let ptr = unsafe { alloc(layout) } as *mut T; + + if ptr.is_null() { + return Err("allocation failed"); + } + + // SAFETY: ptr is non-null and properly aligned for T + unsafe { core::ptr::write(ptr, value) }; + + // SAFETY: ptr points to valid allocated memory of size `size` + if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { + // SAFETY: ptr and layout match the allocation above + unsafe { dealloc(ptr as *mut u8, layout) }; + return Err("mlock failed"); + } + + // SAFETY: ptr points to valid locked memory of size `size` + if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { + // SAFETY: cleanup on failure - unlock and deallocate + unsafe { + libc::munlock(ptr as *const libc::c_void, size); + dealloc(ptr as *mut u8, layout); + } + return Err("mprotect failed"); + } + + Ok(Self { + // SAFETY: ptr is non-null (checked above) + ptr: unsafe { NonNull::new_unchecked(ptr) }, + size, + }) + } + + /// Exposes the secret value for use. + /// + /// Returns a guard that re-protects memory when dropped. + #[inline] + pub fn expose(&self) -> SecretGuard<'_, T> { + // SAFETY: self.ptr points to valid protected memory of self.size bytes + let result = unsafe { + libc::mprotect( + self.ptr.as_ptr() as *mut libc::c_void, + self.size, + libc::PROT_READ, + ) + }; + assert_eq!(result, 0, "mprotect failed to unprotect memory"); + SecretGuard { secret: self } + } + + /// Exposes the secret value mutably. + /// + /// Returns a guard that re-protects memory when dropped. + #[inline] + pub fn expose_mut(&mut self) -> SecretGuardMut<'_, T> { + // SAFETY: self.ptr points to valid protected memory of self.size bytes + let result = unsafe { + libc::mprotect( + self.ptr.as_ptr() as *mut libc::c_void, + self.size, + libc::PROT_READ | libc::PROT_WRITE, + ) + }; + assert_eq!(result, 0, "mprotect failed to unprotect memory"); + SecretGuardMut { secret: self } + } } -} -impl Debug for Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") + impl Debug for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") + } } -} -impl Display for Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") + impl Display for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") + } } -} -impl Zeroize for Secret { - fn zeroize(&mut self) { - self.0.zeroize(); + impl Drop for Secret { + fn drop(&mut self) { + let page_size = page_size(); + // SAFETY: self.ptr points to valid memory that was allocated with page_size + // alignment and self.size bytes. We unprotect, zeroize, unlock, and deallocate + // in proper sequence. This is safe because we have exclusive access (&mut self). + unsafe { + libc::mprotect( + self.ptr.as_ptr() as *mut libc::c_void, + self.size, + libc::PROT_READ | libc::PROT_WRITE, + ); + (*self.ptr.as_ptr()).zeroize(); + libc::munlock(self.ptr.as_ptr() as *const libc::c_void, self.size); + let layout = Layout::from_size_align_unchecked(self.size, page_size); + dealloc(self.ptr.as_ptr() as *mut u8, layout); + } + } } -} -impl Drop for Secret { - fn drop(&mut self) { - self.0.zeroize(); + impl ZeroizeOnDrop for Secret {} + + impl Clone for Secret { + fn clone(&self) -> Self { + let guard = self.expose(); + Self::new((*guard).clone()) + } } -} -// SAFETY: Secret auto-zeroizes on drop via the Drop impl above. -// This marker trait indicates this behavior to users. -impl ZeroizeOnDrop for Secret {} + impl PartialEq for Secret { + fn eq(&self, other: &Self) -> bool { + let guard_self = self.expose(); + let guard_other = other.expose(); + // SAFETY: We're reading the raw bytes of T for constant-time comparison. + // This is safe because T is Sized and we only read size_of::() bytes. + let self_bytes = unsafe { + core::slice::from_raw_parts( + &*guard_self as *const T as *const u8, + core::mem::size_of::(), + ) + }; + // SAFETY: Same as above - reading raw bytes of a Sized type. + let other_bytes = unsafe { + core::slice::from_raw_parts( + &*guard_other as *const T as *const u8, + core::mem::size_of::(), + ) + }; + super::ct_eq_bytes(self_bytes, other_bytes) + } + } + + impl Eq for Secret {} -impl Clone for Secret { - fn clone(&self) -> Self { - Self(self.0.clone()) + impl Hash for Secret { + fn hash(&self, state: &mut H) { + let guard = self.expose(); + (*guard).hash(state); + } } -} -impl PartialEq for Secret { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 + impl PartialOrd for Secret { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } -} -impl Eq for Secret {} + impl Ord for Secret { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + let guard_self = self.expose(); + let guard_other = other.expose(); + // SAFETY: We're reading the raw bytes of T for constant-time comparison. + // This is safe because T is Sized and we only read size_of::() bytes. + let self_bytes = unsafe { + core::slice::from_raw_parts( + &*guard_self as *const T as *const u8, + core::mem::size_of::(), + ) + }; + // SAFETY: Same as above - reading raw bytes of a Sized type. + let other_bytes = unsafe { + core::slice::from_raw_parts( + &*guard_other as *const T as *const u8, + core::mem::size_of::(), + ) + }; + if super::ct_eq_bytes(self_bytes, other_bytes) { + core::cmp::Ordering::Equal + } else if super::ct_lt_bytes(self_bytes, other_bytes) { + core::cmp::Ordering::Less + } else { + core::cmp::Ordering::Greater + } + } + } -impl Hash for Secret { - fn hash(&self, state: &mut H) { - self.0.hash(state); + /// RAII guard for read access to a secret. + pub struct SecretGuard<'a, T: Zeroize> { + secret: &'a Secret, + } + + impl Deref for SecretGuard<'_, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + // SAFETY: The memory is currently unprotected (PROT_READ) because + // this guard exists, and the pointer is valid for the lifetime of Secret. + unsafe { self.secret.ptr.as_ref() } + } + } + + impl Drop for SecretGuard<'_, T> { + fn drop(&mut self) { + // SAFETY: Re-protect the memory when the guard is dropped. + // The pointer and size are valid from the Secret. + unsafe { + libc::mprotect( + self.secret.ptr.as_ptr() as *mut libc::c_void, + self.secret.size, + libc::PROT_NONE, + ); + } + } + } + + /// RAII guard for mutable access to a secret. + pub struct SecretGuardMut<'a, T: Zeroize> { + secret: &'a mut Secret, + } + + impl Deref for SecretGuardMut<'_, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + // SAFETY: The memory is currently unprotected (PROT_READ|PROT_WRITE) because + // this guard exists, and the pointer is valid for the lifetime of Secret. + unsafe { self.secret.ptr.as_ref() } + } } -} -impl PartialOrd for Secret { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) + impl DerefMut for SecretGuardMut<'_, T> { + #[inline] + fn deref_mut(&mut self) -> &mut T { + // SAFETY: The memory is currently unprotected (PROT_READ|PROT_WRITE) because + // this guard exists, and we have exclusive mutable access. + unsafe { self.secret.ptr.as_mut() } + } + } + + impl Drop for SecretGuardMut<'_, T> { + fn drop(&mut self) { + // SAFETY: Re-protect the memory when the guard is dropped. + // The pointer and size are valid from the Secret. + unsafe { + libc::mprotect( + self.secret.ptr.as_ptr() as *mut libc::c_void, + self.secret.size, + libc::PROT_NONE, + ); + } + } } } -impl Ord for Secret { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.cmp(&other.0) +// Simple implementation for non-Unix platforms +#[cfg(not(unix))] +mod implementation { + use core::{ + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + }; + use zeroize::{Zeroize, ZeroizeOnDrop}; + + /// A wrapper for secret values that prevents accidental leakage. + /// + /// Without OS-level protection (non-Unix): + /// - Debug and Display show `[REDACTED]` + /// - Zeroized on drop + /// - Access requires explicit `expose()` call + pub struct Secret(T); + + impl Secret { + /// Creates a new `Secret` wrapping the given value. + #[inline] + pub const fn new(value: T) -> Self { + Self(value) + } + + /// Exposes the secret value for use. + /// + /// # Warning + /// + /// This method should be used sparingly and only when the secret + /// value is actually needed for cryptographic operations. + #[inline] + pub fn expose(&self) -> SecretGuard<'_, T> { + SecretGuard(&self.0) + } + + /// Exposes the secret value mutably. + /// + /// # Warning + /// + /// This method should be used sparingly and only when mutable access + /// to the secret value is actually needed. + #[inline] + pub fn expose_mut(&mut self) -> SecretGuardMut<'_, T> { + SecretGuardMut(&mut self.0) + } + } + + impl Debug for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") + } + } + + impl Display for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") + } + } + + impl Zeroize for Secret { + fn zeroize(&mut self) { + self.0.zeroize(); + } + } + + impl Drop for Secret { + fn drop(&mut self) { + self.0.zeroize(); + } + } + + impl ZeroizeOnDrop for Secret {} + + impl Clone for Secret { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + + impl PartialEq for Secret { + fn eq(&self, other: &Self) -> bool { + // SAFETY: We're reading the raw bytes of T for constant-time comparison. + // This is safe because T is Sized and we only read size_of::() bytes. + let self_bytes = unsafe { + core::slice::from_raw_parts( + &self.0 as *const T as *const u8, + core::mem::size_of::(), + ) + }; + let other_bytes = unsafe { + core::slice::from_raw_parts( + &other.0 as *const T as *const u8, + core::mem::size_of::(), + ) + }; + super::ct_eq_bytes(self_bytes, other_bytes) + } + } + + impl Eq for Secret {} + + impl Hash for Secret { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } + } + + impl PartialOrd for Secret { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for Secret { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + // SAFETY: We're reading the raw bytes of T for constant-time comparison. + // This is safe because T is Sized and we only read size_of::() bytes. + let self_bytes = unsafe { + core::slice::from_raw_parts( + &self.0 as *const T as *const u8, + core::mem::size_of::(), + ) + }; + let other_bytes = unsafe { + core::slice::from_raw_parts( + &other.0 as *const T as *const u8, + core::mem::size_of::(), + ) + }; + if super::ct_eq_bytes(self_bytes, other_bytes) { + core::cmp::Ordering::Equal + } else if super::ct_lt_bytes(self_bytes, other_bytes) { + core::cmp::Ordering::Less + } else { + core::cmp::Ordering::Greater + } + } + } + + use core::ops::{Deref, DerefMut}; + + /// RAII guard for read access to a secret. + /// + /// On non-Unix platforms, this is a simple wrapper around a reference. + pub struct SecretGuard<'a, T: Zeroize>(&'a T); + + impl Deref for SecretGuard<'_, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.0 + } + } + + /// RAII guard for mutable access to a secret. + /// + /// On non-Unix platforms, this is a simple wrapper around a mutable reference. + pub struct SecretGuardMut<'a, T: Zeroize>(&'a mut T); + + impl Deref for SecretGuardMut<'_, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.0 + } + } + + impl DerefMut for SecretGuardMut<'_, T> { + #[inline] + fn deref_mut(&mut self) -> &mut T { + self.0 + } } } +pub use implementation::*; + #[cfg(test)] mod tests { use super::*; @@ -145,21 +579,26 @@ mod tests { #[test] fn test_expose() { let secret = Secret::new([1u8, 2, 3, 4]); - assert_eq!(secret.expose(), &[1u8, 2, 3, 4]); + let guard = secret.expose(); + assert_eq!(&*guard, &[1u8, 2, 3, 4]); } #[test] fn test_expose_mut() { let mut secret = Secret::new([1u8, 2, 3, 4]); - secret.expose_mut()[0] = 5; - assert_eq!(secret.expose(), &[5u8, 2, 3, 4]); + { + let mut guard = secret.expose_mut(); + guard[0] = 5; + } + let guard = secret.expose(); + assert_eq!(&*guard, &[5u8, 2, 3, 4]); } #[test] fn test_clone() { let secret = Secret::new([1u8, 2, 3, 4]); let cloned = secret.clone(); - assert_eq!(secret.expose(), cloned.expose()); + assert_eq!(&*secret.expose(), &*cloned.expose()); } #[test] @@ -172,32 +611,35 @@ mod tests { } #[test] - fn test_zeroize() { - let mut secret = Secret::new([1u8, 2, 3, 4]); - secret.zeroize(); - assert_eq!(secret.expose(), &[0u8, 0, 0, 0]); - } - - #[test] - fn test_hash() { - use std::collections::hash_map::DefaultHasher; + fn test_multiple_expose() { + let secret = Secret::new([42u8; 32]); - let s1 = Secret::new([1u8, 2, 3, 4]); - let s2 = Secret::new([1u8, 2, 3, 4]); - - let mut hasher1 = DefaultHasher::new(); - let mut hasher2 = DefaultHasher::new(); - s1.hash(&mut hasher1); - s2.hash(&mut hasher2); + // First expose + { + let guard = secret.expose(); + assert_eq!(guard[0], 42); + } - assert_eq!(hasher1.finish(), hasher2.finish()); + // Second expose after first guard dropped + { + let guard = secret.expose(); + assert_eq!(guard[31], 42); + } } + #[cfg(unix)] #[test] - fn test_ordering() { - let s1 = Secret::new([1u8, 2, 3, 4]); - let s2 = Secret::new([5u8, 6, 7, 8]); - assert!(s1 < s2); - assert!(s2 > s1); + fn test_with_bls_scalar() { + use crate::bls12381::primitives::group::Scalar; + use commonware_math::algebra::Random; + use rand::rngs::OsRng; + + let scalar = Scalar::random(&mut OsRng); + let secret = Secret::new(scalar); + + { + let guard = secret.expose(); + let _ = format!("{:?}", *guard); + } } } From a246b08176e70369ad1f265d226940ae70298a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 14:58:05 +0000 Subject: [PATCH 03/65] [cryptography] fix non-unix --- cryptography/src/bls12381/dkg.rs | 23 ---------- cryptography/src/bls12381/primitives/group.rs | 21 --------- cryptography/src/bls12381/scheme.rs | 27 +---------- cryptography/src/ed25519/scheme.rs | 27 +---------- cryptography/src/lib.rs | 11 ----- cryptography/src/secp256r1/common.rs | 46 ------------------- 6 files changed, 4 insertions(+), 151 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index c64043cf4a..31af8d34e4 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -690,20 +690,6 @@ impl DealerPrivMsg { /// /// The share is wrapped in a `Secret` for secure handling. /// On Unix, this includes OS-level memory protection (mlock + mprotect). - /// - /// Note: This function is const on non-Unix platforms only. - #[cfg(not(unix))] - pub const fn new(share: Scalar) -> Self { - Self { - share: Secret::new(share), - } - } - - /// Creates a new DealerPrivMsg with the given share. - /// - /// The share is wrapped in a `Secret` for secure handling. - /// On Unix, this includes OS-level memory protection (mlock + mprotect). - #[cfg(unix)] pub fn new(share: Scalar) -> Self { Self { share: Secret::new(share), @@ -713,15 +699,6 @@ impl DealerPrivMsg { /// Temporarily exposes the share for reading. /// /// The returned guard re-protects the memory when dropped (on supported platforms). - #[cfg(not(unix))] - pub const fn expose_share(&self) -> SecretGuard<'_, Scalar> { - self.share.expose() - } - - /// Temporarily exposes the share for reading. - /// - /// The returned guard re-protects the memory when dropped (on supported platforms). - #[cfg(unix)] pub fn expose_share(&self) -> SecretGuard<'_, Scalar> { self.share.expose() } diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index d486ca0fdb..d995233e78 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -501,31 +501,10 @@ pub struct Share { private: Secret, } -// AsRef is only available on non-protected platforms because protected memory -// requires holding a guard to keep the memory accessible. -#[cfg(not(unix))] -impl AsRef for Share { - fn as_ref(&self) -> &Private { - &*self.private.expose() - } -} - impl Share { /// Creates a new Share with the given index and private key. /// /// The private key is wrapped in a `Secret` for secure handling. - #[cfg(not(unix))] - pub const fn new(index: u32, private: Private) -> Self { - Self { - index, - private: Secret::new(private), - } - } - - /// Creates a new Share with the given index and private key. - /// - /// The private key is wrapped in a `Secret` for secure handling. - #[cfg(unix)] pub fn new(index: u32, private: Private) -> Self { Self { index, diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index b5305d70b3..8bb6142257 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -30,7 +30,7 @@ use super::primitives::{ ops, variant::{MinPk, Variant}, }; -use crate::{Array, BatchVerifier, Secret, Signer as _}; +use crate::{BatchVerifier, Secret, Signer as _}; #[cfg(not(feature = "std"))] use alloc::borrow::Cow; #[cfg(not(feature = "std"))] @@ -40,7 +40,7 @@ use commonware_codec::{ DecodeExt, EncodeFixed, Error as CodecError, FixedSize, Read, ReadExt, Write, }; use commonware_math::algebra::Random; -use commonware_utils::{hex, union_unique, Span}; +use commonware_utils::{hex, union_unique, Array, Span}; use core::{ fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, @@ -85,12 +85,6 @@ impl FixedSize for PrivateKey { impl Span for PrivateKey {} -// Array is only available on non-protected platforms because it requires -// AsRef<[u8]> and Deref, which need the memory to stay accessible -// without a guard. -#[cfg(not(unix))] -impl Array for PrivateKey {} - impl Hash for PrivateKey { fn hash(&self, state: &mut H) { let guard = self.raw.expose(); @@ -111,23 +105,6 @@ impl PartialOrd for PrivateKey { } } -// AsRef and Deref are only available on non-protected platforms because -// protected memory requires holding a guard to keep the memory accessible. -#[cfg(not(unix))] -impl AsRef<[u8]> for PrivateKey { - fn as_ref(&self) -> &[u8] { - &*self.raw.expose() - } -} - -#[cfg(not(unix))] -impl Deref for PrivateKey { - type Target = [u8]; - fn deref(&self) -> &[u8] { - &*self.raw.expose() - } -} - impl From for PrivateKey { fn from(key: Scalar) -> Self { let raw = key.encode_fixed(); diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 9d684a5a07..423204a75c 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -1,4 +1,4 @@ -use crate::{Array, BatchVerifier, Secret}; +use crate::{BatchVerifier, Secret}; #[cfg(not(feature = "std"))] use alloc::{ borrow::{Cow, ToOwned}, @@ -7,7 +7,7 @@ use alloc::{ use bytes::{Buf, BufMut}; use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write}; use commonware_math::algebra::Random; -use commonware_utils::{hex, union_unique, Span}; +use commonware_utils::{hex, union_unique, Array, Span}; use core::{ fmt::{Debug, Display}, hash::{Hash, Hasher}, @@ -96,12 +96,6 @@ impl FixedSize for PrivateKey { impl Span for PrivateKey {} -// Array is only available on non-protected platforms because it requires -// AsRef<[u8]> and Deref, which need the memory to stay accessible -// without a guard. -#[cfg(not(unix))] -impl Array for PrivateKey {} - impl Eq for PrivateKey {} impl Hash for PrivateKey { @@ -130,23 +124,6 @@ impl PartialOrd for PrivateKey { } } -// AsRef and Deref are only available on non-protected platforms because -// protected memory requires holding a guard to keep the memory accessible. -#[cfg(not(unix))] -impl AsRef<[u8]> for PrivateKey { - fn as_ref(&self) -> &[u8] { - &*self.raw.expose() - } -} - -#[cfg(not(unix))] -impl Deref for PrivateKey { - type Target = [u8]; - fn deref(&self) -> &[u8] { - &*self.raw.expose() - } -} - impl From for PrivateKey { fn from(key: ed25519_consensus::SigningKey) -> Self { let raw = key.to_bytes(); diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index e1f8e7841e..586930d98e 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -74,17 +74,6 @@ pub trait Signer: Random + Send + Sync + Clone + 'static { } /// A [Signer] that can be serialized/deserialized. -/// -/// Note: On Unix, `Array` is not a bound because protected secrets cannot -/// implement `AsRef`/`Deref` without holding a guard. -#[cfg(not(unix))] -pub trait PrivateKey: Signer + Sized + ReadExt + Encode + PartialEq + Array {} - -/// A [Signer] that can be serialized/deserialized. -/// -/// Note: On Unix, `Array` is not a bound because protected secrets cannot -/// implement `AsRef`/`Deref` without holding a guard. -#[cfg(unix)] pub trait PrivateKey: Signer + Sized + ReadExt + Encode + PartialEq {} /// Verifies [Signature]s over messages. diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 356c6b39b1..01e1aefb46 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -87,12 +87,6 @@ impl FixedSize for PrivateKeyInner { impl Span for PrivateKeyInner {} -// Array is only available on non-protected platforms because it requires -// AsRef<[u8]> and Deref, which need the memory to stay accessible -// without a guard. -#[cfg(not(unix))] -impl Array for PrivateKeyInner {} - impl Hash for PrivateKeyInner { fn hash(&self, state: &mut H) { let guard = self.raw.expose(); @@ -113,23 +107,6 @@ impl PartialOrd for PrivateKeyInner { } } -// AsRef and Deref are only available on non-protected platforms because -// protected memory requires holding a guard to keep the memory accessible. -#[cfg(not(unix))] -impl AsRef<[u8]> for PrivateKeyInner { - fn as_ref(&self) -> &[u8] { - &*self.raw.expose() - } -} - -#[cfg(not(unix))] -impl Deref for PrivateKeyInner { - type Target = [u8]; - fn deref(&self) -> &[u8] { - &*self.raw.expose() - } -} - impl From for PrivateKeyInner { fn from(signer: SigningKey) -> Self { Self::new(signer) @@ -286,12 +263,6 @@ macro_rules! impl_private_key_wrapper { impl commonware_utils::Span for $name {} - // Array is only available on non-protected platforms because it requires - // AsRef<[u8]> and Deref, which need the memory to stay accessible - // without a guard. - #[cfg(not(unix))] - impl commonware_utils::Array for $name {} - impl core::hash::Hash for $name { fn hash(&self, state: &mut H) { self.0.hash(state); @@ -310,23 +281,6 @@ macro_rules! impl_private_key_wrapper { } } - // AsRef and Deref are only available on non-protected platforms because - // protected memory requires holding a guard to keep the memory accessible. - #[cfg(not(unix))] - impl AsRef<[u8]> for $name { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } - } - - #[cfg(not(unix))] - impl core::ops::Deref for $name { - type Target = [u8]; - fn deref(&self) -> &[u8] { - &self.0 - } - } - impl From for $name { fn from(signer: p256::ecdsa::SigningKey) -> Self { Self(PrivateKeyInner::from(signer)) From 01fc662beb8c868ae8094f499afbaf588ba9012f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 16:49:18 +0000 Subject: [PATCH 04/65] [cryptography] use subtle for constant-time operations --- Cargo.lock | 1 + Cargo.toml | 1 + cryptography/Cargo.toml | 2 + cryptography/src/secret.rs | 196 ++++++++++++++++++------------------- 4 files changed, 102 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cda4bd6abe..fa7d3f67cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,6 +1220,7 @@ dependencies = [ "rayon", "rstest", "sha2 0.10.9", + "subtle", "thiserror 2.0.17", "x25519-dalek", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 5f4d0b35f9..474a6aa16c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,6 +139,7 @@ serde = "1.0.218" serde_json = "1.0.122" serde_yaml = "0.9.34" sha2 = { version = "0.10.8", default-features = false } +subtle = { version = "2.6.1", default-features = false } syn = "2.0.0" sysinfo = "0.37.2" thiserror = { version = "2.0.12", default-features = false } diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index b3b87d9b70..312bf6fcd0 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -32,6 +32,7 @@ rand_chacha.workspace = true rand_core.workspace = true rayon = { workspace = true, optional = true } sha2.workspace = true +subtle.workspace = true thiserror.workspace = true x25519-dalek = { workspace = true, features = ["zeroize"] } zeroize = { workspace = true, features = ["zeroize_derive"] } @@ -87,6 +88,7 @@ std = [ "rand_core/std", "rayon", "sha2/std", + "subtle/std", "thiserror/std", "zeroize/std", ] diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 979c0bc8ef..7866d0cfc2 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -24,49 +24,38 @@ //! self-contained types (no heap pointers). Types like `Vec` or `String` //! will only have their metadata protected, not heap data. -/// Constant-time equality comparison for byte slices. -/// -/// XORs all bytes together and checks if the result is zero. -/// This prevents timing attacks by always comparing all bytes. +use core::cmp::Ordering; +use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; + +/// Constant-time lexicographic comparison for byte slices. #[inline] -fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { - if a.len() != b.len() { - return false; - } - let mut diff = 0u8; - for (x, y) in a.iter().zip(b.iter()) { - diff |= x ^ y; +fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { + let mut result = 0u8; + for (&x, &y) in a.iter().zip(b.iter()) { + let is_eq = result.ct_eq(&0); + result = u8::conditional_select(&result, &1, is_eq & x.ct_lt(&y)); + result = u8::conditional_select(&result, &2, is_eq & y.ct_lt(&x)); } - diff == 0 -} -/// Constant-time less-than comparison for byte slices (big-endian). -/// -/// Returns true if a < b, using constant-time operations. -#[inline] -fn ct_lt_bytes(a: &[u8], b: &[u8]) -> bool { - debug_assert_eq!(a.len(), b.len()); - let mut result = 0u8; // 0 = equal so far, 1 = a < b, 2 = a > b - for (x, y) in a.iter().zip(b.iter()) { - // Only update result if we haven't found a difference yet (result == 0) - let is_equal_so_far = result.wrapping_sub(1) >> 7; // 1 if result == 0, 0 otherwise - let x_lt_y = ((*x as u16).wrapping_sub(*y as u16) >> 8) as u8; // 1 if x < y - let x_gt_y = ((*y as u16).wrapping_sub(*x as u16) >> 8) as u8; // 1 if x > y - result |= is_equal_so_far & ((x_lt_y) | (x_gt_y << 1)); - } - result == 1 + match result { + 1 => Ordering::Less, + 2 => Ordering::Greater, + _ => a.len().cmp(&b.len()), + } } // Use protected implementation on Unix with the feature enabled #[cfg(unix)] mod implementation { use core::{ + cmp::Ordering, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, ops::{Deref, DerefMut}, ptr::NonNull, }; use std::alloc::{alloc, dealloc, Layout}; + use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; /// Returns the system page size. @@ -248,22 +237,20 @@ mod implementation { fn eq(&self, other: &Self) -> bool { let guard_self = self.expose(); let guard_other = other.expose(); - // SAFETY: We're reading the raw bytes of T for constant-time comparison. - // This is safe because T is Sized and we only read size_of::() bytes. - let self_bytes = unsafe { - core::slice::from_raw_parts( - &*guard_self as *const T as *const u8, - core::mem::size_of::(), + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + &*guard_self as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + &*guard_other as *const T as *const u8, + core::mem::size_of::(), + ), ) }; - // SAFETY: Same as above - reading raw bytes of a Sized type. - let other_bytes = unsafe { - core::slice::from_raw_parts( - &*guard_other as *const T as *const u8, - core::mem::size_of::(), - ) - }; - super::ct_eq_bytes(self_bytes, other_bytes) + a.ct_eq(b).into() } } @@ -277,37 +264,30 @@ mod implementation { } impl PartialOrd for Secret { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Secret { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { + fn cmp(&self, other: &Self) -> Ordering { let guard_self = self.expose(); let guard_other = other.expose(); - // SAFETY: We're reading the raw bytes of T for constant-time comparison. - // This is safe because T is Sized and we only read size_of::() bytes. - let self_bytes = unsafe { - core::slice::from_raw_parts( - &*guard_self as *const T as *const u8, - core::mem::size_of::(), + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + &*guard_self as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + &*guard_other as *const T as *const u8, + core::mem::size_of::(), + ), ) }; - // SAFETY: Same as above - reading raw bytes of a Sized type. - let other_bytes = unsafe { - core::slice::from_raw_parts( - &*guard_other as *const T as *const u8, - core::mem::size_of::(), - ) - }; - if super::ct_eq_bytes(self_bytes, other_bytes) { - core::cmp::Ordering::Equal - } else if super::ct_lt_bytes(self_bytes, other_bytes) { - core::cmp::Ordering::Less - } else { - core::cmp::Ordering::Greater - } + + super::ct_cmp_bytes(a, b) } } @@ -385,9 +365,11 @@ mod implementation { #[cfg(not(unix))] mod implementation { use core::{ + cmp::Ordering, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, }; + use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; /// A wrapper for secret values that prevents accidental leakage. @@ -462,21 +444,20 @@ mod implementation { impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { - // SAFETY: We're reading the raw bytes of T for constant-time comparison. - // This is safe because T is Sized and we only read size_of::() bytes. - let self_bytes = unsafe { - core::slice::from_raw_parts( - &self.0 as *const T as *const u8, - core::mem::size_of::(), - ) - }; - let other_bytes = unsafe { - core::slice::from_raw_parts( - &other.0 as *const T as *const u8, - core::mem::size_of::(), + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + &self.0 as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + &other.0 as *const T as *const u8, + core::mem::size_of::(), + ), ) }; - super::ct_eq_bytes(self_bytes, other_bytes) + a.ct_eq(b).into() } } @@ -489,34 +470,28 @@ mod implementation { } impl PartialOrd for Secret { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Secret { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - // SAFETY: We're reading the raw bytes of T for constant-time comparison. - // This is safe because T is Sized and we only read size_of::() bytes. - let self_bytes = unsafe { - core::slice::from_raw_parts( - &self.0 as *const T as *const u8, - core::mem::size_of::(), - ) - }; - let other_bytes = unsafe { - core::slice::from_raw_parts( - &other.0 as *const T as *const u8, - core::mem::size_of::(), + fn cmp(&self, other: &Self) -> Ordering { + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + &self.0 as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + &other.0 as *const T as *const u8, + core::mem::size_of::(), + ), ) }; - if super::ct_eq_bytes(self_bytes, other_bytes) { - core::cmp::Ordering::Equal - } else if super::ct_lt_bytes(self_bytes, other_bytes) { - core::cmp::Ordering::Less - } else { - core::cmp::Ordering::Greater - } + + super::ct_cmp_bytes(a, b) } } @@ -610,6 +585,31 @@ mod tests { assert_ne!(s1, s3); } + #[test] + fn test_ordering() { + use core::cmp::Ordering; + + // Test the specific bug case: [2, 1] vs [1, 2] + let a = Secret::new([2u8, 1]); + let b = Secret::new([1u8, 2]); + assert_eq!(a.cmp(&b), Ordering::Greater); // [2, 1] > [1, 2] lexicographically + + // Additional ordering tests + let c = Secret::new([1u8, 1]); + let d = Secret::new([1u8, 2]); + assert_eq!(c.cmp(&d), Ordering::Less); + + let e = Secret::new([1u8, 2]); + let f = Secret::new([1u8, 2]); + assert_eq!(e.cmp(&f), Ordering::Equal); + + // Single byte + let g = Secret::new([0u8]); + let h = Secret::new([255u8]); + assert_eq!(g.cmp(&h), Ordering::Less); + assert_eq!(h.cmp(&g), Ordering::Greater); + } + #[test] fn test_multiple_expose() { let secret = Secret::new([42u8; 32]); From 4c9941f9e9cf6b823589368e14f5b5e033db39dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 16:53:39 +0000 Subject: [PATCH 05/65] [cryptography] use mmap for secrets --- cryptography/src/secret.rs | 62 ++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 7866d0cfc2..257d5e471d 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -54,7 +54,6 @@ mod implementation { ops::{Deref, DerefMut}, ptr::NonNull, }; - use std::alloc::{alloc, dealloc, Layout}; use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -71,7 +70,13 @@ mod implementation { /// A wrapper for secret values with OS-level memory protection. /// + /// Uses `mmap` for allocation instead of the global allocator because: + /// - The global allocator may sub-allocate within pages, sharing pages with other data + /// - `mmap` guarantees page-aligned, exclusively-owned memory + /// - This ensures `mlock` and `mprotect` apply only to our secret data + /// /// On Unix: + /// - Memory is allocated via mmap (page-isolated) /// - Memory is locked to prevent swapping (mlock) /// - Memory is marked no-access except during expose() (mprotect) /// - Zeroized on drop @@ -104,16 +109,6 @@ mod implementation { } /// Creates a new `Secret`, returning an error on failure. - /// - /// # Safety Invariants - /// - /// This function performs several unsafe operations to set up protected memory: - /// - Allocates page-aligned memory using the global allocator - /// - Writes the value to the allocated memory - /// - Locks the memory with mlock to prevent swapping - /// - Protects the memory with mprotect to prevent unauthorized access - /// - /// All unsafe operations are properly sequenced and cleaned up on failure. #[allow(clippy::undocumented_unsafe_blocks)] pub fn try_new(value: T) -> Result { let page_size = page_size(); @@ -121,37 +116,46 @@ mod implementation { // Round up to page boundary (minimum one page) let size = type_size.max(1).next_multiple_of(page_size); - let layout = Layout::from_size_align(size, page_size).map_err(|_| "invalid layout")?; - - // SAFETY: layout is valid (checked above), ptr may be null (checked below) - let ptr = unsafe { alloc(layout) } as *mut T; + // SAFETY: mmap with MAP_ANONYMOUS returns page-aligned memory or MAP_FAILED + let ptr = unsafe { + libc::mmap( + core::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) + }; - if ptr.is_null() { - return Err("allocation failed"); + if ptr == libc::MAP_FAILED { + return Err("mmap failed"); } - // SAFETY: ptr is non-null and properly aligned for T + let ptr = ptr as *mut T; + + // SAFETY: ptr is valid and properly aligned (mmap returns page-aligned memory) unsafe { core::ptr::write(ptr, value) }; - // SAFETY: ptr points to valid allocated memory of size `size` + // SAFETY: ptr points to valid mmap'd memory of size `size` if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { - // SAFETY: ptr and layout match the allocation above - unsafe { dealloc(ptr as *mut u8, layout) }; + // SAFETY: ptr and size match the mmap above + unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; return Err("mlock failed"); } // SAFETY: ptr points to valid locked memory of size `size` if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { - // SAFETY: cleanup on failure - unlock and deallocate + // SAFETY: cleanup on failure - unlock and unmap unsafe { libc::munlock(ptr as *const libc::c_void, size); - dealloc(ptr as *mut u8, layout); + libc::munmap(ptr as *mut libc::c_void, size); } return Err("mprotect failed"); } Ok(Self { - // SAFETY: ptr is non-null (checked above) + // SAFETY: ptr is non-null (mmap succeeded) ptr: unsafe { NonNull::new_unchecked(ptr) }, size, }) @@ -206,10 +210,9 @@ mod implementation { impl Drop for Secret { fn drop(&mut self) { - let page_size = page_size(); - // SAFETY: self.ptr points to valid memory that was allocated with page_size - // alignment and self.size bytes. We unprotect, zeroize, unlock, and deallocate - // in proper sequence. This is safe because we have exclusive access (&mut self). + // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. + // We unprotect, zeroize, unlock, and unmap in proper sequence. + // This is safe because we have exclusive access (&mut self). unsafe { libc::mprotect( self.ptr.as_ptr() as *mut libc::c_void, @@ -218,8 +221,7 @@ mod implementation { ); (*self.ptr.as_ptr()).zeroize(); libc::munlock(self.ptr.as_ptr() as *const libc::c_void, self.size); - let layout = Layout::from_size_align_unchecked(self.size, page_size); - dealloc(self.ptr.as_ptr() as *mut u8, layout); + libc::munmap(self.ptr.as_ptr() as *mut libc::c_void, self.size); } } } From 71bd4ed65c1c906e17472e651d3b6c31bf443a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 17:25:51 +0000 Subject: [PATCH 06/65] [cryptography] use closure-based API for exposing secrets this is easier to safeguard than the guard based approach --- cryptography/src/bls12381/dkg.rs | 38 +- cryptography/src/bls12381/primitives/group.rs | 12 +- cryptography/src/bls12381/primitives/ops.rs | 26 +- cryptography/src/bls12381/scheme.rs | 15 +- cryptography/src/ed25519/scheme.rs | 32 +- cryptography/src/lib.rs | 2 +- cryptography/src/secp256r1/common.rs | 12 +- cryptography/src/secret.rs | 348 ++++++------------ 8 files changed, 172 insertions(+), 313 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 31af8d34e4..3b75fef751 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -290,7 +290,7 @@ use crate::{ variant::Variant, }, transcript::{Summary, Transcript}, - PublicKey, Secret, SecretGuard, SecretGuardMut, Signer, + PublicKey, Secret, Signer, }; use commonware_codec::{Encode, EncodeSize, RangeCfg, Read, ReadExt, Write}; use commonware_math::{ @@ -565,8 +565,9 @@ impl Info { return false; }; let expected = pub_msg.commitment.eval_msm(&scalar); - let guard = priv_msg.expose_share(); - expected == V::Public::generator() * &*guard + priv_msg + .share() + .expose(|share| expected == V::Public::generator() * share) } } @@ -696,30 +697,21 @@ impl DealerPrivMsg { } } - /// Temporarily exposes the share for reading. - /// - /// The returned guard re-protects the memory when dropped (on supported platforms). - pub fn expose_share(&self) -> SecretGuard<'_, Scalar> { - self.share.expose() - } - - /// Temporarily exposes the share for mutation. - /// - /// The returned guard re-protects the memory when dropped (on supported platforms). - pub fn expose_share_mut(&mut self) -> SecretGuardMut<'_, Scalar> { - self.share.expose_mut() + /// Returns a reference to the share wrapped in `Secret`. + pub const fn share(&self) -> &Secret { + &self.share } } impl EncodeSize for DealerPrivMsg { fn encode_size(&self) -> usize { - self.share.expose().encode_size() + self.share.expose(|s| s.encode_size()) } } impl Write for DealerPrivMsg { fn write(&self, buf: &mut impl bytes::BufMut) { - self.share.expose().write(buf); + self.share.expose(|s| s.write(buf)); } } @@ -1201,8 +1193,8 @@ impl Dealer { ) -> Result<(Self, DealerPubMsg, Vec<(S::PublicKey, DealerPrivMsg)>), Error> { // Check that this dealer is defined in the round. info.dealer_index(&me.public_key())?; - let share = - info.unwrap_or_random_share(&mut rng, share.map(|x| x.private().expose().clone()))?; + let share = info + .unwrap_or_random_share(&mut rng, share.map(|x| x.private().expose(|s| s.clone())))?; let my_poly = Poly::new_with_constant(&mut rng, info.degree(), share); let priv_msgs = info .players @@ -1506,7 +1498,7 @@ impl Player { let share = self .view .get(dealer) - .map(|(_, priv_msg)| (*priv_msg.expose_share()).clone()) + .map(|(_, priv_msg)| priv_msg.share().expose(|s| s.clone())) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( || { @@ -1514,7 +1506,7 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - |priv_msg| (*priv_msg.expose_share()).clone(), + |priv_msg| priv_msg.share().expose(|s| s.clone()), ) }); (dealer.clone(), share) @@ -1963,7 +1955,7 @@ mod test_plan { let share = info .unwrap_or_random_share( &mut rng, - share.map(|s| s.private().expose().clone()), + share.map(|s| s.private().expose(|k| k.clone())), ) .expect("Failed to generate dealer share"); @@ -2007,7 +1999,7 @@ mod test_plan { for (player, priv_msg) in &mut priv_msgs { let player_key_idx = pk_to_key_idx[player]; if round.bad_shares.contains(&(i_dealer, player_key_idx)) { - *priv_msg.expose_share_mut() = Scalar::random(&mut rng); + *priv_msg = DealerPrivMsg::new(Scalar::random(&mut rng)); } } assert_eq!(priv_msgs.len(), players.len()); diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index d995233e78..093897d980 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -516,25 +516,19 @@ impl Share { /// /// This can be verified against the public polynomial. pub fn public(&self) -> V::Public { - let guard = self.private.expose(); - V::Public::generator() * &*guard + self.private.expose(|key| V::Public::generator() * key) } /// Returns a reference to the wrapped private key. pub const fn private(&self) -> &Secret { &self.private } - - /// Returns a mutable reference to the wrapped private key. - pub const fn private_mut(&mut self) -> &mut Secret { - &mut self.private - } } impl Write for Share { fn write(&self, buf: &mut impl BufMut) { UInt(self.index).write(buf); - self.private.expose().write(buf); + self.private.expose(|key| key.write(buf)); } } @@ -553,7 +547,7 @@ impl Read for Share { impl EncodeSize for Share { fn encode_size(&self) -> usize { - UInt(self.index).encode_size() + self.private.expose().encode_size() + UInt(self.index).encode_size() + self.private.expose(|key| key.encode_size()) } } diff --git a/cryptography/src/bls12381/primitives/ops.rs b/cryptography/src/bls12381/primitives/ops.rs index 9448901486..f2e480f683 100644 --- a/cryptography/src/bls12381/primitives/ops.rs +++ b/cryptography/src/bls12381/primitives/ops.rs @@ -137,14 +137,14 @@ pub fn verify_message( } /// Generates a proof of possession for the private key share. -#[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected pub fn partial_sign_proof_of_possession( sharing: &Sharing, private: &Share, ) -> PartialSignature { // Sign the public key - let guard = private.private().expose(); - let sig = sign::(&guard, V::PROOF_OF_POSSESSION, &sharing.public().encode()); + let sig = private + .private() + .expose(|key| sign::(key, V::PROOF_OF_POSSESSION, &sharing.public().encode())); PartialSignature { value: sig, index: private.index, @@ -169,14 +169,14 @@ pub fn partial_verify_proof_of_possession( } /// Signs the provided message with the key share. -#[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected pub fn partial_sign_message( private: &Share, namespace: Option<&[u8]>, message: &[u8], ) -> PartialSignature { - let guard = private.private().expose(); - let sig = sign_message::(&guard, namespace, message); + let sig = private + .private() + .expose(|key| sign_message::(key, namespace, message)); PartialSignature { value: sig, index: private.index, @@ -1444,8 +1444,8 @@ mod tests { dkg::deal_anonymous::(&mut rng, Default::default(), NZU32!(n)); // Corrupt a share - let share = shares.get_mut(3).unwrap(); - *share.private_mut().expose_mut() = Private::random(&mut rand::thread_rng()); + let corrupt_index = 3; + shares[corrupt_index] = Share::new(shares[corrupt_index].index, Private::random(&mut rng)); // Generate the partial signatures let namespace = Some(&b"test"[..]); @@ -1504,7 +1504,8 @@ mod tests { // Corrupt the second share's private key let corrupted_index = 1; - *shares[corrupted_index].private_mut().expose_mut() = Private::random(&mut rng); + shares[corrupted_index] = + Share::new(shares[corrupted_index].index, Private::random(&mut rng)); // Generate partial signatures let partials: Vec<_> = shares @@ -1544,7 +1545,7 @@ mod tests { // Corrupt shares at indices 1 and 3 let corrupted_indices = vec![1, 3]; for &idx in &corrupted_indices { - *shares[idx].private_mut().expose_mut() = Private::random(&mut rng); + shares[idx] = Share::new(shares[idx].index, Private::random(&mut rng)); } // Generate partial signatures @@ -1639,7 +1640,7 @@ mod tests { let namespace = Some(&b"test"[..]); let msg = b"hello"; - *shares[0].private_mut().expose_mut() = Private::random(&mut rng); + shares[0] = Share::new(shares[0].index, Private::random(&mut rng)); let partials: Vec<_> = shares .iter() @@ -1667,7 +1668,8 @@ mod tests { let msg = b"hello"; let corrupted_index = n - 1; - *shares[corrupted_index as usize].private_mut().expose_mut() = Private::random(&mut rng); + let idx = corrupted_index as usize; + shares[idx] = Share::new(shares[idx].index, Private::random(&mut rng)); let partials: Vec<_> = shares .iter() diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 8bb6142257..49d12d0262 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -61,7 +61,7 @@ pub struct PrivateKey { impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { - self.raw.expose().write(buf); + self.raw.expose(|bytes| bytes.write(buf)); } } @@ -87,8 +87,7 @@ impl Span for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - let guard = self.raw.expose(); - (*guard).hash(state); + self.raw.expose(|bytes| bytes.hash(state)); } } @@ -133,10 +132,9 @@ impl crate::Signer for PrivateKey { type Signature = Signature; type PublicKey = PublicKey; - #[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected fn public_key(&self) -> Self::PublicKey { - let guard = self.key.expose(); - PublicKey::from(ops::compute_public::(&guard)) + self.key + .expose(|key| PublicKey::from(ops::compute_public::(key))) } fn sign(&self, namespace: &[u8], msg: &[u8]) -> Self::Signature { @@ -146,10 +144,9 @@ impl crate::Signer for PrivateKey { impl PrivateKey { #[inline(always)] - #[allow(clippy::needless_borrow)] // Guard is &T on non-protected, SecretGuard on protected fn sign_inner(&self, namespace: Option<&[u8]>, message: &[u8]) -> Signature { - let guard = self.key.expose(); - ops::sign_message::(&guard, namespace, message).into() + self.key + .expose(|key| ops::sign_message::(key, namespace, message).into()) } } diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 423204a75c..2b012c132b 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -41,11 +41,13 @@ impl crate::Signer for PrivateKey { } fn public_key(&self) -> Self::PublicKey { - let raw = self.key.expose().verification_key().to_bytes(); - Self::PublicKey { - raw, - key: self.key.expose().verification_key().to_owned(), - } + self.key.expose(|key| { + let raw = key.verification_key().to_bytes(); + Self::PublicKey { + raw, + key: key.verification_key().to_owned(), + } + }) } } @@ -55,8 +57,7 @@ impl PrivateKey { let payload = namespace .map(|namespace| Cow::Owned(union_unique(namespace, msg))) .unwrap_or_else(|| Cow::Borrowed(msg)); - let sig = self.key.expose().sign(&payload); - Signature::from(sig) + self.key.expose(|key| Signature::from(key.sign(&payload))) } } @@ -73,7 +74,7 @@ impl Random for PrivateKey { impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { - self.raw.expose().write(buf); + self.raw.expose(|bytes| bytes.write(buf)); } } @@ -100,8 +101,7 @@ impl Eq for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - let guard = self.raw.expose(); - (*guard).hash(state); + self.raw.expose(|bytes| bytes.hash(state)); } } @@ -165,11 +165,13 @@ pub struct PublicKey { impl From for PublicKey { fn from(value: PrivateKey) -> Self { - let raw = value.key.expose().verification_key().to_bytes(); - Self { - raw, - key: value.key.expose().verification_key(), - } + value.key.expose(|key| { + let raw = key.verification_key().to_bytes(); + Self { + raw, + key: key.verification_key(), + } + }) } } diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index 586930d98e..a3f1e1a269 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -36,7 +36,7 @@ pub mod lthash; pub use lthash::LtHash; pub mod secp256r1; pub mod secret; -pub use secret::{Secret, SecretGuard, SecretGuardMut}; +pub use secret::Secret; pub mod transcript; /// Produces [Signature]s over messages that can be verified with a corresponding [PublicKey]. diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 01e1aefb46..d4bdaf87a3 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -41,9 +41,10 @@ impl PrivateKeyInner { /// This is called on-demand to avoid keeping an unprotected `SigningKey` /// in memory. pub(crate) fn signing_key(&self) -> SigningKey { - let guard = self.raw.expose(); - // This cannot fail since we only store valid keys - SigningKey::from_slice(&*guard).expect("stored key bytes are always valid") + self.raw.expose(|bytes| { + // This cannot fail since we only store valid keys + SigningKey::from_slice(bytes).expect("stored key bytes are always valid") + }) } /// Returns the `VerifyingKey` corresponding to this private key. @@ -60,7 +61,7 @@ impl Random for PrivateKeyInner { impl Write for PrivateKeyInner { fn write(&self, buf: &mut impl BufMut) { - self.raw.expose().write(buf); + self.raw.expose(|bytes| bytes.write(buf)); } } @@ -89,8 +90,7 @@ impl Span for PrivateKeyInner {} impl Hash for PrivateKeyInner { fn hash(&self, state: &mut H) { - let guard = self.raw.expose(); - (*guard).hash(state); + self.raw.expose(|bytes| bytes.hash(state)); } } diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 257d5e471d..e3d6145e0c 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -51,7 +51,6 @@ mod implementation { cmp::Ordering, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, - ops::{Deref, DerefMut}, ptr::NonNull, }; use subtle::ConstantTimeEq; @@ -161,11 +160,11 @@ mod implementation { }) } - /// Exposes the secret value for use. + /// Exposes the secret value for read-only access within a closure. /// - /// Returns a guard that re-protects memory when dropped. + /// Memory is re-protected immediately after the closure returns. #[inline] - pub fn expose(&self) -> SecretGuard<'_, T> { + pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { // SAFETY: self.ptr points to valid protected memory of self.size bytes let result = unsafe { libc::mprotect( @@ -175,24 +174,20 @@ mod implementation { ) }; assert_eq!(result, 0, "mprotect failed to unprotect memory"); - SecretGuard { secret: self } - } - /// Exposes the secret value mutably. - /// - /// Returns a guard that re-protects memory when dropped. - #[inline] - pub fn expose_mut(&mut self) -> SecretGuardMut<'_, T> { - // SAFETY: self.ptr points to valid protected memory of self.size bytes - let result = unsafe { + // SAFETY: Memory is now readable and ptr is valid + let value = unsafe { self.ptr.as_ref() }; + let result = f(value); + + // SAFETY: Re-protect after use + unsafe { libc::mprotect( self.ptr.as_ptr() as *mut libc::c_void, self.size, - libc::PROT_READ | libc::PROT_WRITE, - ) - }; - assert_eq!(result, 0, "mprotect failed to unprotect memory"); - SecretGuardMut { secret: self } + libc::PROT_NONE, + ); + } + result } } @@ -230,29 +225,30 @@ mod implementation { impl Clone for Secret { fn clone(&self) -> Self { - let guard = self.expose(); - Self::new((*guard).clone()) + self.expose(|v| Self::new(v.clone())) } } impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { - let guard_self = self.expose(); - let guard_other = other.expose(); - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - &*guard_self as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - &*guard_other as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - a.ct_eq(b).into() + self.expose(|a| { + other.expose(|b| { + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + a as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + b as *const T as *const u8, + core::mem::size_of::(), + ), + ) + }; + a.ct_eq(b).into() + }) + }) } } @@ -260,8 +256,7 @@ mod implementation { impl Hash for Secret { fn hash(&self, state: &mut H) { - let guard = self.expose(); - (*guard).hash(state); + self.expose(|v| v.hash(state)); } } @@ -273,92 +268,24 @@ mod implementation { impl Ord for Secret { fn cmp(&self, other: &Self) -> Ordering { - let guard_self = self.expose(); - let guard_other = other.expose(); - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - &*guard_self as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - &*guard_other as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - - super::ct_cmp_bytes(a, b) - } - } - - /// RAII guard for read access to a secret. - pub struct SecretGuard<'a, T: Zeroize> { - secret: &'a Secret, - } - - impl Deref for SecretGuard<'_, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &T { - // SAFETY: The memory is currently unprotected (PROT_READ) because - // this guard exists, and the pointer is valid for the lifetime of Secret. - unsafe { self.secret.ptr.as_ref() } - } - } - - impl Drop for SecretGuard<'_, T> { - fn drop(&mut self) { - // SAFETY: Re-protect the memory when the guard is dropped. - // The pointer and size are valid from the Secret. - unsafe { - libc::mprotect( - self.secret.ptr.as_ptr() as *mut libc::c_void, - self.secret.size, - libc::PROT_NONE, - ); - } - } - } - - /// RAII guard for mutable access to a secret. - pub struct SecretGuardMut<'a, T: Zeroize> { - secret: &'a mut Secret, - } - - impl Deref for SecretGuardMut<'_, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &T { - // SAFETY: The memory is currently unprotected (PROT_READ|PROT_WRITE) because - // this guard exists, and the pointer is valid for the lifetime of Secret. - unsafe { self.secret.ptr.as_ref() } - } - } - - impl DerefMut for SecretGuardMut<'_, T> { - #[inline] - fn deref_mut(&mut self) -> &mut T { - // SAFETY: The memory is currently unprotected (PROT_READ|PROT_WRITE) because - // this guard exists, and we have exclusive mutable access. - unsafe { self.secret.ptr.as_mut() } - } - } - - impl Drop for SecretGuardMut<'_, T> { - fn drop(&mut self) { - // SAFETY: Re-protect the memory when the guard is dropped. - // The pointer and size are valid from the Secret. - unsafe { - libc::mprotect( - self.secret.ptr.as_ptr() as *mut libc::c_void, - self.secret.size, - libc::PROT_NONE, - ); - } + self.expose(|a| { + other.expose(|b| { + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + a as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + b as *const T as *const u8, + core::mem::size_of::(), + ), + ) + }; + super::ct_cmp_bytes(a, b) + }) + }) } } } @@ -389,26 +316,10 @@ mod implementation { Self(value) } - /// Exposes the secret value for use. - /// - /// # Warning - /// - /// This method should be used sparingly and only when the secret - /// value is actually needed for cryptographic operations. - #[inline] - pub fn expose(&self) -> SecretGuard<'_, T> { - SecretGuard(&self.0) - } - - /// Exposes the secret value mutably. - /// - /// # Warning - /// - /// This method should be used sparingly and only when mutable access - /// to the secret value is actually needed. + /// Exposes the secret value for read-only access within a closure. #[inline] - pub fn expose_mut(&mut self) -> SecretGuardMut<'_, T> { - SecretGuardMut(&mut self.0) + pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { + f(&self.0) } } @@ -440,26 +351,30 @@ mod implementation { impl Clone for Secret { fn clone(&self) -> Self { - Self(self.0.clone()) + self.expose(|v| Self::new(v.clone())) } } impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - &self.0 as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - &other.0 as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - a.ct_eq(b).into() + self.expose(|a| { + other.expose(|b| { + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + a as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + b as *const T as *const u8, + core::mem::size_of::(), + ), + ) + }; + a.ct_eq(b).into() + }) + }) } } @@ -467,7 +382,7 @@ mod implementation { impl Hash for Secret { fn hash(&self, state: &mut H) { - self.0.hash(state); + self.expose(|v| v.hash(state)); } } @@ -479,58 +394,24 @@ mod implementation { impl Ord for Secret { fn cmp(&self, other: &Self) -> Ordering { - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - &self.0 as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - &other.0 as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - - super::ct_cmp_bytes(a, b) - } - } - - use core::ops::{Deref, DerefMut}; - - /// RAII guard for read access to a secret. - /// - /// On non-Unix platforms, this is a simple wrapper around a reference. - pub struct SecretGuard<'a, T: Zeroize>(&'a T); - - impl Deref for SecretGuard<'_, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &T { - self.0 - } - } - - /// RAII guard for mutable access to a secret. - /// - /// On non-Unix platforms, this is a simple wrapper around a mutable reference. - pub struct SecretGuardMut<'a, T: Zeroize>(&'a mut T); - - impl Deref for SecretGuardMut<'_, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &T { - self.0 - } - } - - impl DerefMut for SecretGuardMut<'_, T> { - #[inline] - fn deref_mut(&mut self) -> &mut T { - self.0 + self.expose(|a| { + other.expose(|b| { + // SAFETY: Reading raw bytes of T for constant-time comparison. + let (a, b) = unsafe { + ( + core::slice::from_raw_parts( + a as *const T as *const u8, + core::mem::size_of::(), + ), + core::slice::from_raw_parts( + b as *const T as *const u8, + core::mem::size_of::(), + ), + ) + }; + super::ct_cmp_bytes(a, b) + }) + }) } } } @@ -556,26 +437,20 @@ mod tests { #[test] fn test_expose() { let secret = Secret::new([1u8, 2, 3, 4]); - let guard = secret.expose(); - assert_eq!(&*guard, &[1u8, 2, 3, 4]); - } - - #[test] - fn test_expose_mut() { - let mut secret = Secret::new([1u8, 2, 3, 4]); - { - let mut guard = secret.expose_mut(); - guard[0] = 5; - } - let guard = secret.expose(); - assert_eq!(&*guard, &[5u8, 2, 3, 4]); + secret.expose(|v| { + assert_eq!(v, &[1u8, 2, 3, 4]); + }); } #[test] fn test_clone() { let secret = Secret::new([1u8, 2, 3, 4]); let cloned = secret.clone(); - assert_eq!(&*secret.expose(), &*cloned.expose()); + secret.expose(|a| { + cloned.expose(|b| { + assert_eq!(a, b); + }); + }); } #[test] @@ -617,16 +492,14 @@ mod tests { let secret = Secret::new([42u8; 32]); // First expose - { - let guard = secret.expose(); - assert_eq!(guard[0], 42); - } + secret.expose(|v| { + assert_eq!(v[0], 42); + }); - // Second expose after first guard dropped - { - let guard = secret.expose(); - assert_eq!(guard[31], 42); - } + // Second expose + secret.expose(|v| { + assert_eq!(v[31], 42); + }); } #[cfg(unix)] @@ -639,9 +512,8 @@ mod tests { let scalar = Scalar::random(&mut OsRng); let secret = Secret::new(scalar); - { - let guard = secret.expose(); - let _ = format!("{:?}", *guard); - } + secret.expose(|v| { + let _ = format!("{:?}", *v); + }); } } From cf161b3abc6ac8c28512766f54a8a246b261d0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 17:28:41 +0000 Subject: [PATCH 07/65] [cryptography] lint --- cryptography/src/secret.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index e3d6145e0c..4e63e5a8bb 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -312,7 +312,8 @@ mod implementation { impl Secret { /// Creates a new `Secret` wrapping the given value. #[inline] - pub const fn new(value: T) -> Self { + #[allow(clippy::missing_const_for_fn)] + pub fn new(value: T) -> Self { Self(value) } From 4a2958d179e1fa1314e05f605daa82ad6ba6cdee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 17:45:11 +0000 Subject: [PATCH 08/65] [cryptography] allow mlock to fail on tests / benchmarks --- .github/workflows/benchmark.yml | 2 +- .github/workflows/slow.yml | 2 +- cryptography/Cargo.toml | 2 ++ cryptography/src/secret.rs | 15 ++++++++++----- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e1ae1e0406..8abad2f15b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -37,7 +37,7 @@ jobs: matrix: include: - package: commonware-cryptography - cargo_flags: "" + cargo_flags: "--features commonware-cryptography/soft-mlock" file_suffix: "" benchmark_name: "commonware-cryptography" - package: commonware-storage diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 5a3245aba5..c76dedf83a 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -83,7 +83,7 @@ jobs: matrix: include: - package: commonware-cryptography - cargo_flags: "" + cargo_flags: "--features commonware-cryptography/soft-mlock" - package: commonware-storage cargo_flags: "" - package: commonware-storage diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 312bf6fcd0..fe0adb47d3 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -59,6 +59,7 @@ crate-type = ["rlib", "cdylib"] [features] default = [ "std" ] +soft-mlock = [] parallel = [ "blake3/rayon", "rayon", "std" ] mocks = [ "std" ] arbitrary = [ @@ -97,6 +98,7 @@ std = [ name = "bls12381" harness = false path = "src/bls12381/benches/bench.rs" +required-features = ["soft-mlock"] [[bench]] name = "ed25519" diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 4e63e5a8bb..062dda5e16 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -137,15 +137,20 @@ mod implementation { unsafe { core::ptr::write(ptr, value) }; // SAFETY: ptr points to valid mmap'd memory of size `size` + // In soft-mlock mode (tests/benchmarks), we continue even if mlock fails. + // The memory will still be protected via mprotect, just not pinned in RAM. if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { - // SAFETY: ptr and size match the mmap above - unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; - return Err("mlock failed"); + #[cfg(not(any(test, feature = "soft-mlock")))] + { + // SAFETY: ptr and size match the mmap above + unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; + return Err("mlock failed"); + } } - // SAFETY: ptr points to valid locked memory of size `size` + // SAFETY: ptr points to valid memory of size `size` if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { - // SAFETY: cleanup on failure - unlock and unmap + // SAFETY: cleanup on failure - unlock (if locked) and unmap unsafe { libc::munlock(ptr as *const libc::c_void, size); libc::munmap(ptr as *mut libc::c_void, size); From d89a55536e35c7c0d1645646e07b2584a4de25e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 17:49:43 +0000 Subject: [PATCH 09/65] [cryptography] zeroize on construction failure --- cryptography/src/secret.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 062dda5e16..e4114b9ee8 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -142,6 +142,8 @@ mod implementation { if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { #[cfg(not(any(test, feature = "soft-mlock")))] { + // SAFETY: ptr points to valid T, zeroize before freeing + unsafe { (*ptr).zeroize() }; // SAFETY: ptr and size match the mmap above unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; return Err("mlock failed"); @@ -150,8 +152,9 @@ mod implementation { // SAFETY: ptr points to valid memory of size `size` if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { - // SAFETY: cleanup on failure - unlock (if locked) and unmap + // SAFETY: cleanup on failure - zeroize, unlock (if locked), and unmap unsafe { + (*ptr).zeroize(); libc::munlock(ptr as *const libc::c_void, size); libc::munmap(ptr as *mut libc::c_void, size); } From 6b38b8dc6bce02485fe70971cf2e10aa0617d433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 18:00:12 +0000 Subject: [PATCH 10/65] [cryptography] require soft-mlock for ed25519 benchmarks --- cryptography/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index fe0adb47d3..d4bf2db5d6 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -104,6 +104,7 @@ required-features = ["soft-mlock"] name = "ed25519" harness = false path = "src/ed25519/benches/bench.rs" +required-features = ["soft-mlock"] [[bench]] name = "secp256r1" From faab5c9377c447ebcd409f7bb8fb7415841bdbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 18:27:42 +0000 Subject: [PATCH 11/65] [cryptography] Secret doesn't require Zeroize --- cryptography/src/secp256r1/common.rs | 47 ++++++------ cryptography/src/secp256r1/recoverable.rs | 4 +- cryptography/src/secp256r1/standard.rs | 2 +- cryptography/src/secret.rs | 90 ++++++++++++----------- 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index d4bdaf87a3..a99d5f374e 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -18,38 +18,35 @@ pub const PUBLIC_KEY_LENGTH: usize = 33; // Y-Parity || X /// Internal Secp256r1 Private Key storage. /// -/// Only stores the raw key bytes in protected memory. The `SigningKey` is -/// reconstructed on demand to avoid keeping unprotected copies in memory. -#[derive(Clone, Eq, PartialEq)] +/// Stores both the raw bytes and the `SigningKey` in protected memory. +#[derive(Clone)] pub struct PrivateKeyInner { raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, + pub(crate) key: Secret, } +impl PartialEq for PrivateKeyInner { + fn eq(&self, other: &Self) -> bool { + self.raw == other.raw + } +} + +impl Eq for PrivateKeyInner {} + impl ZeroizeOnDrop for PrivateKeyInner {} impl PrivateKeyInner { pub fn new(key: SigningKey) -> Self { - let bytes = key.to_bytes(); - let raw: [u8; PRIVATE_KEY_LENGTH] = bytes.into(); + let raw: [u8; PRIVATE_KEY_LENGTH] = key.to_bytes().into(); Self { raw: Secret::new(raw), + key: Secret::new(key), } } - /// Reconstructs the `SigningKey` from the protected raw bytes. - /// - /// This is called on-demand to avoid keeping an unprotected `SigningKey` - /// in memory. - pub(crate) fn signing_key(&self) -> SigningKey { - self.raw.expose(|bytes| { - // This cannot fail since we only store valid keys - SigningKey::from_slice(bytes).expect("stored key bytes are always valid") - }) - } - /// Returns the `VerifyingKey` corresponding to this private key. pub fn verifying_key(&self) -> VerifyingKey { - *self.signing_key().verifying_key() + self.key.expose(|k| *k.verifying_key()) } } @@ -61,7 +58,7 @@ impl Random for PrivateKeyInner { impl Write for PrivateKeyInner { fn write(&self, buf: &mut impl BufMut) { - self.raw.expose(|bytes| bytes.write(buf)); + self.raw.expose(|raw| raw.write(buf)); } } @@ -70,15 +67,13 @@ impl Read for PrivateKeyInner { fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let raw = <[u8; PRIVATE_KEY_LENGTH]>::read(buf)?; - // Validate that the bytes form a valid key - let result = SigningKey::from_slice(&raw); + let key = SigningKey::from_slice(&raw); #[cfg(feature = "std")] - result.map_err(|e| CodecError::Wrapped(CURVE_NAME, e.into()))?; + let key = key.map_err(|e| CodecError::Wrapped(CURVE_NAME, e.into()))?; #[cfg(not(feature = "std"))] - result.map_err(|e| CodecError::Wrapped(CURVE_NAME, alloc::format!("{:?}", e).into()))?; - Ok(Self { - raw: Secret::new(raw), - }) + let key = + key.map_err(|e| CodecError::Wrapped(CURVE_NAME, alloc::format!("{:?}", e).into()))?; + Ok(Self::new(key)) } } @@ -90,7 +85,7 @@ impl Span for PrivateKeyInner {} impl Hash for PrivateKeyInner { fn hash(&self, state: &mut H) { - self.raw.expose(|bytes| bytes.hash(state)); + self.raw.expose(|raw| raw.hash(state)); } } diff --git a/cryptography/src/secp256r1/recoverable.rs b/cryptography/src/secp256r1/recoverable.rs index 09a811c08c..cc3c2a0326 100644 --- a/cryptography/src/secp256r1/recoverable.rs +++ b/cryptography/src/secp256r1/recoverable.rs @@ -50,8 +50,8 @@ impl PrivateKey { }); let (mut signature, mut recovery_id) = self .0 - .signing_key() - .sign_recoverable(&payload) + .key + .expose(|k| k.sign_recoverable(&payload)) .expect("signing must succeed"); // The signing algorithm generates k, then calculates r <- x(k * G). Normalizing s by negating it is equivalent diff --git a/cryptography/src/secp256r1/standard.rs b/cryptography/src/secp256r1/standard.rs index 2583769e76..5325c09c0b 100644 --- a/cryptography/src/secp256r1/standard.rs +++ b/cryptography/src/secp256r1/standard.rs @@ -49,7 +49,7 @@ impl PrivateKey { let payload = namespace.map_or(Cow::Borrowed(msg), |namespace| { Cow::Owned(union_unique(namespace, msg)) }); - let signature: p256::ecdsa::Signature = self.0.signing_key().sign(&payload); + let signature: p256::ecdsa::Signature = self.0.key.expose(|k| k.sign(&payload)); let signature = signature.normalize_s().unwrap_or(signature); Signature::from(signature) } diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index e4114b9ee8..b8e4506557 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -44,6 +44,18 @@ fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { } } +/// Zeroize memory at the given pointer using volatile writes. +/// +/// # Safety +/// +/// `ptr` must point to valid, writable memory of at least `size_of::()` bytes. +#[inline] +unsafe fn zeroize_ptr(ptr: *mut T) { + use zeroize::Zeroize; + let slice = core::slice::from_raw_parts_mut(ptr as *mut u8, core::mem::size_of::()); + slice.zeroize(); +} + // Use protected implementation on Unix with the feature enabled #[cfg(unix)] mod implementation { @@ -54,7 +66,7 @@ mod implementation { ptr::NonNull, }; use subtle::ConstantTimeEq; - use zeroize::{Zeroize, ZeroizeOnDrop}; + use zeroize::ZeroizeOnDrop; /// Returns the system page size. fn page_size() -> usize { @@ -82,7 +94,7 @@ mod implementation { /// /// Access requires explicit `expose()` call which returns a guard. /// Memory is re-protected when the guard is dropped. - pub struct Secret { + pub struct Secret { ptr: NonNull, size: usize, } @@ -91,12 +103,12 @@ mod implementation { // through the guard pattern. Access to the protected memory region is // controlled by mprotect calls that make the memory accessible only // during the lifetime of guard objects. - unsafe impl Send for Secret {} + unsafe impl Send for Secret {} // SAFETY: Same reasoning as Send - the guard pattern ensures proper // synchronization of memory access. - unsafe impl Sync for Secret {} + unsafe impl Sync for Secret {} - impl Secret { + impl Secret { /// Creates a new `Secret` wrapping the given value. /// /// # Panics @@ -143,7 +155,7 @@ mod implementation { #[cfg(not(any(test, feature = "soft-mlock")))] { // SAFETY: ptr points to valid T, zeroize before freeing - unsafe { (*ptr).zeroize() }; + unsafe { super::zeroize_ptr(ptr) }; // SAFETY: ptr and size match the mmap above unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; return Err("mlock failed"); @@ -154,7 +166,7 @@ mod implementation { if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { // SAFETY: cleanup on failure - zeroize, unlock (if locked), and unmap unsafe { - (*ptr).zeroize(); + super::zeroize_ptr(ptr); libc::munlock(ptr as *const libc::c_void, size); libc::munmap(ptr as *mut libc::c_void, size); } @@ -199,19 +211,19 @@ mod implementation { } } - impl Debug for Secret { + impl Debug for Secret { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_str("[REDACTED]") } } - impl Display for Secret { + impl Display for Secret { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_str("[REDACTED]") } } - impl Drop for Secret { + impl Drop for Secret { fn drop(&mut self) { // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. // We unprotect, zeroize, unlock, and unmap in proper sequence. @@ -222,22 +234,22 @@ mod implementation { self.size, libc::PROT_READ | libc::PROT_WRITE, ); - (*self.ptr.as_ptr()).zeroize(); + super::zeroize_ptr(self.ptr.as_ptr()); libc::munlock(self.ptr.as_ptr() as *const libc::c_void, self.size); libc::munmap(self.ptr.as_ptr() as *mut libc::c_void, self.size); } } } - impl ZeroizeOnDrop for Secret {} + impl ZeroizeOnDrop for Secret {} - impl Clone for Secret { + impl Clone for Secret { fn clone(&self) -> Self { self.expose(|v| Self::new(v.clone())) } } - impl PartialEq for Secret { + impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { self.expose(|a| { other.expose(|b| { @@ -260,21 +272,21 @@ mod implementation { } } - impl Eq for Secret {} + impl Eq for Secret {} - impl Hash for Secret { + impl Hash for Secret { fn hash(&self, state: &mut H) { self.expose(|v| v.hash(state)); } } - impl PartialOrd for Secret { + impl PartialOrd for Secret { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } - impl Ord for Secret { + impl Ord for Secret { fn cmp(&self, other: &Self) -> Ordering { self.expose(|a| { other.expose(|b| { @@ -305,9 +317,10 @@ mod implementation { cmp::Ordering, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, + mem::MaybeUninit, }; use subtle::ConstantTimeEq; - use zeroize::{Zeroize, ZeroizeOnDrop}; + use zeroize::ZeroizeOnDrop; /// A wrapper for secret values that prevents accidental leakage. /// @@ -315,56 +328,51 @@ mod implementation { /// - Debug and Display show `[REDACTED]` /// - Zeroized on drop /// - Access requires explicit `expose()` call - pub struct Secret(T); + pub struct Secret(MaybeUninit); - impl Secret { + impl Secret { /// Creates a new `Secret` wrapping the given value. #[inline] - #[allow(clippy::missing_const_for_fn)] pub fn new(value: T) -> Self { - Self(value) + Self(MaybeUninit::new(value)) } /// Exposes the secret value for read-only access within a closure. #[inline] pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { - f(&self.0) + // SAFETY: self.0 is always initialized (set in new, only zeroed in drop) + f(unsafe { self.0.assume_init_ref() }) } } - impl Debug for Secret { + impl Debug for Secret { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_str("[REDACTED]") } } - impl Display for Secret { + impl Display for Secret { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_str("[REDACTED]") } } - impl Zeroize for Secret { - fn zeroize(&mut self) { - self.0.zeroize(); - } - } - - impl Drop for Secret { + impl Drop for Secret { fn drop(&mut self) { - self.0.zeroize(); + // SAFETY: self.0 is initialized and we have exclusive access + unsafe { super::zeroize_ptr(self.0.as_mut_ptr()) }; } } - impl ZeroizeOnDrop for Secret {} + impl ZeroizeOnDrop for Secret {} - impl Clone for Secret { + impl Clone for Secret { fn clone(&self) -> Self { self.expose(|v| Self::new(v.clone())) } } - impl PartialEq for Secret { + impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { self.expose(|a| { other.expose(|b| { @@ -387,21 +395,21 @@ mod implementation { } } - impl Eq for Secret {} + impl Eq for Secret {} - impl Hash for Secret { + impl Hash for Secret { fn hash(&self, state: &mut H) { self.expose(|v| v.hash(state)); } } - impl PartialOrd for Secret { + impl PartialOrd for Secret { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } - impl Ord for Secret { + impl Ord for Secret { fn cmp(&self, other: &Self) -> Ordering { self.expose(|a| { other.expose(|b| { From b8162013b8dbba6454461186f33b10d72d596670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 18:31:40 +0000 Subject: [PATCH 12/65] [cryptography] reprotect on panic --- cryptography/Cargo.toml | 1 + cryptography/src/secret.rs | 57 ++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index d4bf2db5d6..4a81b2904c 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -110,6 +110,7 @@ required-features = ["soft-mlock"] name = "secp256r1" harness = false path = "src/secp256r1/benches/bench.rs" +required-features = ["soft-mlock"] [[bench]] name = "sha256" diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index b8e4506557..eca45cfe0b 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -182,9 +182,25 @@ mod implementation { /// Exposes the secret value for read-only access within a closure. /// - /// Memory is re-protected immediately after the closure returns. + /// Memory is re-protected immediately after the closure returns, even if + /// the closure panics. #[inline] pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { + // Scope guard that re-protects memory on drop (including panic unwinding) + struct ReprotectGuard { + ptr: *mut libc::c_void, + size: usize, + } + + impl Drop for ReprotectGuard { + fn drop(&mut self) { + // SAFETY: ptr and size are valid from the Secret that created us + unsafe { + libc::mprotect(self.ptr, self.size, libc::PROT_NONE); + } + } + } + // SAFETY: self.ptr points to valid protected memory of self.size bytes let result = unsafe { libc::mprotect( @@ -195,19 +211,15 @@ mod implementation { }; assert_eq!(result, 0, "mprotect failed to unprotect memory"); + // Create guard AFTER unprotecting - it will re-protect on drop + let _guard = ReprotectGuard { + ptr: self.ptr.as_ptr() as *mut libc::c_void, + size: self.size, + }; + // SAFETY: Memory is now readable and ptr is valid let value = unsafe { self.ptr.as_ref() }; - let result = f(value); - - // SAFETY: Re-protect after use - unsafe { - libc::mprotect( - self.ptr.as_ptr() as *mut libc::c_void, - self.size, - libc::PROT_NONE, - ); - } - result + f(value) } } @@ -533,4 +545,25 @@ mod tests { let _ = format!("{:?}", *v); }); } + + #[cfg(unix)] + #[test] + fn test_expose_reprotects_on_panic() { + use std::panic; + + let secret = Secret::new([42u8; 32]); + + // Panic inside expose - memory should still be re-protected + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + secret.expose(|_v| { + panic!("intentional panic"); + }); + })); + assert!(result.is_err()); + + // Should be able to expose again (memory was re-protected) + secret.expose(|v| { + assert_eq!(v[0], 42); + }); + } } From 2bff3481ee268a939fbcb7cf5e5cb87195e5b592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 18:47:04 +0000 Subject: [PATCH 13/65] [consensus] require cryptography/soft-mlock for tests --- consensus/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index acc43ac0b4..aedd88c356 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -40,6 +40,7 @@ tracing.workspace = true [dev-dependencies] commonware-conformance.workspace = true commonware-consensus = { path = ".", features = ["mocks"] } +commonware-cryptography = { workspace = true, features = ["soft-mlock"] } commonware-math.workspace = true commonware-resolver = { workspace = true, features = ["mocks"] } rstest.workspace = true From a100efe38c1138d2f46fb3000b5d1d574e442d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 18:47:31 +0000 Subject: [PATCH 14/65] [cryptography] drop in place before zeroizing --- cryptography/src/secret.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index eca45cfe0b..ed709f4e18 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -154,7 +154,8 @@ mod implementation { if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { #[cfg(not(any(test, feature = "soft-mlock")))] { - // SAFETY: ptr points to valid T, zeroize before freeing + // SAFETY: ptr points to valid T, drop then zeroize before freeing + unsafe { core::ptr::drop_in_place(ptr) }; unsafe { super::zeroize_ptr(ptr) }; // SAFETY: ptr and size match the mmap above unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; @@ -164,8 +165,9 @@ mod implementation { // SAFETY: ptr points to valid memory of size `size` if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { - // SAFETY: cleanup on failure - zeroize, unlock (if locked), and unmap + // SAFETY: cleanup on failure - drop, zeroize, unlock (if locked), and unmap unsafe { + core::ptr::drop_in_place(ptr); super::zeroize_ptr(ptr); libc::munlock(ptr as *const libc::c_void, size); libc::munmap(ptr as *mut libc::c_void, size); @@ -238,7 +240,7 @@ mod implementation { impl Drop for Secret { fn drop(&mut self) { // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. - // We unprotect, zeroize, unlock, and unmap in proper sequence. + // We unprotect, drop inner value, zeroize, unlock, and unmap in proper sequence. // This is safe because we have exclusive access (&mut self). unsafe { libc::mprotect( @@ -246,6 +248,7 @@ mod implementation { self.size, libc::PROT_READ | libc::PROT_WRITE, ); + core::ptr::drop_in_place(self.ptr.as_ptr()); super::zeroize_ptr(self.ptr.as_ptr()); libc::munlock(self.ptr.as_ptr() as *const libc::c_void, self.size); libc::munmap(self.ptr.as_ptr() as *mut libc::c_void, self.size); @@ -371,8 +374,12 @@ mod implementation { impl Drop for Secret { fn drop(&mut self) { - // SAFETY: self.0 is initialized and we have exclusive access - unsafe { super::zeroize_ptr(self.0.as_mut_ptr()) }; + // SAFETY: self.0 is initialized and we have exclusive access. + // We drop the inner value first to run its destructor, then zeroize. + unsafe { + core::ptr::drop_in_place(self.0.as_mut_ptr()); + super::zeroize_ptr(self.0.as_mut_ptr()); + } } } From fd600b60c0d0c8feb09573fdb3a318ac63d75c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 18:54:23 +0000 Subject: [PATCH 15/65] [cryptography] lint --- cryptography/src/secret.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index ed709f4e18..939f406f76 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -120,7 +120,6 @@ mod implementation { } /// Creates a new `Secret`, returning an error on failure. - #[allow(clippy::undocumented_unsafe_blocks)] pub fn try_new(value: T) -> Result { let page_size = page_size(); let type_size = core::mem::size_of::(); @@ -348,6 +347,7 @@ mod implementation { impl Secret { /// Creates a new `Secret` wrapping the given value. #[inline] + #[allow(clippy::missing_const_for_fn)] pub fn new(value: T) -> Self { Self(MaybeUninit::new(value)) } From cdeb2e4cbb924ac28c54d7df22de90caec0a8dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 19:01:53 +0000 Subject: [PATCH 16/65] [cryptography] remove share method --- cryptography/src/bls12381/dkg.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 3b75fef751..239b57d9fd 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -566,7 +566,7 @@ impl Info { }; let expected = pub_msg.commitment.eval_msm(&scalar); priv_msg - .share() + .share .expose(|share| expected == V::Public::generator() * share) } } @@ -696,11 +696,6 @@ impl DealerPrivMsg { share: Secret::new(share), } } - - /// Returns a reference to the share wrapped in `Secret`. - pub const fn share(&self) -> &Secret { - &self.share - } } impl EncodeSize for DealerPrivMsg { @@ -1498,7 +1493,7 @@ impl Player { let share = self .view .get(dealer) - .map(|(_, priv_msg)| priv_msg.share().expose(|s| s.clone())) + .map(|(_, priv_msg)| priv_msg.share.expose(|s| s.clone())) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( || { @@ -1506,7 +1501,7 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - |priv_msg| priv_msg.share().expose(|s| s.clone()), + |priv_msg| priv_msg.share.expose(|s| s.clone()), ) }); (dealer.clone(), share) From 23b16989786b43f86e6be2a2df9f8de8142b59a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 19:05:22 +0000 Subject: [PATCH 17/65] [cryptography] derive partialeq/eq --- cryptography/src/bls12381/dkg.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 239b57d9fd..9dcd7a0e8e 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -681,7 +681,7 @@ where } } -#[derive(Debug, Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct DealerPrivMsg { share: Secret, } @@ -730,15 +730,6 @@ impl arbitrary::Arbitrary<'_> for DealerPrivMsg { } } -impl PartialEq for DealerPrivMsg { - fn eq(&self, other: &Self) -> bool { - // Use Secret's constant-time comparison - self.share == other.share - } -} - -impl Eq for DealerPrivMsg {} - #[derive(Clone, Debug)] pub struct PlayerAck { sig: P::Signature, From e5f7b6da78bc41d00304dce8cc5a9104097a7f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 21:05:54 +0000 Subject: [PATCH 18/65] [cryptography] specialize Secret for Eq/Ord impls --- cryptography/src/bls12381/primitives/group.rs | 4 +- cryptography/src/bls12381/scheme.rs | 10 +- cryptography/src/secret.rs | 139 ++++++------------ 3 files changed, 58 insertions(+), 95 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 093897d980..cdb2a01445 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -285,8 +285,8 @@ impl Scalar { Self(ret) } - /// Encodes the scalar into a slice. - fn as_slice(&self) -> [u8; Self::SIZE] { + /// Encodes the scalar into a byte array. + pub(crate) fn as_slice(&self) -> [u8; Self::SIZE] { let mut slice = [0u8; Self::SIZE]; // SAFETY: All pointers valid; blst_bendian_from_scalar writes exactly 32 bytes. unsafe { diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 49d12d0262..5b5df6bad7 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -53,12 +53,20 @@ use std::borrow::Cow; const CURVE_NAME: &str = "bls12381"; /// BLS12-381 private key. -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone)] pub struct PrivateKey { raw: Secret<[u8; group::PRIVATE_KEY_LENGTH]>, key: Secret, } +impl PartialEq for PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.raw == other.raw + } +} + +impl Eq for PrivateKey {} + impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { self.raw.expose(|bytes| bytes.write(buf)); diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 939f406f76..d53e2e7d03 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -24,6 +24,7 @@ //! self-contained types (no heap pointers). Types like `Vec` or `String` //! will only have their metadata protected, not heap data. +use crate::bls12381::primitives::group::Scalar; use core::cmp::Ordering; use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; @@ -263,63 +264,29 @@ mod implementation { } } - impl PartialEq for Secret { + impl PartialEq for Secret<[u8; N]> { fn eq(&self, other: &Self) -> bool { - self.expose(|a| { - other.expose(|b| { - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - a as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - b as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - a.ct_eq(b).into() - }) - }) + self.expose(|a| other.expose(|b| a.ct_eq(b).into())) } } - impl Eq for Secret {} - - impl Hash for Secret { - fn hash(&self, state: &mut H) { - self.expose(|v| v.hash(state)); - } - } + impl Eq for Secret<[u8; N]> {} - impl PartialOrd for Secret { + impl PartialOrd for Secret<[u8; N]> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } - impl Ord for Secret { + impl Ord for Secret<[u8; N]> { fn cmp(&self, other: &Self) -> Ordering { - self.expose(|a| { - other.expose(|b| { - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - a as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - b as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - super::ct_cmp_bytes(a, b) - }) - }) + self.expose(|a| other.expose(|b| super::ct_cmp_bytes(a, b))) + } + } + + impl Hash for Secret { + fn hash(&self, state: &mut H) { + self.expose(|v| v.hash(state)); } } } @@ -391,69 +358,57 @@ mod implementation { } } - impl PartialEq for Secret { + // Only implement comparison traits for byte arrays (no padding bytes) + impl PartialEq for Secret<[u8; N]> { fn eq(&self, other: &Self) -> bool { - self.expose(|a| { - other.expose(|b| { - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - a as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - b as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - a.ct_eq(b).into() - }) - }) + self.expose(|a| other.expose(|b| a.ct_eq(b).into())) } } - impl Eq for Secret {} - - impl Hash for Secret { - fn hash(&self, state: &mut H) { - self.expose(|v| v.hash(state)); - } - } + impl Eq for Secret<[u8; N]> {} - impl PartialOrd for Secret { + impl PartialOrd for Secret<[u8; N]> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } - impl Ord for Secret { + impl Ord for Secret<[u8; N]> { fn cmp(&self, other: &Self) -> Ordering { - self.expose(|a| { - other.expose(|b| { - // SAFETY: Reading raw bytes of T for constant-time comparison. - let (a, b) = unsafe { - ( - core::slice::from_raw_parts( - a as *const T as *const u8, - core::mem::size_of::(), - ), - core::slice::from_raw_parts( - b as *const T as *const u8, - core::mem::size_of::(), - ), - ) - }; - super::ct_cmp_bytes(a, b) - }) - }) + self.expose(|a| other.expose(|b| super::ct_cmp_bytes(a, b))) + } + } + + impl Hash for Secret { + fn hash(&self, state: &mut H) { + self.expose(|v| v.hash(state)); } } } +// Specialized comparison impls for Secret pub use implementation::*; +impl PartialEq for Secret { + fn eq(&self, other: &Self) -> bool { + self.expose(|a| other.expose(|b| a.as_slice().ct_eq(&b.as_slice()).into())) + } +} + +impl Eq for Secret {} + +impl PartialOrd for Secret { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Secret { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.expose(|a| other.expose(|b| ct_cmp_bytes(&a.as_slice(), &b.as_slice()))) + } +} + #[cfg(test)] mod tests { use super::*; From 9f7aa179ed1a7ba41f8da60d0bea9c187324e1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 21:41:45 +0000 Subject: [PATCH 19/65] [cryptography] use libc from workspace --- cryptography/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 4a81b2904c..365c7e1f9d 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -43,7 +43,7 @@ version = "0.2.15" features = ["js"] [target.'cfg(unix)'.dependencies] -libc = "0.2" +libc.workspace = true [dev-dependencies] anyhow.workspace = true From 4b4a15edf89f183c6d576747ce412440ce7f2a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 21:47:06 +0000 Subject: [PATCH 20/65] [cryptography] extract AccessGuard out of function --- cryptography/src/secret.rs | 62 ++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index d53e2e7d03..7bc0ba5db1 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -80,6 +80,37 @@ mod implementation { } } + /// Guard that unprotects memory on creation and re-protects on drop. + /// + /// This ensures memory is always re-protected, even during panic unwinding. + struct AccessGuard { + ptr: *mut libc::c_void, + size: usize, + } + + impl AccessGuard { + /// Unprotects memory for read access, returning a guard that re-protects on drop. + /// + /// # Panics + /// + /// Panics if mprotect fails to unprotect the memory. + fn new(ptr: *mut libc::c_void, size: usize) -> Self { + // SAFETY: ptr points to valid mmap'd memory of the given size + let result = unsafe { libc::mprotect(ptr, size, libc::PROT_READ) }; + assert_eq!(result, 0, "mprotect failed to unprotect memory"); + Self { ptr, size } + } + } + + impl Drop for AccessGuard { + fn drop(&mut self) { + // SAFETY: ptr and size are valid from the Secret that created us + unsafe { + libc::mprotect(self.ptr, self.size, libc::PROT_NONE); + } + } + } + /// A wrapper for secret values with OS-level memory protection. /// /// Uses `mmap` for allocation instead of the global allocator because: @@ -188,36 +219,7 @@ mod implementation { /// the closure panics. #[inline] pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { - // Scope guard that re-protects memory on drop (including panic unwinding) - struct ReprotectGuard { - ptr: *mut libc::c_void, - size: usize, - } - - impl Drop for ReprotectGuard { - fn drop(&mut self) { - // SAFETY: ptr and size are valid from the Secret that created us - unsafe { - libc::mprotect(self.ptr, self.size, libc::PROT_NONE); - } - } - } - - // SAFETY: self.ptr points to valid protected memory of self.size bytes - let result = unsafe { - libc::mprotect( - self.ptr.as_ptr() as *mut libc::c_void, - self.size, - libc::PROT_READ, - ) - }; - assert_eq!(result, 0, "mprotect failed to unprotect memory"); - - // Create guard AFTER unprotecting - it will re-protect on drop - let _guard = ReprotectGuard { - ptr: self.ptr.as_ptr() as *mut libc::c_void, - size: self.size, - }; + let _guard = AccessGuard::new(self.ptr.as_ptr() as *mut libc::c_void, self.size); // SAFETY: Memory is now readable and ptr is valid let value = unsafe { self.ptr.as_ref() }; From 9e1bc6c05691782dfa1ea002292050bebd4d15da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 21:52:10 +0000 Subject: [PATCH 21/65] [cryptography] reduce duplication --- cryptography/src/secret.rs | 143 +++++++++++-------------------------- 1 file changed, 43 insertions(+), 100 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 7bc0ba5db1..d2d5892562 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -25,8 +25,13 @@ //! will only have their metadata protected, not heap data. use crate::bls12381::primitives::group::Scalar; -use core::cmp::Ordering; +use core::{ + cmp::Ordering, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, +}; use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; +use zeroize::ZeroizeOnDrop; /// Constant-time lexicographic comparison for byte slices. #[inline] @@ -60,14 +65,7 @@ unsafe fn zeroize_ptr(ptr: *mut T) { // Use protected implementation on Unix with the feature enabled #[cfg(unix)] mod implementation { - use core::{ - cmp::Ordering, - fmt::{Debug, Display, Formatter}, - hash::{Hash, Hasher}, - ptr::NonNull, - }; - use subtle::ConstantTimeEq; - use zeroize::ZeroizeOnDrop; + use core::ptr::NonNull; /// Returns the system page size. fn page_size() -> usize { @@ -227,18 +225,6 @@ mod implementation { } } - impl Debug for Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } - } - - impl Display for Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } - } - impl Drop for Secret { fn drop(&mut self) { // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. @@ -257,53 +243,12 @@ mod implementation { } } } - - impl ZeroizeOnDrop for Secret {} - - impl Clone for Secret { - fn clone(&self) -> Self { - self.expose(|v| Self::new(v.clone())) - } - } - - impl PartialEq for Secret<[u8; N]> { - fn eq(&self, other: &Self) -> bool { - self.expose(|a| other.expose(|b| a.ct_eq(b).into())) - } - } - - impl Eq for Secret<[u8; N]> {} - - impl PartialOrd for Secret<[u8; N]> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - - impl Ord for Secret<[u8; N]> { - fn cmp(&self, other: &Self) -> Ordering { - self.expose(|a| other.expose(|b| super::ct_cmp_bytes(a, b))) - } - } - - impl Hash for Secret { - fn hash(&self, state: &mut H) { - self.expose(|v| v.hash(state)); - } - } } // Simple implementation for non-Unix platforms #[cfg(not(unix))] mod implementation { - use core::{ - cmp::Ordering, - fmt::{Debug, Display, Formatter}, - hash::{Hash, Hasher}, - mem::MaybeUninit, - }; - use subtle::ConstantTimeEq; - use zeroize::ZeroizeOnDrop; + use core::mem::MaybeUninit; /// A wrapper for secret values that prevents accidental leakage. /// @@ -329,18 +274,6 @@ mod implementation { } } - impl Debug for Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } - } - - impl Display for Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } - } - impl Drop for Secret { fn drop(&mut self) { // SAFETY: self.0 is initialized and we have exclusive access. @@ -351,45 +284,55 @@ mod implementation { } } } +} - impl ZeroizeOnDrop for Secret {} +pub use implementation::*; - impl Clone for Secret { - fn clone(&self) -> Self { - self.expose(|v| Self::new(v.clone())) - } +impl Debug for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("Secret([REDACTED])") } +} - // Only implement comparison traits for byte arrays (no padding bytes) - impl PartialEq for Secret<[u8; N]> { - fn eq(&self, other: &Self) -> bool { - self.expose(|a| other.expose(|b| a.ct_eq(b).into())) - } +impl Display for Secret { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str("[REDACTED]") } +} - impl Eq for Secret<[u8; N]> {} +impl ZeroizeOnDrop for Secret {} - impl PartialOrd for Secret<[u8; N]> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } +impl Clone for Secret { + fn clone(&self) -> Self { + self.expose(|v| Self::new(v.clone())) + } +} + +impl PartialEq for Secret<[u8; N]> { + fn eq(&self, other: &Self) -> bool { + self.expose(|a| other.expose(|b| a.ct_eq(b).into())) } +} - impl Ord for Secret<[u8; N]> { - fn cmp(&self, other: &Self) -> Ordering { - self.expose(|a| other.expose(|b| super::ct_cmp_bytes(a, b))) - } +impl Eq for Secret<[u8; N]> {} + +impl PartialOrd for Secret<[u8; N]> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } +} - impl Hash for Secret { - fn hash(&self, state: &mut H) { - self.expose(|v| v.hash(state)); - } +impl Ord for Secret<[u8; N]> { + fn cmp(&self, other: &Self) -> Ordering { + self.expose(|a| other.expose(|b| ct_cmp_bytes(a, b))) } } -// Specialized comparison impls for Secret -pub use implementation::*; +impl Hash for Secret { + fn hash(&self, state: &mut H) { + self.expose(|v| v.hash(state)); + } +} impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { From ddb9a798b80c914a10f2815e98195ed931105fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 21:55:26 +0000 Subject: [PATCH 22/65] [cryptography] add more tests --- cryptography/src/secret.rs | 77 +++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index d2d5892562..a618029f04 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -361,7 +361,7 @@ mod tests { #[test] fn test_debug_redacted() { let secret = Secret::new([1u8, 2, 3, 4]); - assert_eq!(format!("{:?}", secret), "[REDACTED]"); + assert_eq!(format!("{:?}", secret), "Secret([REDACTED])"); } #[test] @@ -473,4 +473,79 @@ mod tests { assert_eq!(v[0], 42); }); } + + #[test] + fn test_hash() { + use std::collections::hash_map::DefaultHasher; + + let s1 = Secret::new([1u8, 2, 3, 4]); + let s2 = Secret::new([1u8, 2, 3, 4]); + let s3 = Secret::new([5u8, 6, 7, 8]); + + let hash = |s: &Secret<[u8; 4]>| { + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + hasher.finish() + }; + + // Equal secrets should hash equal + assert_eq!(hash(&s1), hash(&s2)); + // Different secrets should (very likely) hash differently + assert_ne!(hash(&s1), hash(&s3)); + } + + #[test] + fn test_partial_ord() { + let s1 = Secret::new([1u8, 2]); + let s2 = Secret::new([1u8, 3]); + let s3 = Secret::new([1u8, 2]); + + assert!(s1 < s2); + assert!(s2 > s1); + assert!(s1 <= s3); + assert!(s1 >= s3); + + assert_eq!(s1.partial_cmp(&s2), Some(core::cmp::Ordering::Less)); + assert_eq!(s2.partial_cmp(&s1), Some(core::cmp::Ordering::Greater)); + assert_eq!(s1.partial_cmp(&s3), Some(core::cmp::Ordering::Equal)); + } + + #[test] + fn test_scalar_equality() { + use crate::bls12381::primitives::group::Scalar; + use commonware_math::algebra::Random; + use rand::rngs::OsRng; + + let scalar1 = Scalar::random(&mut OsRng); + let scalar2 = scalar1.clone(); + let scalar3 = Scalar::random(&mut OsRng); + + let s1 = Secret::new(scalar1); + let s2 = Secret::new(scalar2); + let s3 = Secret::new(scalar3); + + // Same scalar should be equal + assert_eq!(s1, s2); + // Different scalars should (very likely) be different + assert_ne!(s1, s3); + } + + #[test] + fn test_scalar_ordering() { + use crate::bls12381::primitives::group::Scalar; + use commonware_math::algebra::{Additive, Ring}; + + let zero = Scalar::zero(); + let one = Scalar::one(); + + let s_zero = Secret::new(zero); + let s_one = Secret::new(one); + + // Zero and one should compare consistently + assert_ne!(s_zero, s_one); + // Ordering should be deterministic + let cmp1 = s_zero.cmp(&s_one); + let cmp2 = s_zero.cmp(&s_one); + assert_eq!(cmp1, cmp2); + } } From 11e68f434fc7c14421566af760559a13fd5080c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 22:08:37 +0000 Subject: [PATCH 23/65] [cryptography] fix sync issues --- cryptography/src/bls12381/primitives/group.rs | 2 + cryptography/src/secret.rs | 227 +++++++++++++++--- 2 files changed, 198 insertions(+), 31 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index cdb2a01445..de5664b51e 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -1467,6 +1467,7 @@ mod tests { let mut scalar_set = BTreeSet::new(); let mut g1_set = BTreeSet::new(); let mut g2_set = BTreeSet::new(); + #[allow(clippy::mutable_key_type)] let mut share_set = BTreeSet::new(); while scalar_set.len() < NUM_ITEMS { let scalar = Scalar::random(&mut rng); @@ -1500,6 +1501,7 @@ mod tests { let scalar_map: HashMap<_, _> = scalar_set.iter().cloned().zip(0..).collect(); let g1_map: HashMap<_, _> = g1_set.iter().cloned().zip(0..).collect(); let g2_map: HashMap<_, _> = g2_set.iter().cloned().zip(0..).collect(); + #[allow(clippy::mutable_key_type)] let share_map: HashMap<_, _> = share_set.iter().cloned().zip(0..).collect(); // Verify that the maps contain the expected number of unique items. diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index a618029f04..064db214c8 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -65,7 +65,10 @@ unsafe fn zeroize_ptr(ptr: *mut T) { // Use protected implementation on Unix with the feature enabled #[cfg(unix)] mod implementation { - use core::ptr::NonNull; + use core::{ + ptr::NonNull, + sync::atomic::{AtomicUsize, Ordering}, + }; /// Returns the system page size. fn page_size() -> usize { @@ -78,33 +81,117 @@ mod implementation { } } - /// Guard that unprotects memory on creation and re-protects on drop. + /// State values for the reader count state machine. + /// - 0: Memory is protected, no readers + /// - 1: Transition in progress (unprotecting or protecting) + /// - n >= 2: Memory is readable with (n - 1) active readers + const PROTECTED: usize = 0; + const TRANSITIONING: usize = 1; + + /// Guard that manages concurrent read access to protected memory. + /// + /// Uses a state machine to support multiple concurrent readers: + /// - State 0 (PROTECTED): Memory is protected + /// - State 1 (TRANSITIONING): mprotect in progress + /// - State n >= 2: (n-1) readers are active, memory is readable /// - /// This ensures memory is always re-protected, even during panic unwinding. - struct AccessGuard { + /// This ensures thread-safety when `Secret` is shared across threads. + struct AccessGuard<'a> { ptr: *mut libc::c_void, size: usize, + readers: &'a AtomicUsize, } - impl AccessGuard { - /// Unprotects memory for read access, returning a guard that re-protects on drop. + impl<'a> AccessGuard<'a> { + /// Acquires read access to protected memory. + /// + /// Uses a state machine to coordinate mprotect calls: + /// - If state is PROTECTED (0), transition to TRANSITIONING, call mprotect, then set to 2 + /// - If state is TRANSITIONING (1), spin-wait until readable + /// - If state >= 2, increment and proceed (memory already readable) /// /// # Panics /// /// Panics if mprotect fails to unprotect the memory. - fn new(ptr: *mut libc::c_void, size: usize) -> Self { - // SAFETY: ptr points to valid mmap'd memory of the given size - let result = unsafe { libc::mprotect(ptr, size, libc::PROT_READ) }; - assert_eq!(result, 0, "mprotect failed to unprotect memory"); - Self { ptr, size } + fn acquire(ptr: *mut libc::c_void, size: usize, readers: &'a AtomicUsize) -> Self { + loop { + let state = readers.load(Ordering::Acquire); + + if state == PROTECTED { + // Try to become the thread that unprotects + if readers + .compare_exchange( + PROTECTED, + TRANSITIONING, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + // We won the race - unprotect memory + // SAFETY: ptr points to valid mmap'd memory of the given size + let result = unsafe { libc::mprotect(ptr, size, libc::PROT_READ) }; + assert_eq!(result, 0, "mprotect failed to unprotect memory"); + + // Transition to readable state with 1 reader (state = 2) + readers.store(2, Ordering::Release); + break; + } + // CAS failed, another thread is transitioning - retry + } else if state == TRANSITIONING { + // Another thread is calling mprotect - spin wait + core::hint::spin_loop(); + } else { + // state >= 2: memory is readable, try to increment + if readers + .compare_exchange(state, state + 1, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + break; + } + // CAS failed, state changed - retry + } + } + + Self { ptr, size, readers } } } - impl Drop for AccessGuard { + impl Drop for AccessGuard<'_> { fn drop(&mut self) { - // SAFETY: ptr and size are valid from the Secret that created us - unsafe { - libc::mprotect(self.ptr, self.size, libc::PROT_NONE); + loop { + let state = self.readers.load(Ordering::Acquire); + debug_assert!(state >= 2, "invalid reader state on drop"); + + if state == 2 { + // We're the last reader - try to transition to protecting + if self + .readers + .compare_exchange(2, TRANSITIONING, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + // Re-protect memory + // SAFETY: ptr and size are valid from the Secret that created us + unsafe { + libc::mprotect(self.ptr, self.size, libc::PROT_NONE); + } + + // Transition to protected state + self.readers.store(PROTECTED, Ordering::Release); + break; + } + // CAS failed - another reader appeared, retry + } else { + // state > 2: other readers exist, just decrement + if self + .readers + .compare_exchange(state, state - 1, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + break; + } + // CAS failed, state changed - retry + } } } } @@ -122,20 +209,25 @@ mod implementation { /// - Memory is marked no-access except during expose() (mprotect) /// - Zeroized on drop /// - /// Access requires explicit `expose()` call which returns a guard. - /// Memory is re-protected when the guard is dropped. + /// Access requires explicit `expose()` call. Multiple concurrent readers are + /// supported via atomic reference counting - memory remains readable as long + /// as at least one reader holds access. pub struct Secret { ptr: NonNull, size: usize, + /// Tracks the number of concurrent readers for safe mprotect management. + readers: AtomicUsize, } - // SAFETY: Secret owns its memory and ensures proper synchronization - // through the guard pattern. Access to the protected memory region is - // controlled by mprotect calls that make the memory accessible only - // during the lifetime of guard objects. + // SAFETY: Secret owns its memory and ensures proper synchronization through + // atomic reference counting. The readers counter ensures mprotect calls are + // coordinated: memory is unprotected when readers > 0 and protected when + // readers == 0. unsafe impl Send for Secret {} - // SAFETY: Same reasoning as Send - the guard pattern ensures proper - // synchronization of memory access. + + // SAFETY: Concurrent expose() calls are safe because AccessGuard uses atomic + // operations to coordinate mprotect calls. Memory remains readable as long as + // any reader holds an AccessGuard. unsafe impl Sync for Secret {} impl Secret { @@ -143,16 +235,32 @@ mod implementation { /// /// # Panics /// - /// Panics if memory protection fails (allocation, mlock, or mprotect). + /// Panics if memory protection fails (allocation, mlock, or mprotect), + /// or if `T` requires alignment greater than the system page size. #[inline] pub fn new(value: T) -> Self { Self::try_new(value).expect("failed to create protected secret") } /// Creates a new `Secret`, returning an error on failure. + /// + /// # Errors + /// + /// Returns an error if: + /// - `T` requires alignment greater than the system page size + /// - Memory allocation (mmap) fails + /// - Memory locking (mlock) fails (except in test/soft-mlock mode) + /// - Memory protection (mprotect) fails pub fn try_new(value: T) -> Result { let page_size = page_size(); + let type_align = core::mem::align_of::(); let type_size = core::mem::size_of::(); + + // Ensure T's alignment doesn't exceed page size (mmap returns page-aligned memory) + if type_align > page_size { + return Err("type alignment exceeds page size"); + } + // Round up to page boundary (minimum one page) let size = type_size.max(1).next_multiple_of(page_size); @@ -174,7 +282,8 @@ mod implementation { let ptr = ptr as *mut T; - // SAFETY: ptr is valid and properly aligned (mmap returns page-aligned memory) + // SAFETY: ptr is valid and properly aligned (mmap returns page-aligned memory, + // and we verified type_align <= page_size above) unsafe { core::ptr::write(ptr, value) }; // SAFETY: ptr points to valid mmap'd memory of size `size` @@ -208,16 +317,32 @@ mod implementation { // SAFETY: ptr is non-null (mmap succeeded) ptr: unsafe { NonNull::new_unchecked(ptr) }, size, + readers: AtomicUsize::new(0), }) } /// Exposes the secret value for read-only access within a closure. /// - /// Memory is re-protected immediately after the closure returns, even if - /// the closure panics. + /// Memory is re-protected when all concurrent readers have finished, + /// even if a closure panics. + /// + /// # Thread Safety + /// + /// Multiple threads can call `expose` concurrently. The memory remains + /// readable as long as at least one reader is active. + /// + /// # Note + /// + /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent + /// the returned value from containing references to the secret data. + /// This ensures the reference cannot escape the closure scope. #[inline] - pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { - let _guard = AccessGuard::new(self.ptr.as_ptr() as *mut libc::c_void, self.size); + pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { + let _guard = AccessGuard::acquire( + self.ptr.as_ptr() as *mut libc::c_void, + self.size, + &self.readers, + ); // SAFETY: Memory is now readable and ptr is valid let value = unsafe { self.ptr.as_ref() }; @@ -229,7 +354,8 @@ mod implementation { fn drop(&mut self) { // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. // We unprotect, drop inner value, zeroize, unlock, and unmap in proper sequence. - // This is safe because we have exclusive access (&mut self). + // This is safe because we have exclusive access (&mut self) - Drop requires &mut. + // No concurrent readers can exist because &mut self means no shared references. unsafe { libc::mprotect( self.ptr.as_ptr() as *mut libc::c_void, @@ -267,8 +393,14 @@ mod implementation { } /// Exposes the secret value for read-only access within a closure. + /// + /// # Note + /// + /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent + /// the returned value from containing references to the secret data. + /// This ensures the reference cannot escape the closure scope. #[inline] - pub fn expose(&self, f: impl FnOnce(&T) -> R) -> R { + pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { // SAFETY: self.0 is always initialized (set in new, only zeroed in drop) f(unsafe { self.0.assume_init_ref() }) } @@ -548,4 +680,37 @@ mod tests { let cmp2 = s_zero.cmp(&s_one); assert_eq!(cmp1, cmp2); } + + #[cfg(unix)] + #[test] + fn test_concurrent_expose() { + use std::{sync::Arc, thread}; + + let secret = Arc::new(Secret::new([42u8; 32])); + let mut handles = vec![]; + + // Spawn multiple threads that concurrently expose the secret + for _ in 0..10 { + let secret = Arc::clone(&secret); + handles.push(thread::spawn(move || { + for _ in 0..100 { + secret.expose(|v| { + // Verify the value is correct + assert_eq!(v[0], 42); + assert_eq!(v[31], 42); + }); + } + })); + } + + // Wait for all threads to complete + for handle in handles { + handle.join().expect("thread panicked"); + } + + // Verify the secret is still accessible after concurrent access + secret.expose(|v| { + assert_eq!(v, &[42u8; 32]); + }); + } } From 539ce5ff2477bcd8a528b47ee2429c7d786f24b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 22:17:24 +0000 Subject: [PATCH 24/65] [cryptography] sync correctness --- cryptography/src/secret.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 064db214c8..7a97a3d556 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -130,8 +130,12 @@ mod implementation { { // We won the race - unprotect memory // SAFETY: ptr points to valid mmap'd memory of the given size - let result = unsafe { libc::mprotect(ptr, size, libc::PROT_READ) }; - assert_eq!(result, 0, "mprotect failed to unprotect memory"); + if unsafe { libc::mprotect(ptr, size, libc::PROT_READ) } != 0 { + // Restore to PROTECTED before panicking. If the panic is caught, + // future expose() calls can retry instead of spinning on TRANSITIONING. + readers.store(PROTECTED, Ordering::Release); + panic!("mprotect failed to unprotect memory"); + } // Transition to readable state with 1 reader (state = 2) readers.store(2, Ordering::Release); @@ -172,8 +176,12 @@ mod implementation { { // Re-protect memory // SAFETY: ptr and size are valid from the Secret that created us - unsafe { - libc::mprotect(self.ptr, self.size, libc::PROT_NONE); + if unsafe { libc::mprotect(self.ptr, self.size, libc::PROT_NONE) } != 0 { + // Restore to PROTECTED so future expose calls can retry. + // Memory remains readable but next expose()'s mprotect(PROT_READ) + // will succeed, allowing recovery. + self.readers.store(PROTECTED, Ordering::Release); + panic!("mprotect failed to re-protect memory"); } // Transition to protected state @@ -297,7 +305,7 @@ mod implementation { unsafe { super::zeroize_ptr(ptr) }; // SAFETY: ptr and size match the mmap above unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; - return Err("mlock failed"); + return Err("mlock failed: memory limit exceeded. Try increasing with `ulimit -l` or check /etc/security/limits.conf"); } } From 40c9231e8437f32a862417ba49a9b28a87b27c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 22:41:29 +0000 Subject: [PATCH 25/65] [cryptography] nits --- cryptography/src/bls12381/dkg.rs | 17 ++++++----------- cryptography/src/bls12381/primitives/group.rs | 7 +------ cryptography/src/bls12381/primitives/ops.rs | 4 ++-- cryptography/src/secret.rs | 10 ++++++++-- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 9dcd7a0e8e..e3ac3eefa8 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -687,10 +687,7 @@ pub struct DealerPrivMsg { } impl DealerPrivMsg { - /// Creates a new DealerPrivMsg with the given share. - /// - /// The share is wrapped in a `Secret` for secure handling. - /// On Unix, this includes OS-level memory protection (mlock + mprotect). + /// Creates a new `DealerPrivMsg` with the given share. pub fn new(share: Scalar) -> Self { Self { share: Secret::new(share), @@ -717,16 +714,14 @@ impl Read for DealerPrivMsg { buf: &mut impl bytes::Buf, _cfg: &Self::Cfg, ) -> Result { - let share: Scalar = ReadExt::read(buf)?; - Ok(Self::new(share)) + Ok(Self::new(ReadExt::read(buf)?)) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for DealerPrivMsg { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { - let share: Scalar = u.arbitrary()?; - Ok(Self::new(share)) + Ok(Self::new(u.arbitrary()?)) } } @@ -1179,8 +1174,8 @@ impl Dealer { ) -> Result<(Self, DealerPubMsg, Vec<(S::PublicKey, DealerPrivMsg)>), Error> { // Check that this dealer is defined in the round. info.dealer_index(&me.public_key())?; - let share = info - .unwrap_or_random_share(&mut rng, share.map(|x| x.private().expose(|s| s.clone())))?; + let share = + info.unwrap_or_random_share(&mut rng, share.map(|x| x.private.expose(|s| s.clone())))?; let my_poly = Poly::new_with_constant(&mut rng, info.degree(), share); let priv_msgs = info .players @@ -1941,7 +1936,7 @@ mod test_plan { let share = info .unwrap_or_random_share( &mut rng, - share.map(|s| s.private().expose(|k| k.clone())), + share.map(|s| s.private.expose(|k| k.clone())), ) .expect("Failed to generate dealer share"); diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index de5664b51e..f079d4af9c 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -498,7 +498,7 @@ pub struct Share { /// The share's index in the polynomial. pub index: u32, /// The scalar corresponding to the share's secret. - private: Secret, + pub private: Secret, } impl Share { @@ -518,11 +518,6 @@ impl Share { pub fn public(&self) -> V::Public { self.private.expose(|key| V::Public::generator() * key) } - - /// Returns a reference to the wrapped private key. - pub const fn private(&self) -> &Secret { - &self.private - } } impl Write for Share { diff --git a/cryptography/src/bls12381/primitives/ops.rs b/cryptography/src/bls12381/primitives/ops.rs index f2e480f683..00eb837755 100644 --- a/cryptography/src/bls12381/primitives/ops.rs +++ b/cryptography/src/bls12381/primitives/ops.rs @@ -143,7 +143,7 @@ pub fn partial_sign_proof_of_possession( ) -> PartialSignature { // Sign the public key let sig = private - .private() + .private .expose(|key| sign::(key, V::PROOF_OF_POSSESSION, &sharing.public().encode())); PartialSignature { value: sig, @@ -175,7 +175,7 @@ pub fn partial_sign_message( message: &[u8], ) -> PartialSignature { let sig = private - .private() + .private .expose(|key| sign_message::(key, namespace, message)); PartialSignature { value: sig, diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 7a97a3d556..317ddd5db5 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -343,7 +343,10 @@ mod implementation { /// /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent /// the returned value from containing references to the secret data. - /// This ensures the reference cannot escape the closure scope. + /// This ensures the reference cannot escape the closure scope. However, + /// this does not prevent copying or cloning the secret value within + /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should + /// avoid leaking secrets through such patterns. #[inline] pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { let _guard = AccessGuard::acquire( @@ -406,7 +409,10 @@ mod implementation { /// /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent /// the returned value from containing references to the secret data. - /// This ensures the reference cannot escape the closure scope. + /// This ensures the reference cannot escape the closure scope. However, + /// this does not prevent copying or cloning the secret value within + /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should + /// avoid leaking secrets through such patterns. #[inline] pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { // SAFETY: self.0 is always initialized (set in new, only zeroed in drop) From bc28df8ccf1d393fae33e4328326c7ab1b51a07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 22:55:34 +0000 Subject: [PATCH 26/65] [cryptography] nits --- cryptography/src/bls12381/primitives/group.rs | 14 +++----------- cryptography/src/bls12381/scheme.rs | 10 ++-------- cryptography/src/ed25519/scheme.rs | 10 ++-------- cryptography/src/secp256r1/common.rs | 10 ++-------- 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index f079d4af9c..c75ba7899b 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -493,7 +493,7 @@ impl Random for Scalar { } /// A share of a threshold signing key. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Share { /// The share's index in the polynomial. pub index: u32, @@ -502,9 +502,7 @@ pub struct Share { } impl Share { - /// Creates a new Share with the given index and private key. - /// - /// The private key is wrapped in a `Secret` for secure handling. + /// Creates a new `Share` with the given index and private key. pub fn new(index: u32, private: Private) -> Self { Self { index, @@ -548,13 +546,7 @@ impl EncodeSize for Share { impl Display for Share { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "Share(index={}, private=[REDACTED])", self.index) - } -} - -impl Debug for Share { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "Share(index={}, private=[REDACTED])", self.index) + write!(f, "{:?}", self) } } diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 5b5df6bad7..66cccd7db3 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -53,7 +53,7 @@ use std::borrow::Cow; const CURVE_NAME: &str = "bls12381"; /// BLS12-381 private key. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PrivateKey { raw: Secret<[u8; group::PRIVATE_KEY_LENGTH]>, key: Secret, @@ -122,15 +122,9 @@ impl From for PrivateKey { } } -impl Debug for PrivateKey { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } -} - impl Display for PrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") + write!(f, "{:?}", self) } } diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 2b012c132b..3854202fbc 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -24,7 +24,7 @@ const PUBLIC_KEY_LENGTH: usize = 32; const SIGNATURE_LENGTH: usize = 64; /// Ed25519 Private Key. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PrivateKey { raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, key: Secret, @@ -134,15 +134,9 @@ impl From for PrivateKey { } } -impl Debug for PrivateKey { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } -} - impl Display for PrivateKey { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") + write!(f, "{:?}", self) } } diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index a99d5f374e..0d23146a8a 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -19,7 +19,7 @@ pub const PUBLIC_KEY_LENGTH: usize = 33; // Y-Parity || X /// Internal Secp256r1 Private Key storage. /// /// Stores both the raw bytes and the `SigningKey` in protected memory. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PrivateKeyInner { raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, pub(crate) key: Secret, @@ -108,15 +108,9 @@ impl From for PrivateKeyInner { } } -impl Debug for PrivateKeyInner { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") - } -} - impl Display for PrivateKeyInner { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("[REDACTED]") + write!(f, "{:?}", self) } } From 490226e83f94c6336128a03087cbc958d20ff5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 22:59:27 +0000 Subject: [PATCH 27/65] [cryptography] nits --- cryptography/src/bls12381/scheme.rs | 1 - cryptography/src/ed25519/scheme.rs | 1 - cryptography/src/secp256r1/common.rs | 5 ----- 3 files changed, 7 deletions(-) diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 66cccd7db3..ebf2b70d70 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -101,7 +101,6 @@ impl Hash for PrivateKey { impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - // Use Secret's constant-time comparison self.raw.cmp(&other.raw) } } diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 3854202fbc..e9e15ceef8 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -113,7 +113,6 @@ impl PartialEq for PrivateKey { impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - // Use Secret's constant-time comparison self.raw.cmp(&other.raw) } } diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 0d23146a8a..f056ffae7a 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -17,8 +17,6 @@ pub const PRIVATE_KEY_LENGTH: usize = 32; pub const PUBLIC_KEY_LENGTH: usize = 33; // Y-Parity || X /// Internal Secp256r1 Private Key storage. -/// -/// Stores both the raw bytes and the `SigningKey` in protected memory. #[derive(Clone, Debug)] pub struct PrivateKeyInner { raw: Secret<[u8; PRIVATE_KEY_LENGTH]>, @@ -33,8 +31,6 @@ impl PartialEq for PrivateKeyInner { impl Eq for PrivateKeyInner {} -impl ZeroizeOnDrop for PrivateKeyInner {} - impl PrivateKeyInner { pub fn new(key: SigningKey) -> Self { let raw: [u8; PRIVATE_KEY_LENGTH] = key.to_bytes().into(); @@ -91,7 +87,6 @@ impl Hash for PrivateKeyInner { impl Ord for PrivateKeyInner { fn cmp(&self, other: &Self) -> core::cmp::Ordering { - // Use Secret's constant-time comparison self.raw.cmp(&other.raw) } } From 0c0d28cec627ecefe6ffa7af39779566cbc49a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 23:07:09 +0000 Subject: [PATCH 28/65] [cryptography] docs --- cryptography/src/secret.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 317ddd5db5..6b32e19208 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -1,7 +1,5 @@ //! A wrapper type for secret values that prevents accidental leakage. //! -//! # Status -//! //! `Secret` provides the following guarantees: //! - Debug and Display always show `[REDACTED]` instead of the actual value //! - The inner value is zeroized on drop @@ -10,8 +8,8 @@ //! //! # Platform-Specific Behavior //! -//! On Unix platforms, `Secret` -//! provides additional OS-level memory protection: +//! On Unix platforms, `Secret` provides additional OS-level memory +//! protection: //! - Memory is locked to prevent swapping (mlock) //! - Memory is marked no-access except during expose() (mprotect) //! @@ -128,7 +126,7 @@ mod implementation { ) .is_ok() { - // We won the race - unprotect memory + // We won the race, unprotect memory // SAFETY: ptr points to valid mmap'd memory of the given size if unsafe { libc::mprotect(ptr, size, libc::PROT_READ) } != 0 { // Restore to PROTECTED before panicking. If the panic is caught, @@ -137,13 +135,13 @@ mod implementation { panic!("mprotect failed to unprotect memory"); } - // Transition to readable state with 1 reader (state = 2) + // Transition to readable state with 1 reader readers.store(2, Ordering::Release); break; } - // CAS failed, another thread is transitioning - retry + // CAS failed, another thread is transitioning, retry } else if state == TRANSITIONING { - // Another thread is calling mprotect - spin wait + // Another thread is calling mprotect, spin wait core::hint::spin_loop(); } else { // state >= 2: memory is readable, try to increment @@ -153,7 +151,7 @@ mod implementation { { break; } - // CAS failed, state changed - retry + // CAS failed, state changed, retry } } @@ -165,10 +163,10 @@ mod implementation { fn drop(&mut self) { loop { let state = self.readers.load(Ordering::Acquire); - debug_assert!(state >= 2, "invalid reader state on drop"); + assert!(state >= 2, "invalid reader state on drop"); if state == 2 { - // We're the last reader - try to transition to protecting + // We're the last reader, try to transition to protecting if self .readers .compare_exchange(2, TRANSITIONING, Ordering::AcqRel, Ordering::Acquire) @@ -188,7 +186,7 @@ mod implementation { self.readers.store(PROTECTED, Ordering::Release); break; } - // CAS failed - another reader appeared, retry + // CAS failed, another reader appeared, retry } else { // state > 2: other readers exist, just decrement if self @@ -198,7 +196,7 @@ mod implementation { { break; } - // CAS failed, state changed - retry + // CAS failed, state changed, retry } } } @@ -218,7 +216,7 @@ mod implementation { /// - Zeroized on drop /// /// Access requires explicit `expose()` call. Multiple concurrent readers are - /// supported via atomic reference counting - memory remains readable as long + /// supported via atomic reference counting, memory remains readable as long /// as at least one reader holds access. pub struct Secret { ptr: NonNull, @@ -332,7 +330,7 @@ mod implementation { /// Exposes the secret value for read-only access within a closure. /// /// Memory is re-protected when all concurrent readers have finished, - /// even if a closure panics. + /// even if the closure panics. /// /// # Thread Safety /// From 49b0a11554f1ffe184b40b8dc53a6e836a896850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 23:09:33 +0000 Subject: [PATCH 29/65] [cryptography] lint --- cryptography/src/secp256r1/common.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index f056ffae7a..df29caf6e4 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -10,7 +10,6 @@ use core::{ }; use p256::ecdsa::{SigningKey, VerifyingKey}; use rand_core::CryptoRngCore; -use zeroize::ZeroizeOnDrop; pub const CURVE_NAME: &str = "secp256r1"; pub const PRIVATE_KEY_LENGTH: usize = 32; From b8ad2079af701419d2cdce88804f9644fda99955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 29 Dec 2025 23:52:45 +0000 Subject: [PATCH 30/65] [cryptography] nits --- cryptography/src/bls12381/dkg.rs | 22 +++++++++++++------ cryptography/src/bls12381/primitives/group.rs | 7 +++--- cryptography/src/bls12381/primitives/ops.rs | 4 ++-- cryptography/src/bls12381/scheme.rs | 4 ++-- cryptography/src/ed25519/scheme.rs | 4 ++-- cryptography/src/secp256r1/common.rs | 2 +- cryptography/src/secp256r1/recoverable.rs | 2 +- cryptography/src/secp256r1/standard.rs | 2 +- 8 files changed, 28 insertions(+), 19 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index e3ac3eefa8..b21f0e2c06 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -697,13 +697,13 @@ impl DealerPrivMsg { impl EncodeSize for DealerPrivMsg { fn encode_size(&self) -> usize { - self.share.expose(|s| s.encode_size()) + self.share.expose(|share| share.encode_size()) } } impl Write for DealerPrivMsg { fn write(&self, buf: &mut impl bytes::BufMut) { - self.share.expose(|s| s.write(buf)); + self.share.expose(|share| share.write(buf)); } } @@ -1174,8 +1174,12 @@ impl Dealer { ) -> Result<(Self, DealerPubMsg, Vec<(S::PublicKey, DealerPrivMsg)>), Error> { // Check that this dealer is defined in the round. info.dealer_index(&me.public_key())?; - let share = - info.unwrap_or_random_share(&mut rng, share.map(|x| x.private.expose(|s| s.clone())))?; + let share = info.unwrap_or_random_share( + &mut rng, + // We are leaking the private scalar since `Poly::new_with_constant` + // needs an owned scalar for polynomial arithmetic + share.map(|x| x.private.expose(|private| private.clone())), + )?; let my_poly = Poly::new_with_constant(&mut rng, info.degree(), share); let priv_msgs = info .players @@ -1479,7 +1483,9 @@ impl Player { let share = self .view .get(dealer) - .map(|(_, priv_msg)| priv_msg.share.expose(|s| s.clone())) + // We are leaking private scalars since interpolation/summation needs + // owned scalars for polynomial arithmetic + .map(|(_, priv_msg)| priv_msg.share.expose(|share| share.clone())) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( || { @@ -1487,7 +1493,9 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - |priv_msg| priv_msg.share.expose(|s| s.clone()), + // We are leaking private scalars since interpolation/summation + // needs owned scalars for polynomial arithmetic + |priv_msg| priv_msg.share.expose(|share| share.clone()), ) }); (dealer.clone(), share) @@ -1936,7 +1944,7 @@ mod test_plan { let share = info .unwrap_or_random_share( &mut rng, - share.map(|s| s.private.expose(|k| k.clone())), + share.map(|s| s.private.expose(|private| private.clone())), ) .expect("Failed to generate dealer share"); diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index c75ba7899b..177aa90959 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -514,14 +514,15 @@ impl Share { /// /// This can be verified against the public polynomial. pub fn public(&self) -> V::Public { - self.private.expose(|key| V::Public::generator() * key) + self.private + .expose(|private| V::Public::generator() * private) } } impl Write for Share { fn write(&self, buf: &mut impl BufMut) { UInt(self.index).write(buf); - self.private.expose(|key| key.write(buf)); + self.private.expose(|private| private.write(buf)); } } @@ -540,7 +541,7 @@ impl Read for Share { impl EncodeSize for Share { fn encode_size(&self) -> usize { - UInt(self.index).encode_size() + self.private.expose(|key| key.encode_size()) + UInt(self.index).encode_size() + self.private.expose(|private| private.encode_size()) } } diff --git a/cryptography/src/bls12381/primitives/ops.rs b/cryptography/src/bls12381/primitives/ops.rs index 00eb837755..30eaf0c8bd 100644 --- a/cryptography/src/bls12381/primitives/ops.rs +++ b/cryptography/src/bls12381/primitives/ops.rs @@ -144,7 +144,7 @@ pub fn partial_sign_proof_of_possession( // Sign the public key let sig = private .private - .expose(|key| sign::(key, V::PROOF_OF_POSSESSION, &sharing.public().encode())); + .expose(|private| sign::(private, V::PROOF_OF_POSSESSION, &sharing.public().encode())); PartialSignature { value: sig, index: private.index, @@ -176,7 +176,7 @@ pub fn partial_sign_message( ) -> PartialSignature { let sig = private .private - .expose(|key| sign_message::(key, namespace, message)); + .expose(|private| sign_message::(private, namespace, message)); PartialSignature { value: sig, index: private.index, diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index ebf2b70d70..9211104fb0 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -69,7 +69,7 @@ impl Eq for PrivateKey {} impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { - self.raw.expose(|bytes| bytes.write(buf)); + self.raw.expose(|raw| raw.write(buf)); } } @@ -95,7 +95,7 @@ impl Span for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - self.raw.expose(|bytes| bytes.hash(state)); + self.raw.expose(|raw| raw.hash(state)); } } diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index e9e15ceef8..b81dc78b5b 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -74,7 +74,7 @@ impl Random for PrivateKey { impl Write for PrivateKey { fn write(&self, buf: &mut impl BufMut) { - self.raw.expose(|bytes| bytes.write(buf)); + self.raw.expose(|raw| raw.write(buf)); } } @@ -101,7 +101,7 @@ impl Eq for PrivateKey {} impl Hash for PrivateKey { fn hash(&self, state: &mut H) { - self.raw.expose(|bytes| bytes.hash(state)); + self.raw.expose(|raw| raw.hash(state)); } } diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index df29caf6e4..7a117cdc58 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -41,7 +41,7 @@ impl PrivateKeyInner { /// Returns the `VerifyingKey` corresponding to this private key. pub fn verifying_key(&self) -> VerifyingKey { - self.key.expose(|k| *k.verifying_key()) + self.key.expose(|key| *key.verifying_key()) } } diff --git a/cryptography/src/secp256r1/recoverable.rs b/cryptography/src/secp256r1/recoverable.rs index cc3c2a0326..b97793c274 100644 --- a/cryptography/src/secp256r1/recoverable.rs +++ b/cryptography/src/secp256r1/recoverable.rs @@ -51,7 +51,7 @@ impl PrivateKey { let (mut signature, mut recovery_id) = self .0 .key - .expose(|k| k.sign_recoverable(&payload)) + .expose(|key| key.sign_recoverable(&payload)) .expect("signing must succeed"); // The signing algorithm generates k, then calculates r <- x(k * G). Normalizing s by negating it is equivalent diff --git a/cryptography/src/secp256r1/standard.rs b/cryptography/src/secp256r1/standard.rs index 5325c09c0b..a714587647 100644 --- a/cryptography/src/secp256r1/standard.rs +++ b/cryptography/src/secp256r1/standard.rs @@ -49,7 +49,7 @@ impl PrivateKey { let payload = namespace.map_or(Cow::Borrowed(msg), |namespace| { Cow::Owned(union_unique(namespace, msg)) }); - let signature: p256::ecdsa::Signature = self.0.key.expose(|k| k.sign(&payload)); + let signature: p256::ecdsa::Signature = self.0.key.expose(|key| key.sign(&payload)); let signature = signature.normalize_s().unwrap_or(signature); Signature::from(signature) } From 1057a37fe18ed83f0c942165712c0bac2567f27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 30 Dec 2025 14:00:47 +0000 Subject: [PATCH 31/65] [cryptography] ct_cmp_bytes enforce equal slice length --- cryptography/src/secret.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 6b32e19208..b5164d2f2f 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -31,9 +31,15 @@ use core::{ use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; use zeroize::ZeroizeOnDrop; -/// Constant-time lexicographic comparison for byte slices. +/// Constant-time lexicographic comparison for equal-length byte slices. +/// +/// # Panics +/// +/// Panics if `a` and `b` have different lengths. #[inline] fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { + assert_eq!(a.len(), b.len()); + let mut result = 0u8; for (&x, &y) in a.iter().zip(b.iter()) { let is_eq = result.ct_eq(&0); @@ -42,9 +48,10 @@ fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { } match result { + 0 => Ordering::Equal, 1 => Ordering::Less, 2 => Ordering::Greater, - _ => a.len().cmp(&b.len()), + _ => unreachable!(), } } From e1b2fb211ffbe8ded02bd1b89a15b860ca84e3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 30 Dec 2025 14:13:10 +0000 Subject: [PATCH 32/65] [cryptography] add Secret::try_new on non-unix --- cryptography/src/secret.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index b5164d2f2f..74d82d4917 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -184,7 +184,7 @@ mod implementation { if unsafe { libc::mprotect(self.ptr, self.size, libc::PROT_NONE) } != 0 { // Restore to PROTECTED so future expose calls can retry. // Memory remains readable but next expose()'s mprotect(PROT_READ) - // will succeed, allowing recovery. + // may succeed, allowing recovery. self.readers.store(PROTECTED, Ordering::Release); panic!("mprotect failed to re-protect memory"); } @@ -370,8 +370,7 @@ mod implementation { fn drop(&mut self) { // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. // We unprotect, drop inner value, zeroize, unlock, and unmap in proper sequence. - // This is safe because we have exclusive access (&mut self) - Drop requires &mut. - // No concurrent readers can exist because &mut self means no shared references. + // This is safe because we have exclusive access (&mut self), no concurrent readers can exist. unsafe { libc::mprotect( self.ptr.as_ptr() as *mut libc::c_void, @@ -408,6 +407,14 @@ mod implementation { Self(MaybeUninit::new(value)) } + /// Creates a new `Secret`, returning an error on failure. + /// + /// On non-Unix platforms, this always succeeds. + #[inline] + pub fn try_new(value: T) -> Result { + Ok(Self::new(value)) + } + /// Exposes the secret value for read-only access within a closure. /// /// # Note From f5f007c306167c23b46890916d00fbc6286ab2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 30 Dec 2025 14:13:30 +0000 Subject: [PATCH 33/65] [cryptography] don't implement Hash for Secret Hash implementation may expose timing information --- cryptography/src/bls12381/primitives/group.rs | 6 ++--- cryptography/src/secret.rs | 27 ------------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 177aa90959..0c15905a28 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -493,7 +493,7 @@ impl Random for Scalar { } /// A share of a threshold signing key. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Share { /// The share's index in the polynomial. pub index: u32, @@ -1141,7 +1141,7 @@ mod tests { use commonware_math::algebra::test_suites; use proptest::prelude::*; use rand::prelude::*; - use std::collections::{BTreeSet, HashMap}; + use std::collections::{BTreeMap, BTreeSet, HashMap}; impl Arbitrary for Scalar { type Parameters = (); @@ -1490,7 +1490,7 @@ mod tests { let g1_map: HashMap<_, _> = g1_set.iter().cloned().zip(0..).collect(); let g2_map: HashMap<_, _> = g2_set.iter().cloned().zip(0..).collect(); #[allow(clippy::mutable_key_type)] - let share_map: HashMap<_, _> = share_set.iter().cloned().zip(0..).collect(); + let share_map: BTreeMap<_, _> = share_set.iter().cloned().zip(0..).collect(); // Verify that the maps contain the expected number of unique items. assert_eq!(scalar_map.len(), NUM_ITEMS); diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 74d82d4917..922adef95f 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -26,7 +26,6 @@ use crate::bls12381::primitives::group::Scalar; use core::{ cmp::Ordering, fmt::{Debug, Display, Formatter}, - hash::{Hash, Hasher}, }; use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; use zeroize::ZeroizeOnDrop; @@ -486,12 +485,6 @@ impl Ord for Secret<[u8; N]> { } } -impl Hash for Secret { - fn hash(&self, state: &mut H) { - self.expose(|v| v.hash(state)); - } -} - impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { self.expose(|a| other.expose(|b| a.as_slice().ct_eq(&b.as_slice()).into())) @@ -632,26 +625,6 @@ mod tests { }); } - #[test] - fn test_hash() { - use std::collections::hash_map::DefaultHasher; - - let s1 = Secret::new([1u8, 2, 3, 4]); - let s2 = Secret::new([1u8, 2, 3, 4]); - let s3 = Secret::new([5u8, 6, 7, 8]); - - let hash = |s: &Secret<[u8; 4]>| { - let mut hasher = DefaultHasher::new(); - s.hash(&mut hasher); - hasher.finish() - }; - - // Equal secrets should hash equal - assert_eq!(hash(&s1), hash(&s2)); - // Different secrets should (very likely) hash differently - assert_ne!(hash(&s1), hash(&s3)); - } - #[test] fn test_partial_ord() { let s1 = Secret::new([1u8, 2]); From 122fb844bcc5630118cbdde6ea88374e2db81496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 30 Dec 2025 14:48:29 +0000 Subject: [PATCH 34/65] [cryptography] docs --- cryptography/src/bls12381/dkg.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index b21f0e2c06..4f5e003ba9 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -1176,8 +1176,10 @@ impl Dealer { info.dealer_index(&me.public_key())?; let share = info.unwrap_or_random_share( &mut rng, - // We are leaking the private scalar since `Poly::new_with_constant` - // needs an owned scalar for polynomial arithmetic + // We are extracting the private scalar from `Secret` protection because + // `Poly::new_with_constant` requires an owned value. The extracted scalar is + // scoped to this function and will be zeroized on drop (i.e. the secret is + // only exposed for the duration of this function). share.map(|x| x.private.expose(|private| private.clone())), )?; let my_poly = Poly::new_with_constant(&mut rng, info.degree(), share); @@ -1477,14 +1479,17 @@ impl Player { concurrency: usize, ) -> Result<(Output, Share), Error> { let selected = select(&self.info, logs)?; + // We are extracting the private scalars from `Secret` protection + // because interpolation/summation needs owned scalars for polynomial + // arithmetic. The extracted scalars are scoped to this function and + // will be zeroized on drop (i.e. the secrets are only exposed for the + // duration of this function). let dealings = selected .iter_pairs() .map(|(dealer, log)| { let share = self .view .get(dealer) - // We are leaking private scalars since interpolation/summation needs - // owned scalars for polynomial arithmetic .map(|(_, priv_msg)| priv_msg.share.expose(|share| share.clone())) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( @@ -1493,8 +1498,6 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - // We are leaking private scalars since interpolation/summation - // needs owned scalars for polynomial arithmetic |priv_msg| priv_msg.share.expose(|share| share.clone()), ) }); From f116581aa689693cc52c29ab97e911ca918cfb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 30 Dec 2025 15:05:52 +0000 Subject: [PATCH 35/65] [cryptography] don't attempt to drop and zeroize if mprotect failed --- cryptography/src/secret.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 922adef95f..77ede9a16c 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -371,13 +371,19 @@ mod implementation { // We unprotect, drop inner value, zeroize, unlock, and unmap in proper sequence. // This is safe because we have exclusive access (&mut self), no concurrent readers can exist. unsafe { - libc::mprotect( + // Only drop and zeroize if we successfully unprotected the memory. + // If mprotect failed attempting to access PROT_NONE memory would cause a segfault. + if libc::mprotect( self.ptr.as_ptr() as *mut libc::c_void, self.size, libc::PROT_READ | libc::PROT_WRITE, - ); - core::ptr::drop_in_place(self.ptr.as_ptr()); - super::zeroize_ptr(self.ptr.as_ptr()); + ) == 0 + { + core::ptr::drop_in_place(self.ptr.as_ptr()); + super::zeroize_ptr(self.ptr.as_ptr()); + } + + // Always clean up the mapping regardless of mprotect result libc::munlock(self.ptr.as_ptr() as *const libc::c_void, self.size); libc::munmap(self.ptr.as_ptr() as *mut libc::c_void, self.size); } From b171bb0165b09d5d3451ac406d6c87532d3017fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 30 Dec 2025 15:07:17 +0000 Subject: [PATCH 36/65] [cryptography] nit --- cryptography/src/secret.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 77ede9a16c..0530148a0f 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -39,7 +39,7 @@ use zeroize::ZeroizeOnDrop; fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { assert_eq!(a.len(), b.len()); - let mut result = 0u8; + let mut result = 0; for (&x, &y) in a.iter().zip(b.iter()) { let is_eq = result.ct_eq(&0); result = u8::conditional_select(&result, &1, is_eq & x.ct_lt(&y)); From b561a52ccb8a974b76577c9ceb9fb87af9c0a93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Thu, 1 Jan 2026 14:15:46 +0000 Subject: [PATCH 37/65] [cryptography] nit imports --- cryptography/src/secret.rs | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 0530148a0f..a62bb12406 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -28,7 +28,7 @@ use core::{ fmt::{Debug, Display, Formatter}, }; use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; -use zeroize::ZeroizeOnDrop; +use zeroize::{Zeroize, ZeroizeOnDrop}; /// Constant-time lexicographic comparison for equal-length byte slices. /// @@ -61,7 +61,6 @@ fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { /// `ptr` must point to valid, writable memory of at least `size_of::()` bytes. #[inline] unsafe fn zeroize_ptr(ptr: *mut T) { - use zeroize::Zeroize; let slice = core::slice::from_raw_parts_mut(ptr as *mut u8, core::mem::size_of::()); slice.zeroize(); } @@ -514,6 +513,11 @@ impl Ord for Secret { #[cfg(test)] mod tests { use super::*; + use crate::bls12381::primitives::group::Scalar; + use commonware_math::algebra::{Additive, Random, Ring}; + use core::cmp::Ordering; + use rand::rngs::OsRng; + use std::{panic, sync::Arc, thread}; #[test] fn test_debug_redacted() { @@ -557,8 +561,6 @@ mod tests { #[test] fn test_ordering() { - use core::cmp::Ordering; - // Test the specific bug case: [2, 1] vs [1, 2] let a = Secret::new([2u8, 1]); let b = Secret::new([1u8, 2]); @@ -598,10 +600,6 @@ mod tests { #[cfg(unix)] #[test] fn test_with_bls_scalar() { - use crate::bls12381::primitives::group::Scalar; - use commonware_math::algebra::Random; - use rand::rngs::OsRng; - let scalar = Scalar::random(&mut OsRng); let secret = Secret::new(scalar); @@ -613,8 +611,6 @@ mod tests { #[cfg(unix)] #[test] fn test_expose_reprotects_on_panic() { - use std::panic; - let secret = Secret::new([42u8; 32]); // Panic inside expose - memory should still be re-protected @@ -649,10 +645,6 @@ mod tests { #[test] fn test_scalar_equality() { - use crate::bls12381::primitives::group::Scalar; - use commonware_math::algebra::Random; - use rand::rngs::OsRng; - let scalar1 = Scalar::random(&mut OsRng); let scalar2 = scalar1.clone(); let scalar3 = Scalar::random(&mut OsRng); @@ -669,9 +661,6 @@ mod tests { #[test] fn test_scalar_ordering() { - use crate::bls12381::primitives::group::Scalar; - use commonware_math::algebra::{Additive, Ring}; - let zero = Scalar::zero(); let one = Scalar::one(); @@ -689,8 +678,6 @@ mod tests { #[cfg(unix)] #[test] fn test_concurrent_expose() { - use std::{sync::Arc, thread}; - let secret = Arc::new(Secret::new([42u8; 32])); let mut handles = vec![]; From 0f9f3f51a2a35157de2a8a03f34c76d5281e2155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Thu, 1 Jan 2026 16:17:35 +0000 Subject: [PATCH 38/65] [cryptography] lint --- cryptography/src/secret.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index a62bb12406..e2ce1d8b2f 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -517,6 +517,7 @@ mod tests { use commonware_math::algebra::{Additive, Random, Ring}; use core::cmp::Ordering; use rand::rngs::OsRng; + #[cfg(unix)] use std::{panic, sync::Arc, thread}; #[test] From 086549c1ba64bdf629eb657bc45fa4b512482ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Sat, 3 Jan 2026 11:52:07 +0000 Subject: [PATCH 39/65] [cryptography/secret] cleanup unsafe block --- cryptography/src/secret.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index e2ce1d8b2f..e7c6523025 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -301,13 +301,13 @@ mod implementation { // In soft-mlock mode (tests/benchmarks), we continue even if mlock fails. // The memory will still be protected via mprotect, just not pinned in RAM. if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { + // SAFETY: ptr points to valid T, drop then zeroize before freeing + // ptr and size match the mmap above #[cfg(not(any(test, feature = "soft-mlock")))] - { - // SAFETY: ptr points to valid T, drop then zeroize before freeing - unsafe { core::ptr::drop_in_place(ptr) }; - unsafe { super::zeroize_ptr(ptr) }; - // SAFETY: ptr and size match the mmap above - unsafe { libc::munmap(ptr as *mut libc::c_void, size) }; + unsafe { + core::ptr::drop_in_place(ptr); + super::zeroize_ptr(ptr); + libc::munmap(ptr as *mut libc::c_void, size); return Err("mlock failed: memory limit exceeded. Try increasing with `ulimit -l` or check /etc/security/limits.conf"); } } From a74cda210a40d26d05a06bcecbd702a147bed557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Sat, 3 Jan 2026 11:53:56 +0000 Subject: [PATCH 40/65] [cryptography/secret] rename soft-mlock to unsafe-mlock --- .github/workflows/benchmark.yml | 2 +- .github/workflows/slow.yml | 2 +- consensus/Cargo.toml | 2 +- cryptography/Cargo.toml | 8 ++++---- cryptography/src/secret.rs | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 8abad2f15b..71b5ef5baf 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -37,7 +37,7 @@ jobs: matrix: include: - package: commonware-cryptography - cargo_flags: "--features commonware-cryptography/soft-mlock" + cargo_flags: "--features commonware-cryptography/unsafe-mlock" file_suffix: "" benchmark_name: "commonware-cryptography" - package: commonware-storage diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index c76dedf83a..20da79536b 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -83,7 +83,7 @@ jobs: matrix: include: - package: commonware-cryptography - cargo_flags: "--features commonware-cryptography/soft-mlock" + cargo_flags: "--features commonware-cryptography/unsafe-mlock" - package: commonware-storage cargo_flags: "" - package: commonware-storage diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index 3065a9f170..cc48f35d84 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -41,7 +41,7 @@ tracing.workspace = true [dev-dependencies] commonware-conformance.workspace = true commonware-consensus = { path = ".", features = ["mocks"] } -commonware-cryptography = { workspace = true, features = ["soft-mlock"] } +commonware-cryptography = { workspace = true, features = ["unsafe-mlock"] } commonware-math.workspace = true commonware-resolver = { workspace = true, features = ["mocks"] } rstest.workspace = true diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 365c7e1f9d..b88c691295 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -59,7 +59,7 @@ crate-type = ["rlib", "cdylib"] [features] default = [ "std" ] -soft-mlock = [] +unsafe-mlock = [] parallel = [ "blake3/rayon", "rayon", "std" ] mocks = [ "std" ] arbitrary = [ @@ -98,19 +98,19 @@ std = [ name = "bls12381" harness = false path = "src/bls12381/benches/bench.rs" -required-features = ["soft-mlock"] +required-features = ["unsafe-mlock"] [[bench]] name = "ed25519" harness = false path = "src/ed25519/benches/bench.rs" -required-features = ["soft-mlock"] +required-features = ["unsafe-mlock"] [[bench]] name = "secp256r1" harness = false path = "src/secp256r1/benches/bench.rs" -required-features = ["soft-mlock"] +required-features = ["unsafe-mlock"] [[bench]] name = "sha256" diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index e7c6523025..424d12fbc2 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -260,7 +260,7 @@ mod implementation { /// Returns an error if: /// - `T` requires alignment greater than the system page size /// - Memory allocation (mmap) fails - /// - Memory locking (mlock) fails (except in test/soft-mlock mode) + /// - Memory locking (mlock) fails (except in test/unsafe-mlock mode) /// - Memory protection (mprotect) fails pub fn try_new(value: T) -> Result { let page_size = page_size(); @@ -298,12 +298,12 @@ mod implementation { unsafe { core::ptr::write(ptr, value) }; // SAFETY: ptr points to valid mmap'd memory of size `size` - // In soft-mlock mode (tests/benchmarks), we continue even if mlock fails. + // In unsafe-mlock mode (tests/benchmarks), we continue even if mlock fails. // The memory will still be protected via mprotect, just not pinned in RAM. if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { // SAFETY: ptr points to valid T, drop then zeroize before freeing // ptr and size match the mmap above - #[cfg(not(any(test, feature = "soft-mlock")))] + #[cfg(not(any(test, feature = "unsafe-mlock")))] unsafe { core::ptr::drop_in_place(ptr); super::zeroize_ptr(ptr); From c5c813eebbc766c4a740c20c9911b05a01dabc0a Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 3 Jan 2026 10:04:38 -0800 Subject: [PATCH 41/65] [cryptography] Remove Hash from PrivateKey types (#2683) Co-authored-by: Claude Opus 4.5 --- cryptography/src/bls12381/scheme.rs | 8 -------- cryptography/src/ed25519/certificate/mocks.rs | 4 ++-- cryptography/src/ed25519/scheme.rs | 8 -------- cryptography/src/secp256r1/common.rs | 16 ---------------- 4 files changed, 2 insertions(+), 34 deletions(-) diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 084b5e1d94..6270b39b77 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -87,14 +87,6 @@ impl FixedSize for PrivateKey { const SIZE: usize = group::PRIVATE_KEY_LENGTH; } -impl Span for PrivateKey {} - -impl Hash for PrivateKey { - fn hash(&self, state: &mut H) { - self.raw.expose(|raw| raw.hash(state)); - } -} - impl Ord for PrivateKey { fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.raw.cmp(&other.raw) diff --git a/cryptography/src/ed25519/certificate/mocks.rs b/cryptography/src/ed25519/certificate/mocks.rs index e2a82b8aab..c2938e270a 100644 --- a/cryptography/src/ed25519/certificate/mocks.rs +++ b/cryptography/src/ed25519/certificate/mocks.rs @@ -6,11 +6,11 @@ use crate::{ Signer as _, }; use commonware_math::algebra::Random; -use commonware_utils::{ordered::BiMap, TryCollect as _}; +use commonware_utils::{ordered::Map, TryCollect as _}; use rand::{CryptoRng, RngCore}; /// Generates ed25519 identity participants. -pub fn participants(rng: &mut R, n: u32) -> BiMap +pub fn participants(rng: &mut R, n: u32) -> Map where R: RngCore + CryptoRng, { diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 824b03d2a2..bd076bfc90 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -95,16 +95,8 @@ impl FixedSize for PrivateKey { const SIZE: usize = PRIVATE_KEY_LENGTH; } -impl Span for PrivateKey {} - impl Eq for PrivateKey {} -impl Hash for PrivateKey { - fn hash(&self, state: &mut H) { - self.raw.expose(|raw| raw.hash(state)); - } -} - impl PartialEq for PrivateKey { fn eq(&self, other: &Self) -> bool { self.raw == other.raw diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 7a117cdc58..8d041cb207 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -76,14 +76,6 @@ impl FixedSize for PrivateKeyInner { const SIZE: usize = PRIVATE_KEY_LENGTH; } -impl Span for PrivateKeyInner {} - -impl Hash for PrivateKeyInner { - fn hash(&self, state: &mut H) { - self.raw.expose(|raw| raw.hash(state)); - } -} - impl Ord for PrivateKeyInner { fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.raw.cmp(&other.raw) @@ -244,14 +236,6 @@ macro_rules! impl_private_key_wrapper { const SIZE: usize = PRIVATE_KEY_LENGTH; } - impl commonware_utils::Span for $name {} - - impl core::hash::Hash for $name { - fn hash(&self, state: &mut H) { - self.0.hash(state); - } - } - impl Ord for $name { fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.0.cmp(&other.0) From c19e19efacdb67ac199419ae075e83e59243c10f Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Sat, 3 Jan 2026 12:04:58 -0800 Subject: [PATCH 42/65] [cryptography] Secrets Hardening (#2685) Co-authored-by: Claude Opus 4.5 --- cryptography/src/secret.rs | 204 ++++++++++++++++++++++++++++++++++--- 1 file changed, 191 insertions(+), 13 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 424d12fbc2..ca4c1b0b3d 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -13,6 +13,13 @@ //! - Memory is locked to prevent swapping (mlock) //! - Memory is marked no-access except during expose() (mprotect) //! +//! On Linux, additional hardening is applied: +//! - `memfd_secret` (Linux 5.14+): Memory is unmapped from the kernel's direct +//! mapping, making it inaccessible via `/proc/pid/mem` even to root. Falls +//! back to regular mmap if unavailable. +//! - `MADV_DONTDUMP`: Prevents the secret from appearing in core dumps +//! - `MADV_WIPEONFORK`: Zeros the memory in child processes after fork +//! //! On other platforms, `Secret` provides software-only protection //! (zeroization and redacted debug output). //! @@ -21,6 +28,16 @@ //! When using protected memory, `Secret` only provides full protection for //! self-contained types (no heap pointers). Types like `Vec` or `String` //! will only have their metadata protected, not heap data. +//! +//! # Security Considerations +//! +//! This module provides defense-in-depth protection but is not a security +//! boundary against privileged attackers. A determined attacker with root +//! access or kernel exploits can still potentially access secrets. The primary +//! protections are: +//! - Preventing accidental leaks via logs, debug output, or core dumps +//! - Reducing the attack surface for memory disclosure bugs +//! - Preventing secrets from persisting on disk via swap use crate::bls12381::primitives::group::Scalar; use core::{ @@ -84,6 +101,100 @@ mod implementation { } } + /// Attempts to allocate memory using [memfd_secret] (Linux 5.14+). + /// + /// memfd_secret provides stronger isolation than regular mmap: + /// - Memory is unmapped from the kernel's direct mapping + /// - Cannot be read via /proc/pid/mem even by root + /// - More resistant to kernel-level attacks + /// + /// Returns None if memfd_secret is not available or fails. + /// + /// [memfd_secret]: https://man7.org/linux/man-pages/man2/memfd_secret.2.html + #[cfg(target_os = "linux")] + fn try_memfd_secret(size: usize) -> Option<*mut libc::c_void> { + // memfd_secret syscall number (added in Linux 5.14) + const SYS_MEMFD_SECRET: libc::c_long = 447; + + // SAFETY: syscall with valid syscall number, flags=0 + let fd = unsafe { libc::syscall(SYS_MEMFD_SECRET, 0 as libc::c_uint) }; + if fd < 0 { + return None; + } + let fd = fd as libc::c_int; + + // SAFETY: fd is valid from successful memfd_secret call above + let (truncate_result, ptr) = unsafe { + let truncate_result = libc::ftruncate(fd, size as libc::off_t); + let ptr = if truncate_result == 0 { + libc::mmap( + core::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ) + } else { + libc::MAP_FAILED + }; + libc::close(fd); + (truncate_result, ptr) + }; + + if truncate_result != 0 || ptr == libc::MAP_FAILED { + return None; + } + + Some(ptr) + } + + /// Allocates memory using [mmap] with MAP_ANONYMOUS. + /// + /// [mmap]: https://man7.org/linux/man-pages/man2/mmap.2.html + fn try_mmap_anonymous(size: usize) -> Option<*mut libc::c_void> { + // SAFETY: mmap with MAP_ANONYMOUS returns page-aligned memory or MAP_FAILED + let ptr = unsafe { + libc::mmap( + core::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) + }; + + if ptr == libc::MAP_FAILED { + None + } else { + Some(ptr) + } + } + + /// Applies madvise hints to protect secret memory (Linux only). + /// + /// - MADV_DONTDUMP: Prevents the memory from appearing in core dumps + /// - MADV_WIPEONFORK (non-memfd_secret only): Zeros memory in child after fork + /// + /// MADV_WIPEONFORK is skipped for memfd_secret allocations because: + /// 1. memfd_secret uses MAP_SHARED, and WIPEONFORK + MAP_SHARED interaction is unclear + /// 2. memfd_secret already provides strong isolation (removed from kernel direct map) + #[cfg(target_os = "linux")] + fn apply_madvise_hints(ptr: *mut libc::c_void, size: usize, is_memfd_secret: bool) { + // MADV_DONTDUMP: Exclude from core dumps + // This is critical - core dumps are often written to disk and may persist + // SAFETY: ptr and size are valid from successful mmap/memfd_secret + unsafe { libc::madvise(ptr, size, libc::MADV_DONTDUMP) }; + + // MADV_WIPEONFORK (Linux 4.14+): Zero this memory in child after fork + // Only apply to MAP_PRIVATE allocations (not memfd_secret which uses MAP_SHARED) + if !is_memfd_secret { + // SAFETY: ptr and size are valid from successful mmap + unsafe { libc::madvise(ptr, size, libc::MADV_WIPEONFORK) }; + } + } + /// State values for the reader count state machine. /// - 0: Memory is protected, no readers /// - 1: Transition in progress (unprotecting or protecting) @@ -262,6 +373,12 @@ mod implementation { /// - Memory allocation (mmap) fails /// - Memory locking (mlock) fails (except in test/unsafe-mlock mode) /// - Memory protection (mprotect) fails + /// + /// # Memory Allocation Strategy + /// + /// On Linux 5.14+, this function first attempts to use `memfd_secret` which + /// provides stronger isolation (memory is unmapped from kernel direct mapping). + /// If unavailable, it falls back to regular `mmap` with `MAP_ANONYMOUS`. pub fn try_new(value: T) -> Result { let page_size = page_size(); let type_align = core::mem::align_of::(); @@ -275,21 +392,24 @@ mod implementation { // Round up to page boundary (minimum one page) let size = type_size.max(1).next_multiple_of(page_size); - // SAFETY: mmap with MAP_ANONYMOUS returns page-aligned memory or MAP_FAILED - let ptr = unsafe { - libc::mmap( - core::ptr::null_mut(), - size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, - -1, - 0, - ) + // Try memfd_secret first on Linux (provides stronger kernel-level isolation) + // Falls back to regular mmap if memfd_secret is unavailable + #[cfg(target_os = "linux")] + let (ptr, is_memfd_secret) = try_memfd_secret(size).map_or_else( + || (try_mmap_anonymous(size), false), + |ptr| (Some(ptr), true), + ); + + #[cfg(not(target_os = "linux"))] + let ptr = try_mmap_anonymous(size); + + let Some(ptr) = ptr else { + return Err("memory allocation failed"); }; - if ptr == libc::MAP_FAILED { - return Err("mmap failed"); - } + // Apply madvise hints for additional protection (Linux only) + #[cfg(target_os = "linux")] + apply_madvise_hints(ptr, size, is_memfd_secret); let ptr = ptr as *mut T; @@ -706,4 +826,62 @@ mod tests { assert_eq!(v, &[42u8; 32]); }); } + + /// Test fork behavior on Linux. + /// + /// The behavior depends on the allocation method: + /// - memfd_secret (MAP_SHARED): Child inherits the secret (0xDE) - this is expected + /// since memfd_secret's protection is against kernel access, not fork inheritance + /// - mmap anonymous (MAP_PRIVATE + WIPEONFORK): Child sees zeroed memory (0x00) + /// + /// Both outcomes are valid - memfd_secret provides stronger kernel isolation, + /// while WIPEONFORK provides fork isolation. + #[cfg(target_os = "linux")] + #[test] + fn test_fork_behavior() { + use std::{ + io::{Read, Write}, + os::unix::net::UnixStream, + }; + + let secret = Secret::new([0xDEu8; 32]); + secret.expose(|v| assert_eq!(v[0], 0xDE)); + + let (mut parent_sock, mut child_sock) = UnixStream::pair().unwrap(); + + // SAFETY: fork is safe, we handle both parent and child cases + let pid = unsafe { libc::fork() }; + + if pid == 0 { + // Child process + drop(parent_sock); + let result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| secret.expose(|v| v[0]))); + let byte = result.unwrap_or(0xFF); + child_sock.write_all(&[byte]).unwrap(); + std::process::exit(0); + } else { + // Parent process + drop(child_sock); + let mut status = 0; + // SAFETY: pid is valid from fork, status is valid pointer + unsafe { libc::waitpid(pid, &mut status, 0) }; + + let mut buf = [0u8; 1]; + parent_sock.read_exact(&mut buf).unwrap(); + + // Valid outcomes: + // - 0xDE: memfd_secret was used (child inherits via MAP_SHARED) + // - 0x00: mmap was used with WIPEONFORK (child sees zeroed memory) + // - 0xFF: access failed in child + assert!( + buf[0] == 0xDE || buf[0] == 0x00 || buf[0] == 0xFF, + "Unexpected value in child: {:#x}", + buf[0] + ); + + // Parent's secret must be unchanged regardless of allocation method + secret.expose(|v| assert_eq!(v[0], 0xDE)); + } + } } From b16dd735f77c4c4ff2401c52bfc3e46c479869a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 17:05:11 +0000 Subject: [PATCH 43/65] [cryptography/secret] remove unix specific implementation --- .github/workflows/benchmark.yml | 2 +- .github/workflows/slow.yml | 2 +- Cargo.lock | 1 - consensus/Cargo.toml | 1 - cryptography/Cargo.toml | 7 - cryptography/src/secret.rs | 662 ++------------------------------ 6 files changed, 37 insertions(+), 638 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 71b5ef5baf..e1ae1e0406 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -37,7 +37,7 @@ jobs: matrix: include: - package: commonware-cryptography - cargo_flags: "--features commonware-cryptography/unsafe-mlock" + cargo_flags: "" file_suffix: "" benchmark_name: "commonware-cryptography" - package: commonware-storage diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 20da79536b..5a3245aba5 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -83,7 +83,7 @@ jobs: matrix: include: - package: commonware-cryptography - cargo_flags: "--features commonware-cryptography/unsafe-mlock" + cargo_flags: "" - package: commonware-storage cargo_flags: "" - package: commonware-storage diff --git a/Cargo.lock b/Cargo.lock index e43a70bc43..9c9a658ae8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,7 +1175,6 @@ dependencies = [ "ecdsa", "ed25519-consensus", "getrandom 0.2.16", - "libc", "p256", "proptest", "rand 0.8.5", diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index cc48f35d84..7a33a4e2a6 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -41,7 +41,6 @@ tracing.workspace = true [dev-dependencies] commonware-conformance.workspace = true commonware-consensus = { path = ".", features = ["mocks"] } -commonware-cryptography = { workspace = true, features = ["unsafe-mlock"] } commonware-math.workspace = true commonware-resolver = { workspace = true, features = ["mocks"] } rstest.workspace = true diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 8118b294ef..bd0329b2b5 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -46,9 +46,6 @@ aws-lc-rs.workspace = true version = "0.2.15" features = ["js"] -[target.'cfg(unix)'.dependencies] -libc.workspace = true - [dev-dependencies] anyhow.workspace = true commonware-conformance.workspace = true @@ -63,7 +60,6 @@ crate-type = ["rlib", "cdylib"] [features] default = [ "std" ] -unsafe-mlock = [] parallel = [ "blake3/rayon", "rayon", "std" ] mocks = [ "std" ] arbitrary = [ @@ -102,19 +98,16 @@ std = [ name = "bls12381" harness = false path = "src/bls12381/benches/bench.rs" -required-features = ["unsafe-mlock"] [[bench]] name = "ed25519" harness = false path = "src/ed25519/benches/bench.rs" -required-features = ["unsafe-mlock"] [[bench]] name = "secp256r1" harness = false path = "src/secp256r1/benches/bench.rs" -required-features = ["unsafe-mlock"] [[bench]] name = "sha256" diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index ca4c1b0b3d..d6fb5dac69 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -6,43 +6,17 @@ //! - Access to the inner value requires an explicit `expose()` call //! - Comparisons use constant-time operations to prevent timing attacks //! -//! # Platform-Specific Behavior -//! -//! On Unix platforms, `Secret` provides additional OS-level memory -//! protection: -//! - Memory is locked to prevent swapping (mlock) -//! - Memory is marked no-access except during expose() (mprotect) -//! -//! On Linux, additional hardening is applied: -//! - `memfd_secret` (Linux 5.14+): Memory is unmapped from the kernel's direct -//! mapping, making it inaccessible via `/proc/pid/mem` even to root. Falls -//! back to regular mmap if unavailable. -//! - `MADV_DONTDUMP`: Prevents the secret from appearing in core dumps -//! - `MADV_WIPEONFORK`: Zeros the memory in child processes after fork -//! -//! On other platforms, `Secret` provides software-only protection -//! (zeroization and redacted debug output). -//! //! # Type Constraints //! -//! When using protected memory, `Secret` only provides full protection for -//! self-contained types (no heap pointers). Types like `Vec` or `String` -//! will only have their metadata protected, not heap data. -//! -//! # Security Considerations -//! -//! This module provides defense-in-depth protection but is not a security -//! boundary against privileged attackers. A determined attacker with root -//! access or kernel exploits can still potentially access secrets. The primary -//! protections are: -//! - Preventing accidental leaks via logs, debug output, or core dumps -//! - Reducing the attack surface for memory disclosure bugs -//! - Preventing secrets from persisting on disk via swap +//! `Secret` only provides full protection for self-contained types (no heap +//! pointers). Types like `Vec` or `String` will only have their stack +//! metadata zeroized, not heap data. use crate::bls12381::primitives::group::Scalar; use core::{ cmp::Ordering, fmt::{Debug, Display, Formatter}, + mem::MaybeUninit, }; use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -82,494 +56,49 @@ unsafe fn zeroize_ptr(ptr: *mut T) { slice.zeroize(); } -// Use protected implementation on Unix with the feature enabled -#[cfg(unix)] -mod implementation { - use core::{ - ptr::NonNull, - sync::atomic::{AtomicUsize, Ordering}, - }; - - /// Returns the system page size. - fn page_size() -> usize { - // SAFETY: sysconf is safe to call with _SC_PAGESIZE - let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; - if size <= 0 { - 4096 - } else { - size as usize - } - } - - /// Attempts to allocate memory using [memfd_secret] (Linux 5.14+). - /// - /// memfd_secret provides stronger isolation than regular mmap: - /// - Memory is unmapped from the kernel's direct mapping - /// - Cannot be read via /proc/pid/mem even by root - /// - More resistant to kernel-level attacks - /// - /// Returns None if memfd_secret is not available or fails. - /// - /// [memfd_secret]: https://man7.org/linux/man-pages/man2/memfd_secret.2.html - #[cfg(target_os = "linux")] - fn try_memfd_secret(size: usize) -> Option<*mut libc::c_void> { - // memfd_secret syscall number (added in Linux 5.14) - const SYS_MEMFD_SECRET: libc::c_long = 447; - - // SAFETY: syscall with valid syscall number, flags=0 - let fd = unsafe { libc::syscall(SYS_MEMFD_SECRET, 0 as libc::c_uint) }; - if fd < 0 { - return None; - } - let fd = fd as libc::c_int; - - // SAFETY: fd is valid from successful memfd_secret call above - let (truncate_result, ptr) = unsafe { - let truncate_result = libc::ftruncate(fd, size as libc::off_t); - let ptr = if truncate_result == 0 { - libc::mmap( - core::ptr::null_mut(), - size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_SHARED, - fd, - 0, - ) - } else { - libc::MAP_FAILED - }; - libc::close(fd); - (truncate_result, ptr) - }; - - if truncate_result != 0 || ptr == libc::MAP_FAILED { - return None; - } - - Some(ptr) - } - - /// Allocates memory using [mmap] with MAP_ANONYMOUS. - /// - /// [mmap]: https://man7.org/linux/man-pages/man2/mmap.2.html - fn try_mmap_anonymous(size: usize) -> Option<*mut libc::c_void> { - // SAFETY: mmap with MAP_ANONYMOUS returns page-aligned memory or MAP_FAILED - let ptr = unsafe { - libc::mmap( - core::ptr::null_mut(), - size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, - -1, - 0, - ) - }; - - if ptr == libc::MAP_FAILED { - None - } else { - Some(ptr) - } - } - - /// Applies madvise hints to protect secret memory (Linux only). - /// - /// - MADV_DONTDUMP: Prevents the memory from appearing in core dumps - /// - MADV_WIPEONFORK (non-memfd_secret only): Zeros memory in child after fork - /// - /// MADV_WIPEONFORK is skipped for memfd_secret allocations because: - /// 1. memfd_secret uses MAP_SHARED, and WIPEONFORK + MAP_SHARED interaction is unclear - /// 2. memfd_secret already provides strong isolation (removed from kernel direct map) - #[cfg(target_os = "linux")] - fn apply_madvise_hints(ptr: *mut libc::c_void, size: usize, is_memfd_secret: bool) { - // MADV_DONTDUMP: Exclude from core dumps - // This is critical - core dumps are often written to disk and may persist - // SAFETY: ptr and size are valid from successful mmap/memfd_secret - unsafe { libc::madvise(ptr, size, libc::MADV_DONTDUMP) }; - - // MADV_WIPEONFORK (Linux 4.14+): Zero this memory in child after fork - // Only apply to MAP_PRIVATE allocations (not memfd_secret which uses MAP_SHARED) - if !is_memfd_secret { - // SAFETY: ptr and size are valid from successful mmap - unsafe { libc::madvise(ptr, size, libc::MADV_WIPEONFORK) }; - } - } - - /// State values for the reader count state machine. - /// - 0: Memory is protected, no readers - /// - 1: Transition in progress (unprotecting or protecting) - /// - n >= 2: Memory is readable with (n - 1) active readers - const PROTECTED: usize = 0; - const TRANSITIONING: usize = 1; - - /// Guard that manages concurrent read access to protected memory. - /// - /// Uses a state machine to support multiple concurrent readers: - /// - State 0 (PROTECTED): Memory is protected - /// - State 1 (TRANSITIONING): mprotect in progress - /// - State n >= 2: (n-1) readers are active, memory is readable - /// - /// This ensures thread-safety when `Secret` is shared across threads. - struct AccessGuard<'a> { - ptr: *mut libc::c_void, - size: usize, - readers: &'a AtomicUsize, - } - - impl<'a> AccessGuard<'a> { - /// Acquires read access to protected memory. - /// - /// Uses a state machine to coordinate mprotect calls: - /// - If state is PROTECTED (0), transition to TRANSITIONING, call mprotect, then set to 2 - /// - If state is TRANSITIONING (1), spin-wait until readable - /// - If state >= 2, increment and proceed (memory already readable) - /// - /// # Panics - /// - /// Panics if mprotect fails to unprotect the memory. - fn acquire(ptr: *mut libc::c_void, size: usize, readers: &'a AtomicUsize) -> Self { - loop { - let state = readers.load(Ordering::Acquire); - - if state == PROTECTED { - // Try to become the thread that unprotects - if readers - .compare_exchange( - PROTECTED, - TRANSITIONING, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - { - // We won the race, unprotect memory - // SAFETY: ptr points to valid mmap'd memory of the given size - if unsafe { libc::mprotect(ptr, size, libc::PROT_READ) } != 0 { - // Restore to PROTECTED before panicking. If the panic is caught, - // future expose() calls can retry instead of spinning on TRANSITIONING. - readers.store(PROTECTED, Ordering::Release); - panic!("mprotect failed to unprotect memory"); - } - - // Transition to readable state with 1 reader - readers.store(2, Ordering::Release); - break; - } - // CAS failed, another thread is transitioning, retry - } else if state == TRANSITIONING { - // Another thread is calling mprotect, spin wait - core::hint::spin_loop(); - } else { - // state >= 2: memory is readable, try to increment - if readers - .compare_exchange(state, state + 1, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - break; - } - // CAS failed, state changed, retry - } - } - - Self { ptr, size, readers } - } - } +/// A wrapper for secret values that prevents accidental leakage. +/// +/// - Debug and Display show `[REDACTED]` +/// - Zeroized on drop +/// - Access requires explicit `expose()` call +pub struct Secret(MaybeUninit); - impl Drop for AccessGuard<'_> { - fn drop(&mut self) { - loop { - let state = self.readers.load(Ordering::Acquire); - assert!(state >= 2, "invalid reader state on drop"); - - if state == 2 { - // We're the last reader, try to transition to protecting - if self - .readers - .compare_exchange(2, TRANSITIONING, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - // Re-protect memory - // SAFETY: ptr and size are valid from the Secret that created us - if unsafe { libc::mprotect(self.ptr, self.size, libc::PROT_NONE) } != 0 { - // Restore to PROTECTED so future expose calls can retry. - // Memory remains readable but next expose()'s mprotect(PROT_READ) - // may succeed, allowing recovery. - self.readers.store(PROTECTED, Ordering::Release); - panic!("mprotect failed to re-protect memory"); - } - - // Transition to protected state - self.readers.store(PROTECTED, Ordering::Release); - break; - } - // CAS failed, another reader appeared, retry - } else { - // state > 2: other readers exist, just decrement - if self - .readers - .compare_exchange(state, state - 1, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - break; - } - // CAS failed, state changed, retry - } - } - } +impl Secret { + /// Creates a new `Secret` wrapping the given value. + #[inline] + #[allow(clippy::missing_const_for_fn)] + pub fn new(value: T) -> Self { + Self(MaybeUninit::new(value)) } - /// A wrapper for secret values with OS-level memory protection. - /// - /// Uses `mmap` for allocation instead of the global allocator because: - /// - The global allocator may sub-allocate within pages, sharing pages with other data - /// - `mmap` guarantees page-aligned, exclusively-owned memory - /// - This ensures `mlock` and `mprotect` apply only to our secret data + /// Exposes the secret value for read-only access within a closure. /// - /// On Unix: - /// - Memory is allocated via mmap (page-isolated) - /// - Memory is locked to prevent swapping (mlock) - /// - Memory is marked no-access except during expose() (mprotect) - /// - Zeroized on drop + /// # Note /// - /// Access requires explicit `expose()` call. Multiple concurrent readers are - /// supported via atomic reference counting, memory remains readable as long - /// as at least one reader holds access. - pub struct Secret { - ptr: NonNull, - size: usize, - /// Tracks the number of concurrent readers for safe mprotect management. - readers: AtomicUsize, - } - - // SAFETY: Secret owns its memory and ensures proper synchronization through - // atomic reference counting. The readers counter ensures mprotect calls are - // coordinated: memory is unprotected when readers > 0 and protected when - // readers == 0. - unsafe impl Send for Secret {} - - // SAFETY: Concurrent expose() calls are safe because AccessGuard uses atomic - // operations to coordinate mprotect calls. Memory remains readable as long as - // any reader holds an AccessGuard. - unsafe impl Sync for Secret {} - - impl Secret { - /// Creates a new `Secret` wrapping the given value. - /// - /// # Panics - /// - /// Panics if memory protection fails (allocation, mlock, or mprotect), - /// or if `T` requires alignment greater than the system page size. - #[inline] - pub fn new(value: T) -> Self { - Self::try_new(value).expect("failed to create protected secret") - } - - /// Creates a new `Secret`, returning an error on failure. - /// - /// # Errors - /// - /// Returns an error if: - /// - `T` requires alignment greater than the system page size - /// - Memory allocation (mmap) fails - /// - Memory locking (mlock) fails (except in test/unsafe-mlock mode) - /// - Memory protection (mprotect) fails - /// - /// # Memory Allocation Strategy - /// - /// On Linux 5.14+, this function first attempts to use `memfd_secret` which - /// provides stronger isolation (memory is unmapped from kernel direct mapping). - /// If unavailable, it falls back to regular `mmap` with `MAP_ANONYMOUS`. - pub fn try_new(value: T) -> Result { - let page_size = page_size(); - let type_align = core::mem::align_of::(); - let type_size = core::mem::size_of::(); - - // Ensure T's alignment doesn't exceed page size (mmap returns page-aligned memory) - if type_align > page_size { - return Err("type alignment exceeds page size"); - } - - // Round up to page boundary (minimum one page) - let size = type_size.max(1).next_multiple_of(page_size); - - // Try memfd_secret first on Linux (provides stronger kernel-level isolation) - // Falls back to regular mmap if memfd_secret is unavailable - #[cfg(target_os = "linux")] - let (ptr, is_memfd_secret) = try_memfd_secret(size).map_or_else( - || (try_mmap_anonymous(size), false), - |ptr| (Some(ptr), true), - ); - - #[cfg(not(target_os = "linux"))] - let ptr = try_mmap_anonymous(size); - - let Some(ptr) = ptr else { - return Err("memory allocation failed"); - }; - - // Apply madvise hints for additional protection (Linux only) - #[cfg(target_os = "linux")] - apply_madvise_hints(ptr, size, is_memfd_secret); - - let ptr = ptr as *mut T; - - // SAFETY: ptr is valid and properly aligned (mmap returns page-aligned memory, - // and we verified type_align <= page_size above) - unsafe { core::ptr::write(ptr, value) }; - - // SAFETY: ptr points to valid mmap'd memory of size `size` - // In unsafe-mlock mode (tests/benchmarks), we continue even if mlock fails. - // The memory will still be protected via mprotect, just not pinned in RAM. - if unsafe { libc::mlock(ptr as *const libc::c_void, size) } != 0 { - // SAFETY: ptr points to valid T, drop then zeroize before freeing - // ptr and size match the mmap above - #[cfg(not(any(test, feature = "unsafe-mlock")))] - unsafe { - core::ptr::drop_in_place(ptr); - super::zeroize_ptr(ptr); - libc::munmap(ptr as *mut libc::c_void, size); - return Err("mlock failed: memory limit exceeded. Try increasing with `ulimit -l` or check /etc/security/limits.conf"); - } - } - - // SAFETY: ptr points to valid memory of size `size` - if unsafe { libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_NONE) } != 0 { - // SAFETY: cleanup on failure - drop, zeroize, unlock (if locked), and unmap - unsafe { - core::ptr::drop_in_place(ptr); - super::zeroize_ptr(ptr); - libc::munlock(ptr as *const libc::c_void, size); - libc::munmap(ptr as *mut libc::c_void, size); - } - return Err("mprotect failed"); - } - - Ok(Self { - // SAFETY: ptr is non-null (mmap succeeded) - ptr: unsafe { NonNull::new_unchecked(ptr) }, - size, - readers: AtomicUsize::new(0), - }) - } - - /// Exposes the secret value for read-only access within a closure. - /// - /// Memory is re-protected when all concurrent readers have finished, - /// even if the closure panics. - /// - /// # Thread Safety - /// - /// Multiple threads can call `expose` concurrently. The memory remains - /// readable as long as at least one reader is active. - /// - /// # Note - /// - /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent - /// the returned value from containing references to the secret data. - /// This ensures the reference cannot escape the closure scope. However, - /// this does not prevent copying or cloning the secret value within - /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should - /// avoid leaking secrets through such patterns. - #[inline] - pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { - let _guard = AccessGuard::acquire( - self.ptr.as_ptr() as *mut libc::c_void, - self.size, - &self.readers, - ); - - // SAFETY: Memory is now readable and ptr is valid - let value = unsafe { self.ptr.as_ref() }; - f(value) - } - } - - impl Drop for Secret { - fn drop(&mut self) { - // SAFETY: self.ptr points to valid mmap'd memory of self.size bytes. - // We unprotect, drop inner value, zeroize, unlock, and unmap in proper sequence. - // This is safe because we have exclusive access (&mut self), no concurrent readers can exist. - unsafe { - // Only drop and zeroize if we successfully unprotected the memory. - // If mprotect failed attempting to access PROT_NONE memory would cause a segfault. - if libc::mprotect( - self.ptr.as_ptr() as *mut libc::c_void, - self.size, - libc::PROT_READ | libc::PROT_WRITE, - ) == 0 - { - core::ptr::drop_in_place(self.ptr.as_ptr()); - super::zeroize_ptr(self.ptr.as_ptr()); - } - - // Always clean up the mapping regardless of mprotect result - libc::munlock(self.ptr.as_ptr() as *const libc::c_void, self.size); - libc::munmap(self.ptr.as_ptr() as *mut libc::c_void, self.size); - } - } + /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent + /// the returned value from containing references to the secret data. + /// This ensures the reference cannot escape the closure scope. However, + /// this does not prevent copying or cloning the secret value within + /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should + /// avoid leaking secrets through such patterns. + #[inline] + pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { + // SAFETY: self.0 is always initialized (set in new, only zeroed in drop) + f(unsafe { self.0.assume_init_ref() }) } } -// Simple implementation for non-Unix platforms -#[cfg(not(unix))] -mod implementation { - use core::mem::MaybeUninit; - - /// A wrapper for secret values that prevents accidental leakage. - /// - /// Without OS-level protection (non-Unix): - /// - Debug and Display show `[REDACTED]` - /// - Zeroized on drop - /// - Access requires explicit `expose()` call - pub struct Secret(MaybeUninit); - - impl Secret { - /// Creates a new `Secret` wrapping the given value. - #[inline] - #[allow(clippy::missing_const_for_fn)] - pub fn new(value: T) -> Self { - Self(MaybeUninit::new(value)) - } - - /// Creates a new `Secret`, returning an error on failure. - /// - /// On non-Unix platforms, this always succeeds. - #[inline] - pub fn try_new(value: T) -> Result { - Ok(Self::new(value)) - } - - /// Exposes the secret value for read-only access within a closure. - /// - /// # Note - /// - /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent - /// the returned value from containing references to the secret data. - /// This ensures the reference cannot escape the closure scope. However, - /// this does not prevent copying or cloning the secret value within - /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should - /// avoid leaking secrets through such patterns. - #[inline] - pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { - // SAFETY: self.0 is always initialized (set in new, only zeroed in drop) - f(unsafe { self.0.assume_init_ref() }) - } - } - - impl Drop for Secret { - fn drop(&mut self) { - // SAFETY: self.0 is initialized and we have exclusive access. - // We drop the inner value first to run its destructor, then zeroize. - unsafe { - core::ptr::drop_in_place(self.0.as_mut_ptr()); - super::zeroize_ptr(self.0.as_mut_ptr()); - } +impl Drop for Secret { + fn drop(&mut self) { + // SAFETY: self.0 is initialized and we have exclusive access. + // We drop the inner value first to run its destructor, then zeroize. + unsafe { + core::ptr::drop_in_place(self.0.as_mut_ptr()); + zeroize_ptr(self.0.as_mut_ptr()); } } } -pub use implementation::*; - impl Debug for Secret { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_str("Secret([REDACTED])") @@ -637,8 +166,6 @@ mod tests { use commonware_math::algebra::{Additive, Random, Ring}; use core::cmp::Ordering; use rand::rngs::OsRng; - #[cfg(unix)] - use std::{panic, sync::Arc, thread}; #[test] fn test_debug_redacted() { @@ -718,36 +245,6 @@ mod tests { }); } - #[cfg(unix)] - #[test] - fn test_with_bls_scalar() { - let scalar = Scalar::random(&mut OsRng); - let secret = Secret::new(scalar); - - secret.expose(|v| { - let _ = format!("{:?}", *v); - }); - } - - #[cfg(unix)] - #[test] - fn test_expose_reprotects_on_panic() { - let secret = Secret::new([42u8; 32]); - - // Panic inside expose - memory should still be re-protected - let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { - secret.expose(|_v| { - panic!("intentional panic"); - }); - })); - assert!(result.is_err()); - - // Should be able to expose again (memory was re-protected) - secret.expose(|v| { - assert_eq!(v[0], 42); - }); - } - #[test] fn test_partial_ord() { let s1 = Secret::new([1u8, 2]); @@ -795,93 +292,4 @@ mod tests { let cmp2 = s_zero.cmp(&s_one); assert_eq!(cmp1, cmp2); } - - #[cfg(unix)] - #[test] - fn test_concurrent_expose() { - let secret = Arc::new(Secret::new([42u8; 32])); - let mut handles = vec![]; - - // Spawn multiple threads that concurrently expose the secret - for _ in 0..10 { - let secret = Arc::clone(&secret); - handles.push(thread::spawn(move || { - for _ in 0..100 { - secret.expose(|v| { - // Verify the value is correct - assert_eq!(v[0], 42); - assert_eq!(v[31], 42); - }); - } - })); - } - - // Wait for all threads to complete - for handle in handles { - handle.join().expect("thread panicked"); - } - - // Verify the secret is still accessible after concurrent access - secret.expose(|v| { - assert_eq!(v, &[42u8; 32]); - }); - } - - /// Test fork behavior on Linux. - /// - /// The behavior depends on the allocation method: - /// - memfd_secret (MAP_SHARED): Child inherits the secret (0xDE) - this is expected - /// since memfd_secret's protection is against kernel access, not fork inheritance - /// - mmap anonymous (MAP_PRIVATE + WIPEONFORK): Child sees zeroed memory (0x00) - /// - /// Both outcomes are valid - memfd_secret provides stronger kernel isolation, - /// while WIPEONFORK provides fork isolation. - #[cfg(target_os = "linux")] - #[test] - fn test_fork_behavior() { - use std::{ - io::{Read, Write}, - os::unix::net::UnixStream, - }; - - let secret = Secret::new([0xDEu8; 32]); - secret.expose(|v| assert_eq!(v[0], 0xDE)); - - let (mut parent_sock, mut child_sock) = UnixStream::pair().unwrap(); - - // SAFETY: fork is safe, we handle both parent and child cases - let pid = unsafe { libc::fork() }; - - if pid == 0 { - // Child process - drop(parent_sock); - let result = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| secret.expose(|v| v[0]))); - let byte = result.unwrap_or(0xFF); - child_sock.write_all(&[byte]).unwrap(); - std::process::exit(0); - } else { - // Parent process - drop(child_sock); - let mut status = 0; - // SAFETY: pid is valid from fork, status is valid pointer - unsafe { libc::waitpid(pid, &mut status, 0) }; - - let mut buf = [0u8; 1]; - parent_sock.read_exact(&mut buf).unwrap(); - - // Valid outcomes: - // - 0xDE: memfd_secret was used (child inherits via MAP_SHARED) - // - 0x00: mmap was used with WIPEONFORK (child sees zeroed memory) - // - 0xFF: access failed in child - assert!( - buf[0] == 0xDE || buf[0] == 0x00 || buf[0] == 0xFF, - "Unexpected value in child: {:#x}", - buf[0] - ); - - // Parent's secret must be unchanged regardless of allocation method - secret.expose(|v| assert_eq!(v[0], 0xDE)); - } - } } From f0645c08c306ed101e73b9ed337d6eec26187fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 17:18:06 +0000 Subject: [PATCH 44/65] [cryptography/secret] use ManuallyDrop instead of MaybeUninit --- cryptography/src/secret.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index d6fb5dac69..5820c76714 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -16,7 +16,7 @@ use crate::bls12381::primitives::group::Scalar; use core::{ cmp::Ordering, fmt::{Debug, Display, Formatter}, - mem::MaybeUninit, + mem::ManuallyDrop, }; use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -49,7 +49,7 @@ fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { /// /// # Safety /// -/// `ptr` must point to valid, writable memory of at least `size_of::()` bytes. +/// `ptr` must point to allocated, writable memory of at least `size_of::()` bytes. #[inline] unsafe fn zeroize_ptr(ptr: *mut T) { let slice = core::slice::from_raw_parts_mut(ptr as *mut u8, core::mem::size_of::()); @@ -61,14 +61,14 @@ unsafe fn zeroize_ptr(ptr: *mut T) { /// - Debug and Display show `[REDACTED]` /// - Zeroized on drop /// - Access requires explicit `expose()` call -pub struct Secret(MaybeUninit); +pub struct Secret(ManuallyDrop); impl Secret { /// Creates a new `Secret` wrapping the given value. #[inline] #[allow(clippy::missing_const_for_fn)] pub fn new(value: T) -> Self { - Self(MaybeUninit::new(value)) + Self(ManuallyDrop::new(value)) } /// Exposes the secret value for read-only access within a closure. @@ -83,18 +83,20 @@ impl Secret { /// avoid leaking secrets through such patterns. #[inline] pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { - // SAFETY: self.0 is always initialized (set in new, only zeroed in drop) - f(unsafe { self.0.assume_init_ref() }) + f(&self.0) } } impl Drop for Secret { fn drop(&mut self) { - // SAFETY: self.0 is initialized and we have exclusive access. - // We drop the inner value first to run its destructor, then zeroize. + let ptr = &mut *self.0 as *mut T; + // SAFETY: + // - Pointer obtained while self.0 is still initialized + // - ManuallyDrop::drop: self.0 is initialized and we have exclusive access + // - zeroize_ptr: uses raw pointer (not reference) to zero memory after drop unsafe { - core::ptr::drop_in_place(self.0.as_mut_ptr()); - zeroize_ptr(self.0.as_mut_ptr()); + ManuallyDrop::drop(&mut self.0); + zeroize_ptr(ptr); } } } From 4fc2fe48c5121eab716979014033333087421830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 18:53:23 +0000 Subject: [PATCH 45/65] [cryptography] add Secret::expose_unwrap --- cryptography/src/bls12381/dkg.rs | 8 ++++---- cryptography/src/secret.rs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index b2ef59ac16..5d36e3505e 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -1180,7 +1180,7 @@ impl Dealer { // `Poly::new_with_constant` requires an owned value. The extracted scalar is // scoped to this function and will be zeroized on drop (i.e. the secret is // only exposed for the duration of this function). - share.map(|x| x.private.expose(|private| private.clone())), + share.map(|x| x.private.expose_unwrap()), )?; let my_poly = Poly::new_with_constant(&mut rng, info.degree(), share); let priv_msgs = info @@ -1490,7 +1490,7 @@ impl Player { let share = self .view .get(dealer) - .map(|(_, priv_msg)| priv_msg.share.expose(|share| share.clone())) + .map(|(_, priv_msg)| priv_msg.share.clone().expose_unwrap()) .unwrap_or_else(|| { log.get_reveal(&self.me_pub).map_or_else( || { @@ -1498,7 +1498,7 @@ impl Player { "select didn't check dealer reveal, or we're not a player?" ) }, - |priv_msg| priv_msg.share.expose(|share| share.clone()), + |priv_msg| priv_msg.share.clone().expose_unwrap(), ) }); (dealer.clone(), share) @@ -1944,7 +1944,7 @@ mod test_plan { let share = info .unwrap_or_random_share( &mut rng, - share.map(|s| s.private.expose(|private| private.clone())), + share.map(|s| s.private.expose_unwrap()), ) .expect("Failed to generate dealer share"); diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 5820c76714..9f53571f38 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -85,6 +85,22 @@ impl Secret { pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { f(&self.0) } + + /// Consumes the [Secret] and returns the inner value, zeroizing the original + /// memory location. + /// + /// Use this when you need to transfer ownership of the secret value (e.g., + /// for APIs that consume the value). + #[inline] + pub fn expose_unwrap(mut self) -> T { + // SAFETY: self.0 is initialized + let value = unsafe { ManuallyDrop::take(&mut self.0) }; + // SAFETY: self.0 memory is still allocated, just logically moved-from + unsafe { zeroize_ptr(&mut self.0 as *mut _ as *mut T) }; + // Prevent Secret::drop from running (would double-zeroize) + core::mem::forget(self); + value + } } impl Drop for Secret { @@ -189,6 +205,13 @@ mod tests { }); } + #[test] + fn test_expose_unwrap() { + let secret = Secret::new([1u8, 2, 3, 4]); + let value = secret.expose_unwrap(); + assert_eq!(value, [1u8, 2, 3, 4]); + } + #[test] fn test_clone() { let secret = Secret::new([1u8, 2, 3, 4]); From c372cb1778bd62695923a30a71a45ec999834934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 18:56:26 +0000 Subject: [PATCH 46/65] [cryptography/handshake] use Secret --- cryptography/src/handshake.rs | 12 +++++--- cryptography/src/handshake/cipher.rs | 24 ++++++---------- cryptography/src/handshake/key_exchange.rs | 32 +++++++++++----------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/cryptography/src/handshake.rs b/cryptography/src/handshake.rs index fc4505672e..9e9bd26831 100644 --- a/cryptography/src/handshake.rs +++ b/cryptography/src/handshake.rs @@ -297,10 +297,12 @@ pub fn dial_end( { return Err(Error::HandshakeFailed); } - let Some(secret) = esk.exchange(&msg.epk) else { + let Some(shared) = esk.exchange(&msg.epk) else { return Err(Error::HandshakeFailed); }; - transcript.commit(secret.as_ref()); + shared + .secret + .expose(|secret| transcript.commit(secret.as_ref())); let recv = RecvCipher::new(transcript.noise(LABEL_CIPHER_L2D)); let send = SendCipher::new(transcript.noise(LABEL_CIPHER_D2L)); let confirmation_l2d = transcript.fork(LABEL_CONFIRMATION_L2D).summarize(); @@ -350,10 +352,12 @@ pub fn listen_start( .commit(current_time.encode()) .commit(epk.encode()) .sign(&my_identity); - let Some(secret) = esk.exchange(&msg.epk) else { + let Some(shared) = esk.exchange(&msg.epk) else { return Err(Error::HandshakeFailed); }; - transcript.commit(secret.as_ref()); + shared + .secret + .expose(|secret| transcript.commit(secret.as_ref())); let send = SendCipher::new(transcript.noise(LABEL_CIPHER_L2D)); let recv = RecvCipher::new(transcript.noise(LABEL_CIPHER_D2L)); let confirmation_l2d = transcript.fork(LABEL_CONFIRMATION_L2D).summarize(); diff --git a/cryptography/src/handshake/cipher.rs b/cryptography/src/handshake/cipher.rs index 5bd87db1e0..27e6bbfc1a 100644 --- a/cryptography/src/handshake/cipher.rs +++ b/cryptography/src/handshake/cipher.rs @@ -1,12 +1,12 @@ // Intentionally avoid depending directly on super, to depend on the sibling. use super::error::Error; +use crate::Secret; use chacha20poly1305::{ aead::{generic_array::typenum::Unsigned, Aead}, AeadCore, ChaCha20Poly1305, KeyInit as _, }; use rand_core::CryptoRngCore; use std::vec::Vec; -use zeroize::ZeroizeOnDrop; /// The amount of overhead in a ciphertext, compared to the plain message. pub const CIPHERTEXT_OVERHEAD: usize = ::TagSize::USIZE; @@ -18,9 +18,6 @@ struct CounterNonce { inner: u128, } -// We don't need to zeroize nonces. -impl ZeroizeOnDrop for CounterNonce {} - impl CounterNonce { /// Creates a new counter nonce starting at zero. pub const fn new() -> Self { @@ -39,10 +36,9 @@ impl CounterNonce { } } -#[derive(ZeroizeOnDrop)] pub struct SendCipher { nonce: CounterNonce, - inner: ChaCha20Poly1305, + inner: Secret, } impl SendCipher { @@ -52,22 +48,22 @@ impl SendCipher { rng.fill_bytes(&mut key[..]); Self { nonce: CounterNonce::new(), - inner: ChaCha20Poly1305::new(&key.into()), + inner: Secret::new(ChaCha20Poly1305::new(&key.into())), } } /// Encrypts data and returns the ciphertext. pub fn send(&mut self, data: &[u8]) -> Result, Error> { + let nonce = self.nonce.inc()?; self.inner - .encrypt((&self.nonce.inc()?[..NONCE_SIZE_BYTES]).into(), data) + .expose(|cipher| cipher.encrypt((&nonce[..NONCE_SIZE_BYTES]).into(), data)) .map_err(|_| Error::EncryptionFailed) } } -#[derive(ZeroizeOnDrop)] pub struct RecvCipher { nonce: CounterNonce, - inner: ChaCha20Poly1305, + inner: Secret, } impl RecvCipher { @@ -77,17 +73,15 @@ impl RecvCipher { rng.fill_bytes(&mut key[..]); Self { nonce: CounterNonce::new(), - inner: ChaCha20Poly1305::new(&key.into()), + inner: Secret::new(ChaCha20Poly1305::new(&key.into())), } } /// Decrypts ciphertext and returns the original data. pub fn recv(&mut self, encrypted_data: &[u8]) -> Result, Error> { + let nonce = self.nonce.inc()?; self.inner - .decrypt( - (&self.nonce.inc()?[..NONCE_SIZE_BYTES]).into(), - encrypted_data, - ) + .expose(|cipher| cipher.decrypt((&nonce[..NONCE_SIZE_BYTES]).into(), encrypted_data)) .map_err(|_| Error::DecryptionFailed) } } diff --git a/cryptography/src/handshake/key_exchange.rs b/cryptography/src/handshake/key_exchange.rs index bbc753bca4..2ed02b2a70 100644 --- a/cryptography/src/handshake/key_exchange.rs +++ b/cryptography/src/handshake/key_exchange.rs @@ -1,16 +1,18 @@ +use crate::Secret; use commonware_codec::{FixedSize, Read, ReadExt, Write}; use rand_core::CryptoRngCore; -use zeroize::ZeroizeOnDrop; /// A shared secret derived from X25519 key exchange. -#[derive(ZeroizeOnDrop)] pub struct SharedSecret { - inner: x25519_dalek::SharedSecret, + pub secret: Secret, } -impl AsRef<[u8]> for SharedSecret { - fn as_ref(&self) -> &[u8] { - self.inner.as_bytes().as_slice() +impl SharedSecret { + /// Creates a new SharedSecret wrapping the given x25519 shared secret. + fn new(secret: x25519_dalek::SharedSecret) -> Self { + Self { + secret: Secret::new(secret), + } } } @@ -55,37 +57,35 @@ impl<'a> arbitrary::Arbitrary<'a> for EphemeralPublicKey { } } -// I would implement `ZeroizeOnDrop`, but this seemingly fails because of a line -// below, where the secret must be consumed. /// An ephemeral X25519 secret key used during handshake. pub struct SecretKey { - inner: x25519_dalek::EphemeralSecret, + inner: Secret, } impl SecretKey { /// Generates a new random ephemeral secret key. pub fn new(rng: impl CryptoRngCore) -> Self { Self { - inner: x25519_dalek::EphemeralSecret::random_from_rng(rng), + inner: Secret::new(x25519_dalek::EphemeralSecret::random_from_rng(rng)), } } /// Derives the corresponding public key. pub fn public(&self) -> EphemeralPublicKey { - EphemeralPublicKey { - inner: (&self.inner).into(), - } + self.inner.expose(|secret| EphemeralPublicKey { + inner: x25519_dalek::PublicKey::from(secret), + }) } /// Performs X25519 key exchange with another public key. /// Returns None if the exchange is non-contributory. pub fn exchange(self, other: &EphemeralPublicKey) -> Option { - // This is the line mentioned above preventing `ZeroizeOnDrop` for this struct. - let out = self.inner.diffie_hellman(&other.inner); + let secret = self.inner.expose_unwrap(); + let out = secret.diffie_hellman(&other.inner); if !out.was_contributory() { return None; } - Some(SharedSecret { inner: out }) + Some(SharedSecret::new(out)) } } From f3b2eaacc96b9c80548e88e1c388d5addec8c38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 22:56:22 +0000 Subject: [PATCH 47/65] [cryptography] don't implement Ord for Secret --- cryptography/src/bls12381/primitives/group.rs | 13 +-- cryptography/src/bls12381/scheme.rs | 12 -- cryptography/src/ed25519/scheme.rs | 12 -- cryptography/src/secp256r1/common.rs | 24 ---- cryptography/src/secret.rs | 109 +----------------- 5 files changed, 4 insertions(+), 166 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 0d86ac9db1..2714a2c241 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -493,7 +493,7 @@ impl Random for Scalar { } /// A share of a threshold signing key. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Share { /// The share's index in the polynomial. pub index: u32, @@ -1141,7 +1141,7 @@ mod tests { use commonware_math::algebra::test_suites; use commonware_utils::test_rng; use proptest::prelude::*; - use std::collections::{BTreeMap, BTreeSet, HashMap}; + use std::collections::{BTreeSet, HashMap}; impl Arbitrary for Scalar { type Parameters = (); @@ -1455,8 +1455,6 @@ mod tests { let mut scalar_set = BTreeSet::new(); let mut g1_set = BTreeSet::new(); let mut g2_set = BTreeSet::new(); - #[allow(clippy::mutable_key_type)] - let mut share_set = BTreeSet::new(); while scalar_set.len() < NUM_ITEMS { let scalar = Scalar::random(&mut rng); let g1 = G1::generator() * &scalar; @@ -1466,14 +1464,12 @@ mod tests { scalar_set.insert(scalar); g1_set.insert(g1); g2_set.insert(g2); - share_set.insert(share); } // Verify that the sets contain the expected number of unique items. assert_eq!(scalar_set.len(), NUM_ITEMS); assert_eq!(g1_set.len(), NUM_ITEMS); assert_eq!(g2_set.len(), NUM_ITEMS); - assert_eq!(share_set.len(), NUM_ITEMS); // Verify that `BTreeSet` iteration is sorted, which relies on `Ord`. let scalars: Vec<_> = scalar_set.iter().collect(); @@ -1482,21 +1478,16 @@ mod tests { assert!(g1s.windows(2).all(|w| w[0] <= w[1])); let g2s: Vec<_> = g2_set.iter().collect(); assert!(g2s.windows(2).all(|w| w[0] <= w[1])); - let shares: Vec<_> = share_set.iter().collect(); - assert!(shares.windows(2).all(|w| w[0] <= w[1])); // Test that we can use these types as keys in hash maps, which relies on `Hash` and `Eq`. let scalar_map: HashMap<_, _> = scalar_set.iter().cloned().zip(0..).collect(); let g1_map: HashMap<_, _> = g1_set.iter().cloned().zip(0..).collect(); let g2_map: HashMap<_, _> = g2_set.iter().cloned().zip(0..).collect(); - #[allow(clippy::mutable_key_type)] - let share_map: BTreeMap<_, _> = share_set.iter().cloned().zip(0..).collect(); // Verify that the maps contain the expected number of unique items. assert_eq!(scalar_map.len(), NUM_ITEMS); assert_eq!(g1_map.len(), NUM_ITEMS); assert_eq!(g2_map.len(), NUM_ITEMS); - assert_eq!(share_map.len(), NUM_ITEMS); } #[test] diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 6270b39b77..65b3cd90a9 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -87,18 +87,6 @@ impl FixedSize for PrivateKey { const SIZE: usize = group::PRIVATE_KEY_LENGTH; } -impl Ord for PrivateKey { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.cmp(&other.raw) - } -} - -impl PartialOrd for PrivateKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for PrivateKey { fn from(key: Scalar) -> Self { let raw = key.encode_fixed(); diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 424b63060b..6936ba50d9 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -103,18 +103,6 @@ impl PartialEq for PrivateKey { } } -impl Ord for PrivateKey { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.cmp(&other.raw) - } -} - -impl PartialOrd for PrivateKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for PrivateKey { fn from(key: ed25519_consensus::SigningKey) -> Self { let raw = key.to_bytes(); diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index d8820f47d0..77c5c322a3 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -76,18 +76,6 @@ impl FixedSize for PrivateKeyInner { const SIZE: usize = PRIVATE_KEY_LENGTH; } -impl Ord for PrivateKeyInner { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.raw.cmp(&other.raw) - } -} - -impl PartialOrd for PrivateKeyInner { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for PrivateKeyInner { fn from(signer: SigningKey) -> Self { Self::new(signer) @@ -244,18 +232,6 @@ macro_rules! impl_private_key_wrapper { const SIZE: usize = PRIVATE_KEY_LENGTH; } - impl Ord for $name { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.cmp(&other.0) - } - } - - impl PartialOrd for $name { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - impl From for $name { fn from(signer: p256::ecdsa::SigningKey) -> Self { Self(PrivateKeyInner::from(signer)) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 9f53571f38..c97d68e27f 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -14,37 +14,12 @@ use crate::bls12381::primitives::group::Scalar; use core::{ - cmp::Ordering, fmt::{Debug, Display, Formatter}, mem::ManuallyDrop, }; -use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; +use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; -/// Constant-time lexicographic comparison for equal-length byte slices. -/// -/// # Panics -/// -/// Panics if `a` and `b` have different lengths. -#[inline] -fn ct_cmp_bytes(a: &[u8], b: &[u8]) -> Ordering { - assert_eq!(a.len(), b.len()); - - let mut result = 0; - for (&x, &y) in a.iter().zip(b.iter()) { - let is_eq = result.ct_eq(&0); - result = u8::conditional_select(&result, &1, is_eq & x.ct_lt(&y)); - result = u8::conditional_select(&result, &2, is_eq & y.ct_lt(&x)); - } - - match result { - 0 => Ordering::Equal, - 1 => Ordering::Less, - 2 => Ordering::Greater, - _ => unreachable!(), - } -} - /// Zeroize memory at the given pointer using volatile writes. /// /// # Safety @@ -145,18 +120,6 @@ impl PartialEq for Secret<[u8; N]> { impl Eq for Secret<[u8; N]> {} -impl PartialOrd for Secret<[u8; N]> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Secret<[u8; N]> { - fn cmp(&self, other: &Self) -> Ordering { - self.expose(|a| other.expose(|b| ct_cmp_bytes(a, b))) - } -} - impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { self.expose(|a| other.expose(|b| a.as_slice().ct_eq(&b.as_slice()).into())) @@ -165,24 +128,11 @@ impl PartialEq for Secret { impl Eq for Secret {} -impl PartialOrd for Secret { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Secret { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.expose(|a| other.expose(|b| ct_cmp_bytes(&a.as_slice(), &b.as_slice()))) - } -} - #[cfg(test)] mod tests { use super::*; use crate::bls12381::primitives::group::Scalar; - use commonware_math::algebra::{Additive, Random, Ring}; - use core::cmp::Ordering; + use commonware_math::algebra::Random; use rand::rngs::OsRng; #[test] @@ -232,29 +182,6 @@ mod tests { assert_ne!(s1, s3); } - #[test] - fn test_ordering() { - // Test the specific bug case: [2, 1] vs [1, 2] - let a = Secret::new([2u8, 1]); - let b = Secret::new([1u8, 2]); - assert_eq!(a.cmp(&b), Ordering::Greater); // [2, 1] > [1, 2] lexicographically - - // Additional ordering tests - let c = Secret::new([1u8, 1]); - let d = Secret::new([1u8, 2]); - assert_eq!(c.cmp(&d), Ordering::Less); - - let e = Secret::new([1u8, 2]); - let f = Secret::new([1u8, 2]); - assert_eq!(e.cmp(&f), Ordering::Equal); - - // Single byte - let g = Secret::new([0u8]); - let h = Secret::new([255u8]); - assert_eq!(g.cmp(&h), Ordering::Less); - assert_eq!(h.cmp(&g), Ordering::Greater); - } - #[test] fn test_multiple_expose() { let secret = Secret::new([42u8; 32]); @@ -270,22 +197,6 @@ mod tests { }); } - #[test] - fn test_partial_ord() { - let s1 = Secret::new([1u8, 2]); - let s2 = Secret::new([1u8, 3]); - let s3 = Secret::new([1u8, 2]); - - assert!(s1 < s2); - assert!(s2 > s1); - assert!(s1 <= s3); - assert!(s1 >= s3); - - assert_eq!(s1.partial_cmp(&s2), Some(core::cmp::Ordering::Less)); - assert_eq!(s2.partial_cmp(&s1), Some(core::cmp::Ordering::Greater)); - assert_eq!(s1.partial_cmp(&s3), Some(core::cmp::Ordering::Equal)); - } - #[test] fn test_scalar_equality() { let scalar1 = Scalar::random(&mut OsRng); @@ -301,20 +212,4 @@ mod tests { // Different scalars should (very likely) be different assert_ne!(s1, s3); } - - #[test] - fn test_scalar_ordering() { - let zero = Scalar::zero(); - let one = Scalar::one(); - - let s_zero = Secret::new(zero); - let s_one = Secret::new(one); - - // Zero and one should compare consistently - assert_ne!(s_zero, s_one); - // Ordering should be deterministic - let cmp1 = s_zero.cmp(&s_one); - let cmp2 = s_zero.cmp(&s_one); - assert_eq!(cmp1, cmp2); - } } From 3df3c939d0e673a5a0a802ff03b4a9d76d9759a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 22:56:53 +0000 Subject: [PATCH 48/65] [cryptography/secret] docs --- cryptography/src/secret.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index c97d68e27f..bdd48c7bbc 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -8,9 +8,11 @@ //! //! # Type Constraints //! -//! `Secret` only provides full protection for self-contained types (no heap -//! pointers). Types like `Vec` or `String` will only have their stack -//! metadata zeroized, not heap data. +//! **Important**: `Secret` is designed for flat data types without pointers +//! (e.g. `[u8; N]`). It does NOT provide full protection for types with +//! indirection. Types like `Vec`, `String`, or `Box` will only have their +//! metadata (pointer, length, capacity) zeroized, the referenced data remains +//! intact. Do not use `Secret` with types that contain pointers. use crate::bls12381::primitives::group::Scalar; use core::{ @@ -36,6 +38,11 @@ unsafe fn zeroize_ptr(ptr: *mut T) { /// - Debug and Display show `[REDACTED]` /// - Zeroized on drop /// - Access requires explicit `expose()` call +/// +/// # Type Constraints +/// +/// Only use with flat data types that have no pointers (e.g. `[u8; N]`). +/// See [module-level documentation](self) for details. pub struct Secret(ManuallyDrop); impl Secret { From 72e4dcce919bc6ee5420315f4c65125585c976ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Mon, 5 Jan 2026 23:03:58 +0000 Subject: [PATCH 49/65] [cryptography] lint --- cryptography/src/bls12381/primitives/group.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 2714a2c241..bd0951d4c9 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -1459,7 +1459,6 @@ mod tests { let scalar = Scalar::random(&mut rng); let g1 = G1::generator() * &scalar; let g2 = G2::generator() * &scalar; - let share = Share::new(scalar_set.len() as u32, scalar.clone()); scalar_set.insert(scalar); g1_set.insert(g1); From b62aef9d7908d6bd758ea9b5bfaf417dc8b4277f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 11:25:52 +0000 Subject: [PATCH 50/65] [cryptography/secret] use raw mut --- cryptography/src/secret.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index bdd48c7bbc..d2346ddd03 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -78,7 +78,7 @@ impl Secret { // SAFETY: self.0 is initialized let value = unsafe { ManuallyDrop::take(&mut self.0) }; // SAFETY: self.0 memory is still allocated, just logically moved-from - unsafe { zeroize_ptr(&mut self.0 as *mut _ as *mut T) }; + unsafe { zeroize_ptr(&raw mut *self.0) }; // Prevent Secret::drop from running (would double-zeroize) core::mem::forget(self); value @@ -87,7 +87,7 @@ impl Secret { impl Drop for Secret { fn drop(&mut self) { - let ptr = &mut *self.0 as *mut T; + let ptr = &raw mut *self.0; // SAFETY: // - Pointer obtained while self.0 is still initialized // - ManuallyDrop::drop: self.0 is initialized and we have exclusive access From 5e9895c81ac1dd713ed63916594cc3702d881211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 11:30:07 +0000 Subject: [PATCH 51/65] [cryptography/secret] use test_rng --- cryptography/src/secret.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index d2346ddd03..881f48e58e 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -140,7 +140,7 @@ mod tests { use super::*; use crate::bls12381::primitives::group::Scalar; use commonware_math::algebra::Random; - use rand::rngs::OsRng; + use commonware_utils::test_rng; #[test] fn test_debug_redacted() { @@ -206,9 +206,10 @@ mod tests { #[test] fn test_scalar_equality() { - let scalar1 = Scalar::random(&mut OsRng); + let mut rng = test_rng(); + let scalar1 = Scalar::random(&mut rng); let scalar2 = scalar1.clone(); - let scalar3 = Scalar::random(&mut OsRng); + let scalar3 = Scalar::random(&mut rng); let s1 = Secret::new(scalar1); let s2 = Secret::new(scalar2); From c85923be29fe7acb8d26a0b05fa41fa9cdef20c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 11:37:41 +0000 Subject: [PATCH 52/65] [cryptography/secret] take ptr before ManuallyDrop::take --- cryptography/src/secret.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 881f48e58e..a3b97a93f1 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -75,10 +75,16 @@ impl Secret { /// for APIs that consume the value). #[inline] pub fn expose_unwrap(mut self) -> T { - // SAFETY: self.0 is initialized - let value = unsafe { ManuallyDrop::take(&mut self.0) }; - // SAFETY: self.0 memory is still allocated, just logically moved-from - unsafe { zeroize_ptr(&raw mut *self.0) }; + let ptr = &raw mut *self.0; + // SAFETY: + // - Pointer obtained while self.0 is still initialized + // - ManuallyDrop::take: self.0 is initialized and we have exclusive access + // - zeroize_ptr: uses raw pointer (not reference) to zero memory after drop + let value = unsafe { + let value = ManuallyDrop::take(&mut self.0); + zeroize_ptr(ptr); + value + }; // Prevent Secret::drop from running (would double-zeroize) core::mem::forget(self); value From fb3b30e65973d27d39a27bd984d0f5f37ebcca3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 14:15:21 +0000 Subject: [PATCH 53/65] [cryptography/secret] zeroize scalar slices --- cryptography/src/secret.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index a3b97a93f1..eb3f78a21b 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -20,7 +20,7 @@ use core::{ mem::ManuallyDrop, }; use subtle::ConstantTimeEq; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; /// Zeroize memory at the given pointer using volatile writes. /// @@ -135,7 +135,13 @@ impl Eq for Secret<[u8; N]> {} impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { - self.expose(|a| other.expose(|b| a.as_slice().ct_eq(&b.as_slice()).into())) + self.expose(|a| { + other.expose(|b| { + let a = Zeroizing::new(a.as_slice()); + let b = Zeroizing::new(b.as_slice()); + a.ct_eq(&*b).into() + }) + }) } } From 3263f2d634f8cfdf78d39e387ba69da1196b4fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 14:15:35 +0000 Subject: [PATCH 54/65] [cryptography/secret] const new --- cryptography/src/bls12381/dkg.rs | 2 +- cryptography/src/bls12381/primitives/group.rs | 2 +- cryptography/src/handshake/key_exchange.rs | 2 +- cryptography/src/secret.rs | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 5d36e3505e..1c2136ef75 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -688,7 +688,7 @@ pub struct DealerPrivMsg { impl DealerPrivMsg { /// Creates a new `DealerPrivMsg` with the given share. - pub fn new(share: Scalar) -> Self { + pub const fn new(share: Scalar) -> Self { Self { share: Secret::new(share), } diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index bd0951d4c9..21ab247f17 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -503,7 +503,7 @@ pub struct Share { impl Share { /// Creates a new `Share` with the given index and private key. - pub fn new(index: u32, private: Private) -> Self { + pub const fn new(index: u32, private: Private) -> Self { Self { index, private: Secret::new(private), diff --git a/cryptography/src/handshake/key_exchange.rs b/cryptography/src/handshake/key_exchange.rs index 2ed02b2a70..380d75b54f 100644 --- a/cryptography/src/handshake/key_exchange.rs +++ b/cryptography/src/handshake/key_exchange.rs @@ -9,7 +9,7 @@ pub struct SharedSecret { impl SharedSecret { /// Creates a new SharedSecret wrapping the given x25519 shared secret. - fn new(secret: x25519_dalek::SharedSecret) -> Self { + const fn new(secret: x25519_dalek::SharedSecret) -> Self { Self { secret: Secret::new(secret), } diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index eb3f78a21b..11fd93b14f 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -48,8 +48,7 @@ pub struct Secret(ManuallyDrop); impl Secret { /// Creates a new `Secret` wrapping the given value. #[inline] - #[allow(clippy::missing_const_for_fn)] - pub fn new(value: T) -> Self { + pub const fn new(value: T) -> Self { Self(ManuallyDrop::new(value)) } From dac480c208530c2853fe77e1b26b7d46eb9e32be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 14:46:46 +0000 Subject: [PATCH 55/65] [cryptography/secret] move eq for Secret this allows avoiding the call to as_slice() which could leak on the stack --- cryptography/src/bls12381/primitives/group.rs | 31 +++++++++++++++- cryptography/src/secret.rs | 37 +------------------ 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 21ab247f17..43feaee11f 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -46,6 +46,7 @@ use core::{ ptr, }; use rand_core::CryptoRngCore; +use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; /// Domain separation tag used when hashing a message to a curve (G1 or G2). @@ -286,7 +287,7 @@ impl Scalar { } /// Encodes the scalar into a byte array. - pub(crate) fn as_slice(&self) -> [u8; Self::SIZE] { + fn as_slice(&self) -> [u8; Self::SIZE] { let mut slice = [0u8; Self::SIZE]; // SAFETY: All pointers valid; blst_bendian_from_scalar writes exactly 32 bytes. unsafe { @@ -349,6 +350,14 @@ impl Hash for Scalar { } } +impl PartialEq for Secret { + fn eq(&self, other: &Self) -> bool { + self.expose(|a| other.expose(|b| a.0.l.ct_eq(&b.0.l)).into()) + } +} + +impl Eq for Secret {} + impl PartialOrd for Scalar { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -1137,8 +1146,9 @@ impl HashToGroup for G2 { #[cfg(test)] mod tests { use super::*; + use crate::bls12381::primitives::group::Scalar; use commonware_codec::{DecodeExt, Encode}; - use commonware_math::algebra::test_suites; + use commonware_math::algebra::{test_suites, Random}; use commonware_utils::test_rng; use proptest::prelude::*; use std::collections::{BTreeSet, HashMap}; @@ -1540,6 +1550,23 @@ mod tests { ); } + #[test] + fn test_secret_scalar_equality() { + let mut rng = test_rng(); + let scalar1 = Scalar::random(&mut rng); + let scalar2 = scalar1.clone(); + let scalar3 = Scalar::random(&mut rng); + + let s1 = Secret::new(scalar1); + let s2 = Secret::new(scalar2); + let s3 = Secret::new(scalar3); + + // Same scalar should be equal + assert_eq!(s1, s2); + // Different scalars should (very likely) be different + assert_ne!(s1, s3); + } + #[cfg(feature = "arbitrary")] mod conformance { use super::*; diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 11fd93b14f..0a9dbcba4d 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -14,13 +14,12 @@ //! metadata (pointer, length, capacity) zeroized, the referenced data remains //! intact. Do not use `Secret` with types that contain pointers. -use crate::bls12381::primitives::group::Scalar; use core::{ fmt::{Debug, Display, Formatter}, mem::ManuallyDrop, }; use subtle::ConstantTimeEq; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +use zeroize::{Zeroize, ZeroizeOnDrop}; /// Zeroize memory at the given pointer using volatile writes. /// @@ -132,26 +131,9 @@ impl PartialEq for Secret<[u8; N]> { impl Eq for Secret<[u8; N]> {} -impl PartialEq for Secret { - fn eq(&self, other: &Self) -> bool { - self.expose(|a| { - other.expose(|b| { - let a = Zeroizing::new(a.as_slice()); - let b = Zeroizing::new(b.as_slice()); - a.ct_eq(&*b).into() - }) - }) - } -} - -impl Eq for Secret {} - #[cfg(test)] mod tests { use super::*; - use crate::bls12381::primitives::group::Scalar; - use commonware_math::algebra::Random; - use commonware_utils::test_rng; #[test] fn test_debug_redacted() { @@ -214,21 +196,4 @@ mod tests { assert_eq!(v[31], 42); }); } - - #[test] - fn test_scalar_equality() { - let mut rng = test_rng(); - let scalar1 = Scalar::random(&mut rng); - let scalar2 = scalar1.clone(); - let scalar3 = Scalar::random(&mut rng); - - let s1 = Secret::new(scalar1); - let s2 = Secret::new(scalar2); - let s3 = Secret::new(scalar3); - - // Same scalar should be equal - assert_eq!(s1, s2); - // Different scalars should (very likely) be different - assert_ne!(s1, s3); - } } From 884714ad7565f536c8109d47a56c47eeb6d2c00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 14:47:28 +0000 Subject: [PATCH 56/65] [cryptography/secret] mention caller responsibility of wiping temporaries --- cryptography/src/secret.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index 0a9dbcba4d..bd0f408e45 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -61,6 +61,11 @@ impl Secret { /// this does not prevent copying or cloning the secret value within /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should /// avoid leaking secrets through such patterns. + /// + /// Additionally, any temporaries derived from the secret (e.g. + /// `s.as_slice()`) may leave secret data on the stack that will not be + /// automatically zeroized. Callers should wrap such temporaries in + /// [`zeroize::Zeroizing`] if they contain sensitive data. #[inline] pub fn expose(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R { f(&self.0) From 8d7a1237c755cf8670ef749f917b1e302d96cc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 15:22:51 +0000 Subject: [PATCH 57/65] [cryptography/handshake] nit --- cryptography/src/handshake/key_exchange.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/src/handshake/key_exchange.rs b/cryptography/src/handshake/key_exchange.rs index 380d75b54f..12e8804090 100644 --- a/cryptography/src/handshake/key_exchange.rs +++ b/cryptography/src/handshake/key_exchange.rs @@ -4,7 +4,7 @@ use rand_core::CryptoRngCore; /// A shared secret derived from X25519 key exchange. pub struct SharedSecret { - pub secret: Secret, + pub(crate) secret: Secret, } impl SharedSecret { From 09fca293f98590125e664672d5cc825e07112f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 15:24:01 +0000 Subject: [PATCH 58/65] [cryptography/secret] zeroize after forget --- cryptography/src/secret.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index bd0f408e45..f3f469a96a 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -80,16 +80,16 @@ impl Secret { pub fn expose_unwrap(mut self) -> T { let ptr = &raw mut *self.0; // SAFETY: - // - Pointer obtained while self.0 is still initialized - // - ManuallyDrop::take: self.0 is initialized and we have exclusive access - // - zeroize_ptr: uses raw pointer (not reference) to zero memory after drop - let value = unsafe { - let value = ManuallyDrop::take(&mut self.0); - zeroize_ptr(ptr); - value - }; - // Prevent Secret::drop from running (would double-zeroize) + // Pointer obtained while self.0 is still initialized, + // self.0 is initialized and we have exclusive access + let value = unsafe { ManuallyDrop::take(&mut self.0) }; + + // Prevent Secret::drop from running (would double-zeroize or double-free on panic) core::mem::forget(self); + + // SAFETY: uses raw pointer (not reference) to zero memory after drop + unsafe { zeroize_ptr(ptr) }; + value } } From 09091f88e8ffe5d8277abca0dadfba4e2232280c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 15:33:05 +0000 Subject: [PATCH 59/65] [cryptography] use ctutils instead of subtle --- Cargo.lock | 17 ++++++++++++++++- Cargo.toml | 2 +- cryptography/Cargo.toml | 3 +-- cryptography/src/bls12381/primitives/group.rs | 10 ++++------ cryptography/src/secret.rs | 6 +++--- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c9a658ae8..2730e60128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11ed919bd3bae4af5ab56372b627dfc32622aba6cec36906e8ab46746037c9d" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1172,6 +1178,7 @@ dependencies = [ "commonware-math", "commonware-utils", "criterion", + "ctutils", "ecdsa", "ed25519-consensus", "getrandom 0.2.16", @@ -1183,7 +1190,6 @@ dependencies = [ "rayon", "rstest", "sha2 0.10.9", - "subtle", "thiserror 2.0.17", "x25519-dalek", "zeroize", @@ -1879,6 +1885,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctutils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c67c81499f542d1dd38c6a2a2fe825f4dd4bca5162965dd2eea0c8119873d3c" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" diff --git a/Cargo.toml b/Cargo.toml index 5444bda87f..0c2453df17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,7 @@ console-subscriber = "0.5.0" crc32fast = "1.5.0" criterion = "0.7.0" crossterm = "0.29.0" +ctutils = "0.3.1" ecdsa = { version = "0.16.9", default-features = false } ed25519-consensus = { version = "2.1.0", default-features = false } ed25519-zebra = "4.1.0" @@ -140,7 +141,6 @@ serde = "1.0.218" serde_json = "1.0.122" serde_yaml = "0.9.34" sha2 = { version = "0.10.8", default-features = false } -subtle = { version = "2.6.1", default-features = false } syn = "2.0.0" sysinfo = "0.37.2" thiserror = { version = "2.0.12", default-features = false } diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index bd0329b2b5..2bd3a38807 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -24,6 +24,7 @@ chacha20poly1305 = { workspace = true, default-features = false, features = ["al commonware-codec.workspace = true commonware-math.workspace = true commonware-utils.workspace = true +ctutils.workspace = true ecdsa.workspace = true ed25519-consensus = { workspace = true, default-features = false } p256 = { workspace = true, features = ["ecdsa"] } @@ -32,7 +33,6 @@ rand_chacha.workspace = true rand_core.workspace = true rayon = { workspace = true, optional = true } sha2.workspace = true -subtle.workspace = true thiserror.workspace = true x25519-dalek = { workspace = true, features = ["zeroize"] } zeroize = { workspace = true, features = ["zeroize_derive"] } @@ -89,7 +89,6 @@ std = [ "rand_core/std", "rayon", "sha2/std", - "subtle/std", "thiserror/std", "zeroize/std", ] diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 43feaee11f..41d117df71 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -45,8 +45,8 @@ use core::{ ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, ptr, }; +use ctutils::CtEq; use rand_core::CryptoRngCore; -use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; /// Domain separation tag used when hashing a message to a curve (G1 or G2). @@ -350,14 +350,12 @@ impl Hash for Scalar { } } -impl PartialEq for Secret { - fn eq(&self, other: &Self) -> bool { - self.expose(|a| other.expose(|b| a.0.l.ct_eq(&b.0.l)).into()) +impl CtEq for Scalar { + fn ct_eq(&self, other: &Self) -> ctutils::Choice { + self.0.l.ct_eq(&other.0.l) } } -impl Eq for Secret {} - impl PartialOrd for Scalar { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/cryptography/src/secret.rs b/cryptography/src/secret.rs index f3f469a96a..1bc0d97b4a 100644 --- a/cryptography/src/secret.rs +++ b/cryptography/src/secret.rs @@ -18,7 +18,7 @@ use core::{ fmt::{Debug, Display, Formatter}, mem::ManuallyDrop, }; -use subtle::ConstantTimeEq; +use ctutils::CtEq; use zeroize::{Zeroize, ZeroizeOnDrop}; /// Zeroize memory at the given pointer using volatile writes. @@ -128,13 +128,13 @@ impl Clone for Secret { } } -impl PartialEq for Secret<[u8; N]> { +impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { self.expose(|a| other.expose(|b| a.ct_eq(b).into())) } } -impl Eq for Secret<[u8; N]> {} +impl Eq for Secret {} #[cfg(test)] mod tests { From 1da908606b15b6f9cfe46f620a917b6ea4d4fe4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 16:34:46 +0000 Subject: [PATCH 60/65] [cryptography] increase test coverage --- cryptography/src/bls12381/dkg.rs | 10 ++++++++ cryptography/src/bls12381/primitives/group.rs | 10 ++++++++ cryptography/src/bls12381/scheme.rs | 25 ++++++++++++++++++- cryptography/src/ed25519/scheme.rs | 25 ++++++++++++++++++- cryptography/src/secp256r1/common.rs | 9 +++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/cryptography/src/bls12381/dkg.rs b/cryptography/src/bls12381/dkg.rs index 1c2136ef75..46941eec09 100644 --- a/cryptography/src/bls12381/dkg.rs +++ b/cryptography/src/bls12381/dkg.rs @@ -2395,6 +2395,8 @@ mod test { use super::{test_plan::*, *}; use crate::{bls12381::primitives::variant::MinPk, ed25519}; use anyhow::anyhow; + use commonware_math::algebra::Random; + use commonware_utils::test_rng; use core::num::NonZeroI32; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; @@ -2570,6 +2572,14 @@ mod test { Ok(()) } + #[test] + fn test_dealer_priv_msg_redacted() { + let mut rng = test_rng(); + let msg = DealerPrivMsg::new(Scalar::random(&mut rng)); + let debug = format!("{:?}", msg); + assert!(debug.contains("REDACTED")); + } + #[cfg(feature = "arbitrary")] mod conformance { use super::*; diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 41d117df71..6cf627eeb3 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -1565,6 +1565,16 @@ mod tests { assert_ne!(s1, s3); } + #[test] + fn test_share_redacted() { + let mut rng = test_rng(); + let share = Share::new(1, Scalar::random(&mut rng)); + let debug = format!("{:?}", share); + let display = format!("{}", share); + assert!(debug.contains("REDACTED")); + assert!(display.contains("REDACTED")); + } + #[cfg(feature = "arbitrary")] mod conformance { use super::*; diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 65b3cd90a9..18e8437d98 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -403,8 +403,10 @@ impl BatchVerifier for Batch { #[cfg(test)] mod tests { use super::*; - use crate::bls12381; + use crate::{bls12381, Signer as _, Verifier as _}; use commonware_codec::{DecodeExt, Encode}; + use commonware_math::algebra::Random; + use commonware_utils::test_rng; #[test] fn test_codec_private_key() { @@ -463,6 +465,27 @@ mod tests { ) } + #[test] + fn test_from_scalar() { + let mut rng = test_rng(); + let scalar = Scalar::random(&mut rng); + let private_key = PrivateKey::from(scalar); + // Verify the key works by signing and verifying + let msg = b"test message"; + let sig = private_key.sign(b"ns", msg); + assert!(private_key.public_key().verify(b"ns", msg, &sig)); + } + + #[test] + fn test_private_key_redacted() { + let mut rng = test_rng(); + let private_key = PrivateKey::random(&mut rng); + let debug = format!("{:?}", private_key); + let display = format!("{}", private_key); + assert!(debug.contains("REDACTED")); + assert!(display.contains("REDACTED")); + } + #[cfg(feature = "arbitrary")] mod conformance { use super::*; diff --git a/cryptography/src/ed25519/scheme.rs b/cryptography/src/ed25519/scheme.rs index 6936ba50d9..422c806211 100644 --- a/cryptography/src/ed25519/scheme.rs +++ b/cryptography/src/ed25519/scheme.rs @@ -404,7 +404,7 @@ impl Batch { #[cfg(test)] mod tests { use super::*; - use crate::ed25519; + use crate::{ed25519, Signer as _}; use commonware_codec::{DecodeExt, Encode}; use commonware_math::algebra::Random; use commonware_utils::test_rng; @@ -800,6 +800,29 @@ mod tests { assert!(!public_key.verify_inner(None, &message, &bad_signature)); } + #[test] + fn test_from_signing_key() { + let signing_key = ed25519_consensus::SigningKey::new(OsRng); + let expected_public = signing_key.verification_key(); + let private_key = PrivateKey::from(signing_key); + assert_eq!(private_key.public_key().key, expected_public); + } + + #[test] + fn test_private_key_redacted() { + let private_key = PrivateKey::random(&mut OsRng); + let debug = format!("{:?}", private_key); + let display = format!("{}", private_key); + assert!(debug.contains("REDACTED")); + assert!(display.contains("REDACTED")); + } + + #[test] + fn test_from_private_key_to_public_key() { + let private_key = PrivateKey::random(&mut OsRng); + assert_eq!(private_key.public_key(), PublicKey::from(private_key)); + } + #[cfg(feature = "arbitrary")] mod conformance { use super::*; diff --git a/cryptography/src/secp256r1/common.rs b/cryptography/src/secp256r1/common.rs index 77c5c322a3..532cc3940f 100644 --- a/cryptography/src/secp256r1/common.rs +++ b/cryptography/src/secp256r1/common.rs @@ -799,6 +799,15 @@ pub(crate) mod tests { (public_key, sig, message, true) } + #[test] + fn test_private_key_redacted() { + let private_key = create_private_key(); + let debug = format!("{:?}", private_key); + let display = format!("{}", private_key); + assert!(debug.contains("REDACTED")); + assert!(display.contains("REDACTED")); + } + #[cfg(feature = "arbitrary")] mod conformance { use super::*; From b2bf52eb3f27d06dcb3020b549b5422c0f0e56d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 6 Jan 2026 16:42:32 +0000 Subject: [PATCH 61/65] [cryptography] lint --- cryptography/src/bls12381/scheme.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/src/bls12381/scheme.rs b/cryptography/src/bls12381/scheme.rs index 18e8437d98..d8b695b479 100644 --- a/cryptography/src/bls12381/scheme.rs +++ b/cryptography/src/bls12381/scheme.rs @@ -403,7 +403,7 @@ impl BatchVerifier for Batch { #[cfg(test)] mod tests { use super::*; - use crate::{bls12381, Signer as _, Verifier as _}; + use crate::{bls12381, Verifier as _}; use commonware_codec::{DecodeExt, Encode}; use commonware_math::algebra::Random; use commonware_utils::test_rng; From 13ab2e9848185f92a2898f02ce9674e40b7f50d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Wed, 7 Jan 2026 10:11:14 +0000 Subject: [PATCH 62/65] [cryptography/cipher] use ChaCha20Poly1305::generate_key --- cryptography/src/handshake/cipher.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cryptography/src/handshake/cipher.rs b/cryptography/src/handshake/cipher.rs index 27e6bbfc1a..889cecb2a8 100644 --- a/cryptography/src/handshake/cipher.rs +++ b/cryptography/src/handshake/cipher.rs @@ -44,11 +44,11 @@ pub struct SendCipher { impl SendCipher { /// Creates a new sending cipher with a random key. pub fn new(mut rng: impl CryptoRngCore) -> Self { - let mut key = [0u8; 32]; - rng.fill_bytes(&mut key[..]); Self { nonce: CounterNonce::new(), - inner: Secret::new(ChaCha20Poly1305::new(&key.into())), + inner: Secret::new(ChaCha20Poly1305::new(&ChaCha20Poly1305::generate_key( + &mut rng, + ))), } } @@ -69,11 +69,11 @@ pub struct RecvCipher { impl RecvCipher { /// Creates a new receiving cipher with a random key. pub fn new(mut rng: impl CryptoRngCore) -> Self { - let mut key = [0u8; 32]; - rng.fill_bytes(&mut key[..]); Self { nonce: CounterNonce::new(), - inner: Secret::new(ChaCha20Poly1305::new(&key.into())), + inner: Secret::new(ChaCha20Poly1305::new(&ChaCha20Poly1305::generate_key( + &mut rng, + ))), } } From 4eca663d2205c33540a71f254d1fca0f0bd4e88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Wed, 7 Jan 2026 10:14:22 +0000 Subject: [PATCH 63/65] [cryptography/bls12381] avoid stack when creating Scalar from bytes --- cryptography/src/bls12381/primitives/group.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index 80949d0f10..db7e345c64 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -217,7 +217,9 @@ pub const PRIVATE_KEY_LENGTH: usize = SCALAR_LENGTH; impl Scalar { /// Generate a non-zero scalar from the randomly populated buffer. - fn from_bytes(mut ikm: [u8; 64]) -> Self { + /// + /// The buffer is zeroized after use. + fn from_bytes(ikm: &mut [u8; 64]) -> Self { let mut sc = blst_scalar::default(); let mut ret = blst_fr::default(); // SAFETY: ikm is a valid 64-byte buffer; blst_keygen handles null key_info. @@ -227,7 +229,7 @@ impl Scalar { blst_fr_from_scalar(&mut ret, &sc); } - // Zeroize the ikm buffer + // Zeroize the ikm buffer in place ikm.zeroize(); Self(ret) @@ -496,7 +498,7 @@ impl Random for Scalar { fn random(mut rng: impl CryptoRngCore) -> Self { let mut ikm = [0u8; 64]; rng.fill_bytes(&mut ikm); - Self::from_bytes(ikm) + Self::from_bytes(&mut ikm) } } @@ -1158,7 +1160,9 @@ mod tests { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - any::<[u8; 64]>().prop_map(Self::from_bytes).boxed() + any::<[u8; 64]>() + .prop_map(|mut b| Self::from_bytes(&mut b)) + .boxed() } } From 2d44b98b3f224fc5de1c78d037b8d002b2d48f36 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 7 Jan 2026 02:26:21 -0800 Subject: [PATCH 64/65] Cleanup from_bytes() --- cryptography/src/bls12381/primitives/group.rs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index db7e345c64..a259bf96f9 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -216,24 +216,6 @@ pub type Private = Scalar; pub const PRIVATE_KEY_LENGTH: usize = SCALAR_LENGTH; impl Scalar { - /// Generate a non-zero scalar from the randomly populated buffer. - /// - /// The buffer is zeroized after use. - fn from_bytes(ikm: &mut [u8; 64]) -> Self { - let mut sc = blst_scalar::default(); - let mut ret = blst_fr::default(); - // SAFETY: ikm is a valid 64-byte buffer; blst_keygen handles null key_info. - unsafe { - // blst_keygen loops until a non-zero value is produced (in accordance with IETF BLS KeyGen 4+). - blst_keygen(&mut sc, ikm.as_ptr(), ikm.len(), ptr::null(), 0); - blst_fr_from_scalar(&mut ret, &sc); - } - - // Zeroize the ikm buffer in place - ikm.zeroize(); - - Self(ret) - } /// Maps arbitrary bytes to a scalar using RFC9380 hash-to-field. pub fn map(dst: DST, msg: &[u8]) -> Self { @@ -498,7 +480,20 @@ impl Random for Scalar { fn random(mut rng: impl CryptoRngCore) -> Self { let mut ikm = [0u8; 64]; rng.fill_bytes(&mut ikm); - Self::from_bytes(&mut ikm) + + let mut sc = blst_scalar::default(); + let mut ret = blst_fr::default(); + // SAFETY: ikm is a valid 64-byte buffer; blst_keygen handles null key_info. + unsafe { + // blst_keygen loops until a non-zero value is produced (in accordance with IETF BLS KeyGen 4+). + blst_keygen(&mut sc, ikm.as_ptr(), ikm.len(), ptr::null(), 0); + blst_fr_from_scalar(&mut ret, &sc); + } + + // Zeroize the ikm buffer + ikm.zeroize(); + + Self(ret) } } @@ -1153,6 +1148,7 @@ mod tests { use commonware_parallel::Sequential; use commonware_utils::test_rng; use proptest::{prelude::*, strategy::Strategy}; + use rand::{rngs::StdRng, SeedableRng}; use std::collections::{BTreeSet, HashMap}; impl Arbitrary for Scalar { @@ -1160,8 +1156,8 @@ mod tests { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - any::<[u8; 64]>() - .prop_map(|mut b| Self::from_bytes(&mut b)) + any::<[u8; 32]>() + .prop_map(|seed| Self::random(&mut StdRng::from_seed(seed))) .boxed() } } From 99b8b430cbe24b8e659b2ea280b0bc93c6c796a5 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Wed, 7 Jan 2026 02:27:55 -0800 Subject: [PATCH 65/65] fmt --- cryptography/src/bls12381/primitives/group.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs index a259bf96f9..c9dfbaeea5 100644 --- a/cryptography/src/bls12381/primitives/group.rs +++ b/cryptography/src/bls12381/primitives/group.rs @@ -216,7 +216,6 @@ pub type Private = Scalar; pub const PRIVATE_KEY_LENGTH: usize = SCALAR_LENGTH; impl Scalar { - /// Maps arbitrary bytes to a scalar using RFC9380 hash-to-field. pub fn map(dst: DST, msg: &[u8]) -> Self { // The BLS12-381 scalar field has a modulus of approximately 255 bits.