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