From 43c5fc50821264f738981aea52a734b6ef0db999 Mon Sep 17 00:00:00 2001 From: James Mayclin Date: Wed, 16 Oct 2024 20:05:16 +0000 Subject: [PATCH] add integration test examples --- bindings/rust/bench/Cargo.toml | 4 +- bindings/rust/bench/benches/handshake.rs | 2 +- bindings/rust/bench/benches/resumption.rs | 4 +- bindings/rust/bench/src/crypto_config.rs | 0 bindings/rust/bench/src/harness/mod.rs | 106 +++++++++++++--- bindings/rust/bench/src/lib.rs | 8 ++ bindings/rust/bench/src/openssl.rs | 2 +- bindings/rust/bench/src/openssl_extension.rs | 48 ++++++++ bindings/rust/bench/src/s2n_tls.rs | 4 +- bindings/rust/bench/src/tests.rs | 115 ++++++++++++++++++ .../rust/bench/src/tests/fragmentation.rs | 53 ++++++++ .../rust/bench/src/tests/record_padding.rs | 62 ++++++++++ 12 files changed, 383 insertions(+), 25 deletions(-) create mode 100644 bindings/rust/bench/src/crypto_config.rs create mode 100644 bindings/rust/bench/src/openssl_extension.rs create mode 100644 bindings/rust/bench/src/tests.rs create mode 100644 bindings/rust/bench/src/tests/fragmentation.rs create mode 100644 bindings/rust/bench/src/tests/record_padding.rs diff --git a/bindings/rust/bench/Cargo.toml b/bindings/rust/bench/Cargo.toml index c061bedbeef..610329c6f7a 100644 --- a/bindings/rust/bench/Cargo.toml +++ b/bindings/rust/bench/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [features] default = ["rustls", "openssl"] rustls = ["dep:rustls", "rustls-pemfile"] -openssl = ["dep:openssl"] +openssl = ["dep:openssl", "dep:openssl-sys"] memory = ["plotters", "crabgrind", "structopt"] historical-perf = ["plotters", "serde_json", "semver"] @@ -18,10 +18,12 @@ strum = { version = "0.25", features = ["derive"] } rustls = { version = "0.23", optional = true } rustls-pemfile = { version = "2", optional = true } openssl = { version = "0.10", features = ["vendored"], optional = true } +openssl-sys = { version = "0.9", features = ["vendored"], optional = true } crabgrind = { version = "0.1", optional = true } structopt = { version = "0.3", optional = true } serde_json = { version = "1.0", optional = true } semver = { version = "1.0", optional = true } +foreign-types-shared = "0.1.1" [dependencies.plotters] version = "0.3" diff --git a/bindings/rust/bench/benches/handshake.rs b/bindings/rust/bench/benches/handshake.rs index bc1507545d8..3025e3eb45e 100644 --- a/bindings/rust/bench/benches/handshake.rs +++ b/bindings/rust/bench/benches/handshake.rs @@ -22,7 +22,7 @@ fn bench_handshake_for_library( kx_group: KXGroup, sig_type: SigType, ) where - T: TlsConnection, + T: TlsConnection + 'static, T::Config: TlsBenchConfig, { // make configs before benching to reuse diff --git a/bindings/rust/bench/benches/resumption.rs b/bindings/rust/bench/benches/resumption.rs index 3a7aaa32579..18d395334c6 100644 --- a/bindings/rust/bench/benches/resumption.rs +++ b/bindings/rust/bench/benches/resumption.rs @@ -11,7 +11,7 @@ use criterion::{ fn bench_handshake_pair(bench_group: &mut BenchmarkGroup, sig_type: SigType) where - T: TlsConnection, + T: TlsConnection + 'static, T::Config: TlsBenchConfig, { // generate all harnesses (TlsConnPair structs) beforehand so that benchmarks @@ -38,7 +38,7 @@ where fn bench_handshake_server_1rtt(bench_group: &mut BenchmarkGroup, sig_type: SigType) where - T: TlsConnection, + T: TlsConnection + 'static, T::Config: TlsBenchConfig, { for handshake in [HandshakeType::Resumption, HandshakeType::ServerAuth] { diff --git a/bindings/rust/bench/src/crypto_config.rs b/bindings/rust/bench/src/crypto_config.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bindings/rust/bench/src/harness/mod.rs b/bindings/rust/bench/src/harness/mod.rs index 3e7dcab884c..4fd8449e066 100644 --- a/bindings/rust/bench/src/harness/mod.rs +++ b/bindings/rust/bench/src/harness/mod.rs @@ -5,12 +5,7 @@ mod io; pub use io::{LocalDataBuffer, ViewIO}; use io::TestPairIO; -use std::{ - error::Error, - fmt::Debug, - fs::read_to_string, - rc::Rc, -}; +use std::{error::Error, fmt::Debug, fs::read_to_string, rc::Rc}; use strum::EnumIter; #[derive(Clone, Copy, EnumIter)] @@ -184,8 +179,57 @@ pub trait TlsConnection: Sized { fn recv(&mut self, data: &mut [u8]) -> Result<(), Box>; } +pub trait TlsConnIo { + /// Run one handshake step: receive msgs from other connection, process, and send new msgs + fn handshake(&mut self) -> Result<(), Box>; + + fn handshake_completed(&self) -> bool; + + /// Send application data to ConnectedBuffer + fn send(&mut self, data: &[u8]) -> Result<(), Box>; + + /// Read application data from ConnectedBuffer + fn recv(&mut self, data: &mut [u8]) -> Result<(), Box>; +} + +impl TlsConnIo for Box { + fn handshake(&mut self) -> Result<(), Box> { + self.handshake() + } + + fn handshake_completed(&self) -> bool { + self.handshake_completed() + } + + fn send(&mut self, data: &[u8]) -> Result<(), Box> { + self.send(data) + } + + fn recv(&mut self, data: &mut [u8]) -> Result<(), Box> { + self.recv(data) + } +} + +impl TlsConnIo for T { + fn handshake(&mut self) -> Result<(), Box> { + self.handshake() + } + + fn handshake_completed(&self) -> bool { + self.handshake_completed() + } + + fn send(&mut self, data: &[u8]) -> Result<(), Box> { + self.send(data) + } + + fn recv(&mut self, data: &mut [u8]) -> Result<(), Box> { + self.recv(data) + } +} + /// A TlsConnPair owns the client and server tls connections along with the IO buffers. -pub struct TlsConnPair { +pub struct TlsConnPair { pub client: C, pub server: S, pub io: TestPairIO, @@ -270,6 +314,42 @@ where (self.client, self.server) } + pub fn get_negotiated_cipher_suite(&self) -> CipherSuite { + assert!(self.handshake_completed()); + assert!( + self.client.get_negotiated_cipher_suite() == self.server.get_negotiated_cipher_suite() + ); + self.client.get_negotiated_cipher_suite() + } + + pub fn negotiated_tls13(&self) -> bool { + self.client.negotiated_tls13() && self.server.negotiated_tls13() + } +} + +impl TlsConnPair +where + C: TlsConnIo + 'static, + S: TlsConnIo + 'static, +{ + pub fn type_erase(self) -> TlsConnPair, Box> { + let TlsConnPair { client, server, io } = self; + let boxed_client = Box::new(client); + let boxed_server = Box::new(server); + TlsConnPair { client: boxed_client, server: boxed_server, io: io } + } +} + +impl TlsConnPair +where + C: TlsConnIo, + S: TlsConnIo, +{ + + // pub fn type_erase(self) -> TlsConnPair, Box> { + + // } + /// Run handshake on connections /// Two round trips are needed for the server to receive the Finished message /// from the client and be ready to send data @@ -287,17 +367,6 @@ where self.client.handshake_completed() && self.server.handshake_completed() } - pub fn get_negotiated_cipher_suite(&self) -> CipherSuite { - assert!(self.handshake_completed()); - assert!( - self.client.get_negotiated_cipher_suite() == self.server.get_negotiated_cipher_suite() - ); - self.client.get_negotiated_cipher_suite() - } - - pub fn negotiated_tls13(&self) -> bool { - self.client.negotiated_tls13() && self.server.negotiated_tls13() - } /// Send data from client to server, and then from server to client pub fn round_trip_transfer(&mut self, data: &mut [u8]) -> Result<(), Box> { @@ -313,6 +382,7 @@ where } } + #[cfg(test)] mod tests { use super::*; diff --git a/bindings/rust/bench/src/lib.rs b/bindings/rust/bench/src/lib.rs index 4d2565ff164..ac7c8831967 100644 --- a/bindings/rust/bench/src/lib.rs +++ b/bindings/rust/bench/src/lib.rs @@ -4,9 +4,17 @@ pub mod harness; #[cfg(feature = "openssl")] pub mod openssl; +#[cfg(feature = "openssl")] +pub mod openssl_extension; #[cfg(feature = "rustls")] pub mod rustls; pub mod s2n_tls; +// Although these are integration tests, we deliberately avoid the "integration" +// provided in the default repo setup, because it will run tests in serial rather +// than parallel/ +// https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html +#[cfg(test)] +mod tests; #[cfg(feature = "openssl")] pub use crate::openssl::OpenSslConnection; diff --git a/bindings/rust/bench/src/openssl.rs b/bindings/rust/bench/src/openssl.rs index 86c2538eaed..408418ec9b2 100644 --- a/bindings/rust/bench/src/openssl.rs +++ b/bindings/rust/bench/src/openssl.rs @@ -38,7 +38,7 @@ impl Drop for OpenSslConnection { } pub struct OpenSslConfig { - config: SslContext, + pub config: SslContext, session_ticket_storage: SessionTicketStorage, } diff --git a/bindings/rust/bench/src/openssl_extension.rs b/bindings/rust/bench/src/openssl_extension.rs new file mode 100644 index 00000000000..b4e1482c0e9 --- /dev/null +++ b/bindings/rust/bench/src/openssl_extension.rs @@ -0,0 +1,48 @@ +//! This module defines "extension" trait to add our own bindings to the openssl +//! crate. Ideally all of this logic would live _in_ the openssl crate, but they +//! don't really accept PRs +//! - add signature type retrieval functions: https://github.com/sfackler/rust-openssl/pull/2164 +//! - Add helper to return &mut SslRef from stream: https://github.com/sfackler/rust-openssl/pull/2223 + +// # define SSL_CTX_set_max_send_fragment(ctx,m) \ +// SSL_CTX_ctrl(ctx,SSL_CTRL_SET_MAX_SEND_FRAGMENT,m,NULL) + +use std::ffi::c_long; + +use openssl::ssl::SslContext; +use openssl_sys::SSL_CTX; + +// very tediously, we need to import exactly the same verion of ForeignType as +// ossl because we need this trait impl to access the raw pointers on all of the +// openssl types. +use foreign_types_shared::ForeignType; + +// expose the macro as a function +fn SSL_CTX_set_max_send_fragment(ctx: *mut SSL_CTX, m: c_long) -> c_long { + // # define SSL_CTRL_SET_MAX_SEND_FRAGMENT 52 + const SSL_CTRL_SET_MAX_SEND_FRAGMENT: std::ffi::c_int = 52; + + // TODO: assert on the return value + unsafe {openssl_sys::SSL_CTX_ctrl(ctx, SSL_CTRL_SET_MAX_SEND_FRAGMENT, m, std::ptr::null_mut())} +} + +extern "C" { + // int SSL_CTX_set_block_padding(SSL_CTX *ctx, size_t block_size); + pub fn SSL_CTX_set_block_padding(ctx: *mut SSL_CTX, block_size: usize) -> std::ffi::c_int; +} + +pub trait SslContextExtension { + fn set_max_send_fragment(&mut self, max_send_fragment: usize); + + fn set_block_padding(&mut self, block_size: usize); +} + +impl SslContextExtension for SslContext { + fn set_max_send_fragment(&mut self, max_send_fragment: usize) { + SSL_CTX_set_max_send_fragment(self.as_ptr(), max_send_fragment as _); + } + + fn set_block_padding(&mut self, block_size: usize) { + unsafe {SSL_CTX_set_block_padding(self.as_ptr(), block_size as _);} + } +} diff --git a/bindings/rust/bench/src/s2n_tls.rs b/bindings/rust/bench/src/s2n_tls.rs index 5a0cdc2d489..c821444be01 100644 --- a/bindings/rust/bench/src/s2n_tls.rs +++ b/bindings/rust/bench/src/s2n_tls.rs @@ -58,7 +58,7 @@ const KEY_VALUE: [u8; 16] = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3]; /// s2n-tls has mode-independent configs, so this struct wraps the config with the mode pub struct S2NConfig { mode: Mode, - config: s2n_tls::config::Config, + pub config: s2n_tls::config::Config, ticket_storage: SessionTicketStorage, } @@ -152,7 +152,7 @@ impl crate::harness::TlsBenchConfig for S2NConfig { } pub struct S2NConnection { - connection: Connection, + pub connection: Connection, handshake_completed: bool, } diff --git a/bindings/rust/bench/src/tests.rs b/bindings/rust/bench/src/tests.rs new file mode 100644 index 00000000000..d9d7175150b --- /dev/null +++ b/bindings/rust/bench/src/tests.rs @@ -0,0 +1,115 @@ +use crate::{ + harness::{self, TlsBenchConfig}, s2n_tls::S2NConfig, CryptoConfig, HandshakeType, Mode, OpenSslConnection, S2NConnection, SigType, TlsConnPair +}; + +mod fragmentation; +mod record_padding; + +trait TestUtils { + /// Assert that application data can be successfully transmitted between + /// clients and servers. + /// + /// Precondition: The connections must be ready to send data (have already + /// handshaked) + /// + /// 1. client sends `data_len` bytes to server + /// 2. server reads `data_len` bytes from client + /// 3. **ASSERT DATA EQUAL** + /// 4. server sends `data_len` bytes to client + /// 5. client reads `data_len` bytes from server + /// 6. **ASSERT DATA EQUAL** + fn round_trip_assert(&mut self, data_len: usize) -> Result<(), Box>; +} + +impl TestUtils for TlsConnPair +where + C: harness::TlsConnIo, + S: harness::TlsConnIo, +{ + fn round_trip_assert(&mut self, data_len: usize) -> Result<(), Box> { + let random_data = vec![0; data_len]; + let mut received_data = vec![0; data_len]; + + self.client.send(&random_data)?; + self.server.recv(&mut received_data)?; + + if !random_data.eq(&received_data) { + return Err("data received by server does not match expected".into()); + } + + self.server.send(&random_data)?; + self.client.recv(&mut received_data)?; + + if !random_data.eq(&received_data) { + return Err("data received by client does not match expected".into()); + } + Ok(()) + } +} + +struct ConfigPair(C, S); + +// new_client_config() + +impl Default for ConfigPair +where + C: TlsBenchConfig, + S: TlsBenchConfig, +{ + fn default() -> Self { + // select certificate + let crypto_config = CryptoConfig::default(); + + let c = C::make_config(Mode::Client, crypto_config, HandshakeType::ServerAuth).unwrap(); + let s = S::make_config(Mode::Server, crypto_config, HandshakeType::ServerAuth).unwrap(); + + // select protocol versions + + // select ciphers + // select kx groups + // select signatures + + //let s2n_config = s2n_tls::config::Config::builder() + // configure with certificates + + // default configuration -> set protocol, sig scheme, ciphers, etc + // configure certs -> mode dependent. Maybe add to trust store, maybe prepare to send + + Self(c, s) + } +} + +impl ConfigPair { + pub fn split(self) -> (C, S) { + (self.0, self.1) + } +} + +fn random_test_data(data_len: usize) -> Vec { + let random_data = vec![0; data_len]; + // fill with random data + random_data +} + +// fn config_pair() +// impl TestUtils for TlsConnPair { +// fn round_trip(&mut self, data_len: usize) -> Result<(), Box> { +// todo!() +// } +// } + + +#[test] +fn type_erasure() { + let (ossl_config, s2n_config) = + ConfigPair::::default().split(); + + let mut pair: TlsConnPair = + TlsConnPair::from_configs(&ossl_config, &s2n_config); + + // type erase the conn pair, which will make it easy to return in different scenarios + + + assert!(pair.handshake().is_ok()); + assert!(pair.round_trip_assert(16_000).is_ok()); +} diff --git a/bindings/rust/bench/src/tests/fragmentation.rs b/bindings/rust/bench/src/tests/fragmentation.rs new file mode 100644 index 00000000000..060f1bc650e --- /dev/null +++ b/bindings/rust/bench/src/tests/fragmentation.rs @@ -0,0 +1,53 @@ +use crate::{ + openssl_extension::SslContextExtension, s2n_tls::S2NConfig, tests::TestUtils, + OpenSslConnection, S2NConnection, TlsConnPair, +}; + +use super::ConfigPair; + +/// Feature: s2n_connection_prefer_low_latency() +/// +/// "Prefer low latency" causes s2n-tls to use smaller record sizes. This is a wire +/// format change, so we use an integration test to make sure things remain correct. +#[test] +fn prefer_low_latency() { + let (ossl_config, s2n_config) = + ConfigPair::::default().split(); + + let mut pair: TlsConnPair = + TlsConnPair::from_configs(&ossl_config, &s2n_config); + + // configure s2n-tls server connection to prefer low latency + pair.server.connection.prefer_low_latency().unwrap(); + + assert!(pair.handshake().is_ok()); + assert!(pair.round_trip_assert(16_000).is_ok()); +} + +/// Correctness: s2n-tls correctly handles different record sizes +/// +/// We configure an openssl client to use a variety of record sizes to confirm +/// that s2n-tls correctly handles the differently sized records. This is done by +/// with the `SSL_CTX_set_max_send_fragment` openssl API. +/// https://docs.openssl.org/3.0/man3/SSL_CTX_set_split_send_fragment/#synopsis +#[test] +fn fragmentation() { + const FRAGMENT_TEST_CASES: [usize; 5] = [512, 2048, 8192, 12345, 16384]; + + fn test_case(client_frag_length: usize) { + let (mut ossl_config, s2n_config) = + ConfigPair::::default().split(); + + ossl_config.config.set_max_send_fragment(client_frag_length); + + let mut pair: TlsConnPair = + TlsConnPair::from_configs(&ossl_config, &s2n_config); + + assert!(pair.handshake().is_ok()); + assert!(pair.round_trip_assert(16_000).is_ok()); + } + + FRAGMENT_TEST_CASES + .into_iter() + .for_each(|frag_length| test_case(frag_length)); +} diff --git a/bindings/rust/bench/src/tests/record_padding.rs b/bindings/rust/bench/src/tests/record_padding.rs new file mode 100644 index 00000000000..4e83fde3aba --- /dev/null +++ b/bindings/rust/bench/src/tests/record_padding.rs @@ -0,0 +1,62 @@ +use std::time::Duration; + +use crate::{ + openssl::OpenSslConfig, + openssl_extension::SslContextExtension, + s2n_tls::S2NConfig, + tests::{ConfigPair, TestUtils}, + OpenSslConnection, S2NConnection, TlsConnPair, +}; + +/// Correctness: s2n-tls correctly handles padded records +/// +/// Record padding is new in TLS 1.3 +/// +/// We configure an openssl client to pad records to a specific size using +/// `SSL_CTX_set_block_padding`. This function will pad records to a multiple +/// of the supplied `pad_to` size. +/// https://docs.openssl.org/1.1.1/man3/SSL_CTX_set_record_padding_callback/ +#[test] +fn record_padding() { + const SEND_SIZES: [usize; 6] = [1, 10, 100, 1_000, 5_000, 10_000]; + const PAD_TO_CASES: [usize; 4] = [512, 1_024, 4_096, 16_000]; + + // we _could_ type erase the TlsConnPair, but it involves a decent amount of + // boilerplate to Box everything. For the time being, the duplication is + // preferred. + + fn s2n_server_case(pad_to: usize) { + let (mut ossl_config, s2n_config) = ConfigPair::::default().split(); + + ossl_config.config.set_block_padding(pad_to); + + let mut pair: TlsConnPair = + TlsConnPair::from_configs(&ossl_config, &s2n_config); + + assert!(pair.handshake().is_ok()); + for send in SEND_SIZES { + assert!(pair.round_trip_assert(send).is_ok()); + } + } + + fn s2n_client_case(pad_to: usize) { + let (s2n_config, mut ossl_config) = ConfigPair::::default().split(); + + ossl_config.config.set_block_padding(pad_to); + + let mut pair: TlsConnPair = + TlsConnPair::from_configs(&s2n_config, &ossl_config); + + assert!(pair.handshake().is_ok()); + for send in SEND_SIZES { + assert!(pair.round_trip_assert(send).is_ok()); + } + } + + PAD_TO_CASES + .into_iter() + .for_each(|pad_to| { + s2n_server_case(pad_to); + s2n_client_case(pad_to); + }); +}