1use crate::result::{PathFailure, VerificationResult};
7use chrono::{DateTime, Utc};
8use data_encoding::HEXLOWER;
9use der::Decode;
10use sha2::{Digest, Sha256};
11
12#[derive(Debug, Clone)]
14pub struct Certificate {
15 inner: x509_cert::Certificate,
16 raw_der: Vec<u8>,
17}
18
19#[derive(Debug, Clone)]
21pub struct CertificateSigningRequest {
22 raw_der: Vec<u8>,
23}
24
25#[derive(Debug, thiserror::Error)]
27pub enum CertError {
28 #[error("DER decode error: {0}")]
30 Der(#[from] der::Error),
31
32 #[error("PEM parse error: {0}")]
34 Pem(String),
35
36 #[error("invalid certificate structure: {0}")]
38 Invalid(String),
39}
40
41impl CertError {
42 pub fn der_offset(&self) -> Option<usize> {
46 match self {
47 Self::Der(e) => e.position().map(|p| u32::from(p) as usize),
48 _ => None,
49 }
50 }
51}
52
53impl Certificate {
54 pub fn from_der(der_bytes: &[u8]) -> Result<Self, CertError> {
56 let inner = x509_cert::Certificate::from_der(der_bytes)?;
57 Ok(Self {
58 inner,
59 raw_der: der_bytes.to_vec(),
60 })
61 }
62
63 pub fn from_pem(pem: &str) -> Result<Self, CertError> {
65 let der = pem_to_der(pem, "CERTIFICATE")?;
66 Self::from_der(&der)
67 }
68
69 pub fn to_der(&self) -> Vec<u8> {
71 self.raw_der.clone()
72 }
73
74 pub fn to_pem(&self) -> String {
76 der_to_pem(&self.raw_der, "CERTIFICATE")
77 }
78
79 pub fn fingerprint_sha256(&self) -> String {
81 let mut hasher = Sha256::new();
82 hasher.update(&self.raw_der);
83 HEXLOWER.encode(&hasher.finalize())
84 }
85
86 pub fn serial_bytes(&self) -> &[u8] {
88 self.inner.tbs_certificate().serial_number().as_bytes()
89 }
90
91 pub fn not_before(&self) -> der::DateTime {
93 self.inner
94 .tbs_certificate()
95 .validity()
96 .not_before
97 .to_date_time()
98 }
99
100 pub fn not_after(&self) -> der::DateTime {
102 self.inner
103 .tbs_certificate()
104 .validity()
105 .not_after
106 .to_date_time()
107 }
108
109 pub fn not_before_chrono(&self) -> DateTime<Utc> {
111 chrono::DateTime::from(self.not_before().to_system_time())
112 }
113
114 pub fn not_after_chrono(&self) -> DateTime<Utc> {
116 chrono::DateTime::from(self.not_after().to_system_time())
117 }
118
119 pub fn is_within_validity(&self, now: DateTime<Utc>) -> bool {
121 let nb = self.not_before_chrono();
122 let na = self.not_after_chrono();
123 now >= nb && now <= na
124 }
125
126 pub fn public_key_bytes(&self) -> &[u8] {
128 self.inner
129 .tbs_certificate()
130 .subject_public_key_info()
131 .subject_public_key
132 .as_bytes()
133 .unwrap_or(&[])
134 }
135
136 pub fn as_inner(&self) -> &x509_cert::Certificate {
138 &self.inner
139 }
140}
141
142impl CertificateSigningRequest {
143 pub fn from_der(der_bytes: &[u8]) -> Result<Self, CertError> {
146 if der_bytes.is_empty() {
147 return Err(CertError::Invalid("CSR bytes empty".into()));
148 }
149 if der_bytes[0] != 0x30 {
151 return Err(CertError::Invalid(format!(
152 "CSR expected to start with SEQUENCE tag 0x30, got {:#x}",
153 der_bytes[0]
154 )));
155 }
156 Ok(Self {
157 raw_der: der_bytes.to_vec(),
158 })
159 }
160
161 pub fn from_pem(pem: &str) -> Result<Self, CertError> {
163 let der = pem_to_der(pem, "CERTIFICATE REQUEST")?;
164 Self::from_der(&der)
165 }
166
167 pub fn to_der(&self) -> Vec<u8> {
169 self.raw_der.clone()
170 }
171
172 pub fn to_pem(&self) -> String {
174 der_to_pem(&self.raw_der, "CERTIFICATE REQUEST")
175 }
176}
177
178pub fn quick_check_leaf_validity(cert: &Certificate, now: DateTime<Utc>) -> VerificationResult {
180 let mut checks = Vec::new();
181 let mut valid = true;
182
183 let nb = cert.not_before_chrono();
184 let na = cert.not_after_chrono();
185
186 if now < nb {
187 checks.push(PathFailure::NotYetValid);
188 valid = false;
189 }
190 if now > na {
191 checks.push(PathFailure::Expired);
192 valid = false;
193 }
194
195 VerificationResult { valid, checks }
196}
197
198fn pem_to_der(pem: &str, expected_label: &str) -> Result<Vec<u8>, CertError> {
199 let trimmed = pem.trim();
200 let header = format!("-----BEGIN {expected_label}-----");
201 let footer = format!("-----END {expected_label}-----");
202
203 let start = trimmed
204 .find(&header)
205 .ok_or_else(|| CertError::Pem(format!("missing {header}")))?
206 + header.len();
207 let end = trimmed
208 .find(&footer)
209 .ok_or_else(|| CertError::Pem(format!("missing {footer}")))?;
210
211 let body: String = trimmed[start..end]
212 .chars()
213 .filter(|c| !c.is_whitespace())
214 .collect();
215 data_encoding::BASE64
216 .decode(body.as_bytes())
217 .map_err(|e| CertError::Pem(format!("base64 decode failed: {e}")))
218}
219
220fn der_to_pem(der: &[u8], label: &str) -> String {
221 let encoded = data_encoding::BASE64.encode(der);
222 let mut out = String::new();
223 out.push_str("-----BEGIN ");
224 out.push_str(label);
225 out.push_str("-----\n");
226 for chunk in encoded.as_bytes().chunks(64) {
227 out.push_str(std::str::from_utf8(chunk).unwrap());
228 out.push('\n');
229 }
230 out.push_str("-----END ");
231 out.push_str(label);
232 out.push_str("-----\n");
233 out
234}
235
236#[cfg(test)]
237mod tests {
238 use super::CertError;
239
240 #[test]
241 fn der_offset_reports_the_truncation_point() {
242 let der = rcgen::generate_simple_self_signed(vec!["offset-test.confium".into()])
243 .expect("keygen")
244 .cert
245 .der()
246 .to_vec();
247
248 let truncated = &der[..20];
249 let err = Certificate::from_der(truncated).expect_err("truncated DER must fail");
250 assert!(err.der_offset().is_some(), "offset missing for: {err}");
251 }
252
253 #[test]
254 fn der_offset_is_none_for_non_der_errors() {
255 let err = Certificate::from_pem("not a pem block").expect_err("garbage PEM must fail");
256 assert!(matches!(&err, CertError::Pem(_)), "unexpected: {err}");
257 assert_eq!(err.der_offset(), None);
258 }
259
260 use super::*;
261
262 #[test]
263 fn pem_der_round_trip_synthetic() {
264 let der = vec![1u8, 2, 3, 4, 5];
265 let pem = der_to_pem(&der, "TEST");
266 assert!(pem.contains("-----BEGIN TEST-----"));
267 assert!(pem.contains("-----END TEST-----"));
268 let recovered = pem_to_der(&pem, "TEST").expect("parse");
269 assert_eq!(recovered, der);
270 }
271
272 #[test]
273 fn pem_to_der_rejects_missing_header() {
274 let result = pem_to_der("no headers here", "CERTIFICATE");
275 assert!(result.is_err());
276 }
277
278 #[test]
279 fn pem_to_der_rejects_missing_footer() {
280 let pem = "-----BEGIN CERTIFICATE-----\nYWJj\n"; let result = pem_to_der(pem, "CERTIFICATE");
282 assert!(result.is_err());
283 }
284
285 #[test]
286 fn csr_rejects_non_sequence_first_byte() {
287 let result = CertificateSigningRequest::from_der(&[0x01, 0x00]);
288 assert!(result.is_err());
289 }
290
291 #[test]
292 fn csr_accepts_sequence_first_byte() {
293 let csr = CertificateSigningRequest::from_der(&[0x30, 0x00]);
294 assert!(csr.is_ok());
295 }
296}