ngdp_client/commands/
certs.rs

1//! Certificate command handlers
2
3use crate::{CertFormat, CertsCommands, OutputFormat, cached_client};
4use ribbit_client::Endpoint;
5use serde_json::json;
6use std::str::FromStr;
7
8/// Handle certificate commands
9pub async fn handle(
10    cmd: CertsCommands,
11    output_format: OutputFormat,
12) -> Result<(), Box<dyn std::error::Error>> {
13    match cmd {
14        CertsCommands::Download {
15            ski,
16            output,
17            region,
18            cert_format,
19            details,
20        } => download(ski, output, region, cert_format, details, output_format).await,
21    }
22}
23
24/// Download a certificate by SKI/hash
25async fn download(
26    ski: String,
27    output: Option<std::path::PathBuf>,
28    region: String,
29    cert_format: CertFormat,
30    show_details: bool,
31    output_format: OutputFormat,
32) -> Result<(), Box<dyn std::error::Error>> {
33    // Parse region
34    let region = ribbit_client::Region::from_str(&region)?;
35
36    // Create cached client
37    let client = cached_client::create_client(region).await?;
38
39    // Request the certificate
40    let endpoint = Endpoint::Cert(ski.clone());
41    let response = client.request(&endpoint).await?;
42
43    // Extract the certificate data
44    let cert_data = response
45        .as_text()
46        .ok_or("No certificate data in response")?;
47
48    // Handle JSON output format specially
49    match output_format {
50        OutputFormat::Json | OutputFormat::JsonPretty => {
51            // For JSON output, always include both certificate and details
52            let mut json_output = json!({
53                "ski": ski,
54                "certificate": cert_data,
55            });
56
57            // Add details if requested
58            if show_details {
59                if let Ok(cert_info) = extract_certificate_info(cert_data) {
60                    json_output["details"] = json!(cert_info);
61                }
62            }
63
64            // Write to file or stdout
65            if let Some(output_path) = output {
66                let json_string = if matches!(output_format, OutputFormat::JsonPretty) {
67                    serde_json::to_string_pretty(&json_output)?
68                } else {
69                    serde_json::to_string(&json_output)?
70                };
71                std::fs::write(&output_path, json_string)?;
72                tracing::info!("Certificate written to: {}", output_path.display());
73            } else if matches!(output_format, OutputFormat::JsonPretty) {
74                println!("{}", serde_json::to_string_pretty(&json_output)?);
75            } else {
76                println!("{}", serde_json::to_string(&json_output)?);
77            }
78        }
79        _ => {
80            // For non-JSON output formats
81
82            // Show details if requested (text format only)
83            if show_details {
84                if let Ok(cert_info) = extract_certificate_info(cert_data) {
85                    use crate::output::{OutputStyle, format_header, format_key_value};
86                    let style = OutputStyle::new();
87
88                    println!("{}", format_header("Certificate Details", &style));
89                    println!(
90                        "{}",
91                        format_key_value("Subject Key Identifier", &ski, &style)
92                    );
93                    println!(
94                        "{}",
95                        format_key_value("Subject", &cert_info.subject, &style)
96                    );
97                    println!("{}", format_key_value("Issuer", &cert_info.issuer, &style));
98                    println!(
99                        "{}",
100                        format_key_value("Not Before", &cert_info.not_before, &style)
101                    );
102                    println!(
103                        "{}",
104                        format_key_value("Not After", &cert_info.not_after, &style)
105                    );
106                    println!(
107                        "{}",
108                        format_key_value("Serial Number", &cert_info.serial_number, &style)
109                    );
110                    if !cert_info.subject_alt_names.is_empty() {
111                        println!("\nSubject Alternative Names:");
112                        for san in &cert_info.subject_alt_names {
113                            println!("  - {san}");
114                        }
115                    }
116                    println!();
117                }
118            }
119
120            // Handle output format conversion
121            let output_data = match cert_format {
122                CertFormat::Pem => cert_data.as_bytes().to_vec(),
123                CertFormat::Der => {
124                    // Convert PEM to DER
125                    convert_pem_to_der(cert_data)?
126                }
127            };
128
129            // Write to output
130            if let Some(output_path) = output {
131                std::fs::write(&output_path, &output_data)?;
132                tracing::info!("Certificate written to: {}", output_path.display());
133            } else {
134                // Write to stdout
135                if cert_format == CertFormat::Pem {
136                    print!("{cert_data}");
137                } else {
138                    // For DER format, write binary to stdout
139                    use std::io::Write;
140                    std::io::stdout().write_all(&output_data)?;
141                }
142            }
143        }
144    }
145
146    Ok(())
147}
148
149/// Convert PEM certificate to DER format
150fn convert_pem_to_der(pem_data: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
151    // Extract base64 content from PEM
152    let base64_content: String = pem_data
153        .lines()
154        .skip_while(|line| !line.contains("BEGIN CERTIFICATE"))
155        .skip(1) // Skip the BEGIN line itself
156        .take_while(|line| !line.contains("END CERTIFICATE"))
157        .map(|line| line.trim())
158        .collect();
159
160    if base64_content.is_empty() {
161        return Err("No certificate content found in PEM data".into());
162    }
163
164    // Decode base64 to DER
165    use base64::{Engine as _, engine::general_purpose::STANDARD};
166    Ok(STANDARD.decode(&base64_content)?)
167}
168
169/// Certificate information for JSON output
170#[derive(serde::Serialize)]
171struct CertificateInfo {
172    subject: String,
173    issuer: String,
174    serial_number: String,
175    not_before: String,
176    not_after: String,
177    subject_alt_names: Vec<String>,
178}
179
180/// Extract certificate information from PEM data
181fn extract_certificate_info(pem_data: &str) -> Result<CertificateInfo, Box<dyn std::error::Error>> {
182    // Convert PEM to DER
183    let der_data = convert_pem_to_der(pem_data)?;
184
185    // Parse certificate
186    use der::Decode;
187    use x509_cert::Certificate;
188    let cert = Certificate::from_der(&der_data)?;
189
190    // Extract information
191    let subject = cert.tbs_certificate.subject.to_string();
192    let issuer = cert.tbs_certificate.issuer.to_string();
193    let serial_number = format!("{}", cert.tbs_certificate.serial_number);
194    let not_before = cert.tbs_certificate.validity.not_before.to_string();
195    let not_after = cert.tbs_certificate.validity.not_after.to_string();
196
197    // Extract SANs if present
198    let mut subject_alt_names = Vec::new();
199    if let Some(extensions) = &cert.tbs_certificate.extensions {
200        for ext in extensions {
201            // Check for Subject Alternative Name extension (OID 2.5.29.17)
202            if ext.extn_id.to_string() == "2.5.29.17" {
203                // For now, just note that SANs are present
204                // Full parsing would require more complex ASN.1 handling
205                subject_alt_names.push("(Subject Alternative Names present)".to_string());
206            }
207        }
208    }
209
210    Ok(CertificateInfo {
211        subject,
212        issuer,
213        serial_number,
214        not_before,
215        not_after,
216        subject_alt_names,
217    })
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_convert_pem_to_der() {
226        let pem = "-----BEGIN CERTIFICATE-----\n\
227                   MIIBkTCB+wIJAKHHIG...\n\
228                   -----END CERTIFICATE-----";
229
230        // This should not panic, even if base64 is invalid
231        let _ = convert_pem_to_der(pem);
232    }
233}