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
61impl<const N: usize> Zeroize for [u8; N] {
62    fn zeroize(&mut self) {
63        self.as_mut_slice().zeroize();
64    }
65}
66
67impl<const N: usize> Zeroize for [u32; N] {
68    fn zeroize(&mut self) {
69        self.as_mut_slice().zeroize();
70    }
71}
72
73impl<const N: usize> Zeroize for [u64; N] {
74    fn zeroize(&mut self) {
75        self.as_mut_slice().zeroize();
76    }
77}
78
79/// A wrapper that zeroizes its contents when dropped.
80///
81/// ```
82/// use ic_core::Zeroizing;
83/// let mut key = Zeroizing::new([0u8; 32]);
84/// key[0] = 0x42;
85/// assert_eq!(key[0], 0x42);
86/// // `key` is wiped when it leaves scope.
87/// ```
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Zeroizing<T: Zeroize>(T);
90
91impl<T: Zeroize> Zeroizing<T> {
92    /// Wrap a value so it is erased on drop.
93    pub const fn new(value: T) -> Self {
94        Self(value)
95    }
96
97    /// Borrow the protected value.
98    pub fn get(&self) -> &T {
99        &self.0
100    }
101
102    /// Mutably borrow the protected value.
103    pub fn get_mut(&mut self) -> &mut T {
104        &mut self.0
105    }
106}
107
108impl<T: Zeroize> core::ops::Deref for Zeroizing<T> {
109    type Target = T;
110    fn deref(&self) -> &T {
111        &self.0
112    }
113}
114
115impl<T: Zeroize> core::ops::DerefMut for Zeroizing<T> {
116    fn deref_mut(&mut self) -> &mut T {
117        &mut self.0
118    }
119}
120
121impl<T: Zeroize> Drop for Zeroizing<T> {
122    fn drop(&mut self) {
123        self.0.zeroize();
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn slices_are_wiped() {
133        let mut buf = [1u8, 2, 3, 4];
134        buf.zeroize();
135        assert_eq!(buf, [0, 0, 0, 0]);
136    }
137
138    #[test]
139    fn zeroizing_derefs() {
140        let mut z = Zeroizing::new([7u8; 8]);
141        assert_eq!(z[0], 7);
142        z[0] = 9;
143        assert_eq!(z.get()[0], 9);
144    }
145
146    #[test]
147    fn eight_byte_chunks_and_the_tail_are_wiped_and_nothing_else() {
148        // Two whole chunks and a three-byte tail, inside a larger array whose
149        // bytes either side must survive: a chunked wipe that overran its
150        // slice would zero them.
151        let mut buf = [0xa5u8; 32];
152        buf[5..24].zeroize();
153        assert_eq!(&buf[..5], &[0xa5; 5]);
154        assert_eq!(&buf[5..24], &[0u8; 19]);
155        assert_eq!(&buf[24..], &[0xa5; 8]);
156        for len in 0..=17 {
157            let mut v = [0xffu8; 17];
158            v[..len].zeroize();
159            assert!(v[..len].iter().all(|&b| b == 0), "length {len}");
160            assert!(v[len..].iter().all(|&b| b == 0xff), "length {len}");
161        }
162    }
163
164    #[test]
165    fn word_slices_are_wiped() {
166        let mut w = [0xDEAD_BEEFu32; 4];
167        w.zeroize();
168        assert_eq!(w, [0u32; 4]);
169        let mut q = [0xDEAD_BEEF_CAFE_F00Du64; 2];
170        q.zeroize();
171        assert_eq!(q, [0u64; 2]);
172    }
173}