ssh_cli/tls/
client_config.rs1#![forbid(unsafe_code)]
3use std::sync::Arc;
6
7use rustls::ClientConfig;
8
9use super::pem::{load_cert_chain, load_private_key};
10use super::provider::ensure_provider;
11use crate::errors::{SshCliError, SshCliResult};
12
13#[derive(Debug, Clone, Default)]
15pub struct TlsClientOptions {
16 pub client_cert: Option<std::path::PathBuf>,
18 pub client_key: Option<std::path::PathBuf>,
20 pub extra_root_pem: Option<std::path::PathBuf>,
22}
23
24pub fn build_client_config(opts: &TlsClientOptions) -> SshCliResult<Arc<ClientConfig>> {
33 ensure_provider()?;
34
35 let mut roots = rustls::RootCertStore::empty();
36 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
37
38 if let Some(ref extra) = opts.extra_root_pem {
39 let extra_certs = load_cert_chain(extra)?;
40 for c in extra_certs {
41 roots
42 .add(c)
43 .map_err(|e| SshCliError::tls_msg(format!("add extra root: {e}")))?;
44 }
45 }
46
47 let builder = ClientConfig::builder().with_root_certificates(roots);
48
49 let config = match (&opts.client_cert, &opts.client_key) {
50 (Some(cert), Some(key)) => {
51 let chain = load_cert_chain(cert)?;
52 let key_der = load_private_key(key)?;
53 builder
54 .with_client_auth_cert(chain, key_der)
55 .map_err(|e| SshCliError::tls_msg(format!("mTLS client auth cert: {e}")))?
56 }
57 (None, None) => builder.with_no_client_auth(),
58 _ => {
59 return Err(SshCliError::InvalidArgument(
60 "mTLS requires both client_cert and client_key".into(),
61 ));
62 }
63 };
64
65 Ok(Arc::new(config))
66}
67
68#[cfg_attr(not(test), allow(dead_code))]
70pub(crate) fn root_only_config() -> SshCliResult<Arc<ClientConfig>> {
71 build_client_config(&TlsClientOptions::default())
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn root_only_builds() {
80 let cfg = root_only_config().expect("client config");
81 assert!(Arc::strong_count(&cfg) >= 1);
83 }
84}