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