From a23e8c56ec29beceacbbc2aa7598ad3a86efe721 Mon Sep 17 00:00:00 2001 From: Justin Zhang <76919968+tinzh@users.noreply.github.com> Date: Wed, 28 Jun 2023 17:47:03 -0700 Subject: [PATCH] feat(bench): add openssl handshake to benchmarking (#4069) --- bindings/rust/bench/Cargo.toml | 1 + bindings/rust/bench/README.md | 27 ++++++ bindings/rust/bench/benches/handshake.rs | 62 +++++++----- bindings/rust/bench/src/harness.rs | 9 +- bindings/rust/bench/src/lib.rs | 2 + bindings/rust/bench/src/openssl.rs | 117 +++++++++++++++++++++++ 6 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 bindings/rust/bench/README.md create mode 100644 bindings/rust/bench/src/openssl.rs diff --git a/bindings/rust/bench/Cargo.toml b/bindings/rust/bench/Cargo.toml index 968546e8157..2addaf728ae 100644 --- a/bindings/rust/bench/Cargo.toml +++ b/bindings/rust/bench/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" s2n-tls = { path = "../s2n-tls" } rustls = "0.21" rustls-pemfile = "1.0" +openssl = "0.10" errno = "0.3" libc = "0.2" diff --git a/bindings/rust/bench/README.md b/bindings/rust/bench/README.md new file mode 100644 index 00000000000..51d4f3a0034 --- /dev/null +++ b/bindings/rust/bench/README.md @@ -0,0 +1,27 @@ +# Benchmarking s2n-tls + +We use to Criterion.rs to benchmark s2n-tls against two commonly used TLS libraries, Rustls and OpenSSL. + +## Setup + +Setup is easy! Just have OpenSSL installed and generate Rust bindings for s2n-tls using `bindings/rust/generate.sh`. + +## Running benchmarks + +The benchmarks can be run with the `cargo bench` command. Criterion will auto-generate an HTML report in `target/criterion/`. + +## Implementation details + +We use Rust bindings for s2n-tls and OpenSSL. All of our benchmarks are run in Rust on a single thread for consistency. + +### IO + +To remove external factors, we use custom IO with our benchmarks, bypassing the networking layer and having the client and server connections transfer data to each other via a local buffer. + +### Certificate generation + +All certs are stored in `certs/` and can be regenerated using `certs/generate_certs.sh`. There is one root cert that directly signs the server and client certs that are used in benchmarking. Currently, we use ECDSA with `secp384r1`. + +### Negotiation parameters + +The cipher suites benchmarked are `TLS_AES_128_GCM_SHA256` and `TLS_AES_256_GCM_SHA384`, and the key exchange methods benchmarked are ECDHE with `secp256r1` and with `x25519`. diff --git a/bindings/rust/bench/benches/handshake.rs b/bindings/rust/bench/benches/handshake.rs index c646543c2aa..4787ff415c6 100644 --- a/bindings/rust/bench/benches/handshake.rs +++ b/bindings/rust/bench/benches/handshake.rs @@ -1,37 +1,47 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use bench::{RustlsHarness, S2NHarness, TlsBenchHarness}; -use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use std::any::type_name; -pub fn bench_handshake(c: &mut Criterion) { - let mut group = c.benchmark_group("handshake"); +use bench::{ + CipherSuite::*, + CryptoConfig, + ECGroup::{self, *}, + OpenSslHarness, RustlsHarness, S2NHarness, TlsBenchHarness, +}; +use criterion::{ + criterion_group, criterion_main, measurement::WallTime, BatchSize, BenchmarkGroup, Criterion, +}; - macro_rules! bench_handshake_for_libraries { - ($(($lib_name:expr, $lib_type:ty),)*) => { - $( - // generate all inputs (TlsBenchHarness structs) before benchmarking handshakes - // timing only includes negotiation, not config/connection initialization - group.bench_function($lib_name, |b| { - b.iter_batched_ref( - || <$lib_type>::default().unwrap(), - |harness| { - harness.handshake().unwrap(); - }, - BatchSize::SmallInput, - ) - }); - )* - } +pub fn bench_handshake_key_exchange(c: &mut Criterion) { + fn bench_handshake_for_library( + bench_group: &mut BenchmarkGroup, + ec_group: &ECGroup, + ) { + bench_group.bench_function(type_name::(), |b| { + b.iter_batched_ref( + || { + T::new(&CryptoConfig { + cipher_suite: AES_128_GCM_SHA256, + ec_group: *ec_group, + }) + .unwrap() + }, + |harness| { + harness.handshake().unwrap(); + }, + BatchSize::SmallInput, + ) + }); } - bench_handshake_for_libraries! { - ("s2n-tls", S2NHarness), - ("rustls", RustlsHarness), + for ec_group in [SECP256R1, X25519] { + let mut bench_group = c.benchmark_group(format!("handshake-{:?}", ec_group)); + bench_handshake_for_library::(&mut bench_group, &ec_group); + bench_handshake_for_library::(&mut bench_group, &ec_group); + bench_handshake_for_library::(&mut bench_group, &ec_group); } - - group.finish(); } -criterion_group!(benches, bench_handshake); +criterion_group!(benches, bench_handshake_key_exchange); criterion_main!(benches); diff --git a/bindings/rust/bench/src/harness.rs b/bindings/rust/bench/src/harness.rs index 56707bbc08f..e582efe0caa 100644 --- a/bindings/rust/bench/src/harness.rs +++ b/bindings/rust/bench/src/harness.rs @@ -22,13 +22,13 @@ pub enum Mode { // these parameters were the only ones readily usable for all three libaries: // s2n-tls, rustls, and openssl #[allow(non_camel_case_types)] -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CipherSuite { AES_128_GCM_SHA256, AES_256_GCM_SHA384, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ECGroup { SECP256R1, X25519, @@ -45,7 +45,7 @@ pub trait TlsBenchHarness: Sized { fn default() -> Result> { Self::new(&CryptoConfig { cipher_suite: CipherSuite::AES_128_GCM_SHA256, - ec_group: ECGroup::SECP256R1, + ec_group: ECGroup::X25519, }) } @@ -151,10 +151,11 @@ macro_rules! test_tls_bench_harnesses { #[cfg(test)] mod tests { use super::*; - use crate::{RustlsHarness, S2NHarness, TlsBenchHarness}; + use crate::{OpenSslHarness, RustlsHarness, S2NHarness, TlsBenchHarness}; test_tls_bench_harnesses! { s2n_tls: S2NHarness, rustls: RustlsHarness, + openssl: OpenSslHarness, } } diff --git a/bindings/rust/bench/src/lib.rs b/bindings/rust/bench/src/lib.rs index 9c55fe8ecd7..90e53b76838 100644 --- a/bindings/rust/bench/src/lib.rs +++ b/bindings/rust/bench/src/lib.rs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 pub mod harness; +pub mod openssl; pub mod rustls; pub mod s2n_tls; pub use crate::{ harness::{CipherSuite, CryptoConfig, ECGroup, TlsBenchHarness}, + openssl::OpenSslHarness, rustls::RustlsHarness, s2n_tls::S2NHarness, }; diff --git a/bindings/rust/bench/src/openssl.rs b/bindings/rust/bench/src/openssl.rs new file mode 100644 index 00000000000..480a31a21f5 --- /dev/null +++ b/bindings/rust/bench/src/openssl.rs @@ -0,0 +1,117 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + harness::{CipherSuite, ConnectedBuffer, CryptoConfig, ECGroup, Mode, TlsBenchHarness}, + CA_CERT_PATH, SERVER_CERT_CHAIN_PATH, SERVER_KEY_PATH, +}; +use openssl::ssl::{ + ErrorCode, Ssl, SslContext, SslContextBuilder, SslFiletype, SslMethod, SslStream, SslVersion, +}; +use std::error::Error; + +pub struct OpenSslHarness { + client_conn: SslStream, + server_conn: SslStream, +} + +impl OpenSslHarness { + fn common_config( + builder: &mut SslContextBuilder, + cipher_suite: &str, + ec_key: &str, + ) -> Result<(), Box> { + builder.set_min_proto_version(Some(SslVersion::TLS1_3))?; + builder.set_ciphersuites(cipher_suite)?; + builder.set_groups_list(ec_key)?; + Ok(()) + } + /// Process handshake for one connection, treating blocking errors as `Ok` + fn handshake_conn(&mut self, mode: Mode) -> Result<(), Box> { + let result = match mode { + Mode::Client => self.client_conn.connect(), + Mode::Server => self.server_conn.accept(), + }; + match result { + Ok(_) => Ok(()), + Err(err) => { + if err.code() != ErrorCode::WANT_READ { + Err(err.into()) + } else { + Ok(()) + } + } + } + } +} + +impl TlsBenchHarness for OpenSslHarness { + fn new(crypto_config: &CryptoConfig) -> Result> { + let client_buf = ConnectedBuffer::new(); + let server_buf = client_buf.clone_inverse(); + + let cipher_suite = match crypto_config.cipher_suite { + CipherSuite::AES_128_GCM_SHA256 => "TLS_AES_128_GCM_SHA256", + CipherSuite::AES_256_GCM_SHA384 => "TLS_AES_256_GCM_SHA384", + }; + + let ec_key = match crypto_config.ec_group { + ECGroup::SECP256R1 => "P-256", + ECGroup::X25519 => "X25519", + }; + + let mut client_builder = SslContext::builder(SslMethod::tls_client())?; + client_builder.set_ca_file(CA_CERT_PATH)?; + Self::common_config(&mut client_builder, cipher_suite, ec_key)?; + + let mut server_builder = SslContext::builder(SslMethod::tls_server())?; + server_builder.set_certificate_chain_file(SERVER_CERT_CHAIN_PATH)?; + server_builder.set_private_key_file(SERVER_KEY_PATH, SslFiletype::PEM)?; + Self::common_config(&mut server_builder, cipher_suite, ec_key)?; + + let client_config = client_builder.build(); + let server_config = server_builder.build(); + + let client_conn = SslStream::new(Ssl::new(&client_config)?, client_buf)?; + let server_conn = SslStream::new(Ssl::new(&server_config)?, server_buf)?; + + Ok(Self { + client_conn, + server_conn, + }) + } + + fn handshake(&mut self) -> Result<(), Box> { + for _ in 0..2 { + self.handshake_conn(Mode::Client)?; + self.handshake_conn(Mode::Server)?; + } + Ok(()) + } + + fn handshake_completed(&self) -> bool { + self.client_conn.ssl().is_init_finished() && self.server_conn.ssl().is_init_finished() + } + + fn get_negotiated_cipher_suite(&self) -> CipherSuite { + let cipher_suite = self + .client_conn + .ssl() + .current_cipher() + .expect("Handshake not completed") + .name(); + match cipher_suite { + "TLS_AES_128_GCM_SHA256" => CipherSuite::AES_128_GCM_SHA256, + "TLS_AES_256_GCM_SHA384" => CipherSuite::AES_256_GCM_SHA384, + _ => panic!("Unknown cipher suite"), + } + } + + fn negotiated_tls13(&self) -> bool { + self.client_conn + .ssl() + .version2() // version() -> &str is deprecated, version2() returns an enum instead + .expect("Handshake not completed") + == SslVersion::TLS1_3 + } +}