umsh_ulcp_runtime/
reseed.rs

1//! A ChaCha20 generator that takes entropy gathered after it was built.
2//!
3//! A generator seeded once at boot knows only what the device knew at
4//! boot. On a board that stays up for months, entropy harvested later
5//! reaches the stored seed and the next boot, but nothing that is
6//! running. [`ReseedingRng`] closes that gap: whatever harvests entropy
7//! offers a fresh seed through a [`ReseedSlot`], and the generator folds
8//! it into its key the next time it is used.
9//!
10//! The slot is a `static`, so the harvester needs no handle to a
11//! generator that lives inside a MAC or a task. A generator nobody
12//! offers a seed to produces exactly the `ChaCha20Rng` stream for its
13//! boot seed.
14
15use core::cell::Cell;
16use core::sync::atomic::{AtomicBool, Ordering};
17
18use embassy_sync::blocking_mutex::Mutex;
19use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
20use rand_chacha::ChaCha20Rng;
21use rand_core::{CryptoRng, RngCore, SeedableRng};
22use umsh_crypto::hkdf_sha256;
23use umsh_crypto::software::SoftwareSha256;
24use zeroize::Zeroize;
25
26const INFO_RESEED: &[u8] = b"umsh-reseeding-rng-v1";
27
28/// Where a fresh seed waits for one running generator.
29pub struct ReseedSlot {
30    /// Checked before the lock, so a generator with nothing waiting pays
31    /// one atomic load per use.
32    pending: AtomicBool,
33    seed: Mutex<CriticalSectionRawMutex, Cell<[u8; 32]>>,
34}
35
36impl ReseedSlot {
37    pub const fn new() -> Self {
38        Self {
39            pending: AtomicBool::new(false),
40            seed: Mutex::new(Cell::new([0; 32])),
41        }
42    }
43
44    /// Offer a fresh seed to the generator reading this slot.
45    ///
46    /// A seed not yet taken is replaced. Offers are expected to come
47    /// from one accumulating pool, where a later draw already carries
48    /// everything an earlier one did.
49    pub fn offer(&self, seed: [u8; 32]) {
50        self.seed.lock(|slot| {
51            slot.set(seed);
52            self.pending.store(true, Ordering::Release);
53        });
54    }
55
56    fn take(&self) -> Option<[u8; 32]> {
57        if !self.pending.load(Ordering::Acquire) {
58            return None;
59        }
60        self.seed.lock(|slot| {
61            self.pending
62                .swap(false, Ordering::AcqRel)
63                .then(|| slot.replace([0; 32]))
64        })
65    }
66}
67
68impl Default for ReseedSlot {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74/// ChaCha20, rekeyed whenever its [`ReseedSlot`] holds a fresh seed.
75pub struct ReseedingRng {
76    rng: ChaCha20Rng,
77    slot: &'static ReseedSlot,
78}
79
80impl ReseedingRng {
81    pub fn new(seed: [u8; 32], slot: &'static ReseedSlot) -> Self {
82        Self {
83            rng: ChaCha20Rng::from_seed(seed),
84            slot,
85        }
86    }
87
88    /// Fold a waiting seed into the key.
89    ///
90    /// The new key is derived from the fresh seed and from output of the
91    /// running generator, so it is at least as strong as either: a weak
92    /// harvest cannot displace what the generator already had, and a
93    /// compromised generator is healed by a good harvest.
94    fn refresh(&mut self) {
95        let Some(mut fresh) = self.slot.take() else {
96            return;
97        };
98        let mut current = [0u8; 32];
99        self.rng.fill_bytes(&mut current);
100        let mut next = [0u8; 32];
101        hkdf_sha256(&SoftwareSha256, &fresh, &current, INFO_RESEED, &mut next);
102        self.rng = ChaCha20Rng::from_seed(next);
103        fresh.zeroize();
104        current.zeroize();
105        next.zeroize();
106    }
107}
108
109impl RngCore for ReseedingRng {
110    fn next_u32(&mut self) -> u32 {
111        self.refresh();
112        self.rng.next_u32()
113    }
114
115    fn next_u64(&mut self) -> u64 {
116        self.refresh();
117        self.rng.next_u64()
118    }
119
120    fn fill_bytes(&mut self, dest: &mut [u8]) {
121        self.refresh();
122        self.rng.fill_bytes(dest);
123    }
124
125    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
126        self.fill_bytes(dest);
127        Ok(())
128    }
129}
130
131// ChaCha20 keyed from a cryptographic seed, rekeyed only through HKDF.
132impl CryptoRng for ReseedingRng {}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    fn slot() -> &'static ReseedSlot {
139        Box::leak(Box::new(ReseedSlot::new()))
140    }
141
142    fn draw(rng: &mut impl RngCore) -> [u8; 48] {
143        let mut out = [0u8; 48];
144        rng.fill_bytes(&mut out);
145        out
146    }
147
148    #[test]
149    fn an_unreseeded_stream_is_plain_chacha20() {
150        let mut reseeding = ReseedingRng::new([7; 32], slot());
151        let mut plain = ChaCha20Rng::from_seed([7; 32]);
152        assert_eq!(draw(&mut reseeding), draw(&mut plain));
153        assert_eq!(reseeding.next_u32(), plain.next_u32());
154        assert_eq!(reseeding.next_u64(), plain.next_u64());
155        assert_eq!(draw(&mut reseeding), draw(&mut plain));
156    }
157
158    #[test]
159    fn a_reseed_takes_effect_at_the_next_use_and_only_once() {
160        let slot = slot();
161        let mut reseeding = ReseedingRng::new([7; 32], slot);
162        let mut plain = ChaCha20Rng::from_seed([7; 32]);
163        assert_eq!(draw(&mut reseeding), draw(&mut plain));
164
165        slot.offer([9; 32]);
166        let after = draw(&mut reseeding);
167        assert_ne!(after, draw(&mut plain));
168        // The slot is spent: the stream continues from the new key
169        // rather than rekeying on every use.
170        let mut twin = ReseedingRng::new([7; 32], self::slot());
171        draw(&mut twin);
172        twin.slot.offer([9; 32]);
173        assert_eq!(draw(&mut twin), after);
174        assert_eq!(draw(&mut twin), draw(&mut reseeding));
175    }
176
177    #[test]
178    fn the_new_key_depends_on_the_old_state_and_the_fresh_seed() {
179        let reseeded = |boot: u8, fresh: u8| {
180            let slot = slot();
181            let mut rng = ReseedingRng::new([boot; 32], slot);
182            slot.offer([fresh; 32]);
183            draw(&mut rng)
184        };
185        // Same harvest, different generators: knowing the harvest does
186        // not give away the stream.
187        assert_ne!(reseeded(1, 9), reseeded(2, 9));
188        // Same generator, different harvests: a cloned boot seed is
189        // healed.
190        assert_ne!(reseeded(1, 9), reseeded(1, 8));
191        assert_eq!(reseeded(1, 9), reseeded(1, 9));
192    }
193
194    #[test]
195    fn where_the_generator_has_got_to_matters() {
196        let slot_a = slot();
197        let mut a = ReseedingRng::new([3; 32], slot_a);
198        let slot_b = slot();
199        let mut b = ReseedingRng::new([3; 32], slot_b);
200        draw(&mut b);
201        slot_a.offer([5; 32]);
202        slot_b.offer([5; 32]);
203        assert_ne!(draw(&mut a), draw(&mut b));
204    }
205
206    #[test]
207    fn a_later_offer_replaces_one_not_yet_taken() {
208        let slot_a = slot();
209        let mut a = ReseedingRng::new([4; 32], slot_a);
210        slot_a.offer([1; 32]);
211        slot_a.offer([2; 32]);
212
213        let slot_b = slot();
214        let mut b = ReseedingRng::new([4; 32], slot_b);
215        slot_b.offer([2; 32]);
216        assert_eq!(draw(&mut a), draw(&mut b));
217    }
218}