Skip to main content

confium_pki/
cert.rs

1//! X.509 v3 certificate wrapper types.
2//!
3//! Wraps `x509_cert::Certificate` to provide idiomatic Rust access plus
4//! Confium-specific helpers (DER/PEM, fingerprint).
5
6use crate::result::{PathFailure, VerificationResult};
7use chrono::{DateTime, Utc};
8use data_encoding::HEXLOWER;
9use der::Decode;
10use sha2::{Digest, Sha256};
11
12/// A parsed X.509 v3 certificate.
13#[derive(Debug, Clone)]
14pub struct Certificate {
15    inner: x509_cert::Certificate,
16    raw_der: Vec<u8>,
17}
18
19/// PKCS#10 certificate signing request (DER wrapper).
20#[derive(Debug, Clone)]
21pub struct CertificateSigningRequest {
22    raw_der: Vec<u8>,
23}
24
25/// Errors encountered parsing or serializing certificates.
26#[derive(Debug, thiserror::Error)]
27pub enum CertError {
28    /// DER decoding error.
29    #[error("DER decode error: {0}")]
30    Der(#[from] der::Error),
31
32    /// PEM parsing error.
33    #[error("PEM parse error: {0}")]
34    Pem(String),
35
36    /// Invalid structure.
37    #[error("invalid certificate structure: {0}")]
38    Invalid(String),
39}
40
41impl CertError {
42    /// Byte offset into the input where a DER decode failed, when the
43    /// underlying decoder reports one. Bindings surface this as a
44    /// structured parse-error field instead of a string-only message.
45    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    /// Parse a certificate from DER bytes.
55    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    /// Parse a certificate from PEM (RFC 7468) text.
64    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    /// Serialize this certificate to DER bytes.
70    pub fn to_der(&self) -> Vec<u8> {
71        self.raw_der.clone()
72    }
73
74    /// Serialize this certificate to PEM (RFC 7468).
75    pub fn to_pem(&self) -> String {
76        der_to_pem(&self.raw_der, "CERTIFICATE")
77    }
78
79    /// Compute the SHA-256 fingerprint of this certificate.
80    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    /// The serial number as a byte slice.
87    pub fn serial_bytes(&self) -> &[u8] {
88        self.inner.tbs_certificate().serial_number().as_bytes()
89    }
90
91    /// Not-before validity bound (raw `der::DateTime`).
92    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    /// Not-after validity bound (raw `der::DateTime`).
101    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    /// Not-before as a chrono `DateTime<Utc>`.
110    pub fn not_before_chrono(&self) -> DateTime<Utc> {
111        chrono::DateTime::from(self.not_before().to_system_time())
112    }
113
114    /// Not-after as a chrono `DateTime<Utc>`.
115    pub fn not_after_chrono(&self) -> DateTime<Utc> {
116        chrono::DateTime::from(self.not_after().to_system_time())
117    }
118
119    /// Whether the certificate is within its validity window at the given instant.
120    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    /// Raw subject public key bytes from the SPKI, if available.
127    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    /// Reference to the underlying `x509_cert` type.
137    pub fn as_inner(&self) -> &x509_cert::Certificate {
138        &self.inner
139    }
140}
141
142impl CertificateSigningRequest {
143    /// Parse a CSR from DER bytes. Performs only a basic structural check
144    /// (top-level SEQUENCE); full field parsing is the application's job.
145    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        // Tag 0x30 = SEQUENCE, the expected first byte of any CSR.
150        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    /// Parse a CSR from PEM text.
162    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    /// Serialize to DER bytes.
168    pub fn to_der(&self) -> Vec<u8> {
169        self.raw_der.clone()
170    }
171
172    /// Serialize to PEM text.
173    pub fn to_pem(&self) -> String {
174        der_to_pem(&self.raw_der, "CERTIFICATE REQUEST")
175    }
176}
177
178/// Quick helper for path validation — checks time validity of the leaf only.
179pub 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"; // base64("abc")
281        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}