From ef214a65b0c8e77a6daf9ddf4b3f32dfe1c76efd Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:42:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=B2=84=ED=8D=BC=20z?= =?UTF-8?q?eroize=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 출력 슬라이스를 받는 _out 계열 함수 진입부에서 out fill(0) 선-초기화 - 오버사이즈 버퍼 뒤쪽이나 조기 반환 시 남는 이전 데이터 노출 방지 - digest 출력 버퍼 뒤쪽까지 0이 되도록 sha3 테스트 1건 갱신 - 행 단위 부분 기록 함수는 다른 행을 지우므로 제외 --- crypto/base64/src/lib.rs | 2 ++ crypto/factory/src/hash_factory.rs | 4 ++++ crypto/factory/src/mac_factory.rs | 4 ++++ crypto/factory/src/rng_factory.rs | 2 ++ crypto/factory/src/xof_factory.rs | 6 ++++++ crypto/hex/src/lib.rs | 4 ++++ crypto/hmac/src/lib.rs | 6 ++++++ crypto/mldsa/src/aux_functions.rs | 2 ++ crypto/mldsa/src/hash_mldsa.rs | 12 ++++++++++++ crypto/mldsa/src/mldsa.rs | 14 ++++++++++++++ crypto/mldsa/src/mldsa_keys.rs | 10 ++++++++++ crypto/mldsa_lowmemory/src/aux_functions.rs | 2 ++ crypto/mldsa_lowmemory/src/hash_mldsa.rs | 8 ++++++++ crypto/mldsa_lowmemory/src/mldsa.rs | 8 ++++++++ crypto/mldsa_lowmemory/src/mldsa_keys.rs | 2 ++ crypto/mlkem/src/matrix.rs | 2 ++ crypto/mlkem/src/mlkem_keys.rs | 10 +++++++++- crypto/mlkem/src/polynomial.rs | 2 ++ crypto/mlkem_lowmemory/src/mlkem_keys.rs | 4 ++++ crypto/mlkem_lowmemory/src/polynomial.rs | 2 ++ crypto/rng/src/hash_drbg80090a.rs | 8 ++++++++ crypto/sha2/src/sha256.rs | 4 ++++ crypto/sha2/src/sha512.rs | 4 ++++ crypto/sha3/src/keccak.rs | 2 ++ crypto/sha3/src/sha3.rs | 8 ++++++++ crypto/sha3/src/shake.rs | 10 +++++++++- crypto/sha3/tests/sha3_tests.rs | 7 +++++-- crypto/utils/src/ct.rs | 2 ++ 28 files changed, 147 insertions(+), 4 deletions(-) diff --git a/crypto/base64/src/lib.rs b/crypto/base64/src/lib.rs index 8b7cfeb..90330d0 100644 --- a/crypto/base64/src/lib.rs +++ b/crypto/base64/src/lib.rs @@ -193,6 +193,8 @@ impl Base64Encoder { assert!(inref.len() >= 3); assert!(out.len() >= 4); + out.fill(0); + out[0] = Self::ct_bin_to_b64(inref[0] >> 2); out[1] = Self::ct_bin_to_b64(((inref[0] & 0x03) << 4) | inref[1] >> 4); out[2] = Self::ct_bin_to_b64(((inref[1] & 0x0F) << 2) | inref[2] >> 6); diff --git a/crypto/factory/src/hash_factory.rs b/crypto/factory/src/hash_factory.rs index d65586f..9d12117 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -129,6 +129,8 @@ impl Hash for HashFactory { } fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + match self { Self::SHA224(h) => h.hash_out(data, output), Self::SHA256(h) => h.hash_out(data, output), @@ -168,6 +170,8 @@ impl Hash for HashFactory { } fn do_final_out(self, output: &mut [u8]) -> usize { + output.fill(0); + match self { Self::SHA224(h) => h.do_final_out(output), Self::SHA256(h) => h.do_final_out(output), diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index 64ae090..141c59f 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -175,6 +175,8 @@ impl MAC for MACFactory { } fn mac_out(self, data: &[u8], out: &mut [u8]) -> Result { + out.fill(0); + match self { Self::HMAC_SHA224(h) => h.mac_out(data, out), Self::HMAC_SHA256(h) => h.mac_out(data, out), @@ -227,6 +229,8 @@ impl MAC for MACFactory { } fn do_final_out(self, mut out: &mut [u8]) -> Result { + out.fill(0); + match self { Self::HMAC_SHA224(h) => h.do_final_out(&mut out), Self::HMAC_SHA256(h) => h.do_final_out(&mut out), diff --git a/crypto/factory/src/rng_factory.rs b/crypto/factory/src/rng_factory.rs index 89aa3e7..f1792d1 100644 --- a/crypto/factory/src/rng_factory.rs +++ b/crypto/factory/src/rng_factory.rs @@ -108,6 +108,8 @@ impl RNG for RNGFactory { } fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { + out.fill(0); + match self { Self::HashDRBG_SHA256(rng) => {rng.next_bytes_out(out) }, Self::HashDRBG_SHA512(rng) => { rng.next_bytes_out(out) }, diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index 2809a39..e35e86e 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -86,6 +86,8 @@ impl XOF for XOFFactory { } fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + match self { Self::SHAKE128(h) => h.hash_xof_out(data, output), Self::SHAKE256(h) => h.hash_xof_out(data, output), @@ -118,6 +120,8 @@ impl XOF for XOFFactory { } fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + output.fill(0); + match self { Self::SHAKE128(h) => h.squeeze_out(output), Self::SHAKE256(h) => h.squeeze_out(output), @@ -136,6 +140,8 @@ impl XOF for XOFFactory { num_bits: usize, output: &mut u8, ) -> Result<(), HashError> { + *output = 0; + match self { Self::SHAKE128(h) => h.squeeze_partial_byte_final_out(num_bits, output), Self::SHAKE256(h) => h.squeeze_partial_byte_final_out(num_bits, output), diff --git a/crypto/hex/src/lib.rs b/crypto/hex/src/lib.rs index 2e71988..6fcf787 100644 --- a/crypto/hex/src/lib.rs +++ b/crypto/hex/src/lib.rs @@ -47,6 +47,8 @@ pub fn encode_out>(input: T, out: &mut [u8]) -> Result> 4); out[2 * i + 1] = ct_word_to_hex(inref[i] & 0x0F); @@ -90,6 +92,8 @@ pub fn decode_out>(input: T, out: &mut [u8]) -> Result HMAC { )); } + out.fill(0); + // Per RFC 2104 Section 2, save our inner digest to calculate our // outer digest. Note that we can't (necessarily) reuse out as a // scratch pad here: if we're truncating the output but not @@ -378,6 +380,8 @@ impl MAC for HMAC { } fn mac_out(mut self, data: &[u8], mut out: &mut [u8]) -> Result { + out.fill(0); + self.do_update(data); self.do_final_out(&mut out) } @@ -398,6 +402,8 @@ impl MAC for HMAC { } fn do_final_out(self, mut out: &mut [u8]) -> Result { + out.fill(0); + self.do_final_internal_out(&mut out) } diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index 8303063..3164838 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -397,6 +397,8 @@ pub(crate) fn sig_encode< h: &Vector, output: &mut [u8; SIG_LEN], ) -> usize { + output.fill(0); + let mut pos = 0; output[..LAMBDA_over_4].copy_from_slice(c_tilde); diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 780bd76..52fbdc8 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -478,6 +478,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut ph_m = [0u8; PH_LEN]; _ = HASH::default().hash_out(msg, &mut ph_m); Self::sign_ph_with_expanded_key_out(sk, &ph_m, ctx, output) @@ -500,6 +502,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut rnd: [u8; MLDSA_RND_LEN] = [0u8; MLDSA_RND_LEN]; HashDRBG_SHA512::new_from_os().next_bytes_out(&mut rnd)?; Self::sign_ph_deterministic_out(&sk.sk, Some(&sk.A_hat), ctx, ph, rnd, output) @@ -556,6 +560,8 @@ impl< return Err(SignatureError::LengthError("ctx value is longer than 255 bytes")); } + output.fill(0); + // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mu = { @@ -860,6 +866,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut ph_m = [0u8; PH_LEN]; _ = HASH::default().hash_out(msg, &mut ph_m); Self::sign_ph_out(sk, &ph_m, ctx, output) @@ -898,6 +906,8 @@ impl< )); } + output.fill(0); + if self.sk.is_some() { if self.signer_rnd.is_none() { Self::sign_ph_out(&self.sk.unwrap(), &ph, Some(&self.ctx[..self.ctx_len]), output) @@ -1045,6 +1055,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut rnd: [u8; MLDSA_RND_LEN] = [0u8; MLDSA_RND_LEN]; HashDRBG_SHA512::new_from_os().next_bytes_out(&mut rnd)?; Self::sign_ph_deterministic_out(sk, None, ctx, ph, rnd, output) diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index d207f01..dd8eb0f 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -845,6 +845,8 @@ impl< rnd: [u8; 32], output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + // 1: (𝜌, 𝐾, 𝑡𝑟, 𝐬1, 𝐬2, 𝐭0) ← skDecode(𝑠𝑘) // 2: 𝐬1̂_hat ← NTT(𝐬1) // 3: 𝐬2̂_hat ← NTT(𝐬2) @@ -1134,6 +1136,8 @@ impl< ctx: Option<&[u8]>, out: &mut [u8; SIG_LEN], ) -> Result { + out.fill(0); + let mu = MuBuilder::compute_mu(&sk.tr(), msg, ctx)?; Self::sign_mu_out(&sk.sk, Some(&sk.A_hat), &mu, out) } @@ -1154,6 +1158,8 @@ impl< mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut rnd: [u8; MLDSA_RND_LEN] = [0u8; MLDSA_RND_LEN]; HashDRBG_SHA512::new_from_os().next_bytes_out(&mut rnd)?; @@ -1175,6 +1181,8 @@ impl< mu: &[u8; 64], out: &mut [u8; SIG_LEN], ) -> Result { + out.fill(0); + Self::sign_mu_out(&sk.sk, A_hat, mu, out) } @@ -1196,6 +1204,8 @@ impl< rnd: [u8; 32], output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + match A_hat { Some(A_hat) => Self::sign_internal(sk, A_hat, mu, rnd, output), None => Self::sign_internal(sk, &sk.A_hat(), mu, rnd, output), @@ -1930,6 +1940,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mu = MuBuilder::compute_mu(&sk.tr(), msg, ctx)?; let bytes_written = Self::sign_mu_out(sk, None, &mu, output)?; @@ -1966,6 +1978,8 @@ impl< )); } + output.fill(0); + if self.sk.is_some() { if self.signer_rnd.is_none() { Self::sign_mu_out(&self.sk.unwrap(), None, &mu, output) diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index 96a3a5d..c1643ba 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -203,6 +203,8 @@ impl SignaturePublicKey usize { + out.fill(0); + self.pk_encode_out(out) } @@ -279,6 +281,8 @@ impl< } fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + self.pk.encode_out(out) } @@ -431,6 +435,8 @@ impl usize { + out.fill(0); + // counter of progress along the output buffer let mut off: usize = 0; @@ -720,6 +726,8 @@ impl usize { + out.fill(0); + self.sk_encode_out(out) } @@ -976,6 +984,8 @@ impl< } fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + self.sk.encode_out(out) } diff --git a/crypto/mldsa_lowmemory/src/aux_functions.rs b/crypto/mldsa_lowmemory/src/aux_functions.rs index ce0f4ca..bcc1e52 100644 --- a/crypto/mldsa_lowmemory/src/aux_functions.rs +++ b/crypto/mldsa_lowmemory/src/aux_functions.rs @@ -167,6 +167,8 @@ pub(crate) fn bitpack_gamma1( z: &Polynomial, out: &mut [u8; POLY_Z_PACKED_LEN], ) { + out.fill(0); + let mut t: [u32; 4] = [0; 4]; match GAMMA1 { MLDSA44_GAMMA1 => { diff --git a/crypto/mldsa_lowmemory/src/hash_mldsa.rs b/crypto/mldsa_lowmemory/src/hash_mldsa.rs index 33b9176..99140fa 100644 --- a/crypto/mldsa_lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa_lowmemory/src/hash_mldsa.rs @@ -590,6 +590,8 @@ impl< return Err(SignatureError::LengthError("ctx value is longer than 255 bytes")); } + output.fill(0); + // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut h = H::new(); @@ -809,6 +811,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut ph_m = [0u8; PH_LEN]; _ = HASH::default().hash_out(msg, &mut ph_m); Self::sign_ph_out(sk, &ph_m, ctx, output) @@ -847,6 +851,8 @@ impl< )); } + output.fill(0); + if self.sk.is_some() { if self.signer_rnd.is_none() { Self::sign_ph_out( @@ -1024,6 +1030,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut rnd: [u8; MLDSA_RND_LEN] = [0u8; MLDSA_RND_LEN]; HashDRBG_SHA512::new_from_os().next_bytes_out(&mut rnd)?; Self::sign_ph_deterministic_out(sk, ctx, ph, rnd, output) diff --git a/crypto/mldsa_lowmemory/src/mldsa.rs b/crypto/mldsa_lowmemory/src/mldsa.rs index a6efa48..dc6ae2e 100644 --- a/crypto/mldsa_lowmemory/src/mldsa.rs +++ b/crypto/mldsa_lowmemory/src/mldsa.rs @@ -949,6 +949,8 @@ impl< mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mut rnd: [u8; MLDSA_RND_LEN] = [0u8; MLDSA_RND_LEN]; HashDRBG_SHA512::new_from_os().next_bytes_out(&mut rnd)?; @@ -1181,6 +1183,8 @@ impl< rnd: [u8; 32], output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + SK::from_keymaterial(&seed)?; Self::sign_mu_deterministic_out(&SK::from_keymaterial(&seed)?, mu, rnd, output) } @@ -1586,6 +1590,8 @@ impl< ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], ) -> Result { + output.fill(0); + let mu = MuBuilder::compute_mu(&sk.tr(), msg, ctx)?; let bytes_written = Self::sign_mu_out(sk, &mu, output)?; @@ -1622,6 +1628,8 @@ impl< )); } + output.fill(0); + if self.sk.is_some() { if self.signer_rnd.is_none() { Self::sign_mu_out(&self.sk.unwrap(), &mu, output) diff --git a/crypto/mldsa_lowmemory/src/mldsa_keys.rs b/crypto/mldsa_lowmemory/src/mldsa_keys.rs index 81985bd..b805d89 100644 --- a/crypto/mldsa_lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa_lowmemory/src/mldsa_keys.rs @@ -183,6 +183,8 @@ impl SignatureP fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { debug_assert_eq!(out.len(), PK_LEN); + out.fill(0); + out[..32].copy_from_slice(&self.rho); out[32..].copy_from_slice(&self.t1_packed); diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index 381e7be..563d19c 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -171,6 +171,8 @@ impl Vector // let mut s = self.clone(); // s.conditional_sub_q(); + out.fill(0); + let mut idx = 0; match du { 10 => { // MLKEM512 and MLKEM 768 diff --git a/crypto/mlkem/src/mlkem_keys.rs b/crypto/mlkem/src/mlkem_keys.rs index df04429..a93934d 100644 --- a/crypto/mlkem/src/mlkem_keys.rs +++ b/crypto/mlkem/src/mlkem_keys.rs @@ -180,6 +180,8 @@ impl KEMPublicKey for MLKEMPublicK debug_assert_eq!(PK_LEN, 12*k*32 + 32); debug_assert_eq!(POLY_BYTES, 12*32); + out.fill(0); + let (pk_chunks, last_chunk) = out.as_chunks_mut::(); // that should divide evenly the remainder of the array, leaving space for rho at the end @@ -276,6 +278,8 @@ impl, const PK_LEN: u } fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + self.ek.encode_out(out) } @@ -390,7 +394,7 @@ impl< /// 3: dk ← (dkPKE ‖ ek ‖ H(ek) ‖ 𝑧) fn sk_encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { out.fill(0); - + debug_assert_eq!(SK_LEN, /* dk_pke*/12*k*32 + /*ek*/PK_LEN + /*H(ek)*/32 + /*z*/32); let mut pos = 0usize; @@ -584,6 +588,8 @@ impl< } fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + self.sk_encode_out(out) } @@ -751,6 +757,8 @@ impl< } fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + self.dk.encode_out(out) } diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 13bc411..1a9bb2e 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -127,6 +127,8 @@ impl Polynomial { // each of the N i16's will take dv bits debug_assert_eq!(out.len(), N * (dv as usize) / 8); + out.fill(0); + let mut t = [0u8; 8]; let mut idx = 0; diff --git a/crypto/mlkem_lowmemory/src/mlkem_keys.rs b/crypto/mlkem_lowmemory/src/mlkem_keys.rs index b897d17..b9e58f3 100644 --- a/crypto/mlkem_lowmemory/src/mlkem_keys.rs +++ b/crypto/mlkem_lowmemory/src/mlkem_keys.rs @@ -174,6 +174,8 @@ impl KEMPublicKe fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { debug_assert_eq!(self.t_hat_packed.len(), T_PACKED_LEN); + out.fill(0); + out[..T_PACKED_LEN].copy_from_slice(&self.t_hat_packed); debug_assert_eq!(out[T_PACKED_LEN..].len(), 32); out[T_PACKED_LEN..].copy_from_slice(&self.rho); @@ -549,6 +551,8 @@ impl< fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { debug_assert_eq!(SK_LEN, 64); + out.fill(0); + out[..32].copy_from_slice(&self.seed_d); out[32..].copy_from_slice(&self.z); diff --git a/crypto/mlkem_lowmemory/src/polynomial.rs b/crypto/mlkem_lowmemory/src/polynomial.rs index bec4ee4..a2b1603 100644 --- a/crypto/mlkem_lowmemory/src/polynomial.rs +++ b/crypto/mlkem_lowmemory/src/polynomial.rs @@ -150,6 +150,8 @@ impl Polynomial { // each of the N i16's will take dv bits debug_assert_eq!(out.len(), N * (dv as usize) / 8); + out.fill(0); + let mut t = [0u8; 8]; let mut idx = 0; diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index 4614692..31486ea 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -376,6 +376,8 @@ impl Sp80090ADrbg for HashDRBG80090A { return Err(RNGError::ReseedRequired); } + out.fill(0); + // 2. If (additional_input ≠ Null), then do // 2.1 w = Hash (0x02 || V || additional_input). // 2.2 V = (V + w) mod 2^seedlen. @@ -490,6 +492,8 @@ impl RNG for HashDRBG80090A { } fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { + out.fill(0); + self.generate_out("next_bytes_out".as_bytes(), out) } @@ -521,6 +525,8 @@ fn hash_df( panic!("hash_df can't produce that much output!") } + out.fill(0); + // out is "temp" in SP 800-90Ar1 let no_of_bits_to_return: u32 = (out.len() * 8) as u32; let len = u32::div_ceil(out.len() as u32, H::OUTPUT_LEN as u32); @@ -614,6 +620,8 @@ fn hashgen(v: &[u8], out: &mut [u8]) { // 6. Return (returned_bits). // 1. m = ceil(requested_no_of_bits / outlen) + out.fill(0); + let m = u32::div_ceil(out.len() as u32, H::OUTPUT_LEN as u32); // requested_no_of_bits = out.len() diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 10d45e1..7073c59 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -197,6 +197,8 @@ impl Hash for SHA256Internal { } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + self.do_update(data); self.do_final_out(output) } @@ -241,6 +243,8 @@ impl Hash for SHA256Internal { } fn do_final_out(mut self, output: &mut [u8]) -> usize { + output.fill(0); + let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); let bit_len: u64 = self.byte_count << 3; diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 66d5265..c404fc6 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -209,6 +209,8 @@ impl Hash for Sha512Internal { } fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + self.do_update(data); self.do_final_out(output) } @@ -252,6 +254,8 @@ impl Hash for Sha512Internal { } fn do_final_out(mut self, output: &mut [u8]) -> usize { + output.fill(0); + let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); let bit_len_hi: u64 = self.byte_count >> 61; diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 55ee617..5e6f6a8 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -271,6 +271,8 @@ impl KeccakDigest { /// Panics if the output buffer is too small. /// Returns the number of bytes written. pub(super) fn squeeze(&mut self, out: &mut [u8]) -> usize { + out.fill(0); + if !self.squeezing { self.pad_and_switch_to_squeezing_phase(); } diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index a55ac26..ed5656c 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -29,6 +29,8 @@ impl SHA3 { /// Swallows errors and simply returns an empty Vec if the hashes fails for whatever reason. fn hash_internal(mut self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + self.do_update(data); self.do_final_out(output) } @@ -121,6 +123,8 @@ impl Hash for SHA3 { } fn hash_out(self, data: &[u8], mut output: &mut [u8]) -> usize { + output.fill(0); + self.hash_internal(data, &mut output) } @@ -140,6 +144,8 @@ impl Hash for SHA3 { // todo -- why doesn't this take a &mut [u8; HASH_LEN] ? // That's probably more user-friendly than this auto-truncating that I have here. fn do_final_out(mut self, output: &mut [u8]) -> usize { + output.fill(0); + self.keccak.absorb_bits(0x02, 2).expect("do_final_out: keccak.absorb_bits failed."); // this shouldn't fail because by construction you can only enter this function once, and this is the only way to absorb partial bits. let bytes_written = if output.len() <= self.output_len() { @@ -172,6 +178,8 @@ impl Hash for SHA3 { num_partial_bits: usize, output: &mut [u8], ) -> Result { + output.fill(0); + // Mutants note: yep, this is just bit-setting into empty space, so it doesn't matter whether it's OR or XOR. let mut final_input: u16 = ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (0x02 << num_partial_bits); diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index f7cd88e..edf953d 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -54,6 +54,8 @@ impl SHAKE { } fn hash_internal_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + self.absorb(data); self.squeeze_out(output) } @@ -200,6 +202,8 @@ impl XOF for SHAKE { } fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + self.hash_internal_out(data, output) } @@ -239,6 +243,8 @@ impl XOF for SHAKE { } fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + output.fill(0); + if !self.keccak.squeezing { self.keccak.absorb_bits(0x0F, 4).expect("Absorb_bits failed"); }; @@ -262,6 +268,8 @@ impl XOF for SHAKE { return Err(HashError::InvalidLength("must be in the range [0,7]")); } + *output = 0; + let mut buf = [0u8; 1]; self.keccak.squeeze(&mut buf); *output = buf[0] >> 8 - num_bits; @@ -271,4 +279,4 @@ impl XOF for SHAKE { fn max_security_strength(&self) -> SecurityStrength { SecurityStrength::from_bits(PARAMS::SIZE as usize) } -} \ No newline at end of file +} diff --git a/crypto/sha3/tests/sha3_tests.rs b/crypto/sha3/tests/sha3_tests.rs index 787e9fb..b2d5311 100644 --- a/crypto/sha3/tests/sha3_tests.rs +++ b/crypto/sha3/tests/sha3_tests.rs @@ -66,10 +66,13 @@ mod sha3_tests { assert_eq!(&out, b"\x58\x4c\xc7\x02\xc2\x22\x9a\x0a\xbc\x78\x9b\xfa\x64\xb4\x27\x1f\xb8\xf0\xbb\x78\x67\x15\x88\xb9\xef\x1d\x09\x3e\xa3\xd4\x72\x58\x4c\x6d\x43\xb5\x68\x33\x59\x47\x2f\x44\x1b\x33\x85\x6f\x68\x28\x59\xf0\xc3\x95\x4b\x56\x80\x8f\xd1\xfb\xa0\xb5\x9c\x9d\x19\x54"); assert_eq!(bytes_written, 64); - // check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes. + // Q. T. Felix NOTE: With the application of zeroize, the entire output buffer is pre-initialized to 0, + // so the bytes after the digest length are now also 0. + // Previously, the contract was to leave the trailing bytes untouched, + // but this has been changed to fill them with zeros to prevent exposure of stale data. let mut out = DUMMY_SEED_512.clone(); SHA3_256::new().hash_out(DUMMY_SEED_512, &mut out); - assert_eq!(&out[32..], &DUMMY_SEED_512[32..]); + assert!(out[32..].iter().all(|&b| b == 0)); } #[test] diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 96b1950..10b23e7 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -292,6 +292,8 @@ pub fn conditional_copy_bytes( out: &mut [u8; LEN], take_a: bool, ) { + out.fill(0); + // we want the behaviour of // if take_a { 0xFF } else { 0x00 } // but without using any branches that could leak timing signals From c642ef633108ce1983bb357f5e338bd34c545345 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:36:21 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20-?= =?UTF-8?q?=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20mlkem=20compress=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20fill(0)=20=EC=A0=9C=EA=B1=B0=20(=EC=84=B1=EB=8A=A5)?= =?UTF-8?q?=20-=20ct=20conditional=5Fcopy=5Fbytes=20fill(0)=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(constant-time=20=EA=B0=80=EC=A0=95=20=EC=9C=84?= =?UTF-8?q?=EB=B0=98=20=EA=B0=80=EB=8A=A5)=20-=20core=20traits=EC=9D=98=20?= =?UTF-8?q?=5Fout=20=ED=95=A8=EC=88=98=20docstring=EC=97=90=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EB=B2=84=ED=8D=BC=20zeroize=20=EB=AA=85=EC=8B=9C?= =?UTF-8?q?=20-=20sha3=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9D=B4=EB=A6=84=EA=B3=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=9D=B4=EB=A0=A5=20=EC=A0=9C=EA=B1=B0=20-=20?= =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=85=B8=ED=8A=B8=EC=9D=98=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=EB=90=9C=20TODO=20=ED=95=AD=EB=AA=A9?= =?UTF-8?q?=EC=9D=84=20changelog=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alpha_0.1.2_release_notes.md | 4 ++-- crypto/core/src/traits.rs | 20 ++++++++++++++++++++ crypto/mlkem/src/matrix.rs | 2 -- crypto/mlkem/src/polynomial.rs | 2 -- crypto/mlkem_lowmemory/src/polynomial.rs | 2 -- crypto/sha3/tests/sha3_tests.rs | 5 +---- crypto/utils/src/ct.rs | 2 -- 7 files changed, 23 insertions(+), 14 deletions(-) diff --git a/alpha_0.1.2_release_notes.md b/alpha_0.1.2_release_notes.md index 06dcefa..d6609db 100644 --- a/alpha_0.1.2_release_notes.md +++ b/alpha_0.1.2_release_notes.md @@ -12,8 +12,6 @@ * Check out Megan's email May 13 about KeyMaterial: "I was wondering if there might be scope for a closure based approach that could guarantee encapsulation of the state change from safe to hazardous back to safe again." -* Anywhere that you have an `_out(.. out: &mut [u8])`, start by zeroizing it with .fill(0); .. a good task for Claude? - And should be documented in the style guide? * Go back to previous algs and apply memory optimization tricks like internal functions. And add a docs section "Memory Usage" that measures with valgrind. * Ensure that all crates have `#![forbid(missing_docs)]` @@ -54,5 +52,7 @@ * ML-DSA * Low-Memory ML-DSA -- runs in about 1/10th of the usual memory (~ 30 kb of stack) with only minor performance impact. +* All public `*_out(.., out: &mut [u8])` functions now begin by zeroizing the entire output buffer with `.fill(0)`, + preventing exposure of stale data in oversized output buffers or on early error returns. * Github issues resolved: * #2, or whatever \ No newline at end of file diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 27dd844..3713fcb 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -30,6 +30,7 @@ pub trait Hash : Default { /// A static one-shot API that hashes the provided data into the provided output slice. /// `data` can be of any length, including zero bytes. + /// The entire output buffer is zeroized before the hash output is written. /// The return value is the number of bytes written. fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize; @@ -50,6 +51,8 @@ pub trait Hash : Default { /// If the provided buffer is smaller than the hash's output length, the output will be truncated. /// If the provided buffor is larger than the hash's output length, the output will be placed in /// the first [Hash::output_len] bytes. + /// The entire output buffer is zeroized before the hash output is written, so any bytes past + /// [Hash::output_len] will be 0. /// /// The return value is the number of bytes written. fn do_final_out(self, output: &mut [u8]) -> usize; @@ -65,6 +68,7 @@ pub trait Hash : Default { /// The same as [Hash::do_final_out], but allows for supplying a partial byte as the last input. /// Assumes that the input is in the least significant bits (big endian). /// will be placed in the first [Hash::output_len] bytes. + /// The entire output buffer is zeroized before the hash output is written. /// The return value is the number of bytes written. fn do_final_partial_bits_out( self, @@ -208,6 +212,7 @@ pub trait KEMPublicKey : PartialEq + Eq + Clone + Debug + D /// Write it out to bytes in its standard encoding. fn encode(&self) -> [u8; PK_LEN]; /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize; /// Read it in from bytes in its standard encoding. fn from_bytes(bytes: &[u8]) -> Result; @@ -218,6 +223,7 @@ pub trait KEMPrivateKey : PartialEq + Eq + Clone + Secret + /// Write it out to bytes in its standard encoding. fn encode(&self) -> [u8; SK_LEN]; /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; /// Read it in from bytes in its standard encoding. fn from_bytes(bytes: &[u8]) -> Result; @@ -293,6 +299,8 @@ pub trait MAC: Sized { /// Depending on the underlying MAC implementation, NIST may require that the library enforce /// a minimum length on the mac output value. See documentation for the underlying implementation /// to see conditions under which it throws [MACError::InvalidLength]. + /// + /// The entire output buffer is zeroized before the MAC value is written. fn mac_out(self, data: &[u8],out: &mut [u8]) -> Result; /// One-shot API that verifies a MAC for the provided data. @@ -318,6 +326,8 @@ pub trait MAC: Sized { /// Depending on the underlying MAC implementation, NIST may require that the library enforce /// a minimum length on the mac output value. See documentation for the underlying implementation /// to see conditions under which it throws [MACError::InvalidLength]. + /// + /// The entire output buffer is zeroized before the MAC value is written. fn do_final_out(self, out: &mut [u8]) -> Result; /// Internally, this will re-compute the MAC value and then compare it to the provided mac value @@ -392,6 +402,7 @@ pub trait RNG : Default { fn next_bytes(&mut self, len: usize) -> Result, RNGError>; /// Returns the number of bytes written. + /// The entire output buffer is zeroized before the random bytes are written. fn next_bytes_out(&mut self, out: &mut [u8]) -> Result; fn fill_keymaterial_out(&mut self, out: &mut impl KeyMaterialTrait) -> Result; @@ -443,6 +454,7 @@ pub trait PHSignature< /// might throw an error, ignore the provided ctx value, or append the ctx to the msg in a non-standard way. fn sign_ph(sk: &SK, ph: &[u8; PH_LEN], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError>; /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. + /// The entire output buffer is zeroized before the signature is written. fn sign_ph_out(sk: &SK, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN]) -> Result; /// On success, returns Ok(()) /// On failure, returns Err([SignatureError::SignatureVerificationFailed]); may also return other types of [SignatureError] as appropriate (such as for invalid-length inputs). @@ -501,6 +513,7 @@ pub trait Signature< fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError>; /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. + /// The entire output buffer is zeroized before the signature is written. fn sign_out(sk: &SK, msg: &[u8], ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN]) -> Result; /* streaming signing API */ @@ -516,6 +529,7 @@ pub trait Signature< fn sign_final(self) -> Result<[u8; SIG_LEN], SignatureError>; /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. + /// The entire output buffer is zeroized before the signature is written. fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result; /// On success, returns Ok(()) @@ -543,6 +557,7 @@ pub trait SignaturePublicKey : PartialEq + Eq + Clone + Deb /// Write it out to bytes in its standard encoding. fn encode(&self) -> [u8; PK_LEN]; /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize; /// Read it in from bytes in its standard encoding. fn from_bytes(bytes: &[u8]) -> Result; @@ -553,6 +568,7 @@ pub trait SignaturePrivateKey : PartialEq + Eq + Clone + Se /// Write it out to bytes in its standard encoding. fn encode(&self) -> [u8; SK_LEN]; /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; /// Read it in from bytes in its standard encoding. fn from_bytes(bytes: &[u8]) -> Result; @@ -583,6 +599,7 @@ pub trait XOF : Default { /// A static one-shot API that digests the input data and produces `result_len` bytes of output. /// Fills the provided output slice. + /// The entire output buffer is zeroized before the output is written. fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; fn absorb(&mut self, data: &[u8]); @@ -599,6 +616,7 @@ pub trait XOF : Default { /// Can be called multiple times. /// Fills the provided output slice. + /// The entire output buffer is zeroized before the output is written. fn squeeze_out(&mut self, output: &mut [u8]) -> usize; /// Squeezes a partial byte from the XOF. @@ -606,6 +624,8 @@ pub trait XOF : Default { /// This is a final call and consumes self. fn squeeze_partial_byte_final(self, num_bits: usize) -> Result; + /// The same as [XOF::squeeze_partial_byte_final], but writes into the provided output byte. + /// The output byte is zeroized before the result is written. fn squeeze_partial_byte_final_out( self, num_bits: usize, diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index 563d19c..381e7be 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -171,8 +171,6 @@ impl Vector // let mut s = self.clone(); // s.conditional_sub_q(); - out.fill(0); - let mut idx = 0; match du { 10 => { // MLKEM512 and MLKEM 768 diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 1a9bb2e..13bc411 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -127,8 +127,6 @@ impl Polynomial { // each of the N i16's will take dv bits debug_assert_eq!(out.len(), N * (dv as usize) / 8); - out.fill(0); - let mut t = [0u8; 8]; let mut idx = 0; diff --git a/crypto/mlkem_lowmemory/src/polynomial.rs b/crypto/mlkem_lowmemory/src/polynomial.rs index a2b1603..bec4ee4 100644 --- a/crypto/mlkem_lowmemory/src/polynomial.rs +++ b/crypto/mlkem_lowmemory/src/polynomial.rs @@ -150,8 +150,6 @@ impl Polynomial { // each of the N i16's will take dv bits debug_assert_eq!(out.len(), N * (dv as usize) / 8); - out.fill(0); - let mut t = [0u8; 8]; let mut idx = 0; diff --git a/crypto/sha3/tests/sha3_tests.rs b/crypto/sha3/tests/sha3_tests.rs index b2d5311..68fd94b 100644 --- a/crypto/sha3/tests/sha3_tests.rs +++ b/crypto/sha3/tests/sha3_tests.rs @@ -66,10 +66,7 @@ mod sha3_tests { assert_eq!(&out, b"\x58\x4c\xc7\x02\xc2\x22\x9a\x0a\xbc\x78\x9b\xfa\x64\xb4\x27\x1f\xb8\xf0\xbb\x78\x67\x15\x88\xb9\xef\x1d\x09\x3e\xa3\xd4\x72\x58\x4c\x6d\x43\xb5\x68\x33\x59\x47\x2f\x44\x1b\x33\x85\x6f\x68\x28\x59\xf0\xc3\x95\x4b\x56\x80\x8f\xd1\xfb\xa0\xb5\x9c\x9d\x19\x54"); assert_eq!(bytes_written, 64); - // Q. T. Felix NOTE: With the application of zeroize, the entire output buffer is pre-initialized to 0, - // so the bytes after the digest length are now also 0. - // Previously, the contract was to leave the trailing bytes untouched, - // but this has been changed to fill them with zeros to prevent exposure of stale data. + // check that the bytes of an oversized output buffer past the digest length get zeroized. let mut out = DUMMY_SEED_512.clone(); SHA3_256::new().hash_out(DUMMY_SEED_512, &mut out); assert!(out[32..].iter().all(|&b| b == 0)); diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 10b23e7..96b1950 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -292,8 +292,6 @@ pub fn conditional_copy_bytes( out: &mut [u8; LEN], take_a: bool, ) { - out.fill(0); - // we want the behaviour of // if take_a { 0xFF } else { 0x00 } // but without using any branches that could leak timing signals