rustls/crypto/aws_lc_rs/
ticketer.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::fmt;
4use core::fmt::{Debug, Formatter};
5use core::sync::atomic::{AtomicUsize, Ordering};
6
7use aws_lc_rs::cipher::{
8    AES_256, AES_256_KEY_LEN, AES_CBC_IV_LEN, DecryptionContext, PaddedBlockDecryptingKey,
9    PaddedBlockEncryptingKey, UnboundCipherKey,
10};
11use aws_lc_rs::{hmac, iv};
12
13use super::ring_like::rand::{SecureRandom, SystemRandom};
14use super::unspecified_err;
15use crate::error::Error;
16#[cfg(debug_assertions)]
17use crate::log::debug;
18use crate::polyfill::try_split_at;
19use crate::rand::GetRandomFailed;
20use crate::server::ProducesTickets;
21use crate::sync::Arc;
22
23/// A concrete, safe ticket creation mechanism.
24pub struct Ticketer {}
25
26impl Ticketer {
27    /// Make the recommended `Ticketer`.  This produces tickets
28    /// with a 12 hour life and randomly generated keys.
29    ///
30    /// The `Ticketer` uses the [RFC 5077 §4] "Recommended Ticket Construction",
31    /// using AES 256 for encryption and HMAC-SHA256 for ciphertext authentication.
32    ///
33    /// [RFC 5077 §4]: https://www.rfc-editor.org/rfc/rfc5077#section-4
34    #[cfg(feature = "std")]
35    pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
36        Ok(Arc::new(crate::ticketer::TicketRotator::new(
37            6 * 60 * 60,
38            make_ticket_generator,
39        )?))
40    }
41}
42
43fn make_ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
44    // NOTE(XXX): Unconditionally mapping errors to `GetRandomFailed` here is slightly
45    //   misleading in some cases (e.g. failure to construct a padded block cipher encrypting key).
46    //   However, we can't change the return type expected from a `TicketSwitcher` `generator`
47    //   without breaking semver.
48    //   Tracking in https://github.com/rustls/rustls/issues/2074
49    Ok(Box::new(
50        Rfc5077Ticketer::new().map_err(|_| GetRandomFailed)?,
51    ))
52}
53
54/// An RFC 5077 "Recommended Ticket Construction" implementation of a [`Ticketer`].
55struct Rfc5077Ticketer {
56    aes_encrypt_key: PaddedBlockEncryptingKey,
57    aes_decrypt_key: PaddedBlockDecryptingKey,
58    hmac_key: hmac::Key,
59    key_name: [u8; 16],
60    lifetime: u32,
61    maximum_ciphertext_len: AtomicUsize,
62}
63
64impl Rfc5077Ticketer {
65    fn new() -> Result<Self, Error> {
66        let rand = SystemRandom::new();
67
68        // Generate a random AES 256 key to use for AES CBC encryption.
69        let mut aes_key = [0u8; AES_256_KEY_LEN];
70        rand.fill(&mut aes_key)
71            .map_err(|_| GetRandomFailed)?;
72
73        // Convert the raw AES 256 key bytes into encrypting and decrypting keys using CBC mode and
74        // PKCS#7 padding. We don't want to store just the raw key bytes as constructing the
75        // cipher keys has some setup overhead. We can't store just the `UnboundCipherKey` since
76        // constructing the padded encrypt/decrypt specific types consume the `UnboundCipherKey`.
77        let aes_encrypt_key =
78            UnboundCipherKey::new(&AES_256, &aes_key[..]).map_err(unspecified_err)?;
79        let aes_encrypt_key =
80            PaddedBlockEncryptingKey::cbc_pkcs7(aes_encrypt_key).map_err(unspecified_err)?;
81
82        // Convert the raw AES 256 key bytes into a decrypting key using CBC PKCS#7 padding.
83        let aes_decrypt_key =
84            UnboundCipherKey::new(&AES_256, &aes_key[..]).map_err(unspecified_err)?;
85        let aes_decrypt_key =
86            PaddedBlockDecryptingKey::cbc_pkcs7(aes_decrypt_key).map_err(unspecified_err)?;
87
88        // Generate a random HMAC SHA256 key to use for HMAC authentication.
89        let hmac_key = hmac::Key::generate(hmac::HMAC_SHA256, &rand).map_err(unspecified_err)?;
90
91        // Generate a random key name.
92        let mut key_name = [0u8; 16];
93        rand.fill(&mut key_name)
94            .map_err(|_| GetRandomFailed)?;
95
96        Ok(Self {
97            aes_encrypt_key,
98            aes_decrypt_key,
99            hmac_key,
100            key_name,
101            lifetime: 60 * 60 * 12,
102            maximum_ciphertext_len: AtomicUsize::new(0),
103        })
104    }
105}
106
107impl ProducesTickets for Rfc5077Ticketer {
108    fn enabled(&self) -> bool {
109        true
110    }
111
112    fn lifetime(&self) -> u32 {
113        self.lifetime
114    }
115
116    /// Encrypt `message` and return the ciphertext.
117    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
118        // Encrypt the ticket state - the cipher module handles generating a random IV of
119        // appropriate size, returning it in the `DecryptionContext`.
120        let mut encrypted_state = Vec::from(message);
121        let dec_ctx = self
122            .aes_encrypt_key
123            .encrypt(&mut encrypted_state)
124            .ok()?;
125        let iv: &[u8] = (&dec_ctx).try_into().ok()?;
126
127        // Produce the MAC tag over the relevant context & encrypted state.
128        // Quoting RFC 5077:
129        //   "The Message Authentication Code (MAC) is calculated using HMAC-SHA-256 over
130        //    key_name (16 octets) and IV (16 octets), followed by the length of
131        //    the encrypted_state field (2 octets) and its contents (variable
132        //    length)."
133        let mut hmac_data =
134            Vec::with_capacity(self.key_name.len() + iv.len() + 2 + encrypted_state.len());
135        hmac_data.extend(&self.key_name);
136        hmac_data.extend(iv);
137        hmac_data.extend(
138            u16::try_from(encrypted_state.len())
139                .ok()?
140                .to_be_bytes(),
141        );
142        hmac_data.extend(&encrypted_state);
143        let tag = hmac::sign(&self.hmac_key, &hmac_data);
144        let tag = tag.as_ref();
145
146        // Combine the context, the encrypted state, and the tag to produce the final ciphertext.
147        // Ciphertext structure is:
148        //   key_name: [u8; 16]
149        //   iv: [u8; 16]
150        //   encrypted_state: [u8, _]
151        //   mac tag: [u8; 32]
152        let mut ciphertext =
153            Vec::with_capacity(self.key_name.len() + iv.len() + encrypted_state.len() + tag.len());
154        ciphertext.extend(self.key_name);
155        ciphertext.extend(iv);
156        ciphertext.extend(encrypted_state);
157        ciphertext.extend(tag);
158
159        self.maximum_ciphertext_len
160            .fetch_max(ciphertext.len(), Ordering::SeqCst);
161
162        Some(ciphertext)
163    }
164
165    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
166        if ciphertext.len()
167            > self
168                .maximum_ciphertext_len
169                .load(Ordering::SeqCst)
170        {
171            #[cfg(debug_assertions)]
172            debug!("rejected over-length ticket");
173            return None;
174        }
175
176        // Split off the key name from the remaining ciphertext.
177        let (alleged_key_name, ciphertext) = try_split_at(ciphertext, self.key_name.len())?;
178
179        // Split off the IV from the remaining ciphertext.
180        let (iv, ciphertext) = try_split_at(ciphertext, AES_CBC_IV_LEN)?;
181
182        // And finally, split the encrypted state from the tag.
183        let tag_len = self
184            .hmac_key
185            .algorithm()
186            .digest_algorithm()
187            .output_len();
188        let (enc_state, mac) = try_split_at(ciphertext, ciphertext.len() - tag_len)?;
189
190        // Reconstitute the HMAC data to verify the tag.
191        let mut hmac_data =
192            Vec::with_capacity(alleged_key_name.len() + iv.len() + 2 + enc_state.len());
193        hmac_data.extend(alleged_key_name);
194        hmac_data.extend(iv);
195        hmac_data.extend(
196            u16::try_from(enc_state.len())
197                .ok()?
198                .to_be_bytes(),
199        );
200        hmac_data.extend(enc_state);
201        hmac::verify(&self.hmac_key, &hmac_data, mac).ok()?;
202
203        // Convert the raw IV back into an appropriate decryption context.
204        let iv = iv::FixedLength::try_from(iv).ok()?;
205        let dec_context = DecryptionContext::Iv128(iv);
206
207        // And finally, decrypt the encrypted state.
208        let mut out = Vec::from(enc_state);
209        let plaintext = self
210            .aes_decrypt_key
211            .decrypt(&mut out, dec_context)
212            .ok()?;
213
214        Some(plaintext.into())
215    }
216}
217
218impl Debug for Rfc5077Ticketer {
219    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
220        // Note: we deliberately omit keys from the debug output.
221        f.debug_struct("Rfc5077Ticketer")
222            .field("lifetime", &self.lifetime)
223            .finish()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use core::time::Duration;
230
231    use pki_types::UnixTime;
232
233    use super::*;
234
235    #[test]
236    fn basic_pairwise_test() {
237        let t = Ticketer::new().unwrap();
238        assert!(t.enabled());
239        let cipher = t.encrypt(b"hello world").unwrap();
240        let plain = t.decrypt(&cipher).unwrap();
241        assert_eq!(plain, b"hello world");
242    }
243
244    #[test]
245    fn refuses_decrypt_before_encrypt() {
246        let t = Ticketer::new().unwrap();
247        assert_eq!(t.decrypt(b"hello"), None);
248    }
249
250    #[test]
251    fn refuses_decrypt_larger_than_largest_encryption() {
252        let t = Ticketer::new().unwrap();
253        let mut cipher = t.encrypt(b"hello world").unwrap();
254        assert_eq!(t.decrypt(&cipher), Some(b"hello world".to_vec()));
255
256        // obviously this would never work anyway, but this
257        // and `cannot_decrypt_before_encrypt` exercise the
258        // first branch in `decrypt()`
259        cipher.push(0);
260        assert_eq!(t.decrypt(&cipher), None);
261    }
262
263    #[test]
264    fn ticketrotator_switching_test() {
265        let t = Arc::new(crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap());
266        let now = UnixTime::now();
267        let cipher1 = t.encrypt(b"ticket 1").unwrap();
268        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
269        {
270            // Trigger new ticketer
271            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
272                now.as_secs() + 10,
273            )));
274        }
275        let cipher2 = t.encrypt(b"ticket 2").unwrap();
276        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
277        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
278        {
279            // Trigger new ticketer
280            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
281                now.as_secs() + 20,
282            )));
283        }
284        let cipher3 = t.encrypt(b"ticket 3").unwrap();
285        assert!(t.decrypt(&cipher1).is_none());
286        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
287        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
288    }
289
290    #[test]
291    fn ticketrotator_remains_usable_over_temporary_ticketer_creation_failure() {
292        let mut t = crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap();
293        let now = UnixTime::now();
294        let cipher1 = t.encrypt(b"ticket 1").unwrap();
295        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
296        t.generator = fail_generator;
297        {
298            // Failed new ticketer; this means we still need to
299            // rotate.
300            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
301                now.as_secs() + 10,
302            )));
303        }
304
305        // check post-failure encryption/decryption still works
306        let cipher2 = t.encrypt(b"ticket 2").unwrap();
307        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
308        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
309
310        // do the rotation for real
311        t.generator = make_ticket_generator;
312        {
313            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
314                now.as_secs() + 20,
315            )));
316        }
317        let cipher3 = t.encrypt(b"ticket 3").unwrap();
318        assert!(t.decrypt(&cipher1).is_some());
319        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
320        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
321    }
322
323    #[test]
324    fn ticketswitcher_switching_test() {
325        #[expect(deprecated)]
326        let t = Arc::new(crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap());
327        let now = UnixTime::now();
328        let cipher1 = t.encrypt(b"ticket 1").unwrap();
329        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
330        {
331            // Trigger new ticketer
332            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
333                now.as_secs() + 10,
334            )));
335        }
336        let cipher2 = t.encrypt(b"ticket 2").unwrap();
337        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
338        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
339        {
340            // Trigger new ticketer
341            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
342                now.as_secs() + 20,
343            )));
344        }
345        let cipher3 = t.encrypt(b"ticket 3").unwrap();
346        assert!(t.decrypt(&cipher1).is_none());
347        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
348        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
349    }
350
351    #[test]
352    fn ticketswitcher_recover_test() {
353        #[expect(deprecated)]
354        let mut t = crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap();
355        let now = UnixTime::now();
356        let cipher1 = t.encrypt(b"ticket 1").unwrap();
357        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
358        t.generator = fail_generator;
359        {
360            // Failed new ticketer
361            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
362                now.as_secs() + 10,
363            )));
364        }
365        t.generator = make_ticket_generator;
366        let cipher2 = t.encrypt(b"ticket 2").unwrap();
367        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
368        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
369        {
370            // recover
371            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
372                now.as_secs() + 20,
373            )));
374        }
375        let cipher3 = t.encrypt(b"ticket 3").unwrap();
376        assert!(t.decrypt(&cipher1).is_none());
377        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
378        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
379    }
380
381    #[test]
382    fn rfc5077ticketer_is_debug_and_producestickets() {
383        use alloc::format;
384
385        use super::*;
386
387        let t = make_ticket_generator().unwrap();
388
389        assert_eq!(format!("{t:?}"), "Rfc5077Ticketer { lifetime: 43200 }");
390        assert!(t.enabled());
391        assert_eq!(t.lifetime(), 43200);
392    }
393
394    fn fail_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
395        Err(GetRandomFailed)
396    }
397}