Skip to main content

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::vec::Vec;
38use core::fmt::Debug;
39#[cfg(feature = "std")]
40use std::sync::Mutex;
41
42use crate::enums::CertificateCompressionAlgorithm;
43use crate::msgs::base::{Payload, PayloadU24};
44use crate::msgs::codec::Codec;
45use crate::msgs::handshake::{CertificatePayloadTls13, CompressedCertificatePayload};
46use crate::sync::Arc;
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::{
129        DeflateConfig, InflateConfig, ReturnCode, compress_bound, compress_slice, decompress_slice,
130    };
131
132    use super::*;
133
134    /// A certificate decompressor for the Zlib algorithm using the `zlib-rs` crate.
135    pub const ZLIB_DECOMPRESSOR: &dyn CertDecompressor = &ZlibRsDecompressor;
136
137    #[derive(Debug)]
138    struct ZlibRsDecompressor;
139
140    impl CertDecompressor for ZlibRsDecompressor {
141        fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> {
142            let output_len = output.len();
143            match decompress_slice(output, input, InflateConfig::default()) {
144                (output_filled, ReturnCode::Ok) if output_filled.len() == output_len => Ok(()),
145                (_, _) => Err(DecompressionFailed),
146            }
147        }
148
149        fn algorithm(&self) -> CertificateCompressionAlgorithm {
150            CertificateCompressionAlgorithm::Zlib
151        }
152    }
153
154    /// A certificate compressor for the Zlib algorithm using the `zlib-rs` crate.
155    pub const ZLIB_COMPRESSOR: &dyn CertCompressor = &ZlibRsCompressor;
156
157    #[derive(Debug)]
158    struct ZlibRsCompressor;
159
160    impl CertCompressor for ZlibRsCompressor {
161        fn compress(
162            &self,
163            input: Vec<u8>,
164            level: CompressionLevel,
165        ) -> Result<Vec<u8>, CompressionFailed> {
166            let mut output = alloc::vec![0u8; compress_bound(input.len())];
167            let config = match level {
168                CompressionLevel::Interactive => DeflateConfig::default(),
169                CompressionLevel::Amortized => DeflateConfig::best_compression(),
170            };
171            let (output_filled, rc) = compress_slice(&mut output, &input, config);
172            if rc != ReturnCode::Ok {
173                return Err(CompressionFailed);
174            }
175
176            let used = output_filled.len();
177            output.truncate(used);
178            Ok(output)
179        }
180
181        fn algorithm(&self) -> CertificateCompressionAlgorithm {
182            CertificateCompressionAlgorithm::Zlib
183        }
184    }
185}
186
187#[cfg(feature = "zlib")]
188pub use feat_zlib_rs::{ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR};
189
190#[allow(clippy::std_instead_of_core)] // awaits core::io::Cursor (1.97) in crate MSRV
191#[cfg(feature = "brotli")]
192mod feat_brotli {
193    use std::io::{Cursor, Write};
194
195    use super::*;
196
197    /// A certificate decompressor for the brotli algorithm using the `brotli` crate.
198    pub const BROTLI_DECOMPRESSOR: &dyn CertDecompressor = &BrotliDecompressor;
199
200    #[derive(Debug)]
201    struct BrotliDecompressor;
202
203    impl CertDecompressor for BrotliDecompressor {
204        fn decompress(&self, input: &[u8], output: &mut [u8]) -> Result<(), DecompressionFailed> {
205            let mut in_cursor = Cursor::new(input);
206            let mut out_cursor = Cursor::new(output);
207
208            brotli::BrotliDecompress(&mut in_cursor, &mut out_cursor)
209                .map_err(|_| DecompressionFailed)?;
210
211            if out_cursor.position() as usize != out_cursor.into_inner().len() {
212                return Err(DecompressionFailed);
213            }
214
215            Ok(())
216        }
217
218        fn algorithm(&self) -> CertificateCompressionAlgorithm {
219            CertificateCompressionAlgorithm::Brotli
220        }
221    }
222
223    /// A certificate compressor for the brotli algorithm using the `brotli` crate.
224    pub const BROTLI_COMPRESSOR: &dyn CertCompressor = &BrotliCompressor;
225
226    #[derive(Debug)]
227    struct BrotliCompressor;
228
229    impl CertCompressor for BrotliCompressor {
230        fn compress(
231            &self,
232            input: Vec<u8>,
233            level: CompressionLevel,
234        ) -> Result<Vec<u8>, CompressionFailed> {
235            let quality = match level {
236                CompressionLevel::Interactive => QUALITY_FAST,
237                CompressionLevel::Amortized => QUALITY_SLOW,
238            };
239            let output = Cursor::new(Vec::with_capacity(input.len() / 2));
240            let mut compressor = brotli::CompressorWriter::new(output, BUFFER_SIZE, quality, LGWIN);
241            compressor
242                .write_all(&input)
243                .map_err(|_| CompressionFailed)?;
244            Ok(compressor.into_inner().into_inner())
245        }
246
247        fn algorithm(&self) -> CertificateCompressionAlgorithm {
248            CertificateCompressionAlgorithm::Brotli
249        }
250    }
251
252    /// Brotli buffer size.
253    ///
254    /// Chosen based on brotli `examples/compress.rs`.
255    const BUFFER_SIZE: usize = 4096;
256
257    /// This is the default lgwin parameter, see `BrotliEncoderInitParams()`
258    const LGWIN: u32 = 22;
259
260    /// Compression quality we use for interactive compressions.
261    /// See <https://blog.cloudflare.com/results-experimenting-brotli> for data.
262    const QUALITY_FAST: u32 = 4;
263
264    /// Compression quality we use for offline compressions (the maximum).
265    const QUALITY_SLOW: u32 = 11;
266}
267
268#[cfg(feature = "brotli")]
269pub use feat_brotli::{BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR};
270
271/// An LRU cache for compressions.
272///
273/// The prospect of being able to reuse a given compression for many connections
274/// means we can afford to spend more time on that compression (by passing
275/// `CompressionLevel::Amortized` to the compressor).
276#[derive(Debug)]
277pub enum CompressionCache {
278    /// No caching happens, and compression happens each time using
279    /// `CompressionLevel::Interactive`.
280    Disabled,
281
282    /// Compressions are stored in an LRU cache.
283    #[cfg(feature = "std")]
284    Enabled(CompressionCacheInner),
285}
286
287/// Innards of an enabled CompressionCache.
288///
289/// You cannot make one of these directly. Use [`CompressionCache::new`].
290#[cfg(feature = "std")]
291#[derive(Debug)]
292pub struct CompressionCacheInner {
293    /// Maximum size of underlying storage.
294    size: usize,
295
296    /// LRU-order entries.
297    ///
298    /// First is least-used, last is most-used.
299    entries: Mutex<VecDeque<Arc<CompressionCacheEntry>>>,
300}
301
302impl CompressionCache {
303    /// Make a `CompressionCache` that stores up to `size` compressed
304    /// certificate messages.
305    #[cfg(feature = "std")]
306    pub fn new(size: usize) -> Self {
307        if size == 0 {
308            return Self::Disabled;
309        }
310
311        Self::Enabled(CompressionCacheInner {
312            size,
313            entries: Mutex::new(VecDeque::with_capacity(size)),
314        })
315    }
316
317    /// Return a `CompressionCacheEntry`, which is an owning
318    /// wrapper for a `CompressedCertificatePayload`.
319    ///
320    /// `compressor` is the compression function we have negotiated.
321    /// `original` is the uncompressed certificate message.
322    pub(crate) fn compression_for(
323        &self,
324        compressor: &dyn CertCompressor,
325        original: &CertificatePayloadTls13<'_>,
326    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
327        match self {
328            Self::Disabled => Self::uncached_compression(compressor, original),
329
330            #[cfg(feature = "std")]
331            Self::Enabled(_) => self.compression_for_impl(compressor, original),
332        }
333    }
334
335    #[cfg(feature = "std")]
336    fn compression_for_impl(
337        &self,
338        compressor: &dyn CertCompressor,
339        original: &CertificatePayloadTls13<'_>,
340    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
341        let (max_size, entries) = match self {
342            Self::Enabled(CompressionCacheInner { size, entries }) => (*size, entries),
343            _ => unreachable!(),
344        };
345
346        // context is a per-connection quantity, and included in the compressed data.
347        // it is not suitable for inclusion in the cache.
348        if !original.context.0.is_empty() {
349            return Self::uncached_compression(compressor, original);
350        }
351
352        // cache probe:
353        let encoding = original.get_encoding();
354        let algorithm = compressor.algorithm();
355
356        let mut cache = entries
357            .lock()
358            .map_err(|_| CompressionFailed)?;
359        for (i, item) in cache.iter().enumerate() {
360            if item.algorithm == algorithm && item.original == encoding {
361                // this item is now MRU
362                let item = cache.remove(i).unwrap();
363                cache.push_back(item.clone());
364                return Ok(item);
365            }
366        }
367        drop(cache);
368
369        // do compression:
370        let uncompressed_len = encoding.len() as u32;
371        let compressed = compressor.compress(encoding.clone(), CompressionLevel::Amortized)?;
372        let new_entry = Arc::new(CompressionCacheEntry {
373            algorithm,
374            original: encoding,
375            compressed: CompressedCertificatePayload {
376                alg: algorithm,
377                uncompressed_len,
378                compressed: PayloadU24(Payload::new(compressed)),
379            },
380        });
381
382        // insert into cache
383        let mut cache = entries
384            .lock()
385            .map_err(|_| CompressionFailed)?;
386        if cache.len() == max_size {
387            cache.pop_front();
388        }
389        cache.push_back(new_entry.clone());
390        Ok(new_entry)
391    }
392
393    /// Compress `original` using `compressor` at `Interactive` level.
394    fn uncached_compression(
395        compressor: &dyn CertCompressor,
396        original: &CertificatePayloadTls13<'_>,
397    ) -> Result<Arc<CompressionCacheEntry>, CompressionFailed> {
398        let algorithm = compressor.algorithm();
399        let encoding = original.get_encoding();
400        let uncompressed_len = encoding.len() as u32;
401        let compressed = compressor.compress(encoding, CompressionLevel::Interactive)?;
402
403        // this `CompressionCacheEntry` in fact never makes it into the cache, so
404        // `original` is left empty
405        Ok(Arc::new(CompressionCacheEntry {
406            algorithm,
407            original: Vec::new(),
408            compressed: CompressedCertificatePayload {
409                alg: algorithm,
410                uncompressed_len,
411                compressed: PayloadU24(Payload::new(compressed)),
412            },
413        }))
414    }
415}
416
417impl Default for CompressionCache {
418    fn default() -> Self {
419        #[cfg(feature = "std")]
420        {
421            // 4 entries allows 2 certificate chains times 2 compression algorithms
422            Self::new(4)
423        }
424
425        #[cfg(not(feature = "std"))]
426        {
427            Self::Disabled
428        }
429    }
430}
431
432#[cfg_attr(not(feature = "std"), allow(dead_code))]
433#[derive(Debug)]
434pub(crate) struct CompressionCacheEntry {
435    // cache key is algorithm + original:
436    algorithm: CertificateCompressionAlgorithm,
437    original: Vec<u8>,
438
439    // cache value is compression result:
440    compressed: CompressedCertificatePayload<'static>,
441}
442
443impl CompressionCacheEntry {
444    pub(crate) fn compressed_cert_payload(&self) -> CompressedCertificatePayload<'_> {
445        self.compressed.as_borrowed()
446    }
447}
448
449#[cfg(all(test, any(feature = "brotli", feature = "zlib")))]
450mod tests {
451    use std::{println, vec};
452
453    use super::*;
454
455    #[test]
456    #[cfg(feature = "zlib")]
457    fn test_zlib() {
458        test_compressor(ZLIB_COMPRESSOR, ZLIB_DECOMPRESSOR);
459    }
460
461    #[test]
462    #[cfg(feature = "brotli")]
463    fn test_brotli() {
464        test_compressor(BROTLI_COMPRESSOR, BROTLI_DECOMPRESSOR);
465    }
466
467    fn test_compressor(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
468        assert_eq!(comp.algorithm(), decomp.algorithm());
469        for sz in [16, 64, 512, 2048, 8192, 16384] {
470            test_trivial_pairwise(comp, decomp, sz);
471        }
472        test_decompress_wrong_len(comp, decomp);
473        test_decompress_garbage(decomp);
474    }
475
476    fn test_trivial_pairwise(
477        comp: &dyn CertCompressor,
478        decomp: &dyn CertDecompressor,
479        plain_len: usize,
480    ) {
481        let original = vec![0u8; plain_len];
482
483        for level in [CompressionLevel::Interactive, CompressionLevel::Amortized] {
484            let compressed = comp
485                .compress(original.clone(), level)
486                .unwrap();
487            println!(
488                "{:?} compressed trivial {} -> {} using {:?} level",
489                comp.algorithm(),
490                original.len(),
491                compressed.len(),
492                level
493            );
494            let mut recovered = vec![0xffu8; plain_len];
495            decomp
496                .decompress(&compressed, &mut recovered)
497                .unwrap();
498            assert_eq!(original, recovered);
499        }
500    }
501
502    fn test_decompress_wrong_len(comp: &dyn CertCompressor, decomp: &dyn CertDecompressor) {
503        let original = vec![0u8; 2048];
504        let compressed = comp
505            .compress(original.clone(), CompressionLevel::Interactive)
506            .unwrap();
507        println!("{compressed:?}");
508
509        // too big
510        let mut recovered = vec![0xffu8; original.len() + 1];
511        decomp
512            .decompress(&compressed, &mut recovered)
513            .unwrap_err();
514
515        // too small
516        let mut recovered = vec![0xffu8; original.len() - 1];
517        decomp
518            .decompress(&compressed, &mut recovered)
519            .unwrap_err();
520    }
521
522    fn test_decompress_garbage(decomp: &dyn CertDecompressor) {
523        let junk = [0u8; 1024];
524        let mut recovered = vec![0u8; 512];
525        decomp
526            .decompress(&junk, &mut recovered)
527            .unwrap_err();
528    }
529
530    #[test]
531    #[cfg(all(feature = "brotli", feature = "zlib"))]
532    fn test_cache_evicts_lru() {
533        use core::sync::atomic::{AtomicBool, Ordering};
534
535        use pki_types::CertificateDer;
536
537        let cache = CompressionCache::default();
538
539        let cert = CertificateDer::from(vec![1]);
540
541        let cert1 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"1"));
542        let cert2 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"2"));
543        let cert3 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"3"));
544        let cert4 = CertificatePayloadTls13::new([&cert].into_iter(), Some(b"4"));
545
546        // insert zlib (1), (2), (3), (4)
547
548        cache
549            .compression_for(
550                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
551                &cert1,
552            )
553            .unwrap();
554        cache
555            .compression_for(
556                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
557                &cert2,
558            )
559            .unwrap();
560        cache
561            .compression_for(
562                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
563                &cert3,
564            )
565            .unwrap();
566        cache
567            .compression_for(
568                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
569                &cert4,
570            )
571            .unwrap();
572
573        // -- now full
574
575        // insert brotli (1) evicts zlib (1)
576        cache
577            .compression_for(
578                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
579                &cert4,
580            )
581            .unwrap();
582
583        // now zlib (2), (3), (4) and brotli (4) exist
584        cache
585            .compression_for(
586                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
587                &cert2,
588            )
589            .unwrap();
590        cache
591            .compression_for(
592                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
593                &cert3,
594            )
595            .unwrap();
596        cache
597            .compression_for(
598                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
599                &cert4,
600            )
601            .unwrap();
602        cache
603            .compression_for(
604                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), false),
605                &cert4,
606            )
607            .unwrap();
608
609        // insert zlib (1) requires re-compression & evicts zlib (2)
610        cache
611            .compression_for(
612                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), true),
613                &cert1,
614            )
615            .unwrap();
616
617        // now zlib (1), (3), (4) and brotli (4) exist
618        // query zlib (4), (3), (1) to demonstrate LRU tracks usage rather than insertion
619        cache
620            .compression_for(
621                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
622                &cert4,
623            )
624            .unwrap();
625        cache
626            .compression_for(
627                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
628                &cert3,
629            )
630            .unwrap();
631        cache
632            .compression_for(
633                &RequireCompress(ZLIB_COMPRESSOR, AtomicBool::default(), false),
634                &cert1,
635            )
636            .unwrap();
637
638        // now brotli (4), zlib (4), (3), (1)
639        // insert brotli (1) evicting brotli (4)
640        cache
641            .compression_for(
642                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
643                &cert1,
644            )
645            .unwrap();
646
647        // verify brotli (4) disappeared
648        cache
649            .compression_for(
650                &RequireCompress(BROTLI_COMPRESSOR, AtomicBool::default(), true),
651                &cert4,
652            )
653            .unwrap();
654
655        #[derive(Debug)]
656        struct RequireCompress(&'static dyn CertCompressor, AtomicBool, bool);
657
658        impl CertCompressor for RequireCompress {
659            fn compress(
660                &self,
661                input: Vec<u8>,
662                level: CompressionLevel,
663            ) -> Result<Vec<u8>, CompressionFailed> {
664                self.1.store(true, Ordering::SeqCst);
665                self.0.compress(input, level)
666            }
667
668            fn algorithm(&self) -> CertificateCompressionAlgorithm {
669                self.0.algorithm()
670            }
671        }
672
673        impl Drop for RequireCompress {
674            fn drop(&mut self) {
675                assert_eq!(self.1.load(Ordering::SeqCst), self.2);
676            }
677        }
678    }
679}