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 = std::fs::read(cert_src)
39 .map_err(|e| SshCliError::tls_msg(format!("read {}: {e}", cert_src.display())))?;
40 let key_bytes = std::fs::read(key_src)
41 .map_err(|e| SshCliError::tls_msg(format!("read {}: {e}", key_src.display())))?;
42 write_secret_file(&cert_path, &cert_bytes)?;
43 write_secret_file(&key_path, &key_bytes)?;
44
45 Ok(MtlsIdentity {
46 name: name.to_owned(),
47 cert_path,
48 key_path,
49 })
50}
51
52pub fn mtls_list(config_override: Option<&Path>) -> SshCliResult<Vec<String>> {
54 let root =
55 super::paths::resolve_tls_root(config_override)?.join(crate::constants::TLS_MTLS_DIR_NAME);
56 if !root.exists() {
57 return Ok(Vec::new());
58 }
59 let mut names = Vec::new();
60 for entry in
61 std::fs::read_dir(&root).map_err(|e| SshCliError::tls_msg(format!("list mtls: {e}")))?
62 {
63 let entry = entry.map_err(|e| SshCliError::tls_msg(format!("list mtls entry: {e}")))?;
64 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
65 if let Some(n) = entry.file_name().to_str() {
66 let cert = cert_pem_path(&entry.path());
67 let key = key_pem_path(&entry.path());
68 if cert.is_file() && key.is_file() {
69 names.push(n.to_owned());
70 }
71 }
72 }
73 }
74 names.sort();
75 Ok(names)
76}
77
78pub fn mtls_show(config_override: Option<&Path>, name: &str) -> SshCliResult<MtlsIdentity> {
80 let dir = mtls_identity_dir(config_override, name)?;
81 let cert_path = cert_pem_path(&dir);
82 let key_path = key_pem_path(&dir);
83 if !cert_path.is_file() || !key_path.is_file() {
84 return Err(SshCliError::FileNotFound(format!(
85 "mTLS identity '{name}' not found under {}",
86 dir.display()
87 )));
88 }
89 let _ = load_cert_chain(&cert_path)?;
91 let _ = load_private_key(&key_path)?;
92 Ok(MtlsIdentity {
93 name: name.to_owned(),
94 cert_path,
95 key_path,
96 })
97}
98
99pub fn mtls_remove(config_override: Option<&Path>, name: &str) -> SshCliResult<()> {
101 let dir = mtls_identity_dir(config_override, name)?;
102 if !dir.exists() {
103 return Err(SshCliError::FileNotFound(format!(
104 "mTLS identity '{name}' not found"
105 )));
106 }
107 std::fs::remove_dir_all(&dir)
108 .map_err(|e| SshCliError::tls_msg(format!("remove mTLS '{name}': {e}")))?;
109 Ok(())
110}
111
112pub fn resolve_mtls_paths(
114 config_override: Option<&Path>,
115 identity: Option<&str>,
116 cert: Option<&Path>,
117 key: Option<&Path>,
118) -> SshCliResult<(Option<PathBuf>, Option<PathBuf>)> {
119 if let Some(id) = identity {
120 let show = mtls_show(config_override, id)?;
121 return Ok((Some(show.cert_path), Some(show.key_path)));
122 }
123 Ok((cert.map(Path::to_path_buf), key.map(Path::to_path_buf)))
124}