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::c_api::Z_BEST_COMPRESSION;
129 use zlib_rs::{ReturnCode, deflate, inflate};
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(size: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}
678