1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::fmt;
4use core::fmt::{Debug, Formatter};
5use core::sync::atomic::{AtomicUsize, Ordering};
6
7use subtle::ConstantTimeEq;
8
9use super::ring_like::aead;
10use super::ring_like::rand::{SecureRandom, SystemRandom};
11use crate::error::Error;
12#[cfg(debug_assertions)]
13use crate::log::debug;
14use crate::polyfill::try_split_at;
15use crate::rand::GetRandomFailed;
16use crate::server::ProducesTickets;
17use crate::sync::Arc;
18
19/// A concrete, safe ticket creation mechanism.
20pub struct Ticketer {}
21
22impl Ticketer {
23 /// Make the recommended `Ticketer`. This produces tickets
24 /// with a 12 hour life and randomly generated keys.
25 ///
26 /// The encryption mechanism used is Chacha20Poly1305.
27 #[cfg(feature = "std")]
28 pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
29 Ok(Arc::new(crate::ticketer::TicketRotator::new(
30 6 * 60 * 60,
31 make_ticket_generator,
32 )?))
33 }
34
35 /// Make the recommended `Ticketer`. This produces tickets
36 /// with a 12 hour life and randomly generated keys.
37 ///
38 /// The encryption mechanism used is Chacha20Poly1305.
39 #[cfg(not(feature = "std"))]
40 pub fn new<M: crate::lock::MakeMutex>(
41 time_provider: &'static dyn TimeProvider,
42 ) -> Result<Arc<dyn ProducesTickets>, Error> {
43 Ok(Arc::new(crate::ticketer::TicketSwitcher::new::<M>(
44 6 * 60 * 60,
45 make_ticket_generator,
46 time_provider,
47 )?))
48 }
49}
50
51fn make_ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
52 Ok(Box::new(AeadTicketer::new()?))
53}
54
55/// This is a `ProducesTickets` implementation which uses
56/// any *ring* `aead::Algorithm` to encrypt and authentication
57/// the ticket payload. It does not enforce any lifetime
58/// constraint.
59struct AeadTicketer {
60 alg: &'static aead::Algorithm,
61 key: aead::LessSafeKey,
62 key_name: [u8; 16],
63 lifetime: u32,
64
65 /// Tracks the largest ciphertext produced by `encrypt`, and
66 /// uses it to early-reject `decrypt` queries that are too long.
67 ///
68 /// Accepting excessively long ciphertexts means a "Partitioning
69 /// Oracle Attack" (see <https://eprint.iacr.org/2020/1491.pdf>)
70 /// can be more efficient, though also note that these are thought
71 /// to be cryptographically hard if the key is full-entropy (as it
72 /// is here).
73 maximum_ciphertext_len: AtomicUsize,
74}
75
76impl AeadTicketer {
77 fn new() -> Result<Self, GetRandomFailed> {
78 let mut key: [u8; 32] = [0u8; 32];
79 SystemRandom::new()
80 .fill(&mut key)
81 .map_err(|_| GetRandomFailed)?;
82
83 let key: UnboundKey = aead::UnboundKey::new(algorithm:TICKETER_AEAD, &key).unwrap();
84
85 let mut key_name: [u8; 16] = [0u8; 16];
86 SystemRandom::new()
87 .fill(&mut key_name)
88 .map_err(|_| GetRandomFailed)?;
89
90 Ok(Self {
91 alg: TICKETER_AEAD,
92 key: aead::LessSafeKey::new(key),
93 key_name,
94 lifetime: 60 * 60 * 12,
95 maximum_ciphertext_len: AtomicUsize::new(0),
96 })
97 }
98}
99
100impl ProducesTickets for AeadTicketer {
101 fn enabled(&self) -> bool {
102 true
103 }
104
105 fn lifetime(&self) -> u32 {
106 self.lifetime
107 }
108
109 /// Encrypt `message` and return the ciphertext.
110 fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
111 // Random nonce, because a counter is a privacy leak.
112 let mut nonce_buf = [0u8; 12];
113 SystemRandom::new()
114 .fill(&mut nonce_buf)
115 .ok()?;
116 let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
117 let aad = aead::Aad::from(self.key_name);
118
119 // ciphertext structure is:
120 // key_name: [u8; 16]
121 // nonce: [u8; 12]
122 // message: [u8, _]
123 // tag: [u8; 16]
124
125 let mut ciphertext = Vec::with_capacity(
126 self.key_name.len() + nonce_buf.len() + message.len() + self.key.algorithm().tag_len(),
127 );
128 ciphertext.extend(self.key_name);
129 ciphertext.extend(nonce_buf);
130 ciphertext.extend(message);
131 let ciphertext = self
132 .key
133 .seal_in_place_separate_tag(
134 nonce,
135 aad,
136 &mut ciphertext[self.key_name.len() + nonce_buf.len()..],
137 )
138 .map(|tag| {
139 ciphertext.extend(tag.as_ref());
140 ciphertext
141 })
142 .ok()?;
143
144 self.maximum_ciphertext_len
145 .fetch_max(ciphertext.len(), Ordering::SeqCst);
146 Some(ciphertext)
147 }
148
149 /// Decrypt `ciphertext` and recover the original message.
150 fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
151 if ciphertext.len()
152 > self
153 .maximum_ciphertext_len
154 .load(Ordering::SeqCst)
155 {
156 #[cfg(debug_assertions)]
157 debug!("rejected over-length ticket");
158 return None;
159 }
160
161 let (alleged_key_name, ciphertext) = try_split_at(ciphertext, self.key_name.len())?;
162
163 let (nonce, ciphertext) = try_split_at(ciphertext, self.alg.nonce_len())?;
164
165 // checking the key_name is the expected one, *and* then putting it into the
166 // additionally authenticated data is duplicative. this check quickly rejects
167 // tickets for a different ticketer (see `TicketSwitcher`), while including it
168 // in the AAD ensures it is authenticated independent of that check and that
169 // any attempted attack on the integrity such as [^1] must happen for each
170 // `key_label`, not over a population of potential keys. this approach
171 // is overall similar to [^2].
172 //
173 // [^1]: https://eprint.iacr.org/2020/1491.pdf
174 // [^2]: "Authenticated Encryption with Key Identification", fig 6
175 // <https://eprint.iacr.org/2022/1680.pdf>
176 if ConstantTimeEq::ct_ne(&self.key_name[..], alleged_key_name).into() {
177 #[cfg(debug_assertions)]
178 debug!("rejected ticket with wrong ticket_name");
179 return None;
180 }
181
182 // This won't fail since `nonce` has the required length.
183 let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?;
184
185 let mut out = Vec::from(ciphertext);
186
187 let plain_len = self
188 .key
189 .open_in_place(nonce, aead::Aad::from(alleged_key_name), &mut out)
190 .ok()?
191 .len();
192 out.truncate(plain_len);
193
194 Some(out)
195 }
196}
197
198impl Debug for AeadTicketer {
199 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
200 // Note: we deliberately omit the key from the debug output.
201 f&mut DebugStruct<'_, '_>.debug_struct("AeadTicketer")
202 .field("alg", &self.alg)
203 .field(name:"lifetime", &self.lifetime)
204 .finish()
205 }
206}
207
208static TICKETER_AEAD: &aead::Algorithm = &aead::CHACHA20_POLY1305;
209
210#[cfg(test)]
211mod tests {
212 use core::time::Duration;
213
214 use pki_types::UnixTime;
215
216 use super::*;
217
218 #[test]
219 fn basic_pairwise_test() {
220 let t = Ticketer::new().unwrap();
221 assert!(t.enabled());
222 let cipher = t.encrypt(b"hello world").unwrap();
223 let plain = t.decrypt(&cipher).unwrap();
224 assert_eq!(plain, b"hello world");
225 }
226
227 #[test]
228 fn refuses_decrypt_before_encrypt() {
229 let t = Ticketer::new().unwrap();
230 assert_eq!(t.decrypt(b"hello"), None);
231 }
232
233 #[test]
234 fn refuses_decrypt_larger_than_largest_encryption() {
235 let t = Ticketer::new().unwrap();
236 let mut cipher = t.encrypt(b"hello world").unwrap();
237 assert_eq!(t.decrypt(&cipher), Some(b"hello world".to_vec()));
238
239 // obviously this would never work anyway, but this
240 // and `cannot_decrypt_before_encrypt` exercise the
241 // first branch in `decrypt()`
242 cipher.push(0);
243 assert_eq!(t.decrypt(&cipher), None);
244 }
245
246 #[test]
247 fn ticketrotator_switching_test() {
248 let t = Arc::new(crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap());
249 let now = UnixTime::now();
250 let cipher1 = t.encrypt(b"ticket 1").unwrap();
251 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
252 {
253 // Trigger new ticketer
254 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
255 now.as_secs() + 10,
256 )));
257 }
258 let cipher2 = t.encrypt(b"ticket 2").unwrap();
259 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
260 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
261 {
262 // Trigger new ticketer
263 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
264 now.as_secs() + 20,
265 )));
266 }
267 let cipher3 = t.encrypt(b"ticket 3").unwrap();
268 assert!(t.decrypt(&cipher1).is_none());
269 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
270 assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
271 }
272
273 #[test]
274 fn ticketrotator_remains_usable_over_temporary_ticketer_creation_failure() {
275 let mut t = crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap();
276 let now = UnixTime::now();
277 let cipher1 = t.encrypt(b"ticket 1").unwrap();
278 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
279 t.generator = fail_generator;
280 {
281 // Failed new ticketer; this means we still need to
282 // rotate.
283 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
284 now.as_secs() + 10,
285 )));
286 }
287
288 // check post-failure encryption/decryption still works
289 let cipher2 = t.encrypt(b"ticket 2").unwrap();
290 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
291 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
292
293 // do the rotation for real
294 t.generator = make_ticket_generator;
295 {
296 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
297 now.as_secs() + 20,
298 )));
299 }
300 let cipher3 = t.encrypt(b"ticket 3").unwrap();
301 assert!(t.decrypt(&cipher1).is_some());
302 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
303 assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
304 }
305
306 #[test]
307 fn ticketswitcher_switching_test() {
308 #[expect(deprecated)]
309 let t = Arc::new(crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap());
310 let now = UnixTime::now();
311 let cipher1 = t.encrypt(b"ticket 1").unwrap();
312 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
313 {
314 // Trigger new ticketer
315 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
316 now.as_secs() + 10,
317 )));
318 }
319 let cipher2 = t.encrypt(b"ticket 2").unwrap();
320 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
321 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
322 {
323 // Trigger new ticketer
324 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
325 now.as_secs() + 20,
326 )));
327 }
328 let cipher3 = t.encrypt(b"ticket 3").unwrap();
329 assert!(t.decrypt(&cipher1).is_none());
330 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
331 assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
332 }
333
334 #[test]
335 fn ticketswitcher_recover_test() {
336 #[expect(deprecated)]
337 let mut t = crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap();
338 let now = UnixTime::now();
339 let cipher1 = t.encrypt(b"ticket 1").unwrap();
340 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
341 t.generator = fail_generator;
342 {
343 // Failed new ticketer
344 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
345 now.as_secs() + 10,
346 )));
347 }
348 t.generator = make_ticket_generator;
349 let cipher2 = t.encrypt(b"ticket 2").unwrap();
350 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
351 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
352 {
353 // recover
354 t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
355 now.as_secs() + 20,
356 )));
357 }
358 let cipher3 = t.encrypt(b"ticket 3").unwrap();
359 assert!(t.decrypt(&cipher1).is_none());
360 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
361 assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
362 }
363
364 #[test]
365 fn aeadticketer_is_debug_and_producestickets() {
366 use alloc::format;
367
368 use super::*;
369
370 let t = make_ticket_generator().unwrap();
371
372 let expect = format!("AeadTicketer {{ alg: {TICKETER_AEAD:?}, lifetime: 43200 }}");
373 assert_eq!(format!("{:?}", t), expect);
374 assert!(t.enabled());
375 assert_eq!(t.lifetime(), 43200);
376 }
377
378 fn fail_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
379 Err(GetRandomFailed)
380 }
381}
382