use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use base64::Engine;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificadoInfo {
pub subject: String,
pub issuer: String,
pub serial_number: String,
pub not_before: String,
pub not_after: String,
pub cnpj: Option<String>,
pub razao_social: Option<String>,
pub valido: bool,
pub dias_para_expirar: i64,
}
#[derive(Clone)]
pub struct CertificadoA1 {
pub pfx_data: Vec<u8>,
pub senha: String,
pub info: CertificadoInfo,
pub cert_der: Vec<u8>,
pub private_key_der: Vec<u8>,
}
impl CertificadoA1 {
pub fn from_file<P: AsRef<Path>>(path: P, senha: &str) -> Result<Self, String> {
let pfx_data = fs::read(path.as_ref())
.map_err(|e| format!("Erro ao ler arquivo do certificado: {}", e))?;
Self::from_bytes(&pfx_data, senha)
}
pub fn from_bytes(pfx_data: &[u8], senha: &str) -> Result<Self, String> {
if pfx_data.len() < 10 {
return Err("Arquivo de certificado inválido ou muito pequeno".to_string());
}
if pfx_data[0] != 0x30 {
return Err("Arquivo não parece ser um certificado PKCS12 válido".to_string());
}
let pfx = p12::PFX::parse(pfx_data)
.map_err(|e| format!("Erro ao parsear PKCS12: {:?}", e))?;
let certs = pfx.cert_bags(senha)
.map_err(|e| format!("Erro ao extrair certificado (senha incorreta?): {:?}", e))?;
let keys = pfx.key_bags(senha)
.map_err(|e| format!("Erro ao extrair chave privada: {:?}", e))?;
let cert_der = certs.into_iter().next()
.ok_or("Certificado não encontrado no arquivo PFX")?;
let private_key_der = keys.into_iter().next()
.ok_or("Chave privada não encontrada no arquivo PFX")?;
let info = Self::extract_cert_info(&cert_der)?;
Ok(Self {
pfx_data: pfx_data.to_vec(),
senha: senha.to_string(),
info,
cert_der,
private_key_der,
})
}
fn extract_cert_info(cert_der: &[u8]) -> Result<CertificadoInfo, String> {
use x509_cert::Certificate;
use der::Decode;
let cert = Certificate::from_der(cert_der)
.map_err(|e| format!("Erro ao parsear certificado X509: {:?}", e))?;
let subject = cert.tbs_certificate.subject.to_string();
let issuer = cert.tbs_certificate.issuer.to_string();
let serial_bytes = cert.tbs_certificate.serial_number.as_bytes();
let serial_number = hex::encode(serial_bytes);
let not_before = cert.tbs_certificate.validity.not_before.to_system_time();
let not_after = cert.tbs_certificate.validity.not_after.to_system_time();
let not_before_dt: DateTime<Utc> = not_before.into();
let not_after_dt: DateTime<Utc> = not_after.into();
let now = Utc::now();
let dias_para_expirar = (not_after_dt - now).num_days();
let valido = now >= not_before_dt && now <= not_after_dt;
let cnpj = Self::extract_cnpj_from_subject(&subject);
let razao_social = Self::extract_cn_from_subject(&subject);
Ok(CertificadoInfo {
subject: subject.clone(),
issuer,
serial_number,
not_before: not_before_dt.format("%Y-%m-%d %H:%M:%S").to_string(),
not_after: not_after_dt.format("%Y-%m-%d %H:%M:%S").to_string(),
cnpj,
razao_social,
valido,
dias_para_expirar,
})
}
fn extract_cnpj_from_subject(subject: &str) -> Option<String> {
if let Some(pos) = subject.find("serialNumber=") {
let start = pos + 13;
let end = subject[start..].find(|c: char| c == ',' || c == '+' || c == '/')
.map(|p| start + p)
.unwrap_or(subject.len());
let value = &subject[start..end];
let digits: String = value.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 14 {
return Some(digits);
}
}
if let Some(pos) = subject.find("2.16.76.1.3.3=") {
let start = pos + 14;
let end = subject[start..].find(|c: char| c == ',' || c == '+' || c == '/')
.map(|p| start + p)
.unwrap_or(subject.len());
let value = &subject[start..end];
let digits: String = value.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 14 {
return Some(digits);
}
}
let re = regex::Regex::new(r"(\d{14})").ok()?;
for cap in re.captures_iter(subject) {
if let Some(m) = cap.get(1) {
let digits = m.as_str();
if !digits.starts_with("000000") {
return Some(digits.to_string());
}
}
}
None
}
fn extract_cn_from_subject(subject: &str) -> Option<String> {
if let Some(pos) = subject.find("CN=") {
let start = pos + 3;
let end = subject[start..].find(|c: char| c == ',' || c == '+')
.map(|p| start + p)
.unwrap_or(subject.len());
let cn = subject[start..end].trim();
if !cn.is_empty() {
return Some(cn.to_string());
}
}
None
}
pub fn cert_base64(&self) -> String {
base64::engine::general_purpose::STANDARD.encode(&self.cert_der)
}
pub fn cert_pem(&self) -> String {
let b64 = self.cert_base64();
let lines: Vec<&str> = b64.as_bytes()
.chunks(64)
.map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
.collect();
format!("-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----", lines.join("\n"))
}
pub fn to_base64(&self) -> String {
base64::engine::general_purpose::STANDARD.encode(&self.pfx_data)
}
pub fn pfx_bytes(&self) -> &[u8] {
&self.pfx_data
}
pub fn senha(&self) -> &str {
&self.senha
}
pub fn is_valid(&self) -> bool {
self.info.valido && self.info.dias_para_expirar > 0
}
pub fn private_key(&self) -> Result<rsa::RsaPrivateKey, String> {
use pkcs8::DecodePrivateKey;
rsa::RsaPrivateKey::from_pkcs8_der(&self.private_key_der)
.map_err(|e| format!("Erro ao carregar chave privada: {:?}", e))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_cnpj_serial_number() {
let subject = "CN=EMPRESA TESTE LTDA:12345678000199,serialNumber=12345678000199,C=BR";
let cnpj = CertificadoA1::extract_cnpj_from_subject(subject);
assert_eq!(cnpj, Some("12345678000199".to_string()));
}
#[test]
fn test_extract_cn() {
let subject = "CN=EMPRESA TESTE LTDA,OU=AR,O=ICP-Brasil,C=BR";
let cn = CertificadoA1::extract_cn_from_subject(subject);
assert_eq!(cn, Some("EMPRESA TESTE LTDA".to_string()));
}
}