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