Skip to main content

stackforge_core/layer/tls/
cert.rs

1//! TLS Certificate handling.
2//!
3//! Wraps raw DER-encoded X.509 certificates for basic inspection.
4//! This is intentionally minimal - just enough for TLS protocol operations.
5
6/// A TLS certificate wrapping raw DER-encoded X.509 data.
7#[derive(Debug, Clone)]
8pub struct TlsCertificate {
9    /// Raw DER-encoded certificate data.
10    pub der: Vec<u8>,
11}
12
13impl TlsCertificate {
14    /// Create from DER-encoded bytes.
15    #[must_use]
16    pub fn from_der(der: Vec<u8>) -> Self {
17        Self { der }
18    }
19
20    /// Create from PEM-encoded string.
21    #[must_use]
22    pub fn from_pem(pem: &str) -> Option<Self> {
23        let lines: Vec<&str> = pem
24            .lines()
25            .filter(|line| !line.starts_with("-----"))
26            .collect();
27        let b64 = lines.join("");
28        let der = base64_decode(&b64)?;
29        Some(Self { der })
30    }
31
32    /// Get the DER-encoded data.
33    #[must_use]
34    pub fn as_der(&self) -> &[u8] {
35        &self.der
36    }
37
38    /// Get the certificate length.
39    #[must_use]
40    pub fn len(&self) -> usize {
41        self.der.len()
42    }
43
44    /// Check if empty.
45    #[must_use]
46    pub fn is_empty(&self) -> bool {
47        self.der.is_empty()
48    }
49
50    /// Extract the subject common name (CN) from the certificate.
51    ///
52    /// This is a basic ASN.1 parser that looks for the CN OID (2.5.4.3).
53    /// In X.509, issuer comes before subject, so subject CN is the second occurrence.
54    /// For self-signed certs (only one CN), falls back to the first occurrence.
55    #[must_use]
56    pub fn subject_cn(&self) -> Option<String> {
57        let cn_oid = [0x55, 0x04, 0x03];
58        find_nth_string_after_oid(&self.der, &cn_oid, 1)
59            .or_else(|| find_nth_string_after_oid(&self.der, &cn_oid, 0))
60    }
61
62    /// Extract the issuer common name from the certificate.
63    #[must_use]
64    pub fn issuer_cn(&self) -> Option<String> {
65        let cn_oid = [0x55, 0x04, 0x03];
66        find_nth_string_after_oid(&self.der, &cn_oid, 0)
67    }
68}
69
70/// Parse a certificate chain from multiple PEM blocks.
71#[must_use]
72pub fn parse_pem_chain(pem: &str) -> Vec<TlsCertificate> {
73    let mut certs = Vec::new();
74    let mut in_cert = false;
75    let mut current = String::new();
76
77    for line in pem.lines() {
78        if line.contains("BEGIN CERTIFICATE") {
79            in_cert = true;
80            current.clear();
81            current.push_str(line);
82            current.push('\n');
83        } else if line.contains("END CERTIFICATE") {
84            current.push_str(line);
85            current.push('\n');
86            if let Some(cert) = TlsCertificate::from_pem(&current) {
87                certs.push(cert);
88            }
89            in_cert = false;
90        } else if in_cert {
91            current.push_str(line);
92            current.push('\n');
93        }
94    }
95
96    certs
97}
98
99/// Simple base64 decoder (no dependencies).
100fn base64_decode(input: &str) -> Option<Vec<u8>> {
101    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
102
103    let input: Vec<u8> = input
104        .bytes()
105        .filter(|&b| b != b'\n' && b != b'\r' && b != b' ')
106        .collect();
107    if input.is_empty() {
108        return Some(Vec::new());
109    }
110
111    let mut output = Vec::with_capacity(input.len() * 3 / 4);
112    let mut buf: u32 = 0;
113    let mut bits = 0;
114
115    for &byte in &input {
116        let val = if byte == b'=' {
117            continue;
118        } else if let Some(pos) = TABLE.iter().position(|&b| b == byte) {
119            pos as u32
120        } else {
121            return None;
122        };
123
124        buf = (buf << 6) | val;
125        bits += 6;
126
127        if bits >= 8 {
128            bits -= 8;
129            output.push((buf >> bits) as u8);
130            buf &= (1 << bits) - 1;
131        }
132    }
133
134    Some(output)
135}
136
137/// Find the nth UTF-8/PrintableString value after an OID in DER data.
138/// `n=0` returns the first occurrence (issuer CN in X.509),
139/// `n=1` returns the second (subject CN in X.509).
140fn find_nth_string_after_oid(data: &[u8], oid: &[u8], n: usize) -> Option<String> {
141    let mut count = 0;
142    for i in 0..data.len().saturating_sub(oid.len()) {
143        if data[i..].starts_with(oid) {
144            let pos = i + oid.len();
145            if pos + 2 > data.len() {
146                continue;
147            }
148            let tag = data[pos];
149            // UTF8String (0x0C), PrintableString (0x13), IA5String (0x16)
150            if tag == 0x0C || tag == 0x13 || tag == 0x16 {
151                let len = data[pos + 1] as usize;
152                if pos + 2 + len <= data.len() {
153                    if count == n {
154                        return String::from_utf8(data[pos + 2..pos + 2 + len].to_vec()).ok();
155                    }
156                    count += 1;
157                }
158            }
159        }
160    }
161    None
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_from_der() {
170        let cert = TlsCertificate::from_der(vec![0x30, 0x82, 0x01, 0x00]);
171        assert_eq!(cert.len(), 4);
172        assert!(!cert.is_empty());
173    }
174
175    #[test]
176    fn test_base64_decode() {
177        assert_eq!(base64_decode("SGVsbG8="), Some(b"Hello".to_vec()));
178        assert_eq!(base64_decode(""), Some(Vec::new()));
179        assert_eq!(base64_decode("YQ=="), Some(b"a".to_vec()));
180    }
181
182    #[test]
183    fn test_from_pem() {
184        let pem = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n";
185        let cert = TlsCertificate::from_pem(pem);
186        assert!(cert.is_some());
187    }
188
189    #[test]
190    fn test_find_cn() {
191        // Construct a minimal DER with CN OID followed by UTF8String "test"
192        let mut data = Vec::new();
193        data.extend_from_slice(&[0x30, 0x20]); // SEQUENCE
194        data.extend_from_slice(&[0x06, 0x03, 0x55, 0x04, 0x03]); // OID 2.5.4.3
195        data.extend_from_slice(&[0x0C, 0x04]); // UTF8String, length 4
196        data.extend_from_slice(b"test");
197
198        let result = find_nth_string_after_oid(&data, &[0x55, 0x04, 0x03], 0);
199        assert_eq!(result, Some("test".to_string()));
200    }
201}