use crate::{CertFormat, CertsCommands, OutputFormat, cached_client};
use ribbit_client::Endpoint;
use serde_json::json;
use std::str::FromStr;
pub async fn handle(
cmd: CertsCommands,
output_format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
match cmd {
CertsCommands::Download {
ski,
output,
region,
cert_format,
details,
} => download(ski, output, region, cert_format, details, output_format).await,
}
}
async fn download(
ski: String,
output: Option<std::path::PathBuf>,
region: String,
cert_format: CertFormat,
show_details: bool,
output_format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let region = ribbit_client::Region::from_str(®ion)?;
let client = cached_client::create_client(region).await?;
let endpoint = Endpoint::Cert(ski.clone());
let response = client.request(&endpoint).await?;
let cert_data = response
.as_text()
.ok_or("No certificate data in response")?;
match output_format {
OutputFormat::Json | OutputFormat::JsonPretty => {
let mut json_output = json!({
"ski": ski,
"certificate": cert_data,
});
if show_details {
if let Ok(cert_info) = extract_certificate_info(cert_data) {
json_output["details"] = json!(cert_info);
}
}
if let Some(output_path) = output {
let json_string = if matches!(output_format, OutputFormat::JsonPretty) {
serde_json::to_string_pretty(&json_output)?
} else {
serde_json::to_string(&json_output)?
};
std::fs::write(&output_path, json_string)?;
tracing::info!("Certificate written to: {}", output_path.display());
} else if matches!(output_format, OutputFormat::JsonPretty) {
println!("{}", serde_json::to_string_pretty(&json_output)?);
} else {
println!("{}", serde_json::to_string(&json_output)?);
}
}
_ => {
if show_details {
if let Ok(cert_info) = extract_certificate_info(cert_data) {
use crate::output::{OutputStyle, format_header, format_key_value};
let style = OutputStyle::new();
println!("{}", format_header("Certificate Details", &style));
println!(
"{}",
format_key_value("Subject Key Identifier", &ski, &style)
);
println!(
"{}",
format_key_value("Subject", &cert_info.subject, &style)
);
println!("{}", format_key_value("Issuer", &cert_info.issuer, &style));
println!(
"{}",
format_key_value("Not Before", &cert_info.not_before, &style)
);
println!(
"{}",
format_key_value("Not After", &cert_info.not_after, &style)
);
println!(
"{}",
format_key_value("Serial Number", &cert_info.serial_number, &style)
);
if !cert_info.subject_alt_names.is_empty() {
println!("\nSubject Alternative Names:");
for san in &cert_info.subject_alt_names {
println!(" - {san}");
}
}
println!();
}
}
let output_data = match cert_format {
CertFormat::Pem => cert_data.as_bytes().to_vec(),
CertFormat::Der => {
convert_pem_to_der(cert_data)?
}
};
if let Some(output_path) = output {
std::fs::write(&output_path, &output_data)?;
tracing::info!("Certificate written to: {}", output_path.display());
} else {
if cert_format == CertFormat::Pem {
print!("{cert_data}");
} else {
use std::io::Write;
std::io::stdout().write_all(&output_data)?;
}
}
}
}
Ok(())
}
fn convert_pem_to_der(pem_data: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let base64_content: String = pem_data
.lines()
.skip_while(|line| !line.contains("BEGIN CERTIFICATE"))
.skip(1) .take_while(|line| !line.contains("END CERTIFICATE"))
.map(|line| line.trim())
.collect();
if base64_content.is_empty() {
return Err("No certificate content found in PEM data".into());
}
use base64::{Engine as _, engine::general_purpose::STANDARD};
Ok(STANDARD.decode(&base64_content)?)
}
#[derive(serde::Serialize)]
struct CertificateInfo {
subject: String,
issuer: String,
serial_number: String,
not_before: String,
not_after: String,
subject_alt_names: Vec<String>,
}
fn extract_certificate_info(pem_data: &str) -> Result<CertificateInfo, Box<dyn std::error::Error>> {
let der_data = convert_pem_to_der(pem_data)?;
use der::Decode;
use x509_cert::Certificate;
let cert = Certificate::from_der(&der_data)?;
let subject = cert.tbs_certificate.subject.to_string();
let issuer = cert.tbs_certificate.issuer.to_string();
let serial_number = format!("{}", cert.tbs_certificate.serial_number);
let not_before = cert.tbs_certificate.validity.not_before.to_string();
let not_after = cert.tbs_certificate.validity.not_after.to_string();
let mut subject_alt_names = Vec::new();
if let Some(extensions) = &cert.tbs_certificate.extensions {
for ext in extensions {
if ext.extn_id.to_string() == "2.5.29.17" {
subject_alt_names.push("(Subject Alternative Names present)".to_string());
}
}
}
Ok(CertificateInfo {
subject,
issuer,
serial_number,
not_before,
not_after,
subject_alt_names,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_pem_to_der() {
let pem = "-----BEGIN CERTIFICATE-----\n\
MIIBkTCB+wIJAKHHIG...\n\
-----END CERTIFICATE-----";
let _ = convert_pem_to_der(pem);
}
}