1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum TlsClientFailure {
61 TrustRootUnavailable,
63 TrustRootPermissions,
65 InvalidConfiguration,
67 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum TlsTrustReloadStatus {
87 Current,
89 RetainedPreviousUnavailable,
91 RetainedPreviousInvalid,
93}
94
95pub struct VerifiedTlsConnector {
101 config: TlsClientConfig,
102 current: TrustRootSnapshot,
103 rejected: Option<TrustRootFileIdentity>,
104 last_reload: TlsTrustReloadStatus,
105}
106
107impl VerifiedTlsConnector {
108 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 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 #[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 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 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 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 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}