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