fiscal_crypto/certificate/
pfx.rs1use std::sync::Once;
4
5use openssl::pkcs12::Pkcs12;
6
7use fiscal_core::FiscalError;
8use fiscal_core::types::{CertificateData, CertificateInfo};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum SignatureAlgorithm {
18 #[default]
20 Sha1,
21 Sha256,
23}
24
25fn ensure_legacy_provider() {
31 static INIT: Once = Once::new();
32 INIT.call_once(|| {
33 if let Ok(provider) = openssl::provider::Provider::try_load(None, "legacy", true) {
34 std::mem::forget(provider);
35 }
36 });
37}
38
39pub fn ensure_modern_pfx(pfx_buffer: &[u8], passphrase: &str) -> Result<Vec<u8>, FiscalError> {
56 ensure_legacy_provider();
57
58 let pkcs12 = Pkcs12::from_der(pfx_buffer)
59 .map_err(|e| FiscalError::Certificate(format!("Invalid PFX data: {e}")))?;
60
61 match pkcs12.parse2(passphrase) {
62 Ok(parsed) => {
63 re_export_pfx(&parsed, passphrase)
67 }
68 Err(e) => {
69 let msg = e.to_string();
70 if msg.contains("unsupported") || msg.contains("RC2") || msg.contains("mac") {
71 Err(FiscalError::Certificate(format!(
72 "Legacy PFX (RC2-40-CBC) detected but OpenSSL legacy provider \
73 could not handle it. Ensure OpenSSL 3.x with legacy provider \
74 support is available. Error: {e}"
75 )))
76 } else {
77 Err(FiscalError::Certificate(format!(
78 "Failed to parse PFX (wrong password?): {e}"
79 )))
80 }
81 }
82 }
83}
84
85fn re_export_pfx(
90 parsed: &openssl::pkcs12::ParsedPkcs12_2,
91 passphrase: &str,
92) -> Result<Vec<u8>, FiscalError> {
93 let pkey = parsed
94 .pkey
95 .as_ref()
96 .ok_or_else(|| FiscalError::Certificate("PFX does not contain a private key".into()))?;
97 let cert = parsed
98 .cert
99 .as_ref()
100 .ok_or_else(|| FiscalError::Certificate("PFX does not contain a certificate".into()))?;
101
102 let mut builder = Pkcs12::builder();
103 if let Some(chain) = &parsed.ca {
104 let mut stack = openssl::stack::Stack::new()
105 .map_err(|e| FiscalError::Certificate(format!("Failed to create CA stack: {e}")))?;
106 for ca in chain {
107 stack
108 .push(ca.to_owned())
109 .map_err(|e| FiscalError::Certificate(format!("Failed to add CA to stack: {e}")))?;
110 }
111 builder.ca(stack);
112 }
113
114 let new_pfx = builder
115 .name("")
116 .pkey(pkey)
117 .cert(cert)
118 .build2(passphrase)
119 .map_err(|e| FiscalError::Certificate(format!("Failed to re-export PFX: {e}")))?;
120
121 new_pfx
122 .to_der()
123 .map_err(|e| FiscalError::Certificate(format!("Failed to serialize PFX: {e}")))
124}
125
126fn parse_pfx(
127 pfx_buffer: &[u8],
128 passphrase: &str,
129) -> Result<openssl::pkcs12::ParsedPkcs12_2, FiscalError> {
130 let modern = ensure_modern_pfx(pfx_buffer, passphrase)?;
131 let pkcs12 = Pkcs12::from_der(&modern)
132 .map_err(|e| FiscalError::Certificate(format!("Invalid PFX data: {e}")))?;
133 pkcs12
134 .parse2(passphrase)
135 .map_err(|e| FiscalError::Certificate(format!("Failed to parse PFX: {e}")))
136}
137
138pub fn load_certificate(
151 pfx_buffer: &[u8],
152 passphrase: &str,
153) -> Result<CertificateData, FiscalError> {
154 ensure_legacy_provider();
155 let parsed = parse_pfx(pfx_buffer, passphrase)?;
156
157 let pkey = parsed
158 .pkey
159 .ok_or_else(|| FiscalError::Certificate("PFX does not contain a private key".into()))?;
160
161 let cert = parsed
162 .cert
163 .ok_or_else(|| FiscalError::Certificate("PFX does not contain a certificate".into()))?;
164
165 let private_key_pem = String::from_utf8(
166 pkey.private_key_to_pem_pkcs8()
167 .map_err(|e| FiscalError::Certificate(format!("Failed to export private key: {e}")))?,
168 )
169 .map_err(|e| FiscalError::Certificate(format!("Private key PEM is not valid UTF-8: {e}")))?;
170
171 let certificate_pem = String::from_utf8(
172 cert.to_pem()
173 .map_err(|e| FiscalError::Certificate(format!("Failed to export certificate: {e}")))?,
174 )
175 .map_err(|e| FiscalError::Certificate(format!("Certificate PEM is not valid UTF-8: {e}")))?;
176
177 Ok(CertificateData::new(
178 private_key_pem,
179 certificate_pem,
180 pfx_buffer.to_vec(),
181 passphrase,
182 ))
183}
184
185pub fn get_certificate_info(
197 pfx_buffer: &[u8],
198 passphrase: &str,
199) -> Result<CertificateInfo, FiscalError> {
200 ensure_legacy_provider();
201 let parsed = parse_pfx(pfx_buffer, passphrase)?;
202
203 let cert = parsed
204 .cert
205 .ok_or_else(|| FiscalError::Certificate("PFX does not contain a certificate".into()))?;
206
207 let common_name = extract_cn_from_x509_name(cert.subject_name());
208 let issuer = extract_cn_from_x509_name(cert.issuer_name());
209
210 let valid_from = asn1_time_to_naive_date(cert.not_before())?;
211 let valid_until = asn1_time_to_naive_date(cert.not_after())?;
212
213 let serial_number = cert
214 .serial_number()
215 .to_bn()
216 .map_err(|e| FiscalError::Certificate(format!("Failed to read serial number: {e}")))?
217 .to_hex_str()
218 .map_err(|e| FiscalError::Certificate(format!("Failed to format serial number: {e}")))?
219 .to_string();
220
221 Ok(CertificateInfo::new(
222 common_name,
223 valid_from,
224 valid_until,
225 serial_number,
226 issuer,
227 ))
228}
229
230fn extract_cn_from_x509_name(name: &openssl::x509::X509NameRef) -> String {
232 for entry in name.entries_by_nid(openssl::nid::Nid::COMMONNAME) {
233 if let Ok(s) = entry.data().as_utf8() {
234 return s.to_string();
235 }
236 }
237 format!("{:?}", name)
239}
240
241fn asn1_time_to_naive_date(
243 time: &openssl::asn1::Asn1TimeRef,
244) -> Result<chrono::NaiveDate, FiscalError> {
245 let epoch = openssl::asn1::Asn1Time::from_unix(0)
246 .map_err(|e| FiscalError::Certificate(format!("ASN1 epoch creation failed: {e}")))?;
247 let diff = epoch
248 .diff(time)
249 .map_err(|e| FiscalError::Certificate(format!("ASN1 time diff failed: {e}")))?;
250
251 let days = diff.days as i64;
252 let secs = diff.secs as i64;
253 let total_secs = days * 86400 + secs;
254
255 let dt = chrono::DateTime::from_timestamp(total_secs, 0)
256 .ok_or_else(|| FiscalError::Certificate("Invalid timestamp from ASN1 time".into()))?;
257
258 Ok(dt.date_naive())
259}