Skip to content

Commit a6f7560

Browse files
authored
feat: support trusted stores in addition to PEM files when using TLS (#263)
Support trusted stores in addition to PEM files when using TLS
1 parent 7f9e084 commit a6f7560

5 files changed

Lines changed: 104 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus
4646
roxmltree = "0.21"
4747
rust-embed = "8.7"
4848
rustls = "0.23"
49+
rustls-native-certs = "0.8"
4950
rustls-pemfile = "2.2"
5051
rustls-pki-types = { version = "1" }
52+
webpki-roots = "1.0"
5153
serde = "^1.0.177"
5254
serde_json = "1.0.143"
5355
smartstring = "1"

crates/hotfix/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ mongodb = { workspace = true, optional = true }
3333
rustls-pki-types = { workspace = true }
3434
redb = { workspace = true, optional = true }
3535
rustls = { workspace = true }
36+
rustls-native-certs = { workspace = true }
3637
rustls-pemfile = { workspace = true }
38+
webpki-roots = { workspace = true }
3739
serde = { workspace = true, features = ["derive"] }
3840
thiserror = { workspace = true }
3941
tokio = { workspace = true, features = ["full"] }

crates/hotfix/src/config.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,19 @@ impl Config {
2525
}
2626
}
2727

28-
/// TLS encryption details.
28+
/// TLS encryption details with configurable trust store.
2929
#[derive(Clone, Debug, Deserialize, PartialEq)]
30-
pub struct TlsConfig {
31-
/// The path to the CA certificate.
32-
pub ca_certificate_path: String,
30+
#[serde(tag = "trust_store", rename_all = "snake_case")]
31+
pub enum TlsConfig {
32+
/// Use a custom CA certificate file (PEM format).
33+
File {
34+
/// Path to the CA certificate file.
35+
ca_certificate_path: String,
36+
},
37+
/// Use the operating system's native certificate store.
38+
Native,
39+
/// Use Mozilla's bundled root certificates (via webpki-roots).
40+
Webpki,
3341
}
3442

3543
/// Session schedule configuration
@@ -123,6 +131,7 @@ data_dictionary_path = "./spec/FIX44.xml"
123131
124132
connection_port = 443
125133
connection_host = "127.0.0.1"
134+
trust_store = "file"
126135
ca_certificate_path = "my_cert.crt"
127136
heartbeat_interval = 30
128137
reset_on_logon = false
@@ -142,14 +151,67 @@ reset_on_logon = false
142151
assert_eq!(session_config.connection_port, 443);
143152
assert_eq!(session_config.connection_host, "127.0.0.1");
144153
assert_eq!(session_config.heartbeat_interval, 30);
145-
let expected_tls_config = TlsConfig {
154+
let expected_tls_config = TlsConfig::File {
146155
ca_certificate_path: "my_cert.crt".to_string(),
147156
};
148157
assert_eq!(session_config.tls_config, Some(expected_tls_config));
149158
assert_eq!(session_config.reconnect_interval, 30);
150159
assert_eq!(session_config.logon_timeout, 10);
151160
}
152161

162+
#[test]
163+
fn test_tls_config_native() {
164+
let config_contents = r#"
165+
[[sessions]]
166+
begin_string = "FIX.4.4"
167+
sender_comp_id = "send-comp-id"
168+
target_comp_id = "target-comp-id"
169+
connection_port = 443
170+
connection_host = "127.0.0.1"
171+
heartbeat_interval = 30
172+
trust_store = "native"
173+
"#;
174+
175+
let config: Config = toml::from_str(config_contents).unwrap();
176+
let session_config = config.sessions.first().unwrap();
177+
assert_eq!(session_config.tls_config, Some(TlsConfig::Native));
178+
}
179+
180+
#[test]
181+
fn test_tls_config_webpki() {
182+
let config_contents = r#"
183+
[[sessions]]
184+
begin_string = "FIX.4.4"
185+
sender_comp_id = "send-comp-id"
186+
target_comp_id = "target-comp-id"
187+
connection_port = 443
188+
connection_host = "127.0.0.1"
189+
heartbeat_interval = 30
190+
trust_store = "webpki"
191+
"#;
192+
193+
let config: Config = toml::from_str(config_contents).unwrap();
194+
let session_config = config.sessions.first().unwrap();
195+
assert_eq!(session_config.tls_config, Some(TlsConfig::Webpki));
196+
}
197+
198+
#[test]
199+
fn test_no_tls_config() {
200+
let config_contents = r#"
201+
[[sessions]]
202+
begin_string = "FIX.4.4"
203+
sender_comp_id = "send-comp-id"
204+
target_comp_id = "target-comp-id"
205+
connection_port = 9880
206+
connection_host = "127.0.0.1"
207+
heartbeat_interval = 30
208+
"#;
209+
210+
let config: Config = toml::from_str(config_contents).unwrap();
211+
let session_config = config.sessions.first().unwrap();
212+
assert_eq!(session_config.tls_config, None);
213+
}
214+
153215
#[test]
154216
fn test_schedule_config_weekdays() {
155217
let config_contents = r#"
@@ -327,6 +389,7 @@ end_day = "Friday"
327389
328390
connection_port = 443
329391
connection_host = "127.0.0.1"
392+
trust_store = "file"
330393
ca_certificate_path = "my_cert.crt"
331394
heartbeat_interval = 30
332395
logon_timeout = 20
@@ -350,6 +413,7 @@ end_day = "Friday"
350413
351414
connection_port = 443
352415
connection_host = "127.0.0.1"
416+
trust_store = "file"
353417
ca_certificate_path = "my_cert.crt"
354418
heartbeat_interval = 30
355419
reconnect_interval = 15

crates/hotfix/src/transport/socket/tls.rs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ use tokio::io::{AsyncRead, AsyncWrite};
99
use tokio::net::TcpStream;
1010
use tokio_rustls::{TlsConnector, client::TlsStream};
1111

12-
use crate::config::SessionConfig;
12+
use crate::config::{SessionConfig, TlsConfig};
1313
use crate::transport::tcp::create_tcp_connection;
1414

1515
pub async fn create_tcp_over_tls_connection(
1616
session_config: &SessionConfig,
1717
) -> io::Result<TlsStream<TcpStream>> {
18-
let client_config = get_client_config(session_config);
18+
let tls_config = session_config
19+
.tls_config
20+
.as_ref()
21+
.expect("TLS config must be present when creating TLS connection");
22+
let client_config = get_client_config(tls_config);
1923
let socket = create_tcp_connection(session_config).await?;
2024
wrap_stream(
2125
socket,
@@ -25,28 +29,36 @@ pub async fn create_tcp_over_tls_connection(
2529
.await
2630
}
2731

28-
fn get_client_config(session_config: &SessionConfig) -> ClientConfig {
29-
let root_store = get_root_store(
30-
&session_config
31-
.tls_config
32-
.clone()
33-
.unwrap()
34-
.ca_certificate_path,
35-
);
32+
fn get_client_config(tls_config: &TlsConfig) -> ClientConfig {
33+
let root_store = get_root_store(tls_config);
3634
ClientConfig::builder()
3735
.with_root_certificates(root_store)
3836
.with_no_client_auth()
3937
}
4038

41-
fn get_root_store(ca_certificate_path: &str) -> RootCertStore {
42-
let mut root_store = RootCertStore::empty();
43-
let certs = load_certs(ca_certificate_path);
44-
root_store.add_parsable_certificates(certs);
45-
46-
root_store
39+
fn get_root_store(tls_config: &TlsConfig) -> RootCertStore {
40+
match tls_config {
41+
TlsConfig::File {
42+
ca_certificate_path,
43+
} => {
44+
let mut root_store = RootCertStore::empty();
45+
let certs = load_certs_from_file(ca_certificate_path);
46+
root_store.add_parsable_certificates(certs);
47+
root_store
48+
}
49+
TlsConfig::Native => {
50+
let mut root_store = RootCertStore::empty();
51+
let native_certs = rustls_native_certs::load_native_certs();
52+
root_store.add_parsable_certificates(native_certs.certs);
53+
root_store
54+
}
55+
TlsConfig::Webpki => {
56+
RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned())
57+
}
58+
}
4759
}
4860

49-
fn load_certs(filename: &str) -> Vec<CertificateDer<'static>> {
61+
fn load_certs_from_file(filename: &str) -> Vec<CertificateDer<'static>> {
5062
let certfile = fs::File::open(filename).expect("certificate file to be open");
5163
let mut reader = BufReader::new(certfile);
5264
rustls_pemfile::certs(&mut reader)

0 commit comments

Comments
 (0)