rustls/ticketer.rs
1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::mem;
4
5use pki_types::UnixTime;
6
7use crate::lock::{Mutex, MutexGuard};
8use crate::server::ProducesTickets;
9#[cfg(not(feature = "std"))]
10use crate::time_provider::TimeProvider;
11use crate::{rand, Error};
12
13#[derive(Debug)]
14pub(crate) struct TicketSwitcherState {
15 next: Option<Box<dyn ProducesTickets>>,
16 current: Box<dyn ProducesTickets>,
17 previous: Option<Box<dyn ProducesTickets>>,
18 next_switch_time: u64,
19}
20
21/// A ticketer that has a 'current' sub-ticketer and a single
22/// 'previous' ticketer. It creates a new ticketer every so
23/// often, demoting the current ticketer.
24#[cfg_attr(feature = "std", derive(Debug))]
25pub struct TicketSwitcher {
26 pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
27 lifetime: u32,
28 state: Mutex<TicketSwitcherState>,
29 #[cfg(not(feature = "std"))]
30 time_provider: &'static dyn TimeProvider,
31}
32
33impl TicketSwitcher {
34 /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
35 /// based on the passage of time.
36 ///
37 /// `lifetime` is in seconds, and is how long the current ticketer
38 /// is used to generate new tickets. Tickets are accepted for no
39 /// longer than twice this duration. `generator` produces a new
40 /// `ProducesTickets` implementation.
41 #[cfg(feature = "std")]
42 pub fn new(
43 lifetime: u32,
44 generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
45 ) -> Result<Self, Error> {
46 Ok(Self {
47 generator,
48 lifetime,
49 state: Mutex::new(TicketSwitcherState {
50 next: Some(generator()?),
51 current: generator()?,
52 previous: None,
53 next_switch_time: UnixTime::now()
54 .as_secs()
55 .saturating_add(u64::from(lifetime)),
56 }),
57 })
58 }
59
60 /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
61 /// based on the passage of time.
62 ///
63 /// `lifetime` is in seconds, and is how long the current ticketer
64 /// is used to generate new tickets. Tickets are accepted for no
65 /// longer than twice this duration. `generator` produces a new
66 /// `ProducesTickets` implementation.
67 #[cfg(not(feature = "std"))]
68 pub fn new<M: crate::lock::MakeMutex>(
69 lifetime: u32,
70 generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
71 time_provider: &'static dyn TimeProvider,
72 ) -> Result<Self, Error> {
73 Ok(Self {
74 generator,
75 lifetime,
76 state: Mutex::new::<M>(TicketSwitcherState {
77 next: Some(generator()?),
78 current: generator()?,
79 previous: None,
80 next_switch_time: time_provider
81 .current_time()
82 .unwrap()
83 .as_secs()
84 .saturating_add(u64::from(lifetime)),
85 }),
86 time_provider,
87 })
88 }
89
90 /// If it's time, demote the `current` ticketer to `previous` (so it
91 /// does no new encryptions but can do decryption) and use next for a
92 /// new `current` ticketer.
93 ///
94 /// Calling this regularly will ensure timely key erasure. Otherwise,
95 /// key erasure will be delayed until the next encrypt/decrypt call.
96 ///
97 /// For efficiency, this is also responsible for locking the state mutex
98 /// and returning the mutexguard.
99 pub(crate) fn maybe_roll(&self, now: UnixTime) -> Option<MutexGuard<'_, TicketSwitcherState>> {
100 // The code below aims to make switching as efficient as possible
101 // in the common case that the generator never fails. To achieve this
102 // we run the following steps:
103 // 1. If no switch is necessary, just return the mutexguard
104 // 2. Shift over all of the ticketers (so current becomes previous,
105 // and next becomes current). After this, other threads can
106 // start using the new current ticketer.
107 // 3. unlock mutex and generate new ticketer.
108 // 4. Place new ticketer in next and return current
109 //
110 // There are a few things to note here. First, we don't check whether
111 // a new switch might be needed in step 4, even though, due to locking
112 // and entropy collection, significant amounts of time may have passed.
113 // This is to guarantee that the thread doing the switch will eventually
114 // make progress.
115 //
116 // Second, because next may be None, step 2 can fail. In that case
117 // we enter a recovery mode where we generate 2 new ticketers, one for
118 // next and one for the current ticketer. We then take the mutex a
119 // second time and redo the time check to see if a switch is still
120 // necessary.
121 //
122 // This somewhat convoluted approach ensures good availability of the
123 // mutex, by ensuring that the state is usable and the mutex not held
124 // during generation. It also ensures that, so long as the inner
125 // ticketer never generates panics during encryption/decryption,
126 // we are guaranteed to never panic when holding the mutex.
127
128 let now = now.as_secs();
129 let mut are_recovering = false; // Are we recovering from previous failure?
130 {
131 // Scope the mutex so we only take it for as long as needed
132 let mut state = self.state.lock()?;
133
134 // Fast path in case we do not need to switch to the next ticketer yet
135 if now <= state.next_switch_time {
136 return Some(state);
137 }
138
139 // Make the switch, or mark for recovery if not possible
140 if let Some(next) = state.next.take() {
141 state.previous = Some(mem::replace(&mut state.current, next));
142 state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
143 } else {
144 are_recovering = true;
145 }
146 }
147
148 // We always need a next, so generate it now
149 let next = (self.generator)().ok()?;
150 if !are_recovering {
151 // Normal path, generate new next and place it in the state
152 let mut state = self.state.lock()?;
153 state.next = Some(next);
154 Some(state)
155 } else {
156 // Recovering, generate also a new current ticketer, and modify state
157 // as needed. (we need to redo the time check, otherwise this might
158 // result in very rapid switching of ticketers)
159 let new_current = (self.generator)().ok()?;
160 let mut state = self.state.lock()?;
161 state.next = Some(next);
162 if now > state.next_switch_time {
163 state.previous = Some(mem::replace(&mut state.current, new_current));
164 state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
165 }
166 Some(state)
167 }
168 }
169}
170
171impl ProducesTickets for TicketSwitcher {
172 fn lifetime(&self) -> u32 {
173 self.lifetime * 2
174 }
175
176 fn enabled(&self) -> bool {
177 true
178 }
179
180 fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
181 #[cfg(feature = "std")]
182 let now = UnixTime::now();
183 #[cfg(not(feature = "std"))]
184 let now = self
185 .time_provider
186 .current_time()
187 .unwrap();
188
189 self.maybe_roll(now)?
190 .current
191 .encrypt(message)
192 }
193
194 fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
195 #[cfg(feature = "std")]
196 let now = UnixTime::now();
197 #[cfg(not(feature = "std"))]
198 let now = self
199 .time_provider
200 .current_time()
201 .unwrap();
202
203 let state = self.maybe_roll(now)?;
204
205 // Decrypt with the current key; if that fails, try with the previous.
206 state
207 .current
208 .decrypt(ciphertext)
209 .or_else(|| {
210 state
211 .previous
212 .as_ref()
213 .and_then(|previous| previous.decrypt(ciphertext))
214 })
215 }
216}
217
218#[cfg(not(feature = "std"))]
219impl core::fmt::Debug for TicketSwitcher {
220 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
221 f.debug_struct("TicketSwitcher")
222 .field("generator", &self.generator)
223 .field("lifetime", &self.lifetime)
224 .field("state", &**self.state.lock().unwrap())
225 .finish()
226 }
227}