diff --git a/Cargo.toml b/Cargo.toml index 39f02805..41ca6a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,6 @@ antidote = "1.0.0" linked_hash_set = "0.1" openssl-macros = "0.1.1" autocfg = "1.3.0" +brotli = "7" +flate2 = "1" +zstd = "0.13" \ No newline at end of file diff --git a/boring/Cargo.toml b/boring/Cargo.toml index fe3da19a..e3820afc 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -64,12 +64,18 @@ kx-client-pq-preferred = ["kx-safe-default", "kx-client-pq-supported"] # Implies "kx-safe-default". kx-client-nist-required = ["kx-safe-default"] +# Certificate compression +cert-compression = ["flate2", "brotli", "zstd"] + [dependencies] bitflags = { workspace = true } foreign-types = { workspace = true } openssl-macros = { workspace = true } libc = { workspace = true } boring-sys = { workspace = true } +brotli = { workspace = true, optional = true } +flate2 = { workspace = true, optional = true } +zstd = { workspace = true, optional = true } [dev-dependencies] hex = { workspace = true } diff --git a/boring/src/ssl/cert_compression.rs b/boring/src/ssl/cert_compression.rs new file mode 100644 index 00000000..6682f610 --- /dev/null +++ b/boring/src/ssl/cert_compression.rs @@ -0,0 +1,190 @@ +use boring_sys as ffi; +use std::{io::Read, slice}; + +/// IANA assigned identifier of compression algorithm. +/// See https://www.rfc-editor.org/rfc/rfc8879.html#name-compression-algorithms +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CertCompressionAlgorithm { + /// The Brotli compression algorithm. + Brotli = ffi::TLSEXT_cert_compression_brotli as _, + /// The zlib compression algorithm. + Zlib = ffi::TLSEXT_cert_compression_zlib as _, + /// The Zstandard compression algorithm. + Zstd = ffi::TLSEXT_cert_compression_zstd as _, +} + +impl CertCompressionAlgorithm { + /// Returns the compression function for the algorithm. + pub(crate) fn compression_fn(&self) -> ffi::ssl_cert_compression_func_t { + match &self { + Self::Brotli => Some(brotli_compressor), + Self::Zlib => Some(zlib_compressor), + Self::Zstd => Some(zstd_compressor), + } + } + + /// Returns the decompression function for the algorithm. + pub(crate) fn decompression_fn(&self) -> ffi::ssl_cert_decompression_func_t { + match &self { + Self::Brotli => Some(brotli_decompressor), + Self::Zlib => Some(zlib_decompressor), + Self::Zstd => Some(zstd_decompressor), + } + } +} + +extern "C" fn brotli_compressor( + _ssl: *mut ffi::SSL, + buffer: *mut ffi::CBB, + in_: *const u8, + in_len: usize, +) -> ::std::os::raw::c_int { + let mut uncompressed = unsafe { slice::from_raw_parts(in_, in_len) }; + let mut compressed = Vec::new(); + + let params = brotli::enc::encode::BrotliEncoderInitParams(); + + if brotli::BrotliCompress(&mut uncompressed, &mut compressed, ¶ms).is_err() { + return 0; + } + + unsafe { ffi::CBB_add_bytes(buffer, compressed.as_ptr(), compressed.len()) } +} + +extern "C" fn zlib_compressor( + _ssl: *mut ffi::SSL, + out: *mut ffi::CBB, + in_: *const u8, + in_len: usize, +) -> ::std::os::raw::c_int { + let mut uncompressed = unsafe { slice::from_raw_parts(in_, in_len) }; + let mut compressed = Vec::new(); + + let params = flate2::Compression::default(); + + let mut encoder = flate2::bufread::ZlibEncoder::new(&mut uncompressed, params); + if encoder.read_to_end(&mut compressed).is_err() { + return 0; + } + + unsafe { ffi::CBB_add_bytes(out, compressed.as_ptr(), compressed.len()) } +} + +extern "C" fn zstd_compressor( + _ssl: *mut ffi::SSL, + out: *mut ffi::CBB, + in_: *const u8, + in_len: usize, +) -> ::std::os::raw::c_int { + let mut uncompressed = unsafe { slice::from_raw_parts(in_, in_len) }; + + let compressed = if let Ok(compressed) = zstd::encode_all(&mut uncompressed, 3) { + compressed + } else { + return 0; + }; + + unsafe { ffi::CBB_add_bytes(out, compressed.as_ptr(), compressed.len()) } +} + +extern "C" fn brotli_decompressor( + _ssl: *mut ffi::SSL, + buffer: *mut *mut ffi::CRYPTO_BUFFER, + uncompressed_len: usize, + in_: *const u8, + in_len: usize, +) -> ::std::os::raw::c_int { + let compressed = unsafe { slice::from_raw_parts(in_, in_len) }; + let mut uncompressed = Vec::with_capacity(uncompressed_len); + + if brotli::BrotliDecompress(&mut &compressed[..], &mut uncompressed).is_err() { + return 0; + } + + if uncompressed.len() != uncompressed_len { + return 0; + } + + unsafe { + *buffer = ffi::CRYPTO_BUFFER_new( + uncompressed.as_ptr(), + uncompressed_len, + std::ptr::null_mut(), + ); + + if buffer.is_null() { + return 0; + } + } + + 1 +} + +extern "C" fn zlib_decompressor( + _ssl: *mut ffi::SSL, + buffer: *mut *mut ffi::CRYPTO_BUFFER, + uncompressed_len: usize, + in_: *const u8, + in_len: usize, +) -> ::std::os::raw::c_int { + let mut compressed = unsafe { slice::from_raw_parts(in_, in_len) }; + let mut uncompressed = Vec::with_capacity(uncompressed_len); + + let mut decoder = flate2::bufread::ZlibDecoder::new(&mut compressed); + if decoder.read_to_end(&mut uncompressed).is_err() { + return 0; + } + + if uncompressed.len() != uncompressed_len { + return 0; + } + + unsafe { + *buffer = ffi::CRYPTO_BUFFER_new( + uncompressed.as_ptr(), + uncompressed_len, + std::ptr::null_mut(), + ); + + if buffer.is_null() { + return 0; + } + } + + 1 +} + +extern "C" fn zstd_decompressor( + _ssl: *mut ffi::SSL, + buffer: *mut *mut ffi::CRYPTO_BUFFER, + uncompressed_len: usize, + in_: *const u8, + in_len: usize, +) -> ::std::os::raw::c_int { + let mut compressed = unsafe { slice::from_raw_parts(in_, in_len) }; + + let uncompressed = if let Ok(uncompressed) = zstd::decode_all(&mut compressed) { + uncompressed + } else { + return 0; + }; + + if uncompressed.len() != uncompressed_len { + return 0; + } + + unsafe { + *buffer = ffi::CRYPTO_BUFFER_new( + uncompressed.as_ptr(), + uncompressed_len, + std::ptr::null_mut(), + ); + + if buffer.is_null() { + return 0; + } + } + + 1 +} diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index f89e7317..b49f2c57 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -101,6 +101,8 @@ pub use self::async_callbacks::{ BoxCustomVerifyFuture, BoxGetSessionFinish, BoxGetSessionFuture, BoxPrivateKeyMethodFinish, BoxPrivateKeyMethodFuture, BoxSelectCertFinish, BoxSelectCertFuture, ExDataFuture, }; +#[cfg(feature = "cert-compression")] +pub use self::cert_compression::CertCompressionAlgorithm; pub use self::connector::{ ConnectConfiguration, SslAcceptor, SslAcceptorBuilder, SslConnector, SslConnectorBuilder, }; @@ -109,6 +111,8 @@ pub use self::error::{Error, ErrorCode, HandshakeError}; mod async_callbacks; mod bio; mod callbacks; +#[cfg(feature = "cert-compression")] +mod cert_compression; mod connector; mod error; mod mut_only; @@ -1353,6 +1357,24 @@ impl SslContextBuilder { unsafe { cvt(ffi::SSL_CTX_use_PrivateKey(self.as_ptr(), key.as_ptr())).map(|_| ()) } } + /// Sets whether a certificate compression algorithm should be used. + #[cfg(feature = "cert-compression")] + #[corresponds(SSL_CTX_add_cert_compression_alg)] + pub fn add_cert_compression_alg( + &mut self, + alg: CertCompressionAlgorithm, + ) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::SSL_CTX_add_cert_compression_alg( + self.as_ptr(), + alg as _, + alg.compression_fn(), + alg.decompression_fn(), + )) + .map(|_| ()) + } + } + /// Sets the list of supported ciphers for protocols before TLSv1.3. /// /// The `set_ciphersuites` method controls the cipher suites for TLSv1.3 in OpenSSL.