Skip to main content

ssh_cli/tls/
mtls.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2#![forbid(unsafe_code)]
3//! mTLS client identity store under XDG `tls/mtls/<name>/`.
4
5use 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/// Named mTLS client identity on disk.
14#[derive(Debug, Clone)]
15pub struct MtlsIdentity {
16    /// Logical name (XDG leaf).
17    pub name: String,
18    /// Absolute path to certificate chain PEM.
19    pub cert_path: PathBuf,
20    /// Absolute path to private key PEM.
21    pub key_path: PathBuf,
22}
23
24/// Imports PEM cert+key into XDG as identity `name` (overwrites).
25pub fn mtls_import(
26    config_override: Option<&Path>,
27    name: &str,
28    cert_src: &Path,
29    key_src: &Path,
30) -> SshCliResult<MtlsIdentity> {
31    // Validate PEMs before writing.
32    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
54/// Lists imported mTLS identity names.
55pub 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
80/// Shows paths for one identity.
81pub 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    // Parse to ensure integrity.
92    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
101/// Removes an identity directory.
102pub 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
114/// Resolves mTLS paths: either explicit paths or an XDG identity name.
115pub 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}