diff --git a/Cargo.toml b/Cargo.toml index bad88ec2..0ad5603f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,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 19e79432..b2bd8fc3 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..b941d5f1 100644 --- a/boring/src/ssl/callbacks.rs +++ b/boring/src/ssl/callbacks.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_op_in_unsafe_fn)] use super::{ - AlpnError, ClientHello, GetSessionPendingError, PrivateKeyMethod, PrivateKeyMethodError, + AlpnError, CertificateCompressor, ClientHello, GetSessionPendingError, PrivateKeyMethod, PrivateKeyMethodError, SelectCertError, SniError, Ssl, SslAlert, SslContext, SslContextRef, SslInfoCallbackAlert, SslInfoCallbackMode, SslInfoCallbackValue, SslRef, SslSession, SslSessionRef, SslSignatureAlgorithm, SslVerifyError, SESSION_CTX_INDEX, @@ -579,3 +579,94 @@ 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, +{ + // SAFETY: boring provides valid inputs. + let ssl = unsafe { SslRef::from_ptr_mut(ssl) }; + + let ssl_context = ssl.ssl_context().to_owned(); + let compressor = ssl_context + .ex_data(SslContext::cached_ex_index::()) + .expect("BUG: certificate compression missed"); + + if !compressor.can_compress() { + return 0; + } + let input_slice = unsafe { std::slice::from_raw_parts(input, input_len) }; + let mut writer = CryptoByteBuilder(out); + if compressor.compress(input_slice, &mut writer).is_err() { + return 0; + } + + return 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, +{ + // SAFETY: boring provides valid inputs. + let ssl = unsafe { SslRef::from_ptr_mut(ssl) }; + + let ssl_context = ssl.ssl_context().to_owned(); + let compressor = ssl_context + .ex_data(SslContext::cached_ex_index::()) + .expect("BUG: certificate compression missed"); + + let mut data: *mut u8 = std::ptr::null_mut(); + + let decompressed = unsafe { boring_sys::CRYPTO_BUFFER_alloc(&mut data, uncompressed_len) }; + + if decompressed.is_null() { + return 0; + } + + let input_slice = unsafe { std::slice::from_raw_parts(input, input_len) }; + + let output_slice = unsafe { std::slice::from_raw_parts_mut(data, uncompressed_len) }; + + let mut cursor = std::io::Cursor::new(output_slice); + if compressor.decompress(input_slice, &mut cursor).is_err() { + return 0; + } + if cursor.position() != uncompressed_len as u64 { + return 0; + } + + unsafe { *out = decompressed }; + 1 +} + +struct CryptoByteBuilder(*mut ffi::CBB); + +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(()) + } +} diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index f0849589..41ffae39 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -793,6 +793,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,46 @@ 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, + { + let algo = compressor.algorithm(); + let (can_compress, can_decompress) = + (compressor.can_compress(), compressor.can_decompress()); + assert!(can_compress || can_decompress); + let success = unsafe { + self.replace_ex_data(SslContext::cached_ex_index::(), compressor); + + ffi::SSL_CTX_add_cert_compression_alg( + self.as_ptr(), + algo.0, + if can_compress { + Some(callbacks::raw_ssl_cert_compress::) + } else { + None + }, + if can_decompress { + Some(callbacks::raw_ssl_cert_decompress::) + } else { + None + }, + ) == 1 + }; + if !success { + return Err(ErrorStack::get()); + } + Ok(()) + } + /// Configures a custom private key method on the context. /// /// See [`PrivateKeyMethod`] for more details. @@ -4260,6 +4310,40 @@ 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 + fn algorithm(&self) -> CertificateCompressionAlgorithm; + + /// Indicates if compressor support compression + fn can_compress(&self) -> bool { + false + } + + /// Indicates if compressor support decompression + fn can_decompress(&self) -> bool { + false + } + + /// 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..71698989 --- /dev/null +++ b/boring/src/ssl/test/cert_compressor.rs @@ -0,0 +1,107 @@ +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 { + fn algorithm(&self) -> crate::ssl::CertificateCompressionAlgorithm { + crate::ssl::CertificateCompressionAlgorithm(1234) + } + + fn can_compress(&self) -> bool { + true + } + + fn can_decompress(&self) -> 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 6010cf98..a1652148 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; mod private_key_method;