Skip to main content

ssh_cli/tls/
commands.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2#![forbid(unsafe_code)]
3//! CLI handlers for `ssh-cli tls …` (feature `tls`).
4
5use std::path::PathBuf;
6
7use crate::cli::{OutputFormat, TlsAcmeAccountAction, TlsAcmeAction, TlsAction, TlsMtlsAction};
8use crate::errors::SshCliError;
9use crate::output;
10use anyhow::Result;
11
12/// Dispatches `tls` subcommands.
13pub async fn run_tls_command(
14    action: TlsAction,
15    config_override: Option<PathBuf>,
16    format: OutputFormat,
17    json_flag: bool,
18) -> Result<()> {
19    let json = format == OutputFormat::Json || json_flag;
20    let cfg = config_override.as_deref();
21
22    match action {
23        TlsAction::Provider => {
24            let installed = super::provider_is_installed();
25            let name = super::provider_name();
26            let payload = serde_json::json!({
27                "provider": name,
28                "installed": installed,
29                "min_rustls": "0.23.18",
30                "stack": "rustls+aws_lc_rs",
31            });
32            output::emit_success(
33                "tls-provider",
34                payload,
35                &format!("TLS provider: {name} (installed={installed})"),
36                json,
37            )?;
38        }
39        TlsAction::Paths => {
40            let root = super::tls_root_dir(cfg)?;
41            let account = super::acme_account_path(cfg)?;
42            let payload = serde_json::json!({
43                "tls_root": root.display().to_string(),
44                "mtls_dir": root.join(crate::constants::TLS_MTLS_DIR_NAME).display().to_string(),
45                "acme_dir": root.join(crate::constants::TLS_ACME_DIR_NAME).display().to_string(),
46                "acme_account": account.display().to_string(),
47            });
48            output::emit_success(
49                "tls-paths",
50                payload,
51                &format!("TLS root: {}", root.display()),
52                json,
53            )?;
54        }
55        TlsAction::Mtls { action } => match action {
56            TlsMtlsAction::List => {
57                let names = super::mtls_list(cfg)?;
58                let payload = serde_json::json!({ "identities": names });
59                let msg = if names.is_empty() {
60                    "no mTLS identities".to_string()
61                } else {
62                    format!("mTLS identities: {}", names.join(", "))
63                };
64                output::emit_success("tls-mtls-list", payload, &msg, json)?;
65            }
66            TlsMtlsAction::Import { name, cert, key } => {
67                let id = super::mtls_import(cfg, &name, &cert, &key)?;
68                let payload = serde_json::json!({
69                    "name": id.name,
70                    "cert_path": id.cert_path.display().to_string(),
71                    "key_path": id.key_path.display().to_string(),
72                });
73                output::emit_success(
74                    "tls-mtls-import",
75                    payload,
76                    &format!("imported mTLS identity '{}'", id.name),
77                    json,
78                )?;
79            }
80            TlsMtlsAction::Show { name } => {
81                let id = super::mtls_show(cfg, &name)?;
82                let payload = serde_json::json!({
83                    "name": id.name,
84                    "cert_path": id.cert_path.display().to_string(),
85                    "key_path": id.key_path.display().to_string(),
86                });
87                output::emit_success(
88                    "tls-mtls-show",
89                    payload,
90                    &format!(
91                        "mTLS '{}': cert={} key={}",
92                        id.name,
93                        id.cert_path.display(),
94                        id.key_path.display()
95                    ),
96                    json,
97                )?;
98            }
99            TlsMtlsAction::Remove { name } => {
100                super::mtls_remove(cfg, &name)?;
101                output::emit_success(
102                    "tls-mtls-remove",
103                    serde_json::json!({ "name": name }),
104                    &format!("removed mTLS identity '{name}'"),
105                    json,
106                )?;
107            }
108        },
109        TlsAction::Acme { action } => match action {
110            TlsAcmeAction::Account { action } => match action {
111                TlsAcmeAccountAction::Create {
112                    staging,
113                    contact,
114                    force,
115                } => {
116                    let dir = if staging {
117                        super::AcmeDirectory::Staging
118                    } else {
119                        super::AcmeDirectory::Production
120                    };
121                    let st = super::create_account(cfg, dir, &contact, force).await?;
122                    let payload = serde_json::to_value(&st)
123                        .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
124                    output::emit_success(
125                        "tls-acme-account-create",
126                        payload,
127                        &format!("ACME account at {}", st.path),
128                        json,
129                    )?;
130                }
131                TlsAcmeAccountAction::Show => {
132                    let st = super::load_account_status(cfg)?;
133                    let payload = serde_json::to_value(&st)
134                        .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
135                    let msg = if st.present {
136                        format!("ACME account present at {}", st.path)
137                    } else {
138                        format!("no ACME account at {}", st.path)
139                    };
140                    output::emit_success("tls-acme-account-show", payload, &msg, json)?;
141                }
142            },
143            TlsAcmeAction::Issue {
144                domain,
145                staging,
146                print_challenge,
147            } => {
148                if !print_challenge {
149                    return Err(SshCliError::InvalidArgument(
150                        "pass --print-challenge (agent two-step DNS-01; no interactive wait)"
151                            .into(),
152                    )
153                    .into());
154                }
155                let dir = if staging {
156                    super::AcmeDirectory::Staging
157                } else {
158                    super::AcmeDirectory::Production
159                };
160                let pending = super::acme_issue_print_challenge(cfg, &domain, dir).await?;
161                let payload = serde_json::to_value(&pending)
162                    .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
163                let msg = format!(
164                    "set DNS TXT {} = {} then run: ssh-cli tls acme complete --domain {}",
165                    pending.dns_name, pending.dns_value, pending.domain
166                );
167                output::emit_success("tls-acme-challenge", payload, &msg, json)?;
168            }
169            TlsAcmeAction::Complete { domain } => {
170                let st = super::acme_complete(cfg, &domain).await?;
171                let payload = serde_json::to_value(&st)
172                    .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
173                output::emit_success(
174                    "tls-acme-complete",
175                    payload,
176                    &format!("certificate issued for {domain}"),
177                    json,
178                )?;
179            }
180            TlsAcmeAction::Status { domain } => {
181                let list = super::acme_status(cfg, domain.as_deref())?;
182                let payload = serde_json::json!({ "domains": list });
183                output::emit_success(
184                    "tls-acme-status",
185                    payload,
186                    &format!("{} ACME domain(s)", list.len()),
187                    json,
188                )?;
189            }
190            TlsAcmeAction::List => {
191                let list = super::acme_list(cfg)?;
192                let payload = serde_json::json!({ "domains": list });
193                output::emit_success(
194                    "tls-acme-list",
195                    payload,
196                    &format!("{} ACME domain(s)", list.len()),
197                    json,
198                )?;
199            }
200        },
201    }
202    Ok(())
203}