hashes: add non_secure_erase for HMAC and HKDF
What changed, and why it matters
This commit adds a best-effort memory-clearing helper for two cryptographic building blocks (HMAC and HKDF) in the rust-bitcoin library. It tries to overwrite secret intermediate data with zeros when those objects are destroyed, but the authors explicitly note this is not a guaranteed security fix because the Rust compiler may still leave copies of the data elsewhere in memory. The change is defensive hardening rather than a fix for a known exploitable bug.
Treat as a defensive hardening improvement, not an urgent vulnerability fix. Review whether downstream code should adopt a stronger secret-erasure crate such as `zeroize` if guaranteed clearing is required. No immediate patching urgency is indicated by the commit itself.
Security signals we found
Adds explicit secret-memory erasure (defense in depth)
Removes Copy derive from Hkdf to avoid implicit duplication of secret PRK
Uses volatile writes and compiler fence to inhibit optimization of erase loop
Authors explicitly label the erasure as non-secure / best-effort
No CVE, advisory, or exploit evidence present in commit or references
Evidence from the diff
The patch introduces a crate-private non_secure_erase helper that uses ptr::write_volatile plus a compiler_fence to overwrite a value’s bytes with zeros. It wires this helper into HmacEngine (erasing inner/outer hash engines and the ipad/opad arrays during construction) and Hkdf (erasing the PRK on Drop). Hkdf also loses its Copy derive. The implementation mirrors the non_secure_erase pattern from rust-secp256k1 and the zeroize crate’s approach, but the documentation repeatedly cautions that this does not prevent compiler copies or moves.
Changed components
hashes/src/hkdf/mod.rshashes/src/hmac/mod.rshashes/src/lib.rsInspect captured patch +52 / −1
diff --git a/hashes/src/hkdf/mod.rs b/hashes/src/hkdf/mod.rs
index 8d852d1a..d1326890 100644
--- a/hashes/src/hkdf/mod.rs
+++ b/hashes/src/hkdf/mod.rs
@@ -32,12 +32,27 @@ impl fmt::Display for MaxLengthError {
impl std::error::Error for MaxLengthError {}
/// HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
-#[derive(Copy, Clone)]
+#[derive(Clone)]
pub struct Hkdf<T: HashEngine> {
/// Pseudorandom key based on the extract step.
prk: Hmac<T::Hash>,
}
+impl<T: HashEngine> Drop for Hkdf<T> {
+ fn drop(&mut self) { self.non_secure_erase(); }
+}
+
+impl<T: HashEngine> Hkdf<T> {
+ /// Attempts to erase the contents of the pseudorandom key.
+ ///
+ /// Note, however, that the compiler is allowed to freely copy or move the
+ /// contents of this type to other places in memory. Preventing this behavior
+ /// is very subtle. For more discussion on this, please see the documentation
+ /// of the [`zeroize`](https://docs.rs/zeroize) crate.
+ #[inline]
+ pub fn non_secure_erase(&mut self) { crate::non_secure_erase(&mut self.prk); }
+}
+
impl<T: HashEngine> Hkdf<T>
where
T: Default,
diff --git a/hashes/src/hmac/mod.rs b/hashes/src/hmac/mod.rs
index 31115011..c8ccdf14 100644
--- a/hashes/src/hmac/mod.rs
+++ b/hashes/src/hmac/mod.rs
@@ -78,11 +78,27 @@ impl<T: HashEngine> HmacEngine<T> {
ret.iengine.input(&ipad[..T::BLOCK_SIZE]);
ret.oengine.input(&opad[..T::BLOCK_SIZE]);
+
+ crate::non_secure_erase(&mut ipad);
+ crate::non_secure_erase(&mut opad);
+
ret
}
/// A special constructor giving direct access to the underlying "inner" and "outer" engines.
pub fn from_inner_engines(iengine: T, oengine: T) -> Self { Self { iengine, oengine } }
+
+ /// Attempts to erase the contents of the underlying hash engines
+ ///
+ /// Note, however, that the compiler is allowed to freely copy or move the
+ /// contents of this type to other places in memory. Preventing this behavior
+ /// is very subtle. For more discussion on this, please see the documentation
+ /// of the [`zeroize`](https://docs.rs/zeroize) crate.
+ #[inline]
+ pub fn non_secure_erase(&mut self) {
+ crate::non_secure_erase(&mut self.iengine);
+ crate::non_secure_erase(&mut self.oengine);
+ }
}
impl<T: HashEngine> HashEngine for HmacEngine<T> {
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index 7e027b42..5b28c138 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -254,6 +254,26 @@ mod sealed {
impl<const N: usize> IsByteArray for [u8; N] {}
}
+/// Does a best attempt at erasing the contents of `val` by writing zeros.
+///
+/// The implementation is based on the approach used by the
+/// [`zeroize`](https://docs.rs/zeroize) crate and the `non_secure_erase` functions in
+/// `rust-secp256k1`.
+///
+/// Note, however, that the compiler is allowed to freely copy or move the contents of `val` to
+/// other places in memory. Preventing this behavior is very subtle. For more discussion on this,
+/// please see the documentation of the [`zeroize`](https://docs.rs/zeroize) crate.
+pub(crate) fn non_secure_erase<T: ?Sized>(val: &mut T) {
+ use core::sync::atomic;
+
+ let ptr = val as *mut T as *mut u8;
+ let len = core::mem::size_of_val(val);
+ for i in 0..len {
+ unsafe { core::ptr::write_volatile(ptr.add(i), 0) };
+ }
+ atomic::compiler_fence(atomic::Ordering::SeqCst);
+}
+
fn incomplete_block_len<H: HashEngine>(eng: &H) -> usize {
let block_size = H::BLOCK_SIZE as u64; // Cast usize to u64 is ok.
Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.