Skip to main content

riffdb_client_rust/
tls.rs

1//! Verified direct-TLS channel construction from the closed deployment config.
2
3use std::error::Error;
4use std::fmt;
5use std::fs;
6use std::io::Read;
7#[cfg(unix)]
8use std::os::unix::fs::{MetadataExt, PermissionsExt};
9use std::path::Path;
10
11use riffdb_config::{ClientTrustRootConfig, TlsClientConfig};
12use rustls_pki_types::{CertificateDer, pem::PemObject};
13use tonic::transport::{Certificate, ClientTlsConfig as TonicTlsConfig, Endpoint};
14
15use crate::{ClientError, RiffDbClient};
16
17const MAX_TRUST_ROOT_BYTES: u64 = 1_024 * 1_024;
18const MAX_TRUST_ROOT_CERTIFICATES: usize = 512;
19const DEFAULT_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
20const SYSTEM_CA_BUNDLE_CANDIDATES: &[&str] = &[
21    "/etc/ssl/certs/ca-certificates.crt",
22    "/etc/pki/tls/certs/ca-bundle.crt",
23    "/etc/ssl/cert.pem",
24    "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
25    "/etc/ssl/ca-bundle.pem",
26];
27
28#[derive(Clone, Debug, Eq, PartialEq)]
29struct TrustRootFileIdentity {
30    bytes: u64,
31    #[cfg(unix)]
32    device: u64,
33    #[cfg(unix)]
34    inode: u64,
35    #[cfg(unix)]
36    modified_seconds: i64,
37    #[cfg(unix)]
38    modified_nanoseconds: i64,
39    #[cfg(not(unix))]
40    modified_since_epoch: Option<std::time::Duration>,
41}
42
43struct TrustRootSnapshot {
44    bytes: Vec<u8>,
45    identity: TrustRootFileIdentity,
46}
47
48impl fmt::Debug for TrustRootSnapshot {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter
51            .debug_struct("TrustRootSnapshot")
52            .field("bytes", &"[TRUST ROOT]")
53            .field("identity", &self.identity)
54            .finish()
55    }
56}
57
58/// Closed, path-redacted failure while constructing a verified TLS channel.
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum TlsClientFailure {
61    /// The configured trust-root file was absent, replaced, empty, or oversized.
62    TrustRootUnavailable,
63    /// The trust-root file was writable by a group or other users.
64    TrustRootPermissions,
65    /// The exact HTTPS endpoint or TLS verifier could not be constructed.
66    InvalidConfiguration,
67    /// The peer could not be reached or did not pass the mandatory TLS handshake.
68    ConnectionOrPeerVerification,
69}
70
71impl fmt::Display for TlsClientFailure {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter.write_str(match self {
74            Self::TrustRootUnavailable => "the configured TLS trust root is unavailable",
75            Self::TrustRootPermissions => "the configured TLS trust root has unsafe permissions",
76            Self::InvalidConfiguration => "the verified TLS endpoint is invalid",
77            Self::ConnectionOrPeerVerification => "the TLS peer could not be reached or verified",
78        })
79    }
80}
81
82impl Error for TlsClientFailure {}
83
84/// Redacted disposition of the latest trust-root observation.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum TlsTrustReloadStatus {
87    /// The configured file and active snapshot are the same valid identity.
88    Current,
89    /// The configured file was unavailable or unsafe, so the prior snapshot remains active.
90    RetainedPreviousUnavailable,
91    /// A complete replacement was malformed, so the prior snapshot remains active.
92    RetainedPreviousInvalid,
93}
94
95/// Stateful verified-TLS connector with atomic trust-root replacement.
96///
97/// A complete valid replacement is used for subsequent connections. An
98/// unavailable, malformed, or unsafe replacement retains the last valid
99/// snapshot. Existing channels are unaffected and never downgrade.
100pub struct VerifiedTlsConnector {
101    config: TlsClientConfig,
102    current: TrustRootSnapshot,
103    rejected: Option<TrustRootFileIdentity>,
104    last_reload: TlsTrustReloadStatus,
105}
106
107impl VerifiedTlsConnector {
108    /// Loads and validates the initial trust-root snapshot.
109    pub fn new(config: TlsClientConfig) -> Result<Self, ClientError> {
110        let current = read_trust_root_snapshot(&config)?;
111        Ok(Self {
112            config,
113            current,
114            rejected: None,
115            last_reload: TlsTrustReloadStatus::Current,
116        })
117    }
118
119    /// Connects one reusable HTTP/2 channel using the current valid snapshot.
120    pub async fn connect(&mut self) -> Result<RiffDbClient, ClientError> {
121        self.reload_trust_root_for_new_handshake();
122        connect_with_trust_root(&self.config, self.current.bytes.clone()).await
123    }
124
125    /// Returns a path-free disposition suitable for operator diagnostics.
126    #[must_use]
127    pub const fn last_reload_status(&self) -> TlsTrustReloadStatus {
128        self.last_reload
129    }
130
131    fn reload_trust_root_for_new_handshake(&mut self) {
132        let observed = match self.config.trust_root_config() {
133            ClientTrustRootConfig::ExplicitPemFile(path) => {
134                trust_root_file_identity(path.as_path())
135            }
136            ClientTrustRootConfig::SystemCaBundle => {
137                resolve_system_ca_bundle_path().and_then(|path| system_ca_bundle_identity(&path))
138            }
139        };
140        let Ok(observed) = observed else {
141            self.last_reload = TlsTrustReloadStatus::RetainedPreviousUnavailable;
142            return;
143        };
144        if observed == self.current.identity || self.rejected.as_ref() == Some(&observed) {
145            if observed == self.current.identity {
146                self.last_reload = TlsTrustReloadStatus::Current;
147            }
148            return;
149        }
150        match read_trust_root_snapshot(&self.config) {
151            Ok(snapshot) => {
152                self.current = snapshot;
153                self.rejected = None;
154                self.last_reload = TlsTrustReloadStatus::Current;
155            }
156            Err(_) => {
157                self.rejected = Some(observed);
158                self.last_reload = TlsTrustReloadStatus::RetainedPreviousInvalid;
159            }
160        }
161    }
162}
163
164impl RiffDbClient {
165    /// Connects through mandatory CA and exact server-name verification.
166    ///
167    /// This path has no trust-all, cleartext fallback, redirect,
168    /// client-certificate, cipher-suite, or provider selector.
169    pub async fn connect_verified_tls(config: &TlsClientConfig) -> Result<Self, ClientError> {
170        VerifiedTlsConnector::new(config.clone())?.connect().await
171    }
172}
173
174async fn connect_with_trust_root(
175    config: &TlsClientConfig,
176    trust_root: Vec<u8>,
177) -> Result<RiffDbClient, ClientError> {
178    let tls = TonicTlsConfig::new()
179        .ca_certificate(Certificate::from_pem(trust_root))
180        .domain_name(config.expected_server_identity().as_str());
181    let endpoint = Endpoint::from_shared(config.endpoint().as_str().to_owned())
182        .map_err(|_| ClientError::Tls(TlsClientFailure::InvalidConfiguration))?
183        .connect_timeout(config.connect_timeout())
184        .timeout(DEFAULT_REQUEST_TIMEOUT)
185        .http2_keep_alive_interval(config.keepalive_interval())
186        .keep_alive_while_idle(true)
187        .tls_config(tls)
188        .map_err(|_| ClientError::Tls(TlsClientFailure::InvalidConfiguration))?;
189    let channel = endpoint
190        .connect()
191        .await
192        .map_err(|_| ClientError::Tls(TlsClientFailure::ConnectionOrPeerVerification))?;
193    Ok(RiffDbClient::from_channel(channel))
194}
195
196fn read_trust_root_snapshot(config: &TlsClientConfig) -> Result<TrustRootSnapshot, ClientError> {
197    match config.trust_root_config() {
198        ClientTrustRootConfig::ExplicitPemFile(path) => {
199            read_explicit_trust_root_snapshot(path.as_path())
200        }
201        ClientTrustRootConfig::SystemCaBundle => read_system_ca_bundle_snapshot(),
202    }
203}
204
205fn read_explicit_trust_root_snapshot(path: &Path) -> Result<TrustRootSnapshot, ClientError> {
206    let identity = trust_root_file_identity(path)?;
207    let file = fs::File::open(path)
208        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
209    let opened = file
210        .metadata()
211        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
212    if !opened.is_file()
213        || opened.len() == 0
214        || opened.len() > MAX_TRUST_ROOT_BYTES
215        || !opened_file_matches_identity(&identity, &opened)
216    {
217        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
218    }
219    let mut contents = Vec::with_capacity(opened.len() as usize);
220    file.take(MAX_TRUST_ROOT_BYTES + 1)
221        .read_to_end(&mut contents)
222        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
223    if contents.is_empty() || contents.len() as u64 > MAX_TRUST_ROOT_BYTES {
224        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
225    }
226    let after = trust_root_file_identity(path)?;
227    if after != identity {
228        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
229    }
230    validate_trust_root(&contents)?;
231    Ok(TrustRootSnapshot {
232        bytes: contents,
233        identity,
234    })
235}
236
237fn resolve_system_ca_bundle_path() -> Result<std::path::PathBuf, ClientError> {
238    for candidate in SYSTEM_CA_BUNDLE_CANDIDATES {
239        let path = Path::new(candidate);
240        if fs::metadata(path).is_ok_and(|meta| meta.is_file() && meta.len() > 0) {
241            return Ok(path.to_path_buf());
242        }
243    }
244    Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable))
245}
246
247fn read_system_ca_bundle_snapshot() -> Result<TrustRootSnapshot, ClientError> {
248    let path = resolve_system_ca_bundle_path()?;
249    read_system_ca_bundle_from_path(&path)
250}
251
252fn read_system_ca_bundle_from_path(path: &Path) -> Result<TrustRootSnapshot, ClientError> {
253    let identity = system_ca_bundle_identity(path)?;
254    let file = fs::File::open(path)
255        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
256    let opened = file
257        .metadata()
258        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
259    if !opened.is_file()
260        || opened.len() == 0
261        || opened.len() > MAX_TRUST_ROOT_BYTES
262        || !opened_file_matches_identity(&identity, &opened)
263    {
264        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
265    }
266    let mut contents = Vec::with_capacity(opened.len() as usize);
267    file.take(MAX_TRUST_ROOT_BYTES + 1)
268        .read_to_end(&mut contents)
269        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
270    if contents.is_empty() || contents.len() as u64 > MAX_TRUST_ROOT_BYTES {
271        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
272    }
273    let after = system_ca_bundle_identity(path)?;
274    if after != identity {
275        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
276    }
277    validate_system_ca_bundle(&contents)?;
278    Ok(TrustRootSnapshot {
279        bytes: contents,
280        identity,
281    })
282}
283
284#[cfg(unix)]
285fn system_ca_bundle_identity(path: &Path) -> Result<TrustRootFileIdentity, ClientError> {
286    let metadata =
287        fs::metadata(path).map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
288    if !metadata.is_file() || metadata.len() == 0 {
289        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
290    }
291    if metadata.permissions().mode() & 0o022 != 0 {
292        return Err(ClientError::Tls(TlsClientFailure::TrustRootPermissions));
293    }
294    Ok(TrustRootFileIdentity {
295        bytes: metadata.len(),
296        device: metadata.dev(),
297        inode: metadata.ino(),
298        modified_seconds: metadata.mtime(),
299        modified_nanoseconds: metadata.mtime_nsec(),
300    })
301}
302
303#[cfg(not(unix))]
304fn system_ca_bundle_identity(path: &Path) -> Result<TrustRootFileIdentity, ClientError> {
305    let metadata =
306        fs::metadata(path).map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
307    if !metadata.is_file() || metadata.len() == 0 {
308        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
309    }
310    Ok(TrustRootFileIdentity {
311        bytes: metadata.len(),
312        modified_since_epoch: metadata
313            .modified()
314            .ok()
315            .and_then(|value| value.duration_since(std::time::UNIX_EPOCH).ok()),
316    })
317}
318
319#[cfg(unix)]
320fn trust_root_file_identity(path: &Path) -> Result<TrustRootFileIdentity, ClientError> {
321    let metadata = fs::symlink_metadata(path)
322        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
323    if !metadata.file_type().is_file() || metadata.len() == 0 {
324        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
325    }
326    if metadata.permissions().mode() & 0o022 != 0 {
327        return Err(ClientError::Tls(TlsClientFailure::TrustRootPermissions));
328    }
329    Ok(TrustRootFileIdentity {
330        bytes: metadata.len(),
331        device: metadata.dev(),
332        inode: metadata.ino(),
333        modified_seconds: metadata.mtime(),
334        modified_nanoseconds: metadata.mtime_nsec(),
335    })
336}
337
338#[cfg(not(unix))]
339fn trust_root_file_identity(path: &Path) -> Result<TrustRootFileIdentity, ClientError> {
340    let metadata = fs::symlink_metadata(path)
341        .map_err(|_| ClientError::Tls(TlsClientFailure::TrustRootUnavailable))?;
342    if !metadata.file_type().is_file() || metadata.len() == 0 {
343        return Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable));
344    }
345    Ok(TrustRootFileIdentity {
346        bytes: metadata.len(),
347        modified_since_epoch: metadata
348            .modified()
349            .ok()
350            .and_then(|value| value.duration_since(std::time::UNIX_EPOCH).ok()),
351    })
352}
353
354#[cfg(unix)]
355fn opened_file_matches_identity(identity: &TrustRootFileIdentity, opened: &fs::Metadata) -> bool {
356    identity.device == opened.dev()
357        && identity.inode == opened.ino()
358        && identity.bytes == opened.len()
359        && identity.modified_seconds == opened.mtime()
360        && identity.modified_nanoseconds == opened.mtime_nsec()
361}
362
363#[cfg(not(unix))]
364fn opened_file_matches_identity(identity: &TrustRootFileIdentity, opened: &fs::Metadata) -> bool {
365    identity.bytes == opened.len()
366        && identity.modified_since_epoch
367            == opened
368                .modified()
369                .ok()
370                .and_then(|value| value.duration_since(std::time::UNIX_EPOCH).ok())
371}
372
373fn validate_trust_root(contents: &[u8]) -> Result<(), ClientError> {
374    let mut reader = std::io::Cursor::new(contents);
375    let mut count = 0_usize;
376    for certificate in CertificateDer::pem_reader_iter(&mut reader) {
377        let certificate =
378            certificate.map_err(|_| ClientError::Tls(TlsClientFailure::InvalidConfiguration))?;
379        count = count
380            .checked_add(1)
381            .ok_or(ClientError::Tls(TlsClientFailure::InvalidConfiguration))?;
382        if count > MAX_TRUST_ROOT_CERTIFICATES
383            || webpki::anchor_from_trusted_cert(&certificate).is_err()
384        {
385            return Err(ClientError::Tls(TlsClientFailure::InvalidConfiguration));
386        }
387    }
388    if count == 0 {
389        return Err(ClientError::Tls(TlsClientFailure::InvalidConfiguration));
390    }
391    Ok(())
392}
393
394fn validate_system_ca_bundle(contents: &[u8]) -> Result<(), ClientError> {
395    let mut reader = std::io::Cursor::new(contents);
396    let mut count = 0_usize;
397    let mut valid_anchors = 0_usize;
398    for certificate in CertificateDer::pem_reader_iter(&mut reader) {
399        let certificate =
400            certificate.map_err(|_| ClientError::Tls(TlsClientFailure::InvalidConfiguration))?;
401        count = count
402            .checked_add(1)
403            .ok_or(ClientError::Tls(TlsClientFailure::InvalidConfiguration))?;
404        if count > MAX_TRUST_ROOT_CERTIFICATES {
405            return Err(ClientError::Tls(TlsClientFailure::InvalidConfiguration));
406        }
407        if webpki::anchor_from_trusted_cert(&certificate).is_ok() {
408            valid_anchors += 1;
409        }
410    }
411    if valid_anchors == 0 {
412        return Err(ClientError::Tls(TlsClientFailure::InvalidConfiguration));
413    }
414    Ok(())
415}
416
417#[cfg(test)]
418mod tests {
419    use std::num::NonZeroU32;
420    #[cfg(unix)]
421    use std::os::unix::fs::{PermissionsExt, symlink};
422    use std::path::PathBuf;
423    use std::sync::atomic::{AtomicU64, Ordering};
424    use std::time::Duration;
425
426    use riffdb_config::{CanonicalHttpsEndpoint, ProtectedFilePath, TlsServerIdentity};
427
428    use super::*;
429
430    static NEXT_TEST_ROOT: AtomicU64 = AtomicU64::new(1);
431
432    #[test]
433    fn verified_tls_channels_have_a_bounded_request_lifetime() {
434        assert_eq!(DEFAULT_REQUEST_TIMEOUT, Duration::from_secs(30));
435        let source = include_str!("tls.rs");
436        assert!(source.contains(".timeout(DEFAULT_REQUEST_TIMEOUT)"));
437    }
438
439    fn config(path: PathBuf) -> TlsClientConfig {
440        TlsClientConfig::new(
441            CanonicalHttpsEndpoint::parse("https://127.0.0.1:7443").expect("endpoint"),
442            ProtectedFilePath::new(path).expect("trust path"),
443            TlsServerIdentity::parse("127.0.0.1").expect("identity"),
444            Duration::from_secs(5),
445            Duration::from_secs(30),
446            NonZeroU32::new(1).expect("pool"),
447            NonZeroU32::new(64).expect("streams"),
448        )
449        .expect("TLS client config")
450    }
451
452    #[cfg(unix)]
453    #[test]
454    fn trust_root_rejects_writable_and_symlinked_files_without_disclosing_paths() {
455        let unique = NEXT_TEST_ROOT.fetch_add(1, Ordering::Relaxed);
456        let root = std::env::current_dir()
457            .expect("test current directory")
458            .join("target/client-tls-tests")
459            .join(format!("{}-{unique}", std::process::id()));
460        fs::create_dir_all(root.parent().expect("test root parent"))
461            .expect("create TLS test parent");
462        fs::create_dir(&root).expect("create TLS test root");
463        fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).expect("protect root");
464        let trust = root.join("ca.pem");
465        fs::write(&trust, b"not-empty").expect("write trust root");
466        fs::set_permissions(&trust, fs::Permissions::from_mode(0o666)).expect("unsafe mode");
467        let error =
468            read_trust_root_snapshot(&config(trust.clone())).expect_err("writable trust root");
469        assert!(matches!(
470            error,
471            ClientError::Tls(TlsClientFailure::TrustRootPermissions)
472        ));
473        assert!(
474            !error
475                .to_string()
476                .contains(root.to_str().expect("UTF-8 root"))
477        );
478
479        fs::set_permissions(&trust, fs::Permissions::from_mode(0o600)).expect("safe mode");
480        let link = root.join("linked.pem");
481        symlink(&trust, &link).expect("create test symlink");
482        assert!(matches!(
483            read_trust_root_snapshot(&config(link)),
484            Err(ClientError::Tls(TlsClientFailure::TrustRootUnavailable))
485        ));
486
487        fs::remove_file(&trust).expect("remove trust root");
488        fs::remove_file(root.join("linked.pem")).expect("remove symlink");
489        fs::remove_dir(&root).expect("remove TLS test root");
490    }
491
492    #[cfg(unix)]
493    #[test]
494    fn connector_atomically_adopts_valid_trust_roots_and_retains_last_good_on_failure() {
495        let unique = NEXT_TEST_ROOT.fetch_add(1, Ordering::Relaxed);
496        let root = std::env::current_dir()
497            .expect("test current directory")
498            .join("target/client-tls-tests")
499            .join(format!("{}-{unique}", std::process::id()));
500        fs::create_dir_all(root.parent().expect("test root parent"))
501            .expect("create TLS test parent");
502        fs::create_dir(&root).expect("create TLS test root");
503        fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).expect("protect root");
504        let trust = root.join("ca.pem");
505        let valid = include_bytes!("../../riffdb-server/tests/fixtures/test-ca.pem");
506        fs::write(&trust, valid).expect("write initial trust root");
507        fs::set_permissions(&trust, fs::Permissions::from_mode(0o600))
508            .expect("protect initial trust root");
509        let mut connector =
510            VerifiedTlsConnector::new(config(trust.clone())).expect("initial trust root is valid");
511        let initial = connector.current.identity.clone();
512
513        let invalid = root.join("invalid.pem");
514        fs::write(&invalid, b"not a certificate\n").expect("write invalid replacement");
515        fs::set_permissions(&invalid, fs::Permissions::from_mode(0o600))
516            .expect("protect invalid replacement");
517        fs::rename(&invalid, &trust).expect("atomically publish invalid replacement");
518        connector.reload_trust_root_for_new_handshake();
519        assert_eq!(connector.current.identity, initial);
520        assert!(connector.rejected.is_some());
521        assert_eq!(
522            connector.last_reload_status(),
523            TlsTrustReloadStatus::RetainedPreviousInvalid
524        );
525
526        let replacement = root.join("replacement.pem");
527        fs::write(&replacement, valid).expect("write valid replacement");
528        fs::set_permissions(&replacement, fs::Permissions::from_mode(0o600))
529            .expect("protect valid replacement");
530        fs::rename(&replacement, &trust).expect("atomically publish valid replacement");
531        connector.reload_trust_root_for_new_handshake();
532        assert_ne!(connector.current.identity, initial);
533        assert!(connector.rejected.is_none());
534        assert_eq!(
535            connector.last_reload_status(),
536            TlsTrustReloadStatus::Current
537        );
538
539        fs::remove_file(&trust).expect("remove trust root");
540        fs::remove_dir(&root).expect("remove TLS test root");
541    }
542
543    #[cfg(unix)]
544    #[test]
545    // req: NET-001, NET-002, NET-005
546    fn remote_https_profile_derives_server_name_and_supports_system_and_explicit_trust_roots() {
547        let endpoint =
548            CanonicalHttpsEndpoint::parse("https://cloud.example.com:7443").expect("endpoint");
549        let system_cfg = TlsClientConfig::system_trust(
550            endpoint.clone(),
551            Duration::from_secs(5),
552            Duration::from_secs(30),
553            NonZeroU32::new(4).expect("pool"),
554            NonZeroU32::new(64).expect("streams"),
555        )
556        .expect("system trust config");
557        assert_eq!(
558            system_cfg.expected_server_identity(),
559            &TlsServerIdentity::parse("cloud.example.com").expect("identity")
560        );
561        assert_eq!(
562            system_cfg.trust_root_config(),
563            &ClientTrustRootConfig::SystemCaBundle
564        );
565        assert!(system_cfg.explicit_trust_root().is_none());
566
567        // Verify system CA bundle snapshot loads when standard OS bundle is present on host.
568        if resolve_system_ca_bundle_path().is_ok() {
569            let connector = VerifiedTlsConnector::new(system_cfg)
570                .expect("system CA bundle loads and validates");
571            assert_eq!(
572                connector.last_reload_status(),
573                TlsTrustReloadStatus::Current
574            );
575        }
576
577        // Verify explicit PEM trust root with derived server identity also works.
578        let unique = NEXT_TEST_ROOT.fetch_add(1, Ordering::Relaxed);
579        let root = std::env::current_dir()
580            .expect("test current directory")
581            .join("target/client-tls-tests")
582            .join(format!("{}-system-{unique}", std::process::id()));
583        fs::create_dir_all(root.parent().expect("test root parent"))
584            .expect("create TLS test parent");
585        fs::create_dir(&root).expect("create TLS test root");
586        fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).expect("protect root");
587        let trust = root.join("ca.pem");
588        let valid = include_bytes!("../../riffdb-server/tests/fixtures/test-ca.pem");
589        fs::write(&trust, valid).expect("write initial trust root");
590        fs::set_permissions(&trust, fs::Permissions::from_mode(0o600))
591            .expect("protect initial trust root");
592        let explicit_cfg = TlsClientConfig::with_trust_root_config(
593            endpoint.clone(),
594            ClientTrustRootConfig::ExplicitPemFile(
595                ProtectedFilePath::new(trust.clone()).expect("protected path"),
596            ),
597            endpoint.identity().clone(),
598            Duration::from_secs(5),
599            Duration::from_secs(30),
600            NonZeroU32::new(4).expect("pool"),
601            NonZeroU32::new(64).expect("streams"),
602        )
603        .expect("explicit trust config");
604        assert!(explicit_cfg.explicit_trust_root().is_some());
605        let connector = VerifiedTlsConnector::new(explicit_cfg)
606            .expect("explicit CA bundle loads and validates");
607        assert_eq!(
608            connector.last_reload_status(),
609            TlsTrustReloadStatus::Current
610        );
611
612        fs::remove_file(&trust).expect("remove trust root");
613        fs::remove_dir(&root).expect("remove TLS test root");
614    }
615}