1#![forbid(unsafe_code)]
3use std::path::{Path, PathBuf};
6
7use super::paths::{cert_pem_path, ensure_dir, key_pem_path, mtls_identity_dir, write_secret_file};
8use super::pem::{load_cert_chain, load_private_key};
9use crate::errors::{SshCliError, SshCliResult};
10
11#[derive(Debug, Clone)]
13pub struct MtlsIdentity {
14 pub name: String,
16 pub cert_path: PathBuf,
18 pub key_path: PathBuf,
20}
21
22pub fn mtls_import(
24 config_override: Option<&Path>,
25 name: &str,
26 cert_src: &Path,
27 key_src: &Path,
28) -> SshCliResult<MtlsIdentity> {
29 let _ = load_cert_chain(cert_src)?;
31 let _ = load_private_key(key_src)?;
32
33 let dir = mtls_identity_dir(config_override, name)?;
34 ensure_dir(&dir)?;
35 let cert_path = cert_pem_path(&dir);
36 let key_path = key_pem_path(&dir);
37
38 let cert_bytes = crate::paths::read_bytes_capped(cert_src, crate::paths::MAX_PEM_FILE_BYTES)
43 .map_err(|e| SshCliError::tls_msg(format!("read {}: {e}", cert_src.display())))?;
44 let key_bytes = crate::paths::read_bytes_capped(key_src, crate::paths::MAX_PEM_FILE_BYTES)
45 .map_err(|e| SshCliError::tls_msg(format!("read {}: {e}", key_src.display())))?;
46 write_secret_file(&cert_path, &cert_bytes)?;
47 write_secret_file(&key_path, &key_bytes)?;
48
49 Ok(MtlsIdentity {
50 name: name.to_owned(),
51 cert_path,
52 key_path,
53 })
54}
55
56pub fn mtls_list(config_override: Option<&Path>) -> SshCliResult<Vec<String>> {
58 let root =
59 super::paths::resolve_tls_root(config_override)?.join(crate::constants::TLS_MTLS_DIR_NAME);
60 if !root.exists() {
61 return Ok(Vec::new());
62 }
63 let mut names = Vec::new();
64 for entry in
65 std::fs::read_dir(&root).map_err(|e| SshCliError::tls_msg(format!("list mtls: {e}")))?
66 {
67 let entry = entry.map_err(|e| SshCliError::tls_msg(format!("list mtls entry: {e}")))?;
68 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
69 if let Some(n) = entry.file_name().to_str() {
70 let cert = cert_pem_path(&entry.path());
71 let key = key_pem_path(&entry.path());
72 if cert.is_file() && key.is_file() {
73 names.push(n.to_owned());
74 }
75 }
76 }
77 }
78 names.sort();
79 Ok(names)
80}
81
82pub fn mtls_show(config_override: Option<&Path>, name: &str) -> SshCliResult<MtlsIdentity> {
84 let dir = mtls_identity_dir(config_override, name)?;
85 let cert_path = cert_pem_path(&dir);
86 let key_path = key_pem_path(&dir);
87 if !cert_path.is_file() || !key_path.is_file() {
88 return Err(SshCliError::FileNotFound(format!(
89 "mTLS identity '{name}' not found under {}",
90 dir.display()
91 )));
92 }
93 let _ = load_cert_chain(&cert_path)?;
95 let _ = load_private_key(&key_path)?;
96 Ok(MtlsIdentity {
97 name: name.to_owned(),
98 cert_path,
99 key_path,
100 })
101}
102
103pub fn mtls_remove(config_override: Option<&Path>, name: &str) -> SshCliResult<()> {
105 let dir = mtls_identity_dir(config_override, name)?;
106 if !dir.exists() {
107 return Err(SshCliError::FileNotFound(format!(
108 "mTLS identity '{name}' not found"
109 )));
110 }
111 std::fs::remove_dir_all(&dir)
112 .map_err(|e| SshCliError::tls_msg(format!("remove mTLS '{name}': {e}")))?;
113 Ok(())
114}
115
116pub fn resolve_mtls_paths(
118 config_override: Option<&Path>,
119 identity: Option<&str>,
120 cert: Option<&Path>,
121 key: Option<&Path>,
122) -> SshCliResult<(Option<PathBuf>, Option<PathBuf>)> {
123 if let Some(id) = identity {
124 let show = mtls_show(config_override, id)?;
125 return Ok((Some(show.cert_path), Some(show.key_path)));
126 }
127 Ok((cert.map(Path::to_path_buf), key.map(Path::to_path_buf)))
128}