Expose API to enable certificate compression. (#241)
This commit is contained in:
parent
5e0002bfa8
commit
5268f63a77
|
|
@ -49,3 +49,4 @@ tower = "0.4"
|
|||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
autocfg = "1.3.0"
|
||||
brotli = "6.0"
|
||||
|
|
|
|||
|
|
@ -81,3 +81,4 @@ boring-sys = { workspace = true }
|
|||
[dev-dependencies]
|
||||
hex = { workspace = true }
|
||||
rusty-hook = { workspace = true }
|
||||
brotli = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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<F>(
|
|||
|
||||
callback(ssl, SslInfoCallbackMode(mode), value);
|
||||
}
|
||||
|
||||
pub(super) unsafe extern "C" fn raw_ssl_cert_compress<C>(
|
||||
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::<C>())
|
||||
.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<C>(
|
||||
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::<C>())
|
||||
.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<usize> {
|
||||
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<CryptoBufferBuilder<'a>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<C>(
|
||||
&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::<C>)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
const {
|
||||
if C::CAN_DECOMPRESS {
|
||||
Some(callbacks::raw_ssl_cert_decompress::<C>)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
) == 1
|
||||
};
|
||||
if !success {
|
||||
return Err(ErrorStack::get());
|
||||
}
|
||||
self.replace_ex_data(SslContext::cached_ex_index::<C>(), 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<W>(&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<W>(&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};
|
||||
|
|
|
|||
|
|
@ -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<W>(&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<W>(&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();
|
||||
}
|
||||
|
|
@ -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"))]
|
||||
|
|
|
|||
Loading…
Reference in New Issue