Skip to content
This repository was archived by the owner on Aug 7, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- Fixed SMT leaf advice decoding by rebuilding decoded entries through `SmtLeaf::new`, so decoded entries must match the supplied leaf index ([#1076](https://github.com/0xMiden/crypto/pull/1076)).
- Made `Felt::from_{u8, u16, u32}` const and added `Felt::MAX` ([#1081](https://github.com/0xMiden/crypto/pull/1081)).

- Added a zeroizing read helper for deserializing sensitive material, fixing secret-key read buffers that were not wiped on error paths (ECDSA) or at all (Falcon, Poseidon2 AEAD) ([#1057](https://github.com/0xMiden/crypto/pull/1057)).
- Made Falcon secret polynomial temporaries wipeable and wiped: added `Zeroize` for `FalconFelt`, removed the unbacked `ZeroizeOnDrop` marker on `Polynomial`, and zeroized the secret-carrying temporaries in `SecretKey` deserialization, serialization, and seed generation ([#1061](https://github.com/0xMiden/crypto/pull/1061)).

## 0.27.0 (2026-06-19)

- [BREAKING] Upgraded the RustCrypto and dalek stack: `der`, `hkdf`, `sha2`, `sha3`, `k256`, `curve25519-dalek`, `ed25519-dalek`, and `x25519-dalek` ([#1045](https://github.com/0xMiden/crypto/pull/1045)).
Expand Down
22 changes: 15 additions & 7 deletions miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use crate::{
ecdh::k256::{EphemeralPublicKey, SharedSecret},
utils::{
ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
read_sensitive_array, zeroize::ZeroizeOnDrop,
read_sensitive_array,
zeroize::{ZeroizeOnDrop, Zeroizing},
},
};

Expand Down Expand Up @@ -96,20 +97,25 @@ impl SecretKey {
// which ensures that the secret key material is securely zeroized when dropped.
impl ZeroizeOnDrop for SecretKey {}

#[cfg(test)]
impl PartialEq for SecretKey {
fn eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq;
self.to_bytes().ct_eq(&other.to_bytes()).into()
let self_bytes = Zeroizing::new(self.inner.to_bytes());
let other_bytes = Zeroizing::new(other.inner.to_bytes());
self_bytes[..].ct_eq(&other_bytes[..]).into()
}
}

#[cfg(test)]
impl Eq for SecretKey {}

// SIGNING KEY
// ================================================================================================

/// A secret key for ECDSA signature verification over the secp256k1 curve.
#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq
#[derive(Clone, SilentDebug, SilentDisplay)]
#[cfg_attr(test, derive(Eq, PartialEq))] // Safe as SecretKey has const-time eq in tests
pub struct SigningKey(SecretKey);

impl SigningKey {
Expand Down Expand Up @@ -170,7 +176,8 @@ impl Deserializable for SigningKey {
// ================================================================================================

/// A secret key for ECDH key-exchange over the secp256k1 curve.
#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq
#[derive(Clone, SilentDebug, SilentDisplay)]
#[cfg_attr(test, derive(Eq, PartialEq))] // Safe as SecretKey has const-time eq in tests
pub struct KeyExchangeKey(SecretKey);

impl KeyExchangeKey {
Expand Down Expand Up @@ -448,9 +455,10 @@ impl Signature {

impl Serializable for SecretKey {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
let mut buffer = Vec::with_capacity(SECRET_KEY_BYTES);
let sk_bytes: [u8; SECRET_KEY_BYTES] = self.inner.to_bytes().into();
buffer.extend_from_slice(&sk_bytes);
let mut buffer = Zeroizing::new(Vec::with_capacity(SECRET_KEY_BYTES));
let sk_bytes: Zeroizing<[u8; SECRET_KEY_BYTES]> =
Zeroizing::new(self.inner.to_bytes().into());
buffer.extend_from_slice(&sk_bytes[..]);

target.write_bytes(&buffer);
}
Expand Down
17 changes: 12 additions & 5 deletions miden-crypto/src/dsa/eddsa_25519_sha512/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
utils::{
ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
bytes_to_packed_u32_elements, read_sensitive_array,
zeroize::{Zeroize, ZeroizeOnDrop},
zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing},
},
};

Expand Down Expand Up @@ -95,20 +95,25 @@ impl SecretKey {
// which ensures that the secret key material is securely zeroized when dropped.
impl ZeroizeOnDrop for SecretKey {}

#[cfg(test)]
impl PartialEq for SecretKey {
fn eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq;
self.inner.to_bytes().ct_eq(&other.inner.to_bytes()).into()
let self_bytes = Zeroizing::new(self.inner.to_bytes());
let other_bytes = Zeroizing::new(other.inner.to_bytes());
self_bytes[..].ct_eq(&other_bytes[..]).into()
}
}

#[cfg(test)]
impl Eq for SecretKey {}

// SIGNING KEY
// ================================================================================================

/// A secret key for EdDSA (Ed25519) signature verification over Curve25519.
#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq
#[derive(Clone, SilentDebug, SilentDisplay)]
#[cfg_attr(test, derive(Eq, PartialEq))] // Safe as SecretKey has const-time eq in tests
pub struct SigningKey(SecretKey);

impl SigningKey {
Expand Down Expand Up @@ -164,7 +169,8 @@ impl Deserializable for SigningKey {
// ================================================================================================

/// A key for ECDH key exchange over Curve25519
#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq
#[derive(Clone, SilentDebug, SilentDisplay)]
#[cfg_attr(test, derive(Eq, PartialEq))] // Safe as SecretKey has const-time eq in tests
pub struct KeyExchangeKey(SecretKey);

impl KeyExchangeKey {
Expand Down Expand Up @@ -509,7 +515,8 @@ impl Signature {

impl Serializable for SecretKey {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_bytes(&self.inner.to_bytes());
let bytes = Zeroizing::new(self.inner.to_bytes());
target.write_bytes(&bytes[..]);
}
}

Expand Down
116 changes: 73 additions & 43 deletions miden-crypto/src/dsa/falcon512_poseidon2/keys/secret_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,16 @@ impl SecretKey {

/// Derives the public key corresponding to this secret key using h = g /f [mod ϕ][mod p].
fn compute_pub_key_poly(&self) -> PublicKey {
let g: Polynomial<FalconFelt> = self.secret_key[0].clone().into();
let g_fft = g.fft();
let minus_f: Polynomial<FalconFelt> = self.secret_key[1].clone().into();
let f = -minus_f;
let f_fft = f.fft();
let h_fft = g_fft.hadamard_div(&f_fft);
let g: Zeroizing<Polynomial<FalconFelt>> =
Zeroizing::new(self.secret_key[0].clone().into());
let g_fft = Zeroizing::new(g.fft());
let minus_f: Zeroizing<Polynomial<FalconFelt>> =
Zeroizing::new(self.secret_key[1].clone().into());
let f = Zeroizing::new(-(&*minus_f));
let f_fft = Zeroizing::new(f.fft());
let f_fft_inv = Zeroizing::new(f_fft.hadamard_inv());
// h = g / f is the public key, so the result needs no wiping.
let h_fft = g_fft.hadamard_mul(&f_fft_inv);
h_fft.ifft().into()
}

Expand Down Expand Up @@ -270,7 +274,9 @@ impl SecretKey {
fn generate_seed(&self, message: &Word) -> [u8; 32] {
let mut buffer = Vec::with_capacity(1 + SK_LEN + Word::SERIALIZED_SIZE);
buffer.push(LOG_N);
buffer.extend_from_slice(&self.to_bytes());
// Bind the serialized key so the temporary holding it is wiped, not just `buffer`.
let sk_bytes = Zeroizing::new(self.to_bytes());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handles the generate_seed copy, but the same full-key serialization still happens in PartialEq just below: self.to_bytes().ct_eq(&other.to_bytes()).

Both calls allocate encoded secret keys and drop them without Zeroizing, so comparing two Falcon secret keys can still leave the same kind of buffer this PR is trying to wipe. Could those two serialized values be bound with Zeroizing too, or compared without serializing?

@Jr-kenny Jr-kenny Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that path slips the wipe too. PartialEq serializes both keys and drops them in the clear on every compare. Bound both to_bytes() in Zeroizing in 53a0f03 so they get wiped after the compare instead. ct_eq still runs over the bound bytes, so the comparison is unchanged and stays constant time.

Swept the rest while I was in there: this was the last unwiped to_bytes() path in Falcon, write_into and generate_seed were already covered. The same pattern showed up in the sibling secret keys though. The k256 ECDSA and ed25519 EdDSA SecretKeys are both ZeroizeOnDrop and their PartialEq had the identical un-wiped to_bytes().ct_eq(), so I folded the same fix into those two as well. Falcon, ecdsa and eddsa suites all pass locally.

buffer.extend_from_slice(&sk_bytes);
buffer.extend_from_slice(&message.to_bytes());

let digest = Blake3_256::hash(&buffer);
Expand All @@ -282,13 +288,17 @@ impl SecretKey {
}
}

#[cfg(test)]
impl PartialEq for SecretKey {
fn eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq;
self.to_bytes().ct_eq(&other.to_bytes()).into()
let self_bytes = Zeroizing::new(self.to_bytes());
let other_bytes = Zeroizing::new(other.to_bytes());
self_bytes.ct_eq(&other_bytes).into()
}
}

#[cfg(test)]
impl Eq for SecretKey {}

// SERIALIZATION / DESERIALIZATION
Expand All @@ -307,39 +317,42 @@ impl Serializable for SecretKey {
let g = &basis[0];
let neg_big_f = &basis[3];

let mut buffer = Vec::with_capacity(1281);
let mut buffer = Zeroizing::new(Vec::with_capacity(1281));
buffer.push(header);

let mut f_i8: Vec<i8> = neg_f
.coefficients
.iter()
.map(|&a| FalconFelt::new(-a).balanced_value() as i8)
.collect();
let f_i8_encoded = encode_i8(&f_i8, WIDTH_SMALL_POLY_COEFFICIENT).unwrap();
let f_i8: Zeroizing<Vec<i8>> = Zeroizing::new(
neg_f
.coefficients
.iter()
.map(|&a| FalconFelt::new(-a).balanced_value() as i8)
.collect(),
);
let f_i8_encoded = Zeroizing::new(encode_i8(&f_i8, WIDTH_SMALL_POLY_COEFFICIENT).unwrap());
buffer.extend_from_slice(&f_i8_encoded);
f_i8.zeroize();

let mut g_i8: Vec<i8> = g
.coefficients
.iter()
.map(|&a| FalconFelt::new(a).balanced_value() as i8)
.collect();
let g_i8_encoded = encode_i8(&g_i8, WIDTH_SMALL_POLY_COEFFICIENT).unwrap();

let g_i8: Zeroizing<Vec<i8>> = Zeroizing::new(
g.coefficients
.iter()
.map(|&a| FalconFelt::new(a).balanced_value() as i8)
.collect(),
);
let g_i8_encoded = Zeroizing::new(encode_i8(&g_i8, WIDTH_SMALL_POLY_COEFFICIENT).unwrap());
buffer.extend_from_slice(&g_i8_encoded);
g_i8.zeroize();

let mut big_f_i8: Vec<i8> = neg_big_f
.coefficients
.iter()
.map(|&a| FalconFelt::new(-a).balanced_value() as i8)
.collect();
let big_f_i8_encoded = encode_i8(&big_f_i8, WIDTH_BIG_POLY_COEFFICIENT).unwrap();

let big_f_i8: Zeroizing<Vec<i8>> = Zeroizing::new(
neg_big_f
.coefficients
.iter()
.map(|&a| FalconFelt::new(-a).balanced_value() as i8)
.collect(),
);
let big_f_i8_encoded =
Zeroizing::new(encode_i8(&big_f_i8, WIDTH_BIG_POLY_COEFFICIENT).unwrap());
buffer.extend_from_slice(&big_f_i8_encoded);
big_f_i8.zeroize();

// `write_bytes` only borrows the buffer, so it is wiped here on drop; the target
// owns its copy of the encoded key and is responsible for it.
target.write_bytes(&buffer);
// Note: buffer is not zeroized here as it's being passed to write_bytes which consumes it
// The caller should ensure proper handling of the written bytes
}
}

Expand Down Expand Up @@ -391,17 +404,34 @@ impl Deserializable for SecretKey {
.unwrap(),
);

let f = Polynomial::new(f.iter().map(|&c| FalconFelt::new(c.into())).collect());
let g = Polynomial::new(g.iter().map(|&c| FalconFelt::new(c.into())).collect());
let big_f = Polynomial::new(big_f.iter().map(|&c| FalconFelt::new(c.into())).collect());

// big_g * f - g * big_f = p (mod X^n + 1)
let big_g = g.fft().hadamard_div(&f.fft()).hadamard_mul(&big_f.fft()).ifft();
let f =
Zeroizing::new(Polynomial::new(f.iter().map(|&c| FalconFelt::new(c.into())).collect()));
let g =
Zeroizing::new(Polynomial::new(g.iter().map(|&c| FalconFelt::new(c.into())).collect()));
let big_f = Zeroizing::new(Polynomial::new(
big_f.iter().map(|&c| FalconFelt::new(c.into())).collect(),
));

// big_g * f - g * big_f = p (mod X^n + 1). Each FFT-domain step is bound in
// `Zeroizing` so every secret-carrying intermediate is wiped, including the
// inverse that `hadamard_div` would otherwise allocate out of reach.
let f_fft = Zeroizing::new(f.fft());
let g_fft = Zeroizing::new(g.fft());
let big_f_fft = Zeroizing::new(big_f.fft());
let f_fft_inv = Zeroizing::new(f_fft.hadamard_inv());
let quotient = Zeroizing::new(g_fft.hadamard_mul(&f_fft_inv));
let big_g_fft = Zeroizing::new(quotient.hadamard_mul(&big_f_fft));
let big_g = Zeroizing::new(big_g_fft.ifft());

// Negate through references so the un-negated temporaries stay wrapped and wiped;
// `-Polynomial` by value would drop them intact.
let f_balanced = Zeroizing::new(Polynomial::new(f.to_balanced_values()));
let big_f_balanced = Zeroizing::new(Polynomial::new(big_f.to_balanced_values()));
let basis = [
Polynomial::new(g.to_balanced_values()),
-Polynomial::new(f.to_balanced_values()),
-(&*f_balanced),
Polynomial::new(big_g.to_balanced_values()),
-Polynomial::new(big_f.to_balanced_values()),
-(&*big_f_balanced),
];
Ok(Self::from_short_lattice_basis(basis))
}
Expand Down Expand Up @@ -460,7 +490,7 @@ pub fn encode_i8(x: &[i8], bits: usize) -> Option<Vec<u8>> {
/// Decodes a sequence of bytes into a sequence of signed integers such that each integer x
/// satisfies |x| < 2^(bits-1) for a given parameter bits. bits can take either the value 6 or 8.
pub fn decode_i8(buf: &[u8], bits: usize) -> Option<Vec<i8>> {
let mut x = [0_i8; N];
let mut x = Zeroizing::new([0_i8; N]);

let mut i = 0;
let mut j = 0;
Expand Down
7 changes: 7 additions & 0 deletions miden-crypto/src/dsa/falcon512_poseidon2/math/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAss
use num::{One, Zero};

use super::{Inverse, MODULUS, fft::CyclotomicFourier};
use crate::utils::zeroize::Zeroize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FalconFelt(u32);
Expand Down Expand Up @@ -33,6 +34,12 @@ impl FalconFelt {
}
}

impl Zeroize for FalconFelt {
fn zeroize(&mut self) {
self.0.zeroize();
}
}

impl Add for FalconFelt {
type Output = Self;

Expand Down
Loading
Loading