Skip to main content

ic_core/
zeroize.rs

1//! Secret erasure.
2//!
3//! [`Zeroizing`] wipes its contents on drop using volatile writes plus a
4//! compiler fence, so the erasure survives optimization. This is the mechanism
5//! behind the FIPS 140-3 zeroisation requirement for CSPs held in memory.
6
7use core::sync::atomic::{compiler_fence, Ordering};
8
9/// Types whose in-memory representation can be securely erased.
10pub trait Zeroize {
11    /// Overwrite `self` with zeroes using non-elidable volatile writes.
12    fn zeroize(&mut self);
13}
14
15impl Zeroize for [u8] {
16    /// Eight bytes per volatile store, then the tail a byte at a time.
17    ///
18    /// A volatile store of a `[u8; 8]` is exactly as non-elidable as eight
19    /// volatile byte stores, and is one store rather than eight. That matters
20    /// because wipes are on hot paths: every SHA-2 state wipes its buffered
21    /// block when dropped, and HMAC drops two per tag, so byte-at-a-time
22    /// wiping was a measurable share of a short HMAC. `[u8; 8]` has the
23    /// alignment of `u8`, so the pointer cast needs nothing a byte slice
24    /// does not already guarantee, and no temporary larger than eight bytes
25    /// is involved -- which a single store of the whole array would need.
26    fn zeroize(&mut self) {
27        let mut chunks = self.chunks_exact_mut(8);
28        for chunk in &mut chunks {
29            // SAFETY: `chunk` is exactly eight valid, uniquely-borrowed bytes,
30            // and `[u8; 8]` has alignment 1.
31            unsafe { core::ptr::write_volatile(chunk.as_mut_ptr() as *mut [u8; 8], [0u8; 8]) };
32        }
33        for byte in chunks.into_remainder() {
34            // SAFETY: `byte` is a valid, aligned, uniquely-borrowed `u8`.
35            unsafe { core::ptr::write_volatile(byte, 0) };
36        }
37        compiler_fence(Ordering::SeqCst);
38    }
39}
40
41impl Zeroize for [u32] {
42    fn zeroize(&mut self) {
43        for w in self.iter_mut() {
44            // SAFETY: `w` is a valid, aligned, uniquely-borrowed `u32`.
45            unsafe { core::ptr::write_volatile(w, 0) };
46        }
47        compiler_fence(Ordering::SeqCst);
48    }
49}
50
51impl Zeroize for [u64] {
52    fn zeroize(&mut self) {
53        for w in self.iter_mut() {
54            // SAFETY: `w` is a valid, aligned, uniquely-borrowed `u64`.
55            unsafe { core::ptr::write_volatile(w, 0) };
56        }
57        compiler_fence(Ordering::SeqCst);
58    }
59}
60
61#[cfg(feature = "std")]
62impl Zeroize for std::vec::Vec<u8> {
63    /// The vector's bytes, through the slice wipe. The length and capacity
64    /// are left as they are; only the contents are secret.
65    fn zeroize(&mut self) {
66        self.as_mut_slice().zeroize();
67    }
68}
69
70impl<const N: usize> Zeroize for [u8; N] {
71    fn zeroize(&mut self) {
72        self.as_mut_slice().zeroize();
73    }
74}
75
76impl<const N: usize> Zeroize for [u32; N] {
77    fn zeroize(&mut self) {
78        self.as_mut_slice().zeroize();
79    }
80}
81
82impl<const N: usize> Zeroize for [u64; N] {
83    fn zeroize(&mut self) {
84        self.as_mut_slice().zeroize();
85    }
86}
87
88/// A wrapper that zeroizes its contents when dropped.
89///
90/// ```
91/// use ic_core::Zeroizing;
92/// let mut key = Zeroizing::new([0u8; 32]);
93/// key[0] = 0x42;
94/// assert_eq!(key[0], 0x42);
95/// // `key` is wiped when it leaves scope.
96/// ```
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct Zeroizing<T: Zeroize>(T);
99
100impl<T: Zeroize> Zeroizing<T> {
101    /// Wrap a value so it is erased on drop.
102    pub const fn new(value: T) -> Self {
103        Self(value)
104    }
105
106    /// Borrow the protected value.
107    pub fn get(&self) -> &T {
108        &self.0
109    }
110
111    /// Mutably borrow the protected value.
112    pub fn get_mut(&mut self) -> &mut T {
113        &mut self.0
114    }
115}
116
117impl<T: Zeroize> core::ops::Deref for Zeroizing<T> {
118    type Target = T;
119    fn deref(&self) -> &T {
120        &self.0
121    }
122}
123
124impl<T: Zeroize> core::ops::DerefMut for Zeroizing<T> {
125    fn deref_mut(&mut self) -> &mut T {
126        &mut self.0
127    }
128}
129
130impl<T: Zeroize> Drop for Zeroizing<T> {
131    fn drop(&mut self) {
132        self.0.zeroize();
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn slices_are_wiped() {
142        let mut buf = [1u8, 2, 3, 4];
143        buf.zeroize();
144        assert_eq!(buf, [0, 0, 0, 0]);
145    }
146
147    #[test]
148    fn zeroizing_derefs() {
149        let mut z = Zeroizing::new([7u8; 8]);
150        assert_eq!(z[0], 7);
151        z[0] = 9;
152        assert_eq!(z.get()[0], 9);
153    }
154
155    #[test]
156    fn eight_byte_chunks_and_the_tail_are_wiped_and_nothing_else() {
157        // Two whole chunks and a three-byte tail, inside a larger array whose
158        // bytes either side must survive: a chunked wipe that overran its
159        // slice would zero them.
160        let mut buf = [0xa5u8; 32];
161        buf[5..24].zeroize();
162        assert_eq!(&buf[..5], &[0xa5; 5]);
163        assert_eq!(&buf[5..24], &[0u8; 19]);
164        assert_eq!(&buf[24..], &[0xa5; 8]);
165        for len in 0..=17 {
166            let mut v = [0xffu8; 17];
167            v[..len].zeroize();
168            assert!(v[..len].iter().all(|&b| b == 0), "length {len}");
169            assert!(v[len..].iter().all(|&b| b == 0xff), "length {len}");
170        }
171    }
172
173    #[test]
174    fn word_slices_are_wiped() {
175        let mut w = [0xDEAD_BEEFu32; 4];
176        w.zeroize();
177        assert_eq!(w, [0u32; 4]);
178        let mut q = [0xDEAD_BEEF_CAFE_F00Du64; 2];
179        q.zeroize();
180        assert_eq!(q, [0u64; 2]);
181    }
182}