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::mem::MaybeUninit;
8use core::sync::atomic::{compiler_fence, Ordering};
9
10/// Types whose in-memory representation can be securely erased.
11pub trait Zeroize {
12    /// Overwrite `self` with zeroes using non-elidable volatile writes.
13    fn zeroize(&mut self);
14}
15
16impl Zeroize for [u8] {
17    /// Eight bytes per volatile store, then the tail a byte at a time.
18    ///
19    /// A volatile store of a `[u8; 8]` is exactly as non-elidable as eight
20    /// volatile byte stores, and is one store rather than eight. That matters
21    /// because wipes are on hot paths: every SHA-2 state wipes its buffered
22    /// block when dropped, and HMAC drops two per tag, so byte-at-a-time
23    /// wiping was a measurable share of a short HMAC. `[u8; 8]` has the
24    /// alignment of `u8`, so the pointer cast needs nothing a byte slice
25    /// does not already guarantee, and no temporary larger than eight bytes
26    /// is involved -- which a single store of the whole array would need.
27    fn zeroize(&mut self) {
28        let mut chunks = self.chunks_exact_mut(8);
29        for chunk in &mut chunks {
30            // SAFETY: `chunk` is exactly eight valid, uniquely-borrowed bytes,
31            // and `[u8; 8]` has alignment 1.
32            unsafe { core::ptr::write_volatile(chunk.as_mut_ptr() as *mut [u8; 8], [0u8; 8]) };
33        }
34        for byte in chunks.into_remainder() {
35            // SAFETY: `byte` is a valid, aligned, uniquely-borrowed `u8`.
36            unsafe { core::ptr::write_volatile(byte, 0) };
37        }
38        compiler_fence(Ordering::SeqCst);
39    }
40}
41
42impl Zeroize for [u32] {
43    fn zeroize(&mut self) {
44        for w in self.iter_mut() {
45            // SAFETY: `w` is a valid, aligned, uniquely-borrowed `u32`.
46            unsafe { core::ptr::write_volatile(w, 0) };
47        }
48        compiler_fence(Ordering::SeqCst);
49    }
50}
51
52impl Zeroize for [u64] {
53    fn zeroize(&mut self) {
54        for w in self.iter_mut() {
55            // SAFETY: `w` is a valid, aligned, uniquely-borrowed `u64`.
56            unsafe { core::ptr::write_volatile(w, 0) };
57        }
58        compiler_fence(Ordering::SeqCst);
59    }
60}
61
62#[cfg(feature = "std")]
63impl Zeroize for std::vec::Vec<u8> {
64    /// The vector's bytes, through the slice wipe. The length and capacity
65    /// are left as they are; only the contents are secret.
66    fn zeroize(&mut self) {
67        self.as_mut_slice().zeroize();
68    }
69}
70
71impl<const N: usize> Zeroize for [u8; N] {
72    fn zeroize(&mut self) {
73        self.as_mut_slice().zeroize();
74    }
75}
76
77impl<const N: usize> Zeroize for [u32; N] {
78    fn zeroize(&mut self) {
79        self.as_mut_slice().zeroize();
80    }
81}
82
83impl<const N: usize> Zeroize for [u64; N] {
84    fn zeroize(&mut self) {
85        self.as_mut_slice().zeroize();
86    }
87}
88
89/// Overwrites the `N` bytes of stack directly below the caller's frame, at the
90/// point of the call, with zeros. Best effort; defence in depth.
91///
92/// Call it from a function `C` after a deeper call chain below `C` has
93/// returned. Those callees' frames were allocated at the stack pointer `C` had
94/// when it made its call, so the dead frames lie where `stack_scrub` now
95/// lives: its own local buffer sits in the `N` bytes (plus its return address
96/// and any alignment) immediately below `C`'s stack pointer, and the volatile
97/// stores zero it. The idea is libsodium's `sodium_stackzero`.
98///
99/// What it guarantees: the buffer's `N` bytes are written with non-elidable
100/// volatile stores and then a compiler fence, so they are zero when the
101/// function returns, whatever the optimiser would otherwise do with a buffer
102/// that is never read.
103///
104/// What it does not guarantee:
105///
106/// - It reaches only the `N` bytes below the caller. A dead frame deeper than
107///   `N` keeps its bytes. `N` must cover the deepest path from the caller
108///   that could have held the secret, with margin.
109/// - It relies on the dead frames having been in that region. A frame placed
110///   elsewhere (another caller's stack, a heap copy) is not reached.
111/// - It cannot reach registers. A secret left in a vector or general register
112///   is untouched.
113/// - It is not a proof that no copy exists. Only the stack region the callees
114///   used is cleared; a copy the caller made, or one in a spill slot that a
115///   later call overwrote with other data, is not something this can see.
116///
117/// It is a hardening measure against later disclosure of dead stack memory.
118/// It is not a constant-time property and does not change any result.
119#[inline(never)]
120pub fn stack_scrub<const N: usize>() {
121    let mut buf = MaybeUninit::<[u8; N]>::uninit();
122    // SAFETY: `buf` is `N` bytes of writable storage owned by this frame, and
123    // the pointer is valid for `N` bytes for the length of the call.
124    unsafe { wipe_raw(buf.as_mut_ptr().cast::<u8>(), N) };
125    // The buffer is never read. Its stores are volatile, so they are not
126    // removed as dead, and the fence keeps them ordered before the return.
127}
128
129/// Writes zeros over `n` bytes at `p`, with volatile stores, then fences.
130///
131/// # Safety
132///
133/// `p` must be valid for writes of `n` bytes for the duration of the call.
134#[inline(always)]
135unsafe fn wipe_raw(p: *mut u8, n: usize) {
136    // Sixteen bytes per volatile store, which is one 128-bit store on every
137    // target this builds for, so a 32 KiB scrub is 2048 stores, not 4096.
138    let mut i = 0;
139    while i + 16 <= n {
140        // SAFETY: `i + 16 <= n`, so these sixteen bytes lie inside the `n`
141        // bytes the caller promised; `[u8; 16]` has alignment 1.
142        unsafe { core::ptr::write_volatile(p.add(i).cast::<[u8; 16]>(), [0u8; 16]) };
143        i += 16;
144    }
145    while i + 8 <= n {
146        // SAFETY: `i + 8 <= n`, as above.
147        unsafe { core::ptr::write_volatile(p.add(i).cast::<[u8; 8]>(), [0u8; 8]) };
148        i += 8;
149    }
150    while i < n {
151        // SAFETY: `i < n`, inside the caller's `n` bytes.
152        unsafe { core::ptr::write_volatile(p.add(i), 0) };
153        i += 1;
154    }
155    compiler_fence(Ordering::SeqCst);
156}
157
158/// A wrapper that zeroizes its contents when dropped.
159///
160/// ```
161/// use ic_core::Zeroizing;
162/// let mut key = Zeroizing::new([0u8; 32]);
163/// key[0] = 0x42;
164/// assert_eq!(key[0], 0x42);
165/// // `key` is wiped when it leaves scope.
166/// ```
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct Zeroizing<T: Zeroize>(T);
169
170impl<T: Zeroize> Zeroizing<T> {
171    /// Wrap a value so it is erased on drop.
172    pub const fn new(value: T) -> Self {
173        Self(value)
174    }
175
176    /// Borrow the protected value.
177    pub fn get(&self) -> &T {
178        &self.0
179    }
180
181    /// Mutably borrow the protected value.
182    pub fn get_mut(&mut self) -> &mut T {
183        &mut self.0
184    }
185}
186
187impl<T: Zeroize> core::ops::Deref for Zeroizing<T> {
188    type Target = T;
189    fn deref(&self) -> &T {
190        &self.0
191    }
192}
193
194impl<T: Zeroize> core::ops::DerefMut for Zeroizing<T> {
195    fn deref_mut(&mut self) -> &mut T {
196        &mut self.0
197    }
198}
199
200impl<T: Zeroize> Drop for Zeroizing<T> {
201    fn drop(&mut self) {
202        self.0.zeroize();
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn slices_are_wiped() {
212        let mut buf = [1u8, 2, 3, 4];
213        buf.zeroize();
214        assert_eq!(buf, [0, 0, 0, 0]);
215    }
216
217    #[test]
218    fn zeroizing_derefs() {
219        let mut z = Zeroizing::new([7u8; 8]);
220        assert_eq!(z[0], 7);
221        z[0] = 9;
222        assert_eq!(z.get()[0], 9);
223    }
224
225    #[test]
226    fn eight_byte_chunks_and_the_tail_are_wiped_and_nothing_else() {
227        // Two whole chunks and a three-byte tail, inside a larger array whose
228        // bytes either side must survive: a chunked wipe that overran its
229        // slice would zero them.
230        let mut buf = [0xa5u8; 32];
231        buf[5..24].zeroize();
232        assert_eq!(&buf[..5], &[0xa5; 5]);
233        assert_eq!(&buf[5..24], &[0u8; 19]);
234        assert_eq!(&buf[24..], &[0xa5; 8]);
235        for len in 0..=17 {
236            let mut v = [0xffu8; 17];
237            v[..len].zeroize();
238            assert!(v[..len].iter().all(|&b| b == 0), "length {len}");
239            assert!(v[len..].iter().all(|&b| b == 0xff), "length {len}");
240        }
241    }
242
243    #[test]
244    fn raw_wipe_zeroes_exactly_its_range() {
245        // Every length up to two chunks and a tail, inside a buffer whose
246        // bytes either side must survive. If the volatile stores were removed
247        // the range would keep its pattern and this would fail.
248        for n in 0..=19 {
249            let mut buf = [0xa5u8; 24];
250            // SAFETY: `buf` is 24 valid bytes and `2 + n <= 21`, so the range
251            // `2..2 + n` lies inside it.
252            unsafe { wipe_raw(buf.as_mut_ptr().add(2), n) };
253            assert!(
254                buf[..2].iter().all(|&b| b == 0xa5),
255                "length {n}: leading guard"
256            );
257            assert!(
258                buf[2..2 + n].iter().all(|&b| b == 0),
259                "length {n}: range not zeroed"
260            );
261            assert!(
262                buf[2 + n..].iter().all(|&b| b == 0xa5),
263                "length {n}: trailing guard"
264            );
265        }
266    }
267
268    #[test]
269    fn stack_scrub_returns_at_several_sizes() {
270        // Its effect is on the stack below this frame, which no safe test can
271        // read. This checks that each size compiles and returns.
272        stack_scrub::<0>();
273        stack_scrub::<7>();
274        stack_scrub::<4096>();
275    }
276
277    #[test]
278    fn word_slices_are_wiped() {
279        let mut w = [0xDEAD_BEEFu32; 4];
280        w.zeroize();
281        assert_eq!(w, [0u32; 4]);
282        let mut q = [0xDEAD_BEEF_CAFE_F00Du64; 2];
283        q.zeroize();
284        assert_eq!(q, [0u64; 2]);
285    }
286}