1 | use alloc::boxed::Box; |
2 | use alloc::vec::Vec; |
3 | use core::mem; |
4 | #[cfg (feature = "std" )] |
5 | use std::sync::{RwLock, RwLockReadGuard}; |
6 | |
7 | use pki_types::UnixTime; |
8 | |
9 | use crate::lock::{Mutex, MutexGuard}; |
10 | use crate::server::ProducesTickets; |
11 | #[cfg (not(feature = "std" ))] |
12 | use crate::time_provider::TimeProvider; |
13 | use crate::{Error, rand}; |
14 | |
15 | #[derive (Debug)] |
16 | pub(crate) struct TicketSwitcherState { |
17 | next: Option<Box<dyn ProducesTickets>>, |
18 | current: Box<dyn ProducesTickets>, |
19 | previous: Option<Box<dyn ProducesTickets>>, |
20 | next_switch_time: u64, |
21 | } |
22 | |
23 | /// A ticketer that has a 'current' sub-ticketer and a single |
24 | /// 'previous' ticketer. It creates a new ticketer every so |
25 | /// often, demoting the current ticketer. |
26 | #[cfg_attr (feature = "std" , derive(Debug))] |
27 | pub struct TicketSwitcher { |
28 | pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
29 | lifetime: u32, |
30 | state: Mutex<TicketSwitcherState>, |
31 | #[cfg (not(feature = "std" ))] |
32 | time_provider: &'static dyn TimeProvider, |
33 | } |
34 | |
35 | impl TicketSwitcher { |
36 | /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers |
37 | /// based on the passage of time. |
38 | /// |
39 | /// `lifetime` is in seconds, and is how long the current ticketer |
40 | /// is used to generate new tickets. Tickets are accepted for no |
41 | /// longer than twice this duration. `generator` produces a new |
42 | /// `ProducesTickets` implementation. |
43 | #[cfg (feature = "std" )] |
44 | #[deprecated (note = "use TicketRotator instead" )] |
45 | pub fn new( |
46 | lifetime: u32, |
47 | generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
48 | ) -> Result<Self, Error> { |
49 | Ok(Self { |
50 | generator, |
51 | lifetime, |
52 | state: Mutex::new(TicketSwitcherState { |
53 | next: Some(generator()?), |
54 | current: generator()?, |
55 | previous: None, |
56 | next_switch_time: UnixTime::now() |
57 | .as_secs() |
58 | .saturating_add(u64::from(lifetime)), |
59 | }), |
60 | }) |
61 | } |
62 | |
63 | /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers |
64 | /// based on the passage of time. |
65 | /// |
66 | /// `lifetime` is in seconds, and is how long the current ticketer |
67 | /// is used to generate new tickets. Tickets are accepted for no |
68 | /// longer than twice this duration. `generator` produces a new |
69 | /// `ProducesTickets` implementation. |
70 | #[cfg (not(feature = "std" ))] |
71 | pub fn new<M: crate::lock::MakeMutex>( |
72 | lifetime: u32, |
73 | generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
74 | time_provider: &'static dyn TimeProvider, |
75 | ) -> Result<Self, Error> { |
76 | Ok(Self { |
77 | generator, |
78 | lifetime, |
79 | state: Mutex::new::<M>(TicketSwitcherState { |
80 | next: Some(generator()?), |
81 | current: generator()?, |
82 | previous: None, |
83 | next_switch_time: time_provider |
84 | .current_time() |
85 | .unwrap() |
86 | .as_secs() |
87 | .saturating_add(u64::from(lifetime)), |
88 | }), |
89 | time_provider, |
90 | }) |
91 | } |
92 | |
93 | /// If it's time, demote the `current` ticketer to `previous` (so it |
94 | /// does no new encryptions but can do decryption) and use next for a |
95 | /// new `current` ticketer. |
96 | /// |
97 | /// Calling this regularly will ensure timely key erasure. Otherwise, |
98 | /// key erasure will be delayed until the next encrypt/decrypt call. |
99 | /// |
100 | /// For efficiency, this is also responsible for locking the state mutex |
101 | /// and returning the mutexguard. |
102 | pub(crate) fn maybe_roll(&self, now: UnixTime) -> Option<MutexGuard<'_, TicketSwitcherState>> { |
103 | // The code below aims to make switching as efficient as possible |
104 | // in the common case that the generator never fails. To achieve this |
105 | // we run the following steps: |
106 | // 1. If no switch is necessary, just return the mutexguard |
107 | // 2. Shift over all of the ticketers (so current becomes previous, |
108 | // and next becomes current). After this, other threads can |
109 | // start using the new current ticketer. |
110 | // 3. unlock mutex and generate new ticketer. |
111 | // 4. Place new ticketer in next and return current |
112 | // |
113 | // There are a few things to note here. First, we don't check whether |
114 | // a new switch might be needed in step 4, even though, due to locking |
115 | // and entropy collection, significant amounts of time may have passed. |
116 | // This is to guarantee that the thread doing the switch will eventually |
117 | // make progress. |
118 | // |
119 | // Second, because next may be None, step 2 can fail. In that case |
120 | // we enter a recovery mode where we generate 2 new ticketers, one for |
121 | // next and one for the current ticketer. We then take the mutex a |
122 | // second time and redo the time check to see if a switch is still |
123 | // necessary. |
124 | // |
125 | // This somewhat convoluted approach ensures good availability of the |
126 | // mutex, by ensuring that the state is usable and the mutex not held |
127 | // during generation. It also ensures that, so long as the inner |
128 | // ticketer never generates panics during encryption/decryption, |
129 | // we are guaranteed to never panic when holding the mutex. |
130 | |
131 | let now = now.as_secs(); |
132 | let mut are_recovering = false; // Are we recovering from previous failure? |
133 | { |
134 | // Scope the mutex so we only take it for as long as needed |
135 | let mut state = self.state.lock()?; |
136 | |
137 | // Fast path in case we do not need to switch to the next ticketer yet |
138 | if now <= state.next_switch_time { |
139 | return Some(state); |
140 | } |
141 | |
142 | // Make the switch, or mark for recovery if not possible |
143 | match state.next.take() { |
144 | Some(next) => { |
145 | state.previous = Some(mem::replace(&mut state.current, next)); |
146 | state.next_switch_time = now.saturating_add(u64::from(self.lifetime)); |
147 | } |
148 | _ => are_recovering = true, |
149 | } |
150 | } |
151 | |
152 | // We always need a next, so generate it now |
153 | let next = (self.generator)().ok()?; |
154 | if !are_recovering { |
155 | // Normal path, generate new next and place it in the state |
156 | let mut state = self.state.lock()?; |
157 | state.next = Some(next); |
158 | Some(state) |
159 | } else { |
160 | // Recovering, generate also a new current ticketer, and modify state |
161 | // as needed. (we need to redo the time check, otherwise this might |
162 | // result in very rapid switching of ticketers) |
163 | let new_current = (self.generator)().ok()?; |
164 | let mut state = self.state.lock()?; |
165 | state.next = Some(next); |
166 | if now > state.next_switch_time { |
167 | state.previous = Some(mem::replace(&mut state.current, new_current)); |
168 | state.next_switch_time = now.saturating_add(u64::from(self.lifetime)); |
169 | } |
170 | Some(state) |
171 | } |
172 | } |
173 | } |
174 | |
175 | impl ProducesTickets for TicketSwitcher { |
176 | fn lifetime(&self) -> u32 { |
177 | self.lifetime * 2 |
178 | } |
179 | |
180 | fn enabled(&self) -> bool { |
181 | true |
182 | } |
183 | |
184 | fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> { |
185 | #[cfg (feature = "std" )] |
186 | let now = UnixTime::now(); |
187 | #[cfg (not(feature = "std" ))] |
188 | let now = self |
189 | .time_provider |
190 | .current_time() |
191 | .unwrap(); |
192 | |
193 | self.maybe_roll(now)? |
194 | .current |
195 | .encrypt(message) |
196 | } |
197 | |
198 | fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> { |
199 | #[cfg (feature = "std" )] |
200 | let now = UnixTime::now(); |
201 | #[cfg (not(feature = "std" ))] |
202 | let now = self |
203 | .time_provider |
204 | .current_time() |
205 | .unwrap(); |
206 | |
207 | let state = self.maybe_roll(now)?; |
208 | |
209 | // Decrypt with the current key; if that fails, try with the previous. |
210 | state |
211 | .current |
212 | .decrypt(ciphertext) |
213 | .or_else(|| { |
214 | state |
215 | .previous |
216 | .as_ref() |
217 | .and_then(|previous| previous.decrypt(ciphertext)) |
218 | }) |
219 | } |
220 | } |
221 | |
222 | #[cfg (not(feature = "std" ))] |
223 | impl core::fmt::Debug for TicketSwitcher { |
224 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
225 | f.debug_struct("TicketSwitcher" ) |
226 | .field("generator" , &self.generator) |
227 | .field("lifetime" , &self.lifetime) |
228 | .field("state" , &**self.state.lock().unwrap()) |
229 | .finish() |
230 | } |
231 | } |
232 | |
233 | #[cfg (feature = "std" )] |
234 | #[derive (Debug)] |
235 | pub(crate) struct TicketRotatorState { |
236 | current: Box<dyn ProducesTickets>, |
237 | previous: Option<Box<dyn ProducesTickets>>, |
238 | next_switch_time: u64, |
239 | } |
240 | |
241 | /// A ticketer that has a 'current' sub-ticketer and a single |
242 | /// 'previous' ticketer. It creates a new ticketer every so |
243 | /// often, demoting the current ticketer. |
244 | #[cfg (feature = "std" )] |
245 | pub struct TicketRotator { |
246 | pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
247 | lifetime: u32, |
248 | state: RwLock<TicketRotatorState>, |
249 | } |
250 | |
251 | #[cfg (feature = "std" )] |
252 | impl TicketRotator { |
253 | /// Creates a new `TicketRotator`, which rotates through sub-ticketers |
254 | /// based on the passage of time. |
255 | /// |
256 | /// `lifetime` is in seconds, and is how long the current ticketer |
257 | /// is used to generate new tickets. Tickets are accepted for no |
258 | /// longer than twice this duration. `generator` produces a new |
259 | /// `ProducesTickets` implementation. |
260 | pub fn new( |
261 | lifetime: u32, |
262 | generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
263 | ) -> Result<Self, Error> { |
264 | Ok(Self { |
265 | generator, |
266 | lifetime, |
267 | state: RwLock::new(TicketRotatorState { |
268 | current: generator()?, |
269 | previous: None, |
270 | next_switch_time: UnixTime::now() |
271 | .as_secs() |
272 | .saturating_add(u64::from(lifetime)), |
273 | }), |
274 | }) |
275 | } |
276 | |
277 | /// If it's time, demote the `current` ticketer to `previous` (so it |
278 | /// does no new encryptions but can do decryption) and replace it |
279 | /// with a new one. |
280 | /// |
281 | /// Calling this regularly will ensure timely key erasure. Otherwise, |
282 | /// key erasure will be delayed until the next encrypt/decrypt call. |
283 | /// |
284 | /// For efficiency, this is also responsible for locking the state rwlock |
285 | /// and returning it for read. |
286 | pub(crate) fn maybe_roll( |
287 | &self, |
288 | now: UnixTime, |
289 | ) -> Option<RwLockReadGuard<'_, TicketRotatorState>> { |
290 | let now = now.as_secs(); |
291 | |
292 | // Fast, common, & read-only path in case we do not need to switch |
293 | // to the next ticketer yet |
294 | { |
295 | let read = self.state.read().ok()?; |
296 | |
297 | if now <= read.next_switch_time { |
298 | return Some(read); |
299 | } |
300 | } |
301 | |
302 | // We need to switch ticketers, and make a new one. |
303 | // Generate a potential "next" ticketer outside the lock. |
304 | let next = (self.generator)().ok()?; |
305 | |
306 | let mut write = self.state.write().ok()?; |
307 | |
308 | if now <= write.next_switch_time { |
309 | // Another thread beat us to it. Nothing to do. |
310 | drop(write); |
311 | |
312 | return self.state.read().ok(); |
313 | } |
314 | |
315 | // Now we have: |
316 | // - confirmed we need rotation |
317 | // - confirmed we are the thread that will do it |
318 | // - successfully made the replacement ticketer |
319 | write.previous = Some(mem::replace(&mut write.current, next)); |
320 | write.next_switch_time = now.saturating_add(u64::from(self.lifetime)); |
321 | drop(write); |
322 | |
323 | self.state.read().ok() |
324 | } |
325 | } |
326 | |
327 | #[cfg (feature = "std" )] |
328 | impl ProducesTickets for TicketRotator { |
329 | fn lifetime(&self) -> u32 { |
330 | self.lifetime * 2 |
331 | } |
332 | |
333 | fn enabled(&self) -> bool { |
334 | true |
335 | } |
336 | |
337 | fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> { |
338 | self.maybe_roll(UnixTime::now())? |
339 | .current |
340 | .encrypt(message) |
341 | } |
342 | |
343 | fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> { |
344 | let state = self.maybe_roll(UnixTime::now())?; |
345 | |
346 | // Decrypt with the current key; if that fails, try with the previous. |
347 | state |
348 | .current |
349 | .decrypt(ciphertext) |
350 | .or_else(|| { |
351 | state |
352 | .previous |
353 | .as_ref() |
354 | .and_then(|previous| previous.decrypt(ciphertext)) |
355 | }) |
356 | } |
357 | } |
358 | |
359 | #[cfg (feature = "std" )] |
360 | impl core::fmt::Debug for TicketRotator { |
361 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
362 | fDebugStruct<'_, '_>.debug_struct(name:"TicketRotator" ) |
363 | .finish_non_exhaustive() |
364 | } |
365 | } |
366 | |