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}