rustls/crypto/ring/
ticketer.rs

1use alloc::boxed::Box;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::fmt;
5use core::fmt::{Debug, Formatter};
6use core::sync::atomic::{AtomicUsize, Ordering};
7
8use subtle::ConstantTimeEq;
9
10use super::ring_like::aead;
11use super::ring_like::rand::{SecureRandom, SystemRandom};
12use crate::error::Error;
13#[cfg(debug_assertions)]
14use crate::log::debug;
15use crate::polyfill::try_split_at;
16use crate::rand::GetRandomFailed;
17use crate::server::ProducesTickets;
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 = [0u8; 32];
79        SystemRandom::new()
80            .fill(&mut key)
81            .map_err(|_| GetRandomFailed)?;
82
83        let key = aead::UnboundKey::new(TICKETER_AEAD, &key).unwrap();
84
85        let mut key_name = [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.debug_struct("AeadTicketer")
202            .field("alg", &self.alg)
203            .field("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}