support for certificate compression

This adds native support for certificate compression (RFC 8879), using
brotli. Before, an application could only support this by providing its
own BoringSSL SSL object, and only through the FFI API.

Due to the additional dependencies, this is an optional feature, but it
is enabled by default at build time. An application still needs to
enable it at runtime though.

Closes #822.
diff --git a/Cargo.toml b/Cargo.toml
index 2b0861f..e2a9520 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,11 +29,14 @@
 ]
 
 [features]
-default = ["boringssl-vendored"]
+default = ["boringssl-vendored", "certificate-compression"]
 
 # Build vendored BoringSSL library.
 boringssl-vendored = []
 
+# Enable certificate compression.
+certificate-compression = ["pkg-config"]
+
 # Generate pkg-config metadata file for libquiche.
 pkg-config-meta = []
 
@@ -51,6 +54,7 @@
 
 [build-dependencies]
 cmake = "0.1"
+pkg-config = { version = "0.3", optional = true }
 
 [dependencies]
 log = { version = "0.4", features = ["std"] }
diff --git a/src/build.rs b/src/build.rs
index 875f556..b810872 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -254,6 +254,19 @@
         println!("cargo:rustc-link-lib=static=ssl");
     }
 
+    #[cfg(feature = "certificate-compression")]
+    {
+        let pkgcfg = pkg_config::Config::new();
+
+        if pkgcfg.probe("libbrotlienc").is_ok() {
+            println!("cargo:rustc-cfg=feature=\"brotlienc\"");
+        }
+
+        if pkgcfg.probe("libbrotlidec").is_ok() {
+            println!("cargo:rustc-cfg=feature=\"brotlidec\"");
+        }
+    }
+
     // MacOS: Allow cdylib to link with undefined symbols
     if cfg!(target_os = "macos") {
         println!("cargo:rustc-cdylib-link-arg=-Wl,-undefined,dynamic_lookup");
diff --git a/src/lib.rs b/src/lib.rs
index fb6875a..b055d1e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -661,6 +661,16 @@
             .load_verify_locations_from_directory(dir)
     }
 
+    /// Enables support for certificate compression (RFC8879).
+    ///
+    /// Note that if support wasn't enabled at build time, this does nothing.
+    pub fn compress_certificates(&mut self) -> Result<()> {
+        self.tls_ctx
+            .lock()
+            .unwrap()
+            .enable_certificate_compression()
+    }
+
     /// Configures whether to verify the peer's certificate.
     ///
     /// The default value is `true` for client connections, and `false` for
@@ -5422,6 +5432,7 @@
             config.set_max_idle_timeout(180_000);
             config.verify_peer(false);
             config.set_ack_delay_exponent(5);
+            config.compress_certificates().unwrap();
 
             Pipe::with_config(&mut config)
         }
diff --git a/src/tls.rs b/src/tls.rs
index ed7a5bf..6569653 100644
--- a/src/tls.rs
+++ b/src/tls.rs
@@ -91,6 +91,10 @@
 #[repr(transparent)]
 struct CRYPTO_BUFFER(c_void);
 
+#[allow(non_camel_case_types)]
+#[repr(transparent)]
+struct CBB(c_void);
+
 #[repr(C)]
 #[allow(non_camel_case_types)]
 struct SSL_QUIC_METHOD {
@@ -278,6 +282,26 @@
         }
     }
 
+    pub fn enable_certificate_compression(&mut self) -> Result<()> {
+        #[cfg(any(feature = "brotlienc", feature = "brotlidec"))]
+        map_result(unsafe {
+            SSL_CTX_add_cert_compression_alg(
+                self.as_ptr(),
+                2, // TLSEXT_cert_compression_brotli
+                #[cfg(feature = "brotlienc")]
+                Some(compress_brotli_cert),
+                #[cfg(not(feature = "brotlienc"))]
+                None,
+                #[cfg(feature = "brotlidec")]
+                Some(decompress_brotli_cert),
+                #[cfg(not(feature = "brotlidec"))]
+                None,
+            )
+        })?;
+
+        Ok(())
+    }
+
     pub fn enable_keylog(&mut self) {
         unsafe {
             SSL_CTX_set_keylog_callback(self.as_ptr(), keylog);
@@ -927,6 +951,65 @@
     0
 }
 
+#[cfg(feature = "brotlienc")]
+extern fn compress_brotli_cert(
+    _ssl: *mut SSL, out: *mut CBB, in_buf: *mut u8, in_len: usize,
+) -> c_int {
+    let mut out_buf: *mut u8 = std::ptr::null_mut();
+
+    let mut out_len = unsafe { BrotliEncoderMaxCompressedSize(in_len) };
+
+    if out_len == 0 {
+        return 0;
+    }
+
+    if unsafe { CBB_reserve(out, &mut out_buf, out_len) } == 0 {
+        return 0;
+    }
+
+    let rc = unsafe {
+        BrotliEncoderCompress(5, 17, 0, in_len, in_buf, &mut out_len, out_buf)
+    };
+
+    if rc == 0 {
+        return 0;
+    }
+
+    if unsafe { CBB_did_write(out, out_len) } == 0 {
+        return 0;
+    }
+
+    return 1;
+}
+
+#[cfg(feature = "brotlidec")]
+extern fn decompress_brotli_cert(
+    _ssl: *mut SSL, out: *mut *mut CRYPTO_BUFFER, uncompressed_len: usize,
+    in_buf: *mut u8, in_len: usize,
+) -> c_int {
+    let mut out_buf: *mut u8 = std::ptr::null_mut();
+
+    let decompressed =
+        unsafe { CRYPTO_BUFFER_alloc(&mut out_buf, uncompressed_len) };
+
+    if decompressed.is_null() {
+        return 0;
+    }
+
+    let mut out_len = uncompressed_len;
+
+    let rc =
+        unsafe { BrotliDecoderDecompress(in_len, in_buf, &mut out_len, out_buf) };
+
+    if rc != 1 || out_len != uncompressed_len {
+        return 0;
+    }
+
+    unsafe { *out = decompressed };
+
+    return 1;
+}
+
 fn map_result(bssl_result: c_int) -> Result<()> {
     match bssl_result {
         1 => Ok(()),
@@ -1074,6 +1157,28 @@
         cb: extern fn(ssl: *mut SSL, session: *mut SSL_SESSION) -> c_int,
     );
 
+    #[allow(dead_code)]
+    fn SSL_CTX_add_cert_compression_alg(
+        ctx: *mut SSL_CTX, alg_id: u16,
+        compress: Option<
+            extern fn(
+                ssl: *mut SSL,
+                out: *mut CBB,
+                in_buf: *mut u8,
+                in_len: usize,
+            ) -> c_int,
+        >,
+        decompress: Option<
+            extern fn(
+                ssl: *mut SSL,
+                out: *mut *mut CRYPTO_BUFFER,
+                uncompressed_len: usize,
+                in_buf: *mut u8,
+                in_len: usize,
+            ) -> c_int,
+        >,
+    ) -> c_int;
+
     // SSL
     fn SSL_get_ex_new_index(
         argl: c_long, argp: *const c_void, unused: *const c_void,
@@ -1199,6 +1304,18 @@
     fn CRYPTO_BUFFER_len(buffer: *const CRYPTO_BUFFER) -> usize;
     fn CRYPTO_BUFFER_data(buffer: *const CRYPTO_BUFFER) -> *const u8;
 
+    #[allow(dead_code)]
+    fn CRYPTO_BUFFER_alloc(
+        out_data: *const *mut u8, len: usize,
+    ) -> *mut CRYPTO_BUFFER;
+
+    // CBB
+    #[allow(dead_code)]
+    fn CBB_reserve(cbb: *mut CBB, out_data: *const *mut u8, len: usize) -> c_int;
+
+    #[allow(dead_code)]
+    fn CBB_did_write(cbb: *mut CBB, len: usize) -> c_int;
+
     // ERR
     fn ERR_peek_error() -> c_uint;
 
@@ -1206,4 +1323,21 @@
 
     // OPENSSL
     fn OPENSSL_free(ptr: *mut c_void);
+
+    // Brotli
+    #[cfg(feature = "brotlienc")]
+    fn BrotliEncoderMaxCompressedSize(input_size: usize) -> usize;
+
+    #[cfg(feature = "brotlienc")]
+    fn BrotliEncoderCompress(
+        quality: c_int, lgwin: c_int, mode: c_int, input_size: usize,
+        input_buffer: *const u8, encoded_size: *mut usize,
+        encoded_buffer: *mut u8,
+    ) -> c_int;
+
+    #[cfg(feature = "brotlidec")]
+    fn BrotliDecoderDecompress(
+        encoded_size: usize, encoded_buffer: *const u8, decoded_size: *mut usize,
+        decoded_buffer: *mut u8,
+    ) -> c_int;
 }
diff --git a/tools/apps/src/bin/quiche-server.rs b/tools/apps/src/bin/quiche-server.rs
index 03b77c2..62600e1 100644
--- a/tools/apps/src/bin/quiche-server.rs
+++ b/tools/apps/src/bin/quiche-server.rs
@@ -82,6 +82,8 @@
     config.load_cert_chain_from_pem_file(&args.cert).unwrap();
     config.load_priv_key_from_pem_file(&args.key).unwrap();
 
+    config.compress_certificates().unwrap();
+
     config.set_application_protos(&conn_args.alpns).unwrap();
 
     config.set_max_idle_timeout(conn_args.idle_timeout);
diff --git a/tools/apps/src/client.rs b/tools/apps/src/client.rs
index 26b86b6..d8d982a 100644
--- a/tools/apps/src/client.rs
+++ b/tools/apps/src/client.rs
@@ -94,6 +94,8 @@
     // Create the configuration for the QUIC connection.
     let mut config = quiche::Config::new(args.version).unwrap();
 
+    config.compress_certificates().unwrap();
+
     config.verify_peer(!args.no_verify);
 
     config.set_application_protos(&conn_args.alpns).unwrap();