| 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 | |