diff --git a/.github/workflows/libssl.yaml b/.github/workflows/libssl.yaml index 5fb9750..2f21355 100644 --- a/.github/workflows/libssl.yaml +++ b/.github/workflows/libssl.yaml @@ -24,7 +24,8 @@ jobs: - stable - beta - nightly - os: [ubuntu-latest] + # TODO(XXX): consider replacing ubuntu-24.04 w/ ubuntu-latest when appropriate + os: [ubuntu-24.04, ubuntu-22.04] steps: - name: Checkout sources uses: actions/checkout@v4 diff --git a/rustls-libssl/MATRIX.md b/rustls-libssl/MATRIX.md index 90a216b..8a0ad77 100644 --- a/rustls-libssl/MATRIX.md +++ b/rustls-libssl/MATRIX.md @@ -48,17 +48,17 @@ | `SSL_COMP_get_id` | | | | | `SSL_COMP_get_name` | | | | | `SSL_COMP_set0_compression_methods` | | | | -| `SSL_CONF_CTX_clear_flags` | | | | -| `SSL_CONF_CTX_finish` | | :white_check_mark: | :exclamation: [^stub] | -| `SSL_CONF_CTX_free` | | :white_check_mark: | :exclamation: [^stub] | -| `SSL_CONF_CTX_new` | | :white_check_mark: | :exclamation: [^stub] | -| `SSL_CONF_CTX_set1_prefix` | | | | -| `SSL_CONF_CTX_set_flags` | | :white_check_mark: | :exclamation: [^stub] | -| `SSL_CONF_CTX_set_ssl` | | | | -| `SSL_CONF_CTX_set_ssl_ctx` | | :white_check_mark: | :exclamation: [^stub] | -| `SSL_CONF_cmd` | | :white_check_mark: | :exclamation: [^stub] | +| `SSL_CONF_CTX_clear_flags` | | | :white_check_mark: | +| `SSL_CONF_CTX_finish` | | :white_check_mark: | :white_check_mark: | +| `SSL_CONF_CTX_free` | | :white_check_mark: | :white_check_mark: | +| `SSL_CONF_CTX_new` | | :white_check_mark: | :white_check_mark: | +| `SSL_CONF_CTX_set1_prefix` | | | :white_check_mark: | +| `SSL_CONF_CTX_set_flags` | | :white_check_mark: | :white_check_mark: | +| `SSL_CONF_CTX_set_ssl` | | | :white_check_mark: | +| `SSL_CONF_CTX_set_ssl_ctx` | | :white_check_mark: | :white_check_mark: | +| `SSL_CONF_cmd` | | :white_check_mark: | :white_check_mark: | | `SSL_CONF_cmd_argv` | | | | -| `SSL_CONF_cmd_value_type` | | :white_check_mark: | :exclamation: [^stub] | +| `SSL_CONF_cmd_value_type` | | :white_check_mark: | :white_check_mark: | | `SSL_CTX_SRP_CTX_free` [^deprecatedin_3_0] [^srp] | | | | | `SSL_CTX_SRP_CTX_init` [^deprecatedin_3_0] [^srp] | | | | | `SSL_CTX_add1_to_CA_list` | | | | diff --git a/rustls-libssl/Makefile b/rustls-libssl/Makefile index f1f937c..550de6b 100644 --- a/rustls-libssl/Makefile +++ b/rustls-libssl/Makefile @@ -19,7 +19,7 @@ ifneq (,$(TARGET)) CARGOFLAGS += --target $(TARGET) endif -all: target/ciphers target/client target/constants target/server target/$(PROFILE)/libssl.so.3 +all: target/ciphers target/client target/config target/constants target/server target/$(PROFILE)/libssl.so.3 test: all ${CARGO} test $(CARGOFLAGS) @@ -45,6 +45,9 @@ target/ciphers: target/ciphers.o target/client: target/client.o $(CC) -o $@ $^ $(LDFLAGS) $(shell pkg-config --libs openssl) +target/config: target/config.o + $(CC) -o $@ $^ $(LDFLAGS) $(shell pkg-config --libs openssl) + target/constants: target/constants.o $(CC) -o $@ $^ $(LDFLAGS) $(shell pkg-config --libs openssl) diff --git a/rustls-libssl/build.rs b/rustls-libssl/build.rs index 56a905a..1776576 100644 --- a/rustls-libssl/build.rs +++ b/rustls-libssl/build.rs @@ -64,7 +64,9 @@ const ENTRYPOINTS: &[&str] = &[ "SSL_CONF_CTX_finish", "SSL_CONF_CTX_free", "SSL_CONF_CTX_new", + "SSL_CONF_CTX_set1_prefix", "SSL_CONF_CTX_set_flags", + "SSL_CONF_CTX_set_ssl", "SSL_CONF_CTX_set_ssl_ctx", "SSL_connect", "SSL_ctrl", diff --git a/rustls-libssl/src/conf.rs b/rustls-libssl/src/conf.rs new file mode 100644 index 0000000..98bbf60 --- /dev/null +++ b/rustls-libssl/src/conf.rs @@ -0,0 +1,346 @@ +/// Implementation of the `SSL_CONF_*` API functions. +use std::os::raw::{c_int, c_uint}; +use std::sync::Arc; + +use rustls::ProtocolVersion; + +use crate::error::Error; +use crate::not_thread_safe::NotThreadSafe; +use crate::{Ssl, SslContext}; + +#[derive(Default)] +pub(super) struct SslConfigCtx { + prefix: String, + flags: Flags, + state: State, +} + +impl SslConfigCtx { + pub(super) fn new() -> Self { + Self { + prefix: "-".to_owned(), // OpenSSL default. + flags: Flags::default(), + state: State::default(), + } + } + + pub(super) fn cmd(&mut self, cmd: &str, value: Option<&str>) -> c_int { + let command = match self.supported_command(cmd) { + Some(command) => command, + None => { + return -2; // "A return value of -2 means option is not recognised." + } + }; + + match (command.action)(self, value) { + Err(mut e) => { + if self.flags.is_show_errors() { + e = Error::bad_data(&format!("cmd={cmd} value={}", value.unwrap_or("none"))) + .raise() + } + e.into() + } + Ok(action_result) => action_result.into(), + } + } + + pub(super) fn cmd_value_type(&self, cmd: &str) -> ValueType { + self.supported_command(cmd) + .map(|c| c.value_type) + .unwrap_or(ValueType::Unknown) + } + + pub(super) fn set_flags(&mut self, flags: u32) -> Flags { + self.flags.0 |= flags; + self.flags + } + + pub(super) fn clear_flags(&mut self, flags: u32) -> Flags { + self.flags.0 &= !flags; + self.flags + } + + pub(super) fn set_prefix(&mut self, prefix: &str) { + // Note: allows setting the prefix to "" explicitly, overriding the default "-". + prefix.clone_into(&mut self.prefix) + } + + pub(super) fn apply_to_ctx(&mut self, ctx: Arc>) { + self.state = ctx.into() + } + + pub(super) fn apply_to_ssl(&mut self, ssl: Arc>) { + self.state = ssl.into() + } + + pub(super) fn validation_only(&mut self) { + self.state = State::Validating + } + + pub(super) fn finish(&mut self) -> bool { + // NOTE(XXX): only CA names and, SSL_CONF_FLAG_REQUIRE_PRIVATE are handled here, and both + // are NYI for this shim. For other cmds, the CTX (if set) is mutated along the way. + true + } + + fn min_protocol(&mut self, proto: Option<&str>) -> Result { + let ver = match Self::parse_protocol_version(proto) { + Some(ver) => ver, + None => return Err(Error::bad_data("unrecognized protocol version")), + }; + + Ok(match &self.state { + // For some reason the upstream returns 0 in this case. + State::Validating => return Err(Error::bad_data("no ctx/ssl")), + State::ApplyingToCtx(ctx) => { + ctx.get_mut().set_min_protocol_version(ver); + ActionResult::Applied + } + State::ApplyingToSsl(ssl) => { + ssl.get_mut().set_min_protocol_version(ver); + ActionResult::Applied + } + }) + } + + fn max_protocol(&mut self, proto: Option<&str>) -> Result { + let ver = match Self::parse_protocol_version(proto) { + Some(ver) => ver, + None => return Err(Error::bad_data("unrecognized protocol version")), + }; + + Ok(match &self.state { + // For some reason the upstream returns 0 in this case. + State::Validating => return Err(Error::bad_data("no ctx/ssl")), + State::ApplyingToCtx(ctx) => { + ctx.get_mut().set_max_protocol_version(ver); + ActionResult::Applied + } + State::ApplyingToSsl(ssl) => { + ssl.get_mut().set_max_protocol_version(ver); + ActionResult::Applied + } + }) + } + + fn parse_protocol_version(proto: Option<&str>) -> Option { + Some(match proto { + Some("None") => 0, + Some("TLSv1.2") => u16::from(ProtocolVersion::TLSv1_2), + Some("TLSv1.3") => u16::from(ProtocolVersion::TLSv1_3), + _ => return None, + }) + } + + fn supported_command(&self, cmd_name: &str) -> Option<&Command> { + SUPPORTED_COMMANDS.iter().find(|cmd| { + // If the cctx flags don't permit the cmd, skip it. + if !cmd.permitted(self) { + return false; + } + + // cmd line flag options are matched case-sensitively, honouring the prefix. + if self.flags.is_cmdline() { + if let Some(cli_cmd) = cmd.name_cmdline { + if cmd_name == format!("{}{cli_cmd}", self.prefix) { + return true; + } + } + } + + // cmd file options are matched **case-insensitively**. The prefix is only considered + // if it's non-default. + if self.flags.is_file() { + if let Some(file_cmd) = cmd.name_file { + if match self.prefix.as_str() { + // Default prefix - ignore in comparison. + "-" => cmd_name.eq_ignore_ascii_case(file_cmd), + // Custom prefix, use in comparison. + prefix => cmd_name.eq_ignore_ascii_case(&format!("{prefix}{file_cmd}")), + } { + return true; + } + } + } + + false + }) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[repr(i32)] +#[allow(dead_code)] // TODO(XXX): Remove once other value types are used. +pub(super) enum ValueType { + /// The option is unrecognized. + Unknown = 0x0, + /// The option value is a string without any specific structure. + String = 0x1, + /// The option value is a filename. + File = 0x2, + /// The option value is a directory name. + Dir = 0x3, + /// The option value is not used. + None = 0x4, + /// The option is an X509 store + // NOTE(XXX): This one is missing from the OpenSSL man pages. + Store = 0x5, +} + +impl From for c_int { + fn from(value: ValueType) -> Self { + value as i32 + } +} + +/// Describes how option calls on a `SslConfigCtx` should be handled. +/// +/// If no `SslContext` or `Ssl` is set, the command is validated but not applied. +/// Otherwise, commands mutate the `SslContext` or `Ssl` as appropriate. +#[derive(Default)] +enum State { + /// Command values are validated, but not applied to a [`Ssl`] or [`SslContext`] + #[default] + Validating, + /// Commands are applied to a [`SslContext`] + ApplyingToCtx(Arc>), + /// Commands are applied to a [`Ssl`] + ApplyingToSsl(Arc>), +} + +impl From>> for State { + fn from(ctx: Arc>) -> Self { + Self::ApplyingToCtx(ctx) + } +} + +impl From>> for State { + fn from(ssl: Arc>) -> Self { + Self::ApplyingToSsl(ssl) + } +} + +/// A command that can be applied to a [`SslConfigCtx`] based on the context and command flags. +struct Command { + /// Name of the command when used by a [`SslConfigCtx`] with [`Flags::FILE`] set. + name_file: Option<&'static str>, + /// Name of the command when used by a [`SslConfigCtx`] with [`Flags::CMDLINE`] set. + name_cmdline: Option<&'static str>, + /// Flags that must be set on the [`SslConfigCtx`] for the command to be permitted. + flags: Flags, + /// Type of value expected by the [`CommandAction`]. + value_type: ValueType, + /// Function that updates the [`SslConfigCtx`] based on the command value. + action: CommandAction, +} + +impl Command { + /// Returns true if the command is permitted based on the context flags. + fn permitted(&self, cctx: &SslConfigCtx) -> bool { + // Matched to the logic from OpenSSL `static ssl_conf_cmd_allowed`. + if self.flags.is_server() && !cctx.flags.is_server() { + return false; + } + + if self.flags.is_client() && !cctx.flags.is_client() { + return false; + } + + if self.flags.is_certificate() && !cctx.flags.is_certificate() { + return false; + } + + true + } +} + +/// A fn that can update `SslConfigCtx` after parsing `value`. +type CommandAction = fn(&mut SslConfigCtx, value: Option<&str>) -> Result; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[repr(i32)] +#[allow(dead_code)] // TODO(XXX): Remove with first usage of NotApplied. +enum ActionResult { + /// The action value was recognized, but not applied. + /// + /// For example, if no `SSL_CTX` has been set a [`CommandAction`] may return `NotApplied` after + /// validating the command value. + NotApplied = 1, + /// The action value was recognized and applied. + Applied = 2, +} + +impl From for c_int { + fn from(value: ActionResult) -> Self { + value as c_int + } +} + +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] +pub(super) struct Flags(c_uint); + +#[allow(dead_code)] // TODO(XXX): Remove once all flags are used. +impl Flags { + const ANY: c_uint = 0x0; + + // See openssl/ssl.h + const CMDLINE: c_uint = 0x1; + const FILE: c_uint = 0x2; + const CLIENT: c_uint = 0x4; + const SERVER: c_uint = 0x8; + const SHOW_ERRORS: c_uint = 0x10; + const CERTIFICATE: c_uint = 0x20; + const REQUIRE_PRIVATE: c_uint = 0x40; + + fn is_cmdline(&self) -> bool { + self.0 & Self::CMDLINE == Self::CMDLINE + } + + fn is_file(&self) -> bool { + self.0 & Self::FILE == Self::FILE + } + + fn is_client(&self) -> bool { + self.0 & Self::CLIENT == Self::CLIENT + } + + fn is_server(&self) -> bool { + self.0 & Self::SERVER == Self::SERVER + } + + fn is_certificate(&self) -> bool { + self.0 & Self::CERTIFICATE == Self::CERTIFICATE + } + + fn is_require_private(&self) -> bool { + self.0 & Self::REQUIRE_PRIVATE == Self::REQUIRE_PRIVATE + } + + fn is_show_errors(&self) -> bool { + self.0 & Self::SHOW_ERRORS == Self::SHOW_ERRORS + } +} + +impl From for c_uint { + fn from(flags: Flags) -> c_uint { + flags.0 + } +} + +/// All the [`Command`]s that are supported by [`SslConfigCtx`]. +const SUPPORTED_COMMANDS: &[Command] = &[ + Command { + name_file: Some("MinProtocol"), + name_cmdline: Some("min_protocol"), + flags: Flags(Flags::ANY), + value_type: ValueType::String, + action: SslConfigCtx::min_protocol, + }, + Command { + name_file: Some("MaxProtocol"), + name_cmdline: Some("max_protocol"), + flags: Flags(Flags::ANY), + value_type: ValueType::String, + action: SslConfigCtx::max_protocol, + }, +]; diff --git a/rustls-libssl/src/entry.rs b/rustls-libssl/src/entry.rs index 9ff195a..4a372e6 100644 --- a/rustls-libssl/src/entry.rs +++ b/rustls-libssl/src/entry.rs @@ -22,13 +22,13 @@ use crate::error::{ffi_panic_boundary, Error, MysteriouslyOppositeReturnValue}; use crate::evp_pkey::EvpPkey; use crate::ex_data::ExData; use crate::ffi::{ - clone_arc, free_arc, free_arc_into_inner, str_from_cstring, to_arc_mut_ptr, try_clone_arc, - try_from, try_mut_slice_int, try_ref_from_ptr, try_slice, try_slice_int, try_str, Castable, - OwnershipArc, OwnershipRef, + clone_arc, free_arc, free_arc_into_inner, free_box, str_from_cstring, string_from_cstring, + to_arc_mut_ptr, to_boxed_mut_ptr, try_clone_arc, try_from, try_mut_slice_int, try_ref_from_ptr, + try_slice, try_slice_int, try_str, Castable, OwnershipArc, OwnershipBox, OwnershipRef, }; use crate::not_thread_safe::NotThreadSafe; use crate::x509::{load_certs, OwnedX509, OwnedX509Stack}; -use crate::{HandshakeState, ShutdownResult}; +use crate::{conf, HandshakeState, ShutdownResult}; /// Makes a entry function definition. /// @@ -1706,6 +1706,95 @@ impl Castable for SSL_SESSION { type RustType = NotThreadSafe; } +entry! { + pub fn _SSL_CONF_CTX_new() -> *mut SSL_CONF_CTX { + to_boxed_mut_ptr(NotThreadSafe::new(conf::SslConfigCtx::new())) + } +} + +entry! { + pub fn _SSL_CONF_CTX_free(cctx: *mut SSL_CONF_CTX) { + free_box(cctx); + } +} + +entry! { + pub fn _SSL_CONF_CTX_finish(cctx: *mut SSL_CONF_CTX) -> c_int { + match try_ref_from_ptr!(cctx).get_mut().finish() { + true => C_INT_SUCCESS, + false => 0, + } + } +} + +entry! { + pub fn _SSL_CONF_CTX_set_flags(cctx: *mut SSL_CONF_CTX, flags: c_uint) -> c_uint { + try_ref_from_ptr!(cctx).get_mut().set_flags(flags).into() + } +} + +entry! { + pub fn _SSL_CONF_CTX_clear_flags(cctx: *mut SSL_CONF_CTX, flags: c_uint) -> c_uint { + try_ref_from_ptr!(cctx).get_mut().clear_flags(flags).into() + } +} + +entry! { + pub fn _SSL_CONF_CTX_set1_prefix(cctx: *mut SSL_CONF_CTX, prefix: *mut c_char) -> c_int { + try_ref_from_ptr!(cctx) + .get_mut() + .set_prefix(try_str!(prefix)); + C_INT_SUCCESS + } +} + +entry! { + pub fn _SSL_CONF_cmd(cctx: *mut SSL_CONF_CTX, cmd: *mut c_char, value: *mut c_char) -> c_int { + // Note: we use string_from_cstring here instead of try_str! because some commands + // may allow NULL as a value. + let value = string_from_cstring(value); + try_ref_from_ptr!(cctx) + .get_mut() + .cmd(try_str!(cmd), value.as_deref()) + } +} + +entry! { + pub fn _SSL_CONF_cmd_value_type(cctx: *mut SSL_CONF_CTX, cmd: *mut c_char) -> c_int { + try_ref_from_ptr!(cctx) + .get() + .cmd_value_type(try_str!(cmd)) + .into() + } +} + +entry! { + pub fn _SSL_CONF_CTX_set_ssl(cctx: *mut SSL_CONF_CTX, ssl: *mut SSL) { + let cctx = try_ref_from_ptr!(cctx).get_mut(); + match ssl.is_null() { + true => cctx.validation_only(), + false => cctx.apply_to_ssl(try_clone_arc!(ssl)), + } + } +} + +entry! { + pub fn _SSL_CONF_CTX_set_ssl_ctx(cctx: *mut SSL_CONF_CTX, ctx: *mut SSL_CTX) { + let cctx = try_ref_from_ptr!(cctx).get_mut(); + match ctx.is_null() { + true => cctx.validation_only(), + false => cctx.apply_to_ctx(try_clone_arc!(ctx)), + } + } +} + +pub type SSL_CONF_CTX = conf::SslConfigCtx; + +impl Castable for SSL_CONF_CTX { + type Ownership = OwnershipBox; // SSL_CONF_CTX does not do reference counting. + type RustType = NotThreadSafe; +} + /// Normal OpenSSL return value convention success indicator. /// /// Compare [`crate::ffi::MysteriouslyOppositeReturnValue`]. @@ -1972,38 +2061,6 @@ entry_stub! { pub fn _SSL_set_post_handshake_auth(_s: *mut SSL, _val: c_int); } -// no configuration command support - -entry_stub! { - pub fn _SSL_CONF_CTX_new() -> *mut SSL_CONF_CTX; -} - -entry_stub! { - pub fn _SSL_CONF_CTX_free(_cctx: *mut SSL_CONF_CTX); -} - -entry_stub! { - pub fn _SSL_CONF_CTX_finish(_cctx: *mut SSL_CONF_CTX) -> c_int; -} - -entry_stub! { - pub fn _SSL_CONF_CTX_set_flags(_cctx: *mut SSL_CONF_CTX, _flags: c_uint) -> c_uint; -} - -entry_stub! { - pub fn _SSL_CONF_CTX_set_ssl_ctx(_cctx: *mut SSL_CONF_CTX, _ctx: *mut SSL_CTX); -} - -entry_stub! { - pub fn _SSL_CONF_cmd(_ctx: *mut SSL_CONF_CTX, _opt: *mut c_char, _value: *mut c_char) -> c_int; -} - -entry_stub! { - pub fn _SSL_CONF_cmd_value_type(_ctx: *mut SSL_CONF_CTX, _opt: *mut c_char) -> c_int; -} - -pub struct SSL_CONF_CTX; - // No kTLS/sendfile support entry_stub! { diff --git a/rustls-libssl/src/lib.rs b/rustls-libssl/src/lib.rs index 041e573..ccc4046 100644 --- a/rustls-libssl/src/lib.rs +++ b/rustls-libssl/src/lib.rs @@ -43,6 +43,7 @@ mod ex_data; #[macro_use] #[allow(unused_macros, dead_code, unused_imports)] mod ffi; +mod conf; #[cfg(miri)] #[allow(non_camel_case_types, dead_code)] mod miri; diff --git a/rustls-libssl/tests/config.c b/rustls-libssl/tests/config.c new file mode 100644 index 0000000..a8108e4 --- /dev/null +++ b/rustls-libssl/tests/config.c @@ -0,0 +1,173 @@ +/** + * Exercises openssl functions like `SSL_CONF_cmd_value_type` + */ + +#include +#include + +#include + +#define CUSTOM_PREFIX "Rustls-" + +static const int conf_flags[] = {SSL_CONF_FLAG_SERVER, SSL_CONF_FLAG_CLIENT, + SSL_CONF_FLAG_CERTIFICATE}; + +#define NUM_FLAGS (sizeof(conf_flags) / sizeof(conf_flags[0])) + +static const char *supported_cmds[] = { + "-min_protocol", CUSTOM_PREFIX "min_protocol", + "MinProtocol", CUSTOM_PREFIX "MinProtocol", + + "-max_protocol", CUSTOM_PREFIX "max_protocol", + "MaxProtocol", CUSTOM_PREFIX "MaxProtocol", +}; + +#define NUM_SUPPORTED_CMDS (sizeof(supported_cmds) / sizeof(supported_cmds[0])) + +void test_supported_cmd_value_types(int base_flags, const char *prefix) { + SSL_CONF_CTX *cctx = SSL_CONF_CTX_new(); + assert(cctx != NULL); + assert(SSL_CONF_CTX_set1_prefix(cctx, prefix)); + + int flags = base_flags; + for (unsigned long i = 0; i <= NUM_FLAGS; i++) { + unsigned int new_flags = SSL_CONF_CTX_set_flags(cctx, flags); + printf("cctx flags = %u\n", new_flags); + + for (unsigned long j = 0; j < NUM_SUPPORTED_CMDS; j++) { + const char *cmd = supported_cmds[j]; + int value = SSL_CONF_cmd_value_type(cctx, cmd); + printf("\tsupported cmd %s has value type %d\n", cmd, value); + } + + if (i < NUM_FLAGS) { + flags |= conf_flags[i]; + } + } + + assert(SSL_CONF_CTX_finish(cctx)); + SSL_CONF_CTX_free(cctx); +} + +static const char *fictional_cmds[] = {"", "does-not-exist", "DoesNotExist"}; + +#define NUM_FICTIONAL_CMDS (sizeof(fictional_cmds) / sizeof(fictional_cmds[0])) + +void test_fictional_cmds(void) { + SSL_CONF_CTX *cctx = SSL_CONF_CTX_new(); + assert(cctx != NULL); + + // Set all possible flags. + int all_flags = 0; + for (unsigned long i = 0; i < NUM_FLAGS; i++) { + all_flags |= conf_flags[i]; + } + unsigned int new_flags = SSL_CONF_CTX_set_flags(cctx, all_flags); + printf("cctx flags = %u\n", new_flags); + + for (unsigned long j = 0; j < NUM_FICTIONAL_CMDS; j++) { + const char *cmd = fictional_cmds[j]; + int value = SSL_CONF_cmd_value_type(cctx, cmd); + printf("\tfictional cmd %s has value type %d\n", cmd, value); + + int res = SSL_CONF_cmd(cctx, cmd, "value"); + printf("\tfictional cmd %s set with \"value\" returns %d\n", cmd, res); + res = SSL_CONF_cmd(cctx, cmd, NULL); + printf("\tfictional cmd %s set with NULL returns %d\n", cmd, res); + } + + assert(SSL_CONF_CTX_finish(cctx)); + SSL_CONF_CTX_free(cctx); +} + +void cmd_apply_protocol_versions(SSL_CONF_CTX *cctx, const char *min_version, + const char *max_version) { + int res = SSL_CONF_cmd(cctx, "MinProtocol", min_version); + printf("\t\tcmd MinProtocol %s returns %d\n", min_version, res); + res = SSL_CONF_cmd(cctx, "MaxProtocol", max_version); + printf("\t\tcmd MaxProtocol %s returns %d\n", max_version, res); +} + +#define BAD_MIN_PROTOCOL "SSLv69" +#define BAD_MAX_PROTOCOL "SSLv70" +#define GOOD_MIN_PROTOCOL "TLSv1.3" +#define GOOD_MAX_PROTOCOL "TLSv1.2" + +void test_min_max_versions(void) { + SSL_CONF_CTX *cctx = SSL_CONF_CTX_new(); + assert(cctx != NULL); + + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_FILE); + + printf("Pre-ctx\n"); + printf("\tBad version:\n"); + cmd_apply_protocol_versions(cctx, BAD_MIN_PROTOCOL, BAD_MAX_PROTOCOL); + + printf("\tGood version:\n"); + cmd_apply_protocol_versions(cctx, GOOD_MIN_PROTOCOL, GOOD_MAX_PROTOCOL); + + SSL_CTX *ctx = SSL_CTX_new(TLS_method()); + assert(ctx != NULL); + SSL_CONF_CTX_set_ssl_ctx(cctx, ctx); + + printf("Post-ctx:\n"); + printf("\tBad version:\n"); + cmd_apply_protocol_versions(cctx, BAD_MIN_PROTOCOL, BAD_MAX_PROTOCOL); + printf("\t\tSSL_CTX_get_min_proto_version 0x%lx\n", + SSL_CTX_get_min_proto_version(ctx)); + printf("\t\tSSL_CTX_get_max_proto_version 0x%lx\n", + SSL_CTX_get_max_proto_version(ctx)); + + printf("\tGood version:\n"); + cmd_apply_protocol_versions(cctx, GOOD_MIN_PROTOCOL, GOOD_MAX_PROTOCOL); + printf("\t\tSSL_CTX_get_min_proto_version 0x%lx\n", + SSL_CTX_get_min_proto_version(ctx)); + printf("\t\tSSL_CTX_get_max_proto_version 0x%lx\n", + SSL_CTX_get_max_proto_version(ctx)); + + SSL *ssl = SSL_new(ctx); + assert(ssl != NULL); + printf("Post-ssl:\n"); + printf("\tBad version:\n"); + cmd_apply_protocol_versions(cctx, BAD_MIN_PROTOCOL, BAD_MAX_PROTOCOL); + printf("\t\tSSL_get_min_proto_version 0x%lx\n", + SSL_get_min_proto_version(ssl)); + printf("\t\tSSL_get_max_proto_version 0x%lx\n", + SSL_get_max_proto_version(ssl)); + + printf("\tGood version:\n"); + cmd_apply_protocol_versions(cctx, GOOD_MIN_PROTOCOL, GOOD_MAX_PROTOCOL); + printf("\t\tSSL_get_min_proto_version 0x%lx\n", + SSL_get_min_proto_version(ssl)); + printf("\t\tSSL_get_max_proto_version 0x%lx\n", + SSL_get_max_proto_version(ssl)); + + assert(SSL_CONF_CTX_finish(cctx)); + SSL_CONF_CTX_free(cctx); + SSL_CTX_free(ctx); + SSL_free(ssl); +} + +int main(void) { + printf("Supported commands:\n"); + printf("no base flags, default prefix:\n"); + test_supported_cmd_value_types(0, ""); + printf("no base flags, custom prefix:\n"); + test_supported_cmd_value_types(0, CUSTOM_PREFIX); + + printf("CMDLINE base flags, default prefix:\n"); + test_supported_cmd_value_types(SSL_CONF_FLAG_CMDLINE, ""); + printf("CMDLINE base flags,custom prefix:\n"); + test_supported_cmd_value_types(SSL_CONF_FLAG_CMDLINE, CUSTOM_PREFIX); + + printf("FILE base flags, default prefix:\n"); + test_supported_cmd_value_types(SSL_CONF_FLAG_FILE, ""); + printf("FILE base flags, custom prefix:\n"); + test_supported_cmd_value_types(SSL_CONF_FLAG_FILE, CUSTOM_PREFIX); + + printf("Fictional commands:\n"); + test_fictional_cmds(); + + printf("Min/Max version:\n"); + test_min_max_versions(); +} diff --git a/rustls-libssl/tests/nginx_1_24.conf b/rustls-libssl/tests/nginx_1_24.conf new file mode 100644 index 0000000..1bcf8b3 --- /dev/null +++ b/rustls-libssl/tests/nginx_1_24.conf @@ -0,0 +1,50 @@ +daemon off; +master_process off; +pid nginx.pid; + +events { +} + +http { + # Default to both supported protocols enabled. + ssl_protocols TLSv1.2 TLSv1.3; + access_log access.log; + + server { + # Custom configuration w/ ssl_conf_command: + # * TLS 1.3 or greater only + listen 8447 ssl; + ssl_certificate ../../../test-ca/rsa/server.cert; + ssl_certificate_key ../../../test-ca/rsa/server.key; + server_name localhost; + + ssl_conf_command MinProtocol TLSv1.3; + + location = / { + return 200 "hello world\n"; + } + + location /ssl-agreed { + return 200 "protocol:$ssl_protocol,cipher:$ssl_cipher\n"; + } + } + + server { + # Custom configuration w/ ssl_conf_command: + # * TLS 1.2 or less only + listen 8448 ssl; + ssl_certificate ../../../test-ca/rsa/server.cert; + ssl_certificate_key ../../../test-ca/rsa/server.key; + server_name localhost; + + ssl_conf_command MaxProtocol TLSv1.2; + + location = / { + return 200 "hello world\n"; + } + + location /ssl-agreed { + return 200 "protocol:$ssl_protocol,cipher:$ssl_cipher\n"; + } + } +} diff --git a/rustls-libssl/tests/runner.rs b/rustls-libssl/tests/runner.rs index 536fa8e..a9bab0e 100644 --- a/rustls-libssl/tests/runner.rs +++ b/rustls-libssl/tests/runner.rs @@ -254,6 +254,27 @@ fn client_real_world() { assert_eq!(openssl_output, rustls_output); } +#[test] +#[ignore] +fn config() { + let openssl_output = Command::new("tests/maybe-valgrind.sh") + .args(["target/config"]) + .env("LD_LIBRARY_PATH", "") + .stdout(Stdio::piped()) + .output() + .map(print_output) + .unwrap(); + + let rustls_output = Command::new("tests/maybe-valgrind.sh") + .args(["target/config"]) + .stdout(Stdio::piped()) + .output() + .map(print_output) + .unwrap(); + + assert_eq!(openssl_output, rustls_output); +} + #[test] #[ignore] fn constants() { @@ -481,37 +502,42 @@ fn nginx() { b"hello world\n" ); - for (port, reused) in [(8443, '.'), (8444, 'r'), (8445, 'r'), (8446, 'r')] { - // multiple requests without http connection reuse - // (second should be a TLS resumption if possible) - assert_eq!( - Command::new("curl") - .env("LD_LIBRARY_PATH", "") - .args([ - "--verbose", - "--cacert", - "test-ca/rsa/ca.cert", - "-H", - "connection: close", - &format!("https://localhost:{port}/"), - &format!("https://localhost:{port}/ssl-agreed"), - &format!("https://localhost:{port}/ssl-server-name"), - &format!("https://localhost:{port}/ssl-was-reused") - ]) - .stdout(Stdio::piped()) - .output() - .map(print_output) - .unwrap() - .stdout, - format!( - "hello world\n\ - protocol:TLSv1.3,cipher:TLS_AES_256_GCM_SHA384\n\ - server-name:localhost\n\ - reused:{reused}\n" - ) - .as_bytes(), - ); - println!("PASS: resumption test for port={port} reused={reused}"); + // TODO(XXX): Session resumption is not working w/ nginx 1.24.0+ + // Until this is fixed skip the resumption specific tests with + // newer Nginx versions. + if matches!(nginx_version(), (1, minor) if minor < 24) { + for (port, reused) in [(8443, '.'), (8444, 'r'), (8445, 'r'), (8446, 'r')] { + // multiple requests without http connection reuse + // (second should be a TLS resumption if possible) + assert_eq!( + Command::new("curl") + .env("LD_LIBRARY_PATH", "") + .args([ + "--verbose", + "--cacert", + "test-ca/rsa/ca.cert", + "-H", + "connection: close", + &format!("https://localhost:{port}/"), + &format!("https://localhost:{port}/ssl-agreed"), + &format!("https://localhost:{port}/ssl-server-name"), + &format!("https://localhost:{port}/ssl-was-reused") + ]) + .stdout(Stdio::piped()) + .output() + .map(print_output) + .unwrap() + .stdout, + format!( + "hello world\n\ + protocol:TLSv1.3,cipher:TLS_AES_256_GCM_SHA384\n\ + server-name:localhost\n\ + reused:{reused}\n" + ) + .as_bytes(), + ); + println!("PASS: resumption test for port={port} reused={reused}"); + } } // big download (throttled by curl to ensure non-blocking writes work) @@ -535,6 +561,129 @@ fn nginx() { drop(nginx_server); } +#[test] +#[ignore] +fn nginx_1_24() { + let (major, minor) = nginx_version(); + if major != 1 || minor < 24 { + println!("skipping Nginx 1.24 tests, installed version is {major}.{minor}.x"); + return; + } + + fs::create_dir_all("target/nginx-tmp/1_24/html").unwrap(); + fs::write( + "target/nginx-tmp/1_24/server.conf", + include_str!("nginx_1_24.conf"), + ) + .unwrap(); + + let nginx_server = KillOnDrop(Some( + Command::new("tests/maybe-valgrind.sh") + .args([ + "nginx", + "-g", + &format!("error_log stderr {NGINX_LOG_LEVEL};"), + "-p", + "./target/nginx-tmp/1_24", + "-c", + "server.conf", + ]) + .spawn() + .unwrap(), + )); + wait_for_port(8447); + wait_for_port(8448); + + // TLS 1.2 to the TLS 1.3 only port should fail w/ exit code 35 + assert_eq!( + Command::new("curl") + .env("LD_LIBRARY_PATH", "") + .args([ + "--cacert", + "test-ca/rsa/ca.cert", + "--tls-max", + "1.2", + "https://localhost:8447/ssl-agreed" + ]) + .stdout(Stdio::piped()) + .status() + .unwrap() + .code() + .unwrap(), + 35 + ); + // TLS 1.3 to the TLS 1.3 only port should succeed. + assert_eq!( + Command::new("curl") + .env("LD_LIBRARY_PATH", "") + .args([ + "--cacert", + "test-ca/rsa/ca.cert", + "--tlsv1.3", + "https://localhost:8447/ssl-agreed" + ]) + .stdout(Stdio::piped()) + .output() + .unwrap() + .stdout, + "protocol:TLSv1.3,cipher:TLS_AES_256_GCM_SHA384\n".as_bytes() + ); + + // TLS 1.3 to the TLS 1.2 only port should fail w/ exit code 35 + assert_eq!( + Command::new("curl") + .env("LD_LIBRARY_PATH", "") + .args([ + "--cacert", + "test-ca/rsa/ca.cert", + "--tlsv1.3", + "https://localhost:8448/ssl-agreed" + ]) + .stdout(Stdio::piped()) + .status() + .unwrap() + .code() + .unwrap(), + 35 + ); + // TLS 1.2 to the TLS 1.2 only port should succeed. + assert_eq!( + Command::new("curl") + .env("LD_LIBRARY_PATH", "") + .args([ + "--cacert", + "test-ca/rsa/ca.cert", + "--tlsv1.2", + "https://localhost:8448/ssl-agreed" + ]) + .stdout(Stdio::piped()) + .output() + .unwrap() + .stdout, + "protocol:TLSv1.2,cipher:ECDHE-RSA-AES256-GCM-SHA384\n".as_bytes() + ); + + drop(nginx_server); +} + +// Return the major and minor version components of the Nginx binary in `$PATH`. +fn nginx_version() -> (u32, u32) { + let nginx_version_output = Command::new("nginx").args(["-v"]).output().unwrap(); + let nginx_version_output = String::from_utf8_lossy(&nginx_version_output.stderr); + let raw_version = nginx_version_output + .lines() + .next() + .unwrap() + .strip_prefix("nginx version: nginx/") + .unwrap(); + let mut version_components = raw_version.split('.'); + let must_parse_numeric = |c: &str| c.parse::().unwrap(); + ( + version_components.next().map(must_parse_numeric).unwrap(), + version_components.next().map(must_parse_numeric).unwrap(), + ) +} + struct KillOnDrop(Option); impl KillOnDrop {