From 11ac68aed97a84e13da796978047041cc12d3a42 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Tue, 13 Jan 2026 14:13:31 +0100 Subject: [PATCH] Support trusted stores in addition to PEM files when using TLS --- Cargo.lock | 2 + Cargo.toml | 2 + crates/hotfix/Cargo.toml | 2 + crates/hotfix/src/config.rs | 74 +++++++++++++++++++++-- crates/hotfix/src/transport/socket/tls.rs | 46 ++++++++------ 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77eca63..a056672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,6 +1285,7 @@ dependencies = [ "mongodb", "redb", "rustls", + "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", @@ -1295,6 +1296,7 @@ dependencies = [ "toml", "tracing", "uuid", + "webpki-roots", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d63c733..ac9f1f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,10 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus roxmltree = "0.21" rust-embed = "8.7" rustls = "0.23" +rustls-native-certs = "0.8" rustls-pemfile = "2.2" rustls-pki-types = { version = "1" } +webpki-roots = "1.0" serde = "^1.0.177" serde_json = "1.0.143" smartstring = "1" diff --git a/crates/hotfix/Cargo.toml b/crates/hotfix/Cargo.toml index bffee79..d80d1bc 100644 --- a/crates/hotfix/Cargo.toml +++ b/crates/hotfix/Cargo.toml @@ -33,7 +33,9 @@ mongodb = { workspace = true, optional = true } rustls-pki-types = { workspace = true } redb = { workspace = true, optional = true } rustls = { workspace = true } +rustls-native-certs = { workspace = true } rustls-pemfile = { workspace = true } +webpki-roots = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/crates/hotfix/src/config.rs b/crates/hotfix/src/config.rs index 64097a5..8eeb820 100644 --- a/crates/hotfix/src/config.rs +++ b/crates/hotfix/src/config.rs @@ -25,11 +25,19 @@ impl Config { } } -/// TLS encryption details. +/// TLS encryption details with configurable trust store. #[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct TlsConfig { - /// The path to the CA certificate. - pub ca_certificate_path: String, +#[serde(tag = "trust_store", rename_all = "snake_case")] +pub enum TlsConfig { + /// Use a custom CA certificate file (PEM format). + File { + /// Path to the CA certificate file. + ca_certificate_path: String, + }, + /// Use the operating system's native certificate store. + Native, + /// Use Mozilla's bundled root certificates (via webpki-roots). + Webpki, } /// Session schedule configuration @@ -123,6 +131,7 @@ data_dictionary_path = "./spec/FIX44.xml" connection_port = 443 connection_host = "127.0.0.1" +trust_store = "file" ca_certificate_path = "my_cert.crt" heartbeat_interval = 30 reset_on_logon = false @@ -142,7 +151,7 @@ reset_on_logon = false assert_eq!(session_config.connection_port, 443); assert_eq!(session_config.connection_host, "127.0.0.1"); assert_eq!(session_config.heartbeat_interval, 30); - let expected_tls_config = TlsConfig { + let expected_tls_config = TlsConfig::File { ca_certificate_path: "my_cert.crt".to_string(), }; assert_eq!(session_config.tls_config, Some(expected_tls_config)); @@ -150,6 +159,59 @@ reset_on_logon = false assert_eq!(session_config.logon_timeout, 10); } + #[test] + fn test_tls_config_native() { + let config_contents = r#" +[[sessions]] +begin_string = "FIX.4.4" +sender_comp_id = "send-comp-id" +target_comp_id = "target-comp-id" +connection_port = 443 +connection_host = "127.0.0.1" +heartbeat_interval = 30 +trust_store = "native" + "#; + + let config: Config = toml::from_str(config_contents).unwrap(); + let session_config = config.sessions.first().unwrap(); + assert_eq!(session_config.tls_config, Some(TlsConfig::Native)); + } + + #[test] + fn test_tls_config_webpki() { + let config_contents = r#" +[[sessions]] +begin_string = "FIX.4.4" +sender_comp_id = "send-comp-id" +target_comp_id = "target-comp-id" +connection_port = 443 +connection_host = "127.0.0.1" +heartbeat_interval = 30 +trust_store = "webpki" + "#; + + let config: Config = toml::from_str(config_contents).unwrap(); + let session_config = config.sessions.first().unwrap(); + assert_eq!(session_config.tls_config, Some(TlsConfig::Webpki)); + } + + #[test] + fn test_no_tls_config() { + let config_contents = r#" +[[sessions]] +begin_string = "FIX.4.4" +sender_comp_id = "send-comp-id" +target_comp_id = "target-comp-id" +connection_port = 9880 +connection_host = "127.0.0.1" +heartbeat_interval = 30 + "#; + + let config: Config = toml::from_str(config_contents).unwrap(); + let session_config = config.sessions.first().unwrap(); + assert_eq!(session_config.tls_config, None); + } + #[test] fn test_schedule_config_weekdays() { let config_contents = r#" @@ -327,6 +389,7 @@ end_day = "Friday" connection_port = 443 connection_host = "127.0.0.1" + trust_store = "file" ca_certificate_path = "my_cert.crt" heartbeat_interval = 30 logon_timeout = 20 @@ -350,6 +413,7 @@ end_day = "Friday" connection_port = 443 connection_host = "127.0.0.1" + trust_store = "file" ca_certificate_path = "my_cert.crt" heartbeat_interval = 30 reconnect_interval = 15 diff --git a/crates/hotfix/src/transport/socket/tls.rs b/crates/hotfix/src/transport/socket/tls.rs index a747361..82cd842 100644 --- a/crates/hotfix/src/transport/socket/tls.rs +++ b/crates/hotfix/src/transport/socket/tls.rs @@ -9,13 +9,17 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tokio_rustls::{TlsConnector, client::TlsStream}; -use crate::config::SessionConfig; +use crate::config::{SessionConfig, TlsConfig}; use crate::transport::tcp::create_tcp_connection; pub async fn create_tcp_over_tls_connection( session_config: &SessionConfig, ) -> io::Result> { - let client_config = get_client_config(session_config); + let tls_config = session_config + .tls_config + .as_ref() + .expect("TLS config must be present when creating TLS connection"); + let client_config = get_client_config(tls_config); let socket = create_tcp_connection(session_config).await?; wrap_stream( socket, @@ -25,28 +29,36 @@ pub async fn create_tcp_over_tls_connection( .await } -fn get_client_config(session_config: &SessionConfig) -> ClientConfig { - let root_store = get_root_store( - &session_config - .tls_config - .clone() - .unwrap() - .ca_certificate_path, - ); +fn get_client_config(tls_config: &TlsConfig) -> ClientConfig { + let root_store = get_root_store(tls_config); ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth() } -fn get_root_store(ca_certificate_path: &str) -> RootCertStore { - let mut root_store = RootCertStore::empty(); - let certs = load_certs(ca_certificate_path); - root_store.add_parsable_certificates(certs); - - root_store +fn get_root_store(tls_config: &TlsConfig) -> RootCertStore { + match tls_config { + TlsConfig::File { + ca_certificate_path, + } => { + let mut root_store = RootCertStore::empty(); + let certs = load_certs_from_file(ca_certificate_path); + root_store.add_parsable_certificates(certs); + root_store + } + TlsConfig::Native => { + let mut root_store = RootCertStore::empty(); + let native_certs = rustls_native_certs::load_native_certs(); + root_store.add_parsable_certificates(native_certs.certs); + root_store + } + TlsConfig::Webpki => { + RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()) + } + } } -fn load_certs(filename: &str) -> Vec> { +fn load_certs_from_file(filename: &str) -> Vec> { let certfile = fs::File::open(filename).expect("certificate file to be open"); let mut reader = BufReader::new(certfile); rustls_pemfile::certs(&mut reader)