ngdp_client/commands/
certs.rs1use crate::{CertFormat, CertsCommands, OutputFormat, cached_client};
4use ribbit_client::Endpoint;
5use serde_json::json;
6use std::str::FromStr;
7
8pub 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
24async 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 let region = ribbit_client::Region::from_str(®ion)?;
35
36 let client = cached_client::create_client(region).await?;
38
39 let endpoint = Endpoint::Cert(ski.clone());
41 let response = client.request(&endpoint).await?;
42
43 let cert_data = response
45 .as_text()
46 .ok_or("No certificate data in response")?;
47
48 match output_format {
50 OutputFormat::Json | OutputFormat::JsonPretty => {
51 let mut json_output = json!({
53 "ski": ski,
54 "certificate": cert_data,
55 });
56
57 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 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 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 let output_data = match cert_format {
122 CertFormat::Pem => cert_data.as_bytes().to_vec(),
123 CertFormat::Der => {
124 convert_pem_to_der(cert_data)?
126 }
127 };
128
129 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 if cert_format == CertFormat::Pem {
136 print!("{cert_data}");
137 } else {
138 use std::io::Write;
140 std::io::stdout().write_all(&output_data)?;
141 }
142 }
143 }
144 }
145
146 Ok(())
147}
148
149fn convert_pem_to_der(pem_data: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
151 let base64_content: String = pem_data
153 .lines()
154 .skip_while(|line| !line.contains("BEGIN CERTIFICATE"))
155 .skip(1) .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 use base64::{Engine as _, engine::general_purpose::STANDARD};
166 Ok(STANDARD.decode(&base64_content)?)
167}
168
169#[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
180fn extract_certificate_info(pem_data: &str) -> Result<CertificateInfo, Box<dyn std::error::Error>> {
182 let der_data = convert_pem_to_der(pem_data)?;
184
185 use der::Decode;
187 use x509_cert::Certificate;
188 let cert = Certificate::from_der(&der_data)?;
189
190 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 let mut subject_alt_names = Vec::new();
199 if let Some(extensions) = &cert.tbs_certificate.extensions {
200 for ext in extensions {
201 if ext.extn_id.to_string() == "2.5.29.17" {
203 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 let _ = convert_pem_to_der(pem);
232 }
233}