diff --git a/Cargo.toml b/Cargo.toml index ac764ff5..719505ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,3 +49,4 @@ tower = "0.4" tower-layer = "0.3" tower-service = "0.3" autocfg = "1.3.0" +brotli = "6.0" diff --git a/boring/Cargo.toml b/boring/Cargo.toml index 5e0b1c28..5bd25bc1 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -81,3 +81,4 @@ boring-sys = { workspace = true } [dev-dependencies] hex = { workspace = true } rusty-hook = { workspace = true } +brotli = { workspace = true } diff --git a/boring/src/ssl/callbacks.rs b/boring/src/ssl/callbacks.rs index f41108d5..8ab17b98 100644 --- a/boring/src/ssl/callbacks.rs +++ b/boring/src/ssl/callbacks.rs @@ -1,10 +1,10 @@ #![forbid(unsafe_op_in_unsafe_fn)] use super::{ - AlpnError, ClientHello, GetSessionPendingError, PrivateKeyMethod, PrivateKeyMethodError, - SelectCertError, SniError, Ssl, SslAlert, SslContext, SslContextRef, SslInfoCallbackAlert, - SslInfoCallbackMode, SslInfoCallbackValue, SslRef, SslSession, SslSessionRef, - SslSignatureAlgorithm, SslVerifyError, SESSION_CTX_INDEX, + AlpnError, CertificateCompressor, ClientHello, GetSessionPendingError, PrivateKeyMethod, + PrivateKeyMethodError, SelectCertError, SniError, Ssl, SslAlert, SslContext, SslContextRef, + SslInfoCallbackAlert, SslInfoCallbackMode, SslInfoCallbackValue, SslRef, SslSession, + SslSessionRef, SslSignatureAlgorithm, SslVerifyError, SESSION_CTX_INDEX, }; use crate::error::ErrorStack; use crate::ffi; @@ -579,3 +579,146 @@ pub(super) unsafe extern "C" fn raw_info_callback( callback(ssl, SslInfoCallbackMode(mode), value); } + +pub(super) unsafe extern "C" fn raw_ssl_cert_compress( + ssl: *mut ffi::SSL, + out: *mut ffi::CBB, + input: *const u8, + input_len: usize, +) -> ::std::os::raw::c_int +where + C: CertificateCompressor, +{ + const { + assert!(C::CAN_COMPRESS); + } + + // SAFETY: boring provides valid inputs. + let ssl = unsafe { SslRef::from_ptr_mut(ssl) }; + + let ssl_context = ssl.ssl_context(); + let compressor = ssl_context + .ex_data(SslContext::cached_ex_index::()) + .expect("BUG: certificate compression missed"); + + let input_slice = unsafe { std::slice::from_raw_parts(input, input_len) }; + let mut writer = CryptoByteBuilder::from_ptr(out); + if compressor.compress(input_slice, &mut writer).is_err() { + return 0; + } + + 1 +} + +pub(super) unsafe extern "C" fn raw_ssl_cert_decompress( + ssl: *mut ffi::SSL, + out: *mut *mut ffi::CRYPTO_BUFFER, + uncompressed_len: usize, + input: *const u8, + input_len: usize, +) -> ::std::os::raw::c_int +where + C: CertificateCompressor, +{ + const { + assert!(C::CAN_DECOMPRESS); + } + + // SAFETY: boring provides valid inputs. + let ssl = unsafe { SslRef::from_ptr_mut(ssl) }; + + let ssl_context = ssl.ssl_context(); + let compressor = ssl_context + .ex_data(SslContext::cached_ex_index::()) + .expect("BUG: certificate compression missed"); + + let Ok(mut decompression_buffer) = CryptoBufferBuilder::with_capacity(uncompressed_len) else { + return 0; + }; + + let input_slice = unsafe { std::slice::from_raw_parts(input, input_len) }; + + if compressor + .decompress(input_slice, decompression_buffer.as_writer()) + .is_err() + { + return 0; + } + + let Ok(crypto_buffer) = decompression_buffer.build() else { + return 0; + }; + + unsafe { *out = crypto_buffer }; + 1 +} + +struct CryptoByteBuilder<'a>(*mut ffi::CBB, std::marker::PhantomData<&'a [u8]>); + +impl CryptoByteBuilder<'_> { + fn from_ptr(ptr: *mut ffi::CBB) -> Self { + Self(ptr, Default::default()) + } +} + +impl std::io::Write for CryptoByteBuilder<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let success = unsafe { ffi::CBB_add_bytes(self.0, buf.as_ptr(), buf.len()) == 1 }; + if !success { + return Err(std::io::Error::other("CBB_add_bytes failed")); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + let success = unsafe { ffi::CBB_flush(self.0) == 1 }; + if !success { + return Err(std::io::Error::other("CBB_flush failed")); + } + Ok(()) + } +} + +struct CryptoBufferBuilder<'a> { + buffer: *mut ffi::CRYPTO_BUFFER, + cursor: std::io::Cursor<&'a mut [u8]>, +} + +impl<'a> CryptoBufferBuilder<'a> { + fn with_capacity(capacity: usize) -> Result, ErrorStack> { + let mut data: *mut u8 = std::ptr::null_mut(); + let buffer = unsafe { crate::cvt_p(ffi::CRYPTO_BUFFER_alloc(&mut data, capacity))? }; + Ok(CryptoBufferBuilder { + buffer, + cursor: std::io::Cursor::new(unsafe { std::slice::from_raw_parts_mut(data, capacity) }), + }) + } + + fn as_writer(&mut self) -> &mut (impl std::io::Write + 'a) { + &mut self.cursor + } + + fn build(mut self) -> Result<*mut ffi::CRYPTO_BUFFER, ErrorStack> { + let buffer_capacity = unsafe { ffi::CRYPTO_BUFFER_len(self.buffer) }; + if self.cursor.position() != buffer_capacity as u64 { + // Make sure all bytes in buffer initialized as required by Boring SSL. + return Err(ErrorStack::get()); + } + unsafe { + let mut result = ptr::null_mut(); + ptr::swap(&mut self.buffer, &mut result); + std::mem::forget(self); + Ok(result) + } + } +} + +impl Drop for CryptoBufferBuilder<'_> { + fn drop(&mut self) { + if !self.buffer.is_null() { + unsafe { + boring_sys::CRYPTO_BUFFER_free(self.buffer); + } + } + } +} diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index 530c4a20..9be3e590 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -795,6 +795,16 @@ impl CompliancePolicy { Self(ffi::ssl_compliance_policy_t::ssl_compliance_policy_wpa3_192_202304); } +// IANA assigned identifier of compression algorithm. See https://www.rfc-editor.org/rfc/rfc8879.html#name-compression-algorithms +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct CertificateCompressionAlgorithm(u16); + +impl CertificateCompressionAlgorithm { + pub const ZLIB: Self = Self(ffi::TLSEXT_cert_compression_zlib as u16); + + pub const BROTLI: Self = Self(ffi::TLSEXT_cert_compression_brotli as u16); +} + /// A standard implementation of protocol selection for Application Layer Protocol Negotiation /// (ALPN). /// @@ -1594,6 +1604,48 @@ impl SslContextBuilder { } } + /// Registers a certificate compression algorithm. + /// + /// Corresponds to [`SSL_CTX_add_cert_compression_alg`]. + /// + /// [`SSL_CTX_add_cert_compression_alg`]: https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_add_cert_compression_alg + pub fn add_certificate_compression_algorithm( + &mut self, + compressor: C, + ) -> Result<(), ErrorStack> + where + C: CertificateCompressor, + { + const { + assert!(C::CAN_COMPRESS || C::CAN_DECOMPRESS, "Either compression or decompression must be supported for algorithm to be registered") + }; + let success = unsafe { + ffi::SSL_CTX_add_cert_compression_alg( + self.as_ptr(), + C::ALGORITHM.0, + const { + if C::CAN_COMPRESS { + Some(callbacks::raw_ssl_cert_compress::) + } else { + None + } + }, + const { + if C::CAN_DECOMPRESS { + Some(callbacks::raw_ssl_cert_decompress::) + } else { + None + } + }, + ) == 1 + }; + if !success { + return Err(ErrorStack::get()); + } + self.replace_ex_data(SslContext::cached_ex_index::(), compressor); + Ok(()) + } + /// Configures a custom private key method on the context. /// /// See [`PrivateKeyMethod`] for more details. @@ -4349,6 +4401,36 @@ impl PrivateKeyMethodError { pub const RETRY: Self = Self(ffi::ssl_private_key_result_t::ssl_private_key_retry); } +/// Describes certificate compression algorithm. Implementation MUST implement transformation at least in one direction. +pub trait CertificateCompressor: Send + Sync + 'static { + /// An IANA assigned identifier of compression algorithm + const ALGORITHM: CertificateCompressionAlgorithm; + + /// Indicates if compressor support compression + const CAN_COMPRESS: bool; + + /// Indicates if compressor support decompression + const CAN_DECOMPRESS: bool; + + /// Perform compression of `input` buffer and write compressed data to `output`. + #[allow(unused_variables)] + fn compress(&self, input: &[u8], output: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + Err(std::io::Error::other("not implemented")) + } + + /// Perform decompression of `input` buffer and write compressed data to `output`. + #[allow(unused_variables)] + fn decompress(&self, input: &[u8], output: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + Err(std::io::Error::other("not implemented")) + } +} + use crate::ffi::{SSL_CTX_up_ref, SSL_SESSION_get_master_key, SSL_SESSION_up_ref, SSL_is_server}; use crate::ffi::{DTLS_method, TLS_client_method, TLS_method, TLS_server_method}; diff --git a/boring/src/ssl/test/cert_compressor.rs b/boring/src/ssl/test/cert_compressor.rs new file mode 100644 index 00000000..d62ffa87 --- /dev/null +++ b/boring/src/ssl/test/cert_compressor.rs @@ -0,0 +1,102 @@ +use std::io::Write as _; + +use super::server::Server; +use crate::ssl::CertificateCompressor; +use crate::x509::store::X509StoreBuilder; +use crate::x509::X509; + +struct BrotliCompressor { + q: u32, + lgwin: u32, +} + +impl Default for BrotliCompressor { + fn default() -> Self { + Self { q: 11, lgwin: 32 } + } +} + +impl CertificateCompressor for BrotliCompressor { + const ALGORITHM: crate::ssl::CertificateCompressionAlgorithm = + crate::ssl::CertificateCompressionAlgorithm(1234); + + const CAN_COMPRESS: bool = true; + + const CAN_DECOMPRESS: bool = true; + + fn compress(&self, input: &[u8], output: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + let mut writer = brotli::CompressorWriter::new(output, 1024, self.q, self.lgwin); + writer.write_all(input)?; + Ok(()) + } + + fn decompress(&self, input: &[u8], output: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + brotli::BrotliDecompress(&mut std::io::Cursor::new(input), output)?; + Ok(()) + } +} + +#[test] +fn server_only_cert_compression() { + let mut server_builder = Server::builder(); + server_builder + .ctx() + .add_certificate_compression_algorithm(BrotliCompressor::default()) + .unwrap(); + + let server = server_builder.build(); + + let mut store = X509StoreBuilder::new().unwrap(); + let x509 = X509::from_pem(super::ROOT_CERT).unwrap(); + store.add_cert(x509).unwrap(); + + let client = server.client(); + + client.connect(); +} + +#[test] +fn client_only_cert_compression() { + let server_builder = Server::builder().build(); + + let mut store = X509StoreBuilder::new().unwrap(); + let x509 = X509::from_pem(super::ROOT_CERT).unwrap(); + store.add_cert(x509).unwrap(); + + let mut client = server_builder.client(); + client + .ctx() + .add_certificate_compression_algorithm(BrotliCompressor::default()) + .unwrap(); + + client.connect(); +} + +#[test] +fn client_and_server_cert_compression() { + let mut server = Server::builder(); + server + .ctx() + .add_certificate_compression_algorithm(BrotliCompressor::default()) + .unwrap(); + + let server = server.build(); + + let mut store = X509StoreBuilder::new().unwrap(); + let x509 = X509::from_pem(super::ROOT_CERT).unwrap(); + store.add_cert(x509).unwrap(); + + let mut client = server.client(); + client + .ctx() + .add_certificate_compression_algorithm(BrotliCompressor::default()) + .unwrap(); + + client.connect(); +} diff --git a/boring/src/ssl/test/mod.rs b/boring/src/ssl/test/mod.rs index ab11780e..4566b73c 100644 --- a/boring/src/ssl/test/mod.rs +++ b/boring/src/ssl/test/mod.rs @@ -24,6 +24,7 @@ use crate::x509::{X509Name, X509}; #[cfg(not(feature = "fips"))] use super::CompliancePolicy; +mod cert_compressor; mod cert_verify; mod custom_verify; #[cfg(not(feature = "fips"))]