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