use std::fs;
use std::path::Path;
use x509_parser::prelude::*;
pub struct CertInfo {
pub subject: String,
pub issuer: String,
pub not_before: String,
pub not_after: String,
pub serial_number: String,
pub fingerprint_sha256: String,
pub fingerprint_sha1: String,
pub fingerprint_md5: String,
pub public_key_algorithm: String,
pub public_key_size: usize,
pub signature_algorithm: String,
pub version: String,
pub sans: Vec<String>,
pub key_usages: Vec<String>,
pub extended_key_usages: Vec<String>,
pub is_wildcard: bool,
pub is_ev: bool,
pub is_ca: bool,
pub is_self_signed: bool,
pub ocsp_responder: Vec<String>,
pub ca_issuers: Vec<String>,
pub crl_distribution: Vec<String>,
pub policy_oids: Vec<String>,
pub basic_constraints: Option<String>,
pub pem: String,
pub der: Vec<u8>,
pub source_type: String,
pub format: CertFormat,
}
#[derive(Clone, Copy, PartialEq)]
pub enum CertFormat {
Pem,
Der,
}
pub fn load_certificate(path: &str) -> Result<CertInfo, Box<dyn std::error::Error>> {
let path = Path::new(path);
if !path.exists() {
return Err(format!("File not found: {}", path.display()).into());
}
let extension = path.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let data = fs::read(path)?;
match extension.as_str() {
"pem" | "crt" | "cert" | "ca-bundle" => parse_pem_certs(&data),
"der" | "cer" => parse_der_single(&data),
"p7b" | "p7c" => parse_pkcs7(&data),
"pfx" | "p12" => Err("PFX/P12 files require a password. Use OpenSSL to extract first.".into()),
_ => try_auto_detect(&data),
}
}
fn try_auto_detect(data: &[u8]) -> Result<CertInfo, Box<dyn std::error::Error>> {
if data.starts_with(b"-----BEGIN") {
return parse_pem_certs(data);
}
if data.len() > 2 && data[0] == 0x30 {
return parse_der_single(data);
}
let text = String::from_utf8_lossy(data);
if text.contains("-----BEGIN CERTIFICATE-----") {
return parse_pem_certs(data);
}
let clean = text.lines()
.filter(|l| !l.starts_with('-') && !l.trim().is_empty())
.collect::<Vec<_>>()
.join("");
if clean.len() > 40 && clean.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=') {
if let Ok(decoded) = base64_decode(clean.trim()) {
if !decoded.is_empty() && decoded[0] == 0x30 {
return parse_der_single(&decoded);
}
}
}
Err("Unrecognised file format. Supported: PEM, DER, CRT, CER, P7B, P7C, PFX/P12 (read-only)".into())
}
fn parse_pem_certs(data: &[u8]) -> Result<CertInfo, Box<dyn std::error::Error>> {
let text = String::from_utf8_lossy(data);
let mut der_blocks: Vec<Vec<u8>> = Vec::new();
let mut in_block = false;
let mut b64 = String::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with("-----BEGIN CERTIFICATE-----") {
in_block = true;
b64.clear();
} else if trimmed.starts_with("-----END CERTIFICATE-----") {
in_block = false;
if let Ok(decoded) = base64_decode(b64.trim()) {
der_blocks.push(decoded);
}
} else if in_block {
b64.push_str(trimmed);
}
}
if der_blocks.is_empty() {
return Err("No valid certificate blocks found in PEM file".into());
}
let pem = der_to_pem(&der_blocks[0]);
let info = parse_x509(&der_blocks[0], &pem)?;
let block_count = der_blocks.len();
if block_count > 1 {
let mut info_with_chain = info;
info_with_chain.source_type = format!("PEM ({} certs)", block_count);
info_with_chain.format = CertFormat::Pem;
Ok(info_with_chain)
} else {
Ok(CertInfo { source_type: "PEM".into(), format: CertFormat::Pem, ..info })
}
}
fn parse_der_single(data: &[u8]) -> Result<CertInfo, Box<dyn std::error::Error>> {
let pem = der_to_pem(data);
let info = parse_x509(data, &pem)?;
Ok(CertInfo { source_type: "DER".into(), format: CertFormat::Der, ..info })
}
fn parse_pkcs7(data: &[u8]) -> Result<CertInfo, Box<dyn std::error::Error>> {
parse_pem_certs(data).or_else(|_| {
parse_der_single(data).map_err(|_| "Failed to parse PKCS7 file. Only PEM-wrapped PKCS7 is supported.".into())
})
}
pub fn parse_x509(der: &[u8], pem: &str) -> Result<CertInfo, Box<dyn std::error::Error>> {
let (_, x509) = X509Certificate::from_der(der)?;
let subject = x509.subject().to_string();
let issuer = x509.issuer().to_string();
let not_before = x509.validity().not_before.to_rfc2822().unwrap_or_else(|e| e);
let not_after = x509.validity().not_after.to_rfc2822().unwrap_or_else(|e| e);
let serial = x509.raw_serial_as_string();
let fingerprint_sha256 = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(der);
hex::encode(hasher.finalize())
};
let fingerprint_sha1 = {
use sha1::Digest;
use sha1::Sha1;
let mut hasher = Sha1::new();
hasher.update(der);
hex::encode(hasher.finalize())
};
let fingerprint_md5 = {
let digest = md5::compute(der);
format!("{:x}", digest)
};
let pubkey_alg = x509.public_key().algorithm.algorithm.to_id_string();
let pubkey_size = match x509.public_key().parsed() {
Ok(pk) => pk.key_size(),
Err(_) => 0,
};
let sig_alg = x509.signature_algorithm.algorithm.to_id_string();
let version = format!("v{}", x509.version().0 + 1);
let mut sans: Vec<String> = Vec::new();
for ext in x509.extensions() {
if ext.oid == oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME {
if let Ok((_, san)) = x509_parser::extensions::SubjectAlternativeName::from_der(ext.value) {
for name in &san.general_names {
sans.push(format!("{:?}", name));
}
}
}
}
let mut key_usages = Vec::new();
if let Ok(Some(ku)) = x509.key_usage() {
let flags = &ku.value;
if flags.digital_signature() { key_usages.push("DigitalSignature".into()); }
if flags.non_repudiation() { key_usages.push("NonRepudiation".into()); }
if flags.key_encipherment() { key_usages.push("KeyEncipherment".into()); }
if flags.data_encipherment() { key_usages.push("DataEncipherment".into()); }
if flags.key_agreement() { key_usages.push("KeyAgreement".into()); }
if flags.key_cert_sign() { key_usages.push("KeyCertSign".into()); }
if flags.crl_sign() { key_usages.push("CRLSign".into()); }
}
let mut ext_key_usages = Vec::new();
if let Ok(Some(eku)) = x509.extended_key_usage() {
for oid in &eku.value.other {
ext_key_usages.push(oid.to_id_string());
}
}
let is_wildcard = subject.contains("*. ") || subject.contains("CN=*");
let is_ev = x509.extensions().iter().any(|ext| {
ext.oid.to_id_string() == "2.23.140.1.1"
});
let mut ocsp_responder = Vec::new();
let mut ca_issuers = Vec::new();
for ext in x509.extensions() {
if ext.oid == oid_registry::OID_PKIX_AUTHORITY_INFO_ACCESS {
if let Ok((_, aia)) = x509_parser::extensions::AuthorityInfoAccess::from_der(ext.value) {
for access in &aia.accessdescs {
let uri = match access.access_location {
x509_parser::extensions::GeneralName::URI(s) => s.to_string(),
_ => continue,
};
if access.access_method == oid_registry::OID_PKIX_ACCESS_DESCRIPTOR_OCSP ||
access.access_method.to_id_string().contains("48.1") {
ocsp_responder.push(uri);
} else if access.access_method == oid_registry::OID_PKIX_ACCESS_DESCRIPTOR_CA_ISSUERS ||
access.access_method.to_id_string().contains("48.2") {
ca_issuers.push(uri);
}
}
}
}
}
let mut crl_distribution = Vec::new();
for ext in x509.extensions() {
if ext.oid == oid_registry::OID_X509_EXT_CRL_DISTRIBUTION_POINTS {
if let Ok((_, crldp)) = x509_parser::extensions::CRLDistributionPoints::from_der(ext.value) {
for dp in &crldp.points {
if let Some(dpn) = &dp.distribution_point {
match dpn {
x509_parser::extensions::DistributionPointName::FullName(names) => {
for name in names {
if let x509_parser::extensions::GeneralName::URI(s) = name {
crl_distribution.push(s.to_string());
}
}
}
_ => {}
}
}
}
}
}
}
let mut policy_oids = Vec::new();
for ext in x509.extensions() {
if ext.oid == oid_registry::OID_X509_EXT_CERTIFICATE_POLICIES {
let text = String::from_utf8_lossy(ext.value);
for part in text.split(|c: char| c == ',' || c == ';' || c == '\n') {
let part = part.trim();
if part.contains('.') && part.chars().all(|c: char| c.is_ascii_digit() || c == '.') {
policy_oids.push(part.to_string());
}
}
if policy_oids.is_empty() && text.contains('.') && text.len() < 200
&& text.chars().all(|c: char| c.is_ascii_digit() || c == '.' || c == '\n' || c == ' ' || c == '\x00')
{
policy_oids.push(text.trim().replace('\x00', "").replace('\n', "").trim().to_string());
}
}
}
let basic_constraints = x509.basic_constraints().ok().flatten().map(|bc| {
format!("CA:{}, pathlen:{:?}", bc.value.ca, bc.value.path_len_constraint)
});
let is_ca = basic_constraints.as_ref().map(|bc| bc.starts_with("CA:true")).unwrap_or(false);
let is_self_signed = subject == issuer;
Ok(CertInfo {
subject,
issuer,
not_before,
not_after,
serial_number: serial,
fingerprint_sha256,
fingerprint_sha1,
fingerprint_md5,
public_key_algorithm: pubkey_alg,
public_key_size: pubkey_size,
signature_algorithm: sig_alg,
version,
sans,
key_usages,
extended_key_usages: ext_key_usages,
is_wildcard,
is_ev,
is_ca,
is_self_signed,
ocsp_responder,
ca_issuers,
crl_distribution,
policy_oids,
basic_constraints,
pem: pem.to_string(),
der: der.to_vec(),
source_type: String::new(),
format: CertFormat::Pem,
})
}
pub fn der_to_pem(der: &[u8]) -> String {
use std::fmt::Write;
let b64 = base64_encode(der);
let mut pem = String::new();
pem.push_str("-----BEGIN CERTIFICATE-----\n");
for chunk in b64.as_bytes().chunks(64) {
let _ = writeln!(pem, "{}", String::from_utf8_lossy(chunk));
}
if !b64.is_empty() && !pem.ends_with('\n') {
pem.push('\n');
}
pem.push_str("-----END CERTIFICATE-----\n");
pem
}
pub fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut output = String::with_capacity(((data.len() + 2) / 3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
output.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
output.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
output.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
output.push('=');
}
if chunk.len() > 2 {
output.push(CHARS[(triple & 0x3F) as usize] as char);
} else {
output.push('=');
}
}
output
}
pub fn base64_decode(input: &str) -> Result<Vec<u8>, &'static str> {
const DECODE: [i8; 128] = {
let mut table = [-1i8; 128];
table[b'A' as usize] = 0;
table[b'B' as usize] = 1;
table[b'C' as usize] = 2;
table[b'D' as usize] = 3;
table[b'E' as usize] = 4;
table[b'F' as usize] = 5;
table[b'G' as usize] = 6;
table[b'H' as usize] = 7;
table[b'I' as usize] = 8;
table[b'J' as usize] = 9;
table[b'K' as usize] = 10;
table[b'L' as usize] = 11;
table[b'M' as usize] = 12;
table[b'N' as usize] = 13;
table[b'O' as usize] = 14;
table[b'P' as usize] = 15;
table[b'Q' as usize] = 16;
table[b'R' as usize] = 17;
table[b'S' as usize] = 18;
table[b'T' as usize] = 19;
table[b'U' as usize] = 20;
table[b'V' as usize] = 21;
table[b'W' as usize] = 22;
table[b'X' as usize] = 23;
table[b'Y' as usize] = 24;
table[b'Z' as usize] = 25;
table[b'a' as usize] = 26;
table[b'b' as usize] = 27;
table[b'c' as usize] = 28;
table[b'd' as usize] = 29;
table[b'e' as usize] = 30;
table[b'f' as usize] = 31;
table[b'g' as usize] = 32;
table[b'h' as usize] = 33;
table[b'i' as usize] = 34;
table[b'j' as usize] = 35;
table[b'k' as usize] = 36;
table[b'l' as usize] = 37;
table[b'm' as usize] = 38;
table[b'n' as usize] = 39;
table[b'o' as usize] = 40;
table[b'p' as usize] = 41;
table[b'q' as usize] = 42;
table[b'r' as usize] = 43;
table[b's' as usize] = 44;
table[b't' as usize] = 45;
table[b'u' as usize] = 46;
table[b'v' as usize] = 47;
table[b'w' as usize] = 48;
table[b'x' as usize] = 49;
table[b'y' as usize] = 50;
table[b'z' as usize] = 51;
table[b'0' as usize] = 52;
table[b'1' as usize] = 53;
table[b'2' as usize] = 54;
table[b'3' as usize] = 55;
table[b'4' as usize] = 56;
table[b'5' as usize] = 57;
table[b'6' as usize] = 58;
table[b'7' as usize] = 59;
table[b'8' as usize] = 60;
table[b'9' as usize] = 61;
table[b'+' as usize] = 62;
table[b'/' as usize] = 63;
table
};
let input = input.trim();
let mut output = Vec::with_capacity(input.len() * 3 / 4);
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'=' || bytes[i] == b'\n' || bytes[i] == b'\r' || bytes[i] == b' ' {
i += 1;
continue;
}
let mut quad = [0u8; 4];
let mut valid = 0;
while valid < 4 && i < bytes.len() {
let b = bytes[i];
if b == b'=' {
quad[valid] = 0;
valid += 1;
i += 1;
break;
}
if b == b'\n' || b == b'\r' || b == b' ' {
i += 1;
continue;
}
if b < 128 {
let val = DECODE[b as usize];
if val >= 0 {
quad[valid] = val as u8;
valid += 1;
}
}
i += 1;
}
if valid == 0 {
break;
}
output.push((quad[0] << 2) | (quad[1] >> 4));
if valid > 2 {
output.push((quad[1] << 4) | (quad[2] >> 2));
}
if valid > 3 {
output.push((quad[2] << 6) | quad[3]);
}
}
Ok(output)
}