rustls/
compress.rs

1//! Certificate compression and decompression support
2//!
3//! This crate supports compression and decompression everywhere
4//! certificates are used, in accordance with [RFC8879][rfc8879].
5//!
6//! Note that this is only supported for TLS1.3 connections.
7//!
8//! # Getting started
9//!
10//! Build this crate with the `brotli` and/or `zlib` crate features.  This
11//! adds dependencies on these crates.  They are used by default if enabled.
12//!
13//! We especially recommend `brotli` as it has the widest deployment so far.
14//!
15//! # Custom compression/decompression implementations
16//!
17//! 1. Implement the [`CertCompressor`] and/or [`CertDecompressor`] traits
18//! 2. Provide those to:
19//!   - [`ClientConfig::cert_compressors`][cc_cc] or [`ServerConfig::cert_compressors`][sc_cc].
20//!   - [`ClientConfig::cert_decompressors`][cc_cd] or [`ServerConfig::cert_decompressors`][sc_cd].
21//!
22//! These are used in these circumstances:
23//!
24//! | Peer | Client authentication | Server authentication |
25//! | ---- | --------------------- | --------------------- |
26//! | *Client* | [`ClientConfig::cert_compressors`][cc_cc] | [`ClientConfig::cert_decompressors`][cc_cd] |
27//! | *Server* | [`ServerConfig::cert_decompressors`][sc_cd] | [`ServerConfig::cert_compressors`][sc_cc] |
28//!
29//! [rfc8879]: https://datatracker.ietf.org/doc/html/rfc8879
30//! [cc_cc]: crate::ClientConfig::cert_compressors
31//! [sc_cc]: crate::ServerConfig::cert_compressors
32//! [cc_cd]: crate::ClientConfig::cert_decompressors
33//! [sc_cd]: crate::ServerConfig::cert_decompressors
34
35#[cfg(feature = "std")]
36use alloc::collections::VecDeque;
37use alloc::sync::Arc;
38use alloc::vec::Vec;
39use core::fmt::Debug;
40#[cfg(feature = "std")]
41use std::sync::Mutex;
42
43use crate::enums::CertificateCompressionAlgorithm;
44use crate::msgs::base::{Payload, PayloadU24};
45use crate::msgs::codec::Codec;
46use crate::msgs::handshake::{CertificatePayloadTls13, CompressedCertificatePayload};
47
48/// Returns the supported `CertDecompressor` implementations enabled
49/// by crate features.
50pub fn default_cert_decompressors() -> &'static [&'static dyn CertDecompressor] {
51    &[
52        #[cfg(feature = "brotli")]
53        BROTLI_DECOMPRESSOR,
54        #[cfg(feature = "zlib")]
55        ZLIB_DECOMPRESSOR,
56    ]
57}
58
59/// An available certificate decompression algorithm.
60pub trait CertDecompressor: Debug + Send + Sync {
61    /// Decompress `input`, writing the result to `output`.
62    ///
63    /// `output` is sized to match the declared length of the decompressed data.
64    ///
65    /// `Err(DecompressionFailed)` should be returned if decompression produces more, or fewer
66    /// bytes than fit in `output`, or if the `input` is in any way malformed.
67    fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed>;
68
69    /// Which algorithm this decompressor handles.
70    fn algorithm(&self) -> CertificateCompressionAlgorithm;
71}
72
73/// Returns the supported `CertCompressor` implementations enabled
74/// by crate features.
75pub fn default_cert_compressors() -> &'static [&'static dyn CertCompressor] {
76    &[
77        #[cfg(feature = "brotli")]
78        BROTLI_COMPRESSOR,
79        #[cfg(feature = "zlib")]
80        ZLIB_COMPRESSOR,
81    ]
82}
83
84/// An available certificate compression algorithm.
85pub trait CertCompressor: Debug + Send + Sync {
86    /// Compress `input`, returning the result.
87    ///
88    /// `input` is consumed by this function so (if the underlying implementation
89    /// supports it) the compression can be performed in-place.
90    ///
91    /// `level` is a hint as to how much effort to expend on the compression.
92    ///
93    /// `Err(CompressionFailed)` may be returned for any reason.
94    fn compress(
95        &self,
96        input: Vec<u8>,
97        level: CompressionLevel,
98    ) -> Result<Vec<u8>, CompressionFailed>;
99
100    /// Which algorithm this compressor handles.
101    fn algorithm(&self) -> CertificateCompressionAlgorithm;
102}
103
104/// A hint for how many resources to dedicate to a compression.
105#[derive(Debug, Copy, Clone, Eq, PartialEq)]
106pub enum CompressionLevel {
107    /// This compression is happening interactively during a handshake.
108    ///
109    /// Implementations may wish to choose a conservative compression level.
110    Interactive,
111
112    /// The compression may be amortized over many connections.
113    ///
114    /// Implementations may wish to choose an aggressive compression level.
115    Amortized,
116}
117
118/// A content-less error for when `CertDecompressor::decompress` fails.
119#[derive(Debug)]
120pub struct DecompressionFailed;
121
122/// A content-less error for when `CertCompressor::compress` fails.
123#[derive(Debug)]
124pub struct CompressionFailed;
125
126#[cfg(feature = "zlib")]
127mod feat_zlib_rs {
128    use zlib_rs::c_api::Z_BEST_COMPRESSION;
129    use zlib_rs::{deflate, inflate, ReturnCode};
130
131    use super::*;
132
133    /// A certificate decompressor for the Zlib algorithm using the `zlib-rs` crate.
134    pub const ZLIB_DECOMPRESSOR: &dyn CertDecompressor = &ZlibRsDecompressor;
135
136    #[derive(Debug)]
137    struct ZlibRsDecompressor;
138
139    impl CertDecompressor for ZlibRsDecompressor {
140        fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> {
141            let output_len = output.len();
142            match inflate::uncompress_slice(output, input, inflate::InflateConfig::default()) {
143                (output_filled, ReturnCode::Ok) if output_filled.len() == output_len => Ok(()),
144                (_, _) => Err(DecompressionFailed),
145            }
146        }
147
148        fn algorithm(&self) -> CertificateCompressionAlgorithm {
149            CertificateCompressionAlgorithm::Zlib
150        }
151    }
152
153    /// A certificate compressor for the Zlib algorithm using the `zlib-rs` crate.
154    pub const ZLIB_COMPRESSOR: &dyn CertCompressor = &ZlibRsCompressor;
155
156    #[derive(Debug)]
157    struct ZlibRsCompressor;
158
159    impl CertCompressor for ZlibRsCompressor {
160        fn compress(
161            &self,
162            input: Vec<u8>,
163            level: CompressionLevel,
164        ) -> Result<Vec<u8>, CompressionFailed> {
165            let mut output = alloc::vec![0u8; deflate::compress_bound(input.len())];
166            let config = match level {
167                CompressionLevel::Interactive => deflate::DeflateConfig::default(),
168                CompressionLevel::Amortized => deflate::DeflateConfig::new(Z_BEST_COMPRESSION),
169            };
170            let (output_filled, rc) = deflate::compress_slice(&mut output, &input, config);
171            if rc != ReturnCode::Ok {
172                return Err(CompressionFailed);
173            }
174
175            let used = output_filled.len();
176            output.truncate(used);
177            Ok(output)
178        }
179
180        fn algorithm(&self) -> CertificateCompressionAlgorithm {
181            CertificateCompressionAlgorithm::Zlib
182        }
183    }
184}
185
186#[cfg(feature = "zlib")]
187pub use feat_zlib_rs::{ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR};
188
189#[cfg(feature = "brotli")]
190mod feat_brotli {
191    use std::io::{Cursor, Write};
192
193    use super::*;
194
195    /// A certificate decompressor for the brotli algorithm using the `brotli` crate.
196    pub const BROTLI_DECOMPRESSOR: &dyn CertDecompressor = &BrotliDecompressor;
197
198    #[derive(Debug)]
199    struct BrotliDecompressor;
200
201    impl CertDecompressor for BrotliDecompressor {
202        fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> {
203            let mut in_cursor = Cursor::new(input);
204            let mut out_cursor = Cursor::new(output);
205
206            brotli::BrotliDecompress(&mut in_cursor, &mut out_cursor)
207                .map_err(|_| DecompressionFailed)?;
208
209            if out_cursor.position() as usize != out_cursor.into_inner().len() {
210                return Err(DecompressionFailed);
211            }
212
213            Ok(())
214        }
215
216        fn algorithm(&self) -> CertificateCompressionAlgorithm {
217            CertificateCompressionAlgorithm::Brotli
218        }
219    }
220
221    /// A certificate compressor for the brotli algorithm using the `brotli` crate.
222    pub const BROTLI_COMPRESSOR: &dyn CertCompressor = &BrotliCompressor;
223
224    #[derive(Debug)]
225    struct BrotliCompressor;
226
227    impl CertCompressor for BrotliCompressor {
228        fn compress(
229            &self,
230            input: Vec<u8>,
231            level: CompressionLevel,
232        ) -> Result<Vec<u8>, CompressionFailed> {
233            let quality = match level {
234                CompressionLevel::Interactive => QUALITY_FAST,
235                CompressionLevel::Amortized => QUALITY_SLOW,
236            };
237            let output = Cursor::new(Vec::with_capacity(input.len() / 2));
238            let mut compressor = brotli::CompressorWriter::new(output, BUFFER_SIZE, quality, LGWIN);
239            compressor
240                .write_all(&input)
241                .map_err(|_| CompressionFailed)?;
242            Ok(compressor.into_inner().into_inner())
243        }
244
245        fn algorithm(&self) -> CertificateCompressionAlgorithm {
246            CertificateCompressionAlgorithm::Brotli
247        }
248    }
249
250    /// Brotli buffer size.
251    ///
252    /// Chosen based on brotli `examples/compress.rs`.
253    const BUFFER_SIZE: usize = 4096;
254
255    /// This is the default lgwin parameter, see `BrotliEncoderInitParams()`
256    const LGWIN: u32 = 22;
257
258    /// Compression quality we use for interactive compressions.
259    /// See <https://blog.cloudflare.com/results-experimenting-brotli> for data.
260    const QUALITY_FAST: u32 = 4;
261
262    /// Compression quality we use for offline compressions (the maximum).
263    const QUALITY_SLOW: u32 = 11;
264}
265
266#[cfg(feature = "brotli")]
267pub use feat_brotli::{BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR};
268
269/// An LRU cache for compressions.
270///
271/// The prospect of being able to reuse a given compression for many connections
272/// means we can afford to spend more time on that compression (by passing
273/// `CompressionLevel::Amortized` to the compressor).
274#[derive(Debug)]
275pub enum CompressionCache {
276    /// No caching happens, and compression happens each time using
277    /// `CompressionLevel::Interactive`.
278    Disabled,
279
280    /// Compressions are stored in an LRU cache.
281    #[cfg(feature = "std")]
282    Enabled(CompressionCacheInner),
283}
284
285/// Innards of an enabled CompressionCache.
286///
287/// You cannot make one of these directly. Use [`CompressionCache::new`].
288#[cfg(feature = "std")]
289#[derive(Debug)]
290pub struct CompressionCacheInner {
291    /// Maximum size of underlying storage.
292    size: usize,
293
294    /// LRU-order entries.
295    ///
296    /// First is least-used, last is most-used.
297    entries: Mutex<VecDeque<Arc<CompressionCacheEntry>>>,
298}
299
300impl CompressionCache {
301    /// Make a `CompressionCache` that stores up to `size` compressed
302    /// certificate messages.
303    #[cfg(feature = "std")]
304    pub fn new(size: usize) -> Self {
305        if size == 0 {
306            return Self::Disabled;
307        }
308
309        Self::Enabled(CompressionCacheInner {
310            size,
311            entries: Mutex::new(VecDeque::with_capacity(size)),
312        })
313    }
314
315    /// Return a `CompressionCacheEntry`, which is an owning
316    /// wrapper for a `CompressedCertificatePayload`.
317    ///
318    /// `compressor` is the compression function we have negotiated.
319    /// `original` is the uncompressed certificate message.
320    pub(crate) fn compression_for(
321        &self,
322        compressor: &dyn CertCompressor,
323        original: &CertificatePayloadTls13<'_>,
324    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
325        match self {
326            Self::Disabled => Self::uncached_compression(compressor, original),
327
328            #[cfg(feature = "std")]
329            Self::Enabled(_) => self.compression_for_impl(compressor, original),
330        }
331    }
332
333    #[cfg(feature = "std")]
334    fn compression_for_impl(
335        &self,
336        compressor: &dyn CertCompressor,
337        original: &CertificatePayloadTls13<'_>,
338    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
339        let (max_size, entries) = match self {
340            Self::Enabled(CompressionCacheInner { size, entries }) => (*size, entries),
341            _ => unreachable!(),
342        };
343
344        // context is a per-connection quantity, and included in the compressed data.
345        // it is not suitable for inclusion in the cache.
346        if !original.context.0.is_empty() {
347            return Self::uncached_compression(compressor, original);
348        }
349
350        // cache probe:
351        let encoding = original.get_encoding();
352        let algorithm = compressor.algorithm();
353
354        let mut cache = entries
355            .lock()
356            .map_err(|_| CompressionFailed)?;
357        for (i, item) in cache.iter().enumerate() {
358            if item.algorithm == algorithm && item.original == encoding {
359                // this item is now MRU
360                let item = cache.remove(i).unwrap();
361                cache.push_back(Arc::clone(&item));
362                return Ok(item);
363            }
364        }
365        drop(cache);
366
367        // do compression:
368        let uncompressed_len = encoding.len() as u32;
369        let compressed = compressor.compress(encoding.clone(), CompressionLevel::Amortized)?;
370        let new_entry = Arc::new(CompressionCacheEntry {
371            algorithm,
372            original: encoding,
373            compressed: CompressedCertificatePayload {
374                alg: algorithm,
375                uncompressed_len,
376                compressed: PayloadU24(Payload::new(compressed)),
377            },
378        });
379
380        // insert into cache
381        let mut cache = entries
382            .lock()
383            .map_err(|_| CompressionFailed)?;
384        if cache.len() == max_size {
385            cache.pop_front();
386        }
387        cache.push_back(Arc::clone(&new_entry));
388        Ok(new_entry)
389    }
390
391    /// Compress `original` using `compressor` at `Interactive` level.
392    fn uncached_compression(
393        compressor: &dyn CertCompressor,
394        original: &CertificatePayloadTls13<'_>,
395    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
396        let algorithm = compressor.algorithm();
397        let encoding = original.get_encoding();
398        let uncompressed_len = encoding.len() as u32;
399        let compressed = compressor.compress(encoding, CompressionLevel::Interactive)?;
400
401        // this `CompressionCacheEntry` in fact never makes it into the cache, so
402        // `original` is left empty
403        Ok(Arc::new(CompressionCacheEntry {
404            algorithm,
405            original: Vec::new(),
406            compressed: CompressedCertificatePayload {
407                alg: algorithm,
408                uncompressed_len,
409                compressed: PayloadU24(Payload::new(compressed)),
410            },
411        }))
412    }
413}
414
415impl Default for CompressionCache {
416    fn default() -> Self {
417        #[cfg(feature = "std")]
418        {
419            // 4 entries allows 2 certificate chains times 2 compression algorithms
420            Self::new(4)
421        }
422
423        #[cfg(not(feature = "std"))]
424        {
425            Self::Disabled
426        }
427    }
428}
429
430#[cfg_attr(not(feature = "std"), allow(dead_code))]
431#[derive(Debug)]
432pub(crate) struct CompressionCacheEntry {
433    // cache key is algorithm + original:
434    algorithm: CertificateCompressionAlgorithm,
435    original: Vec<u8>,
436
437    // cache value is compression result:
438    compressed: CompressedCertificatePayload<'static>,
439}
440
441impl CompressionCacheEntry {
442    pub(crate) fn compressed_cert_payload(&self) -> CompressedCertificatePayload<'_> {
443        self.compressed.as_borrowed()
444    }
445}
446
447#[cfg(all(test, any(feature = "brotli", feature = "zlib")))]
448mod tests {
449    use std::{println, vec};
450
451    use super::*;
452
453    #[test]
454    #[cfg(feature = "zlib")]
455    fn test_zlib() {
456        test_compressor(ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR);
457    }
458
459    #[test]
460    #[cfg(feature = "brotli")]
461    fn test_brotli() {
462        test_compressor(BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR);
463    }
464
465    fn test_compressor(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
466        assert_eq!(comp.algorithm(), decomp.algorithm());
467        for sz in [16, 64, 512, 2048, 8192, 16384] {
468            test_trivial_pairwise(comp, decomp, sz);
469        }
470        test_decompress_wrong_len(comp, decomp);
471        test_decompress_garbage(decomp);
472    }
473
474    fn test_trivial_pairwise(
475        comp: &dyn CertCompressor,
476        decomp: &dyn CertDecompressor,
477        plain_len: usize,
478    ) {
479        let original = vec![0u8; plain_len];
480
481        for level in [CompressionLevel::Interactive, CompressionLevel::Amortized] {
482            let compressed = comp
483                .compress(original.clone(), level)
484                .unwrap();
485            println!(
486                "{:?} compressed trivial {} -> {} using {:?} level",
487                comp.algorithm(),
488                original.len(),
489                compressed.len(),
490                level
491            );
492            let mut recovered = vec![0xffu8; plain_len];
493            decomp
494                .decompress(&compressed, &mut recovered)
495                .unwrap();
496            assert_eq!(original, recovered);
497        }
498    }
499
500    fn test_decompress_wrong_len(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
501        let original = vec![0u8; 2048];
502        let compressed = comp
503            .compress(original.clone(), CompressionLevel::Interactive)
504            .unwrap();
505        println!("{compressed:?}");
506
507        // too big
508        let mut recovered = vec![0xffu8; original.len() + 1];
509        decomp
510            .decompress(&compressed, &mut recovered)
511            .unwrap_err();
512
513        // too small
514        let mut recovered = vec![0xffu8; original.len() - 1];
515        decomp
516            .decompress(&compressed, &mut recovered)
517            .unwrap_err();
518    }
519
520    fn test_decompress_garbage(decomp: &dyn CertDecompressor) {
521        let junk = [0u8; 1024];
522        let mut recovered = vec![0u8; 512];
523        decomp
524            .decompress(&junk, &mut recovered)
525            .unwrap_err();
526    }
527
528    #[test]
529    #[cfg(all(feature = "brotli", feature = "zlib"))]
530    fn test_cache_evicts_lru() {
531        use core::sync::atomic::{AtomicBool, Ordering};
532
533        use pki_types::CertificateDer;
534
535        let cache = CompressionCache::default();
536
537        let cert = CertificateDer::from(vec![1]);
538
539        let cert1 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"1"));
540        let cert2 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"2"));
541        let cert3 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"3"));
542        let cert4 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"4"));
543
544        // insert zlib (1), (2), (3), (4)
545
546        cache
547            .compression_for(
548                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
549                &cert1,
550            )
551            .unwrap();
552        cache
553            .compression_for(
554                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
555                &cert2,
556            )
557            .unwrap();
558        cache
559            .compression_for(
560                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
561                &cert3,
562            )
563            .unwrap();
564        cache
565            .compression_for(
566                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
567                &cert4,
568            )
569            .unwrap();
570
571        // -- now full
572
573        // insert brotli (1) evicts zlib (1)
574        cache
575            .compression_for(
576                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
577                &cert4,
578            )
579            .unwrap();
580
581        // now zlib (2), (3), (4) and brotli (4) exist
582        cache
583            .compression_for(
584                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
585                &cert2,
586            )
587            .unwrap();
588        cache
589            .compression_for(
590                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
591                &cert3,
592            )
593            .unwrap();
594        cache
595            .compression_for(
596                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
597                &cert4,
598            )
599            .unwrap();
600        cache
601            .compression_for(
602                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), false),
603                &cert4,
604            )
605            .unwrap();
606
607        // insert zlib (1) requires re-compression & evicts zlib (2)
608        cache
609            .compression_for(
610                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
611                &cert1,
612            )
613            .unwrap();
614
615        // now zlib (1), (3), (4) and brotli (4) exist
616        // query zlib (4), (3), (1) to demonstrate LRU tracks usage rather than insertion
617        cache
618            .compression_for(
619                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
620                &cert4,
621            )
622            .unwrap();
623        cache
624            .compression_for(
625                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
626                &cert3,
627            )
628            .unwrap();
629        cache
630            .compression_for(
631                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
632                &cert1,
633            )
634            .unwrap();
635
636        // now brotli (4), zlib (4), (3), (1)
637        // insert brotli (1) evicting brotli (4)
638        cache
639            .compression_for(
640                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
641                &cert1,
642            )
643            .unwrap();
644
645        // verify brotli (4) disappeared
646        cache
647            .compression_for(
648                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
649                &cert4,
650            )
651            .unwrap();
652
653        #[derive(Debug)]
654        struct RequireCompress(&'static dyn CertCompressor, AtomicBool, bool);
655
656        impl CertCompressor for RequireCompress {
657            fn compress(
658                &self,
659                input: Vec<u8>,
660                level: CompressionLevel,
661            ) -> Result<Vec<u8>, CompressionFailed> {
662                self.1.store(true, Ordering::SeqCst);
663                self.0.compress(input, level)
664            }
665
666            fn algorithm(&self) -> CertificateCompressionAlgorithm {
667                self.0.algorithm()
668            }
669        }
670
671        impl Drop for RequireCompress {
672            fn drop(&mut self) {
673                assert_eq!(self.1.load(Ordering::SeqCst), self.2);
674            }
675        }
676    }
677}