Skip to main content

rust_web_server/core/
x509.rs

1//! X.509 certificate validity (notBefore/notAfter) and the RFC 9773 ARI
2//! certificate id, read with a minimal DER walker — shared by ACME renewal
3//! and the TLS certificate-expiry metric, so it isn't behind a feature.
4//! Certificate bytes may come from a CA or a file: every length is checked,
5//! nothing panics on malformed input.
6
7// ── base64url ─────────────────────────────────────────────────────────────────
8
9pub fn base64url(data: &[u8]) -> String {
10    const ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
11    let mut out = Vec::with_capacity((data.len() + 2) / 3 * 4);
12    for chunk in data.chunks(3) {
13        let b0 = chunk[0] as usize;
14        let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
15        let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
16        out.push(ALPHA[b0 >> 2]);
17        out.push(ALPHA[((b0 & 3) << 4) | (b1 >> 4)]);
18        if chunk.len() > 1 { out.push(ALPHA[((b1 & 0xf) << 2) | (b2 >> 6)]); }
19        if chunk.len() > 2 { out.push(ALPHA[b2 & 0x3f]); }
20    }
21    String::from_utf8(out).unwrap()
22}
23
24// ── Certificate expiry ────────────────────────────────────────────────────────
25
26/// What renewal needs from the leaf certificate: its validity window (Unix
27/// seconds) and its ACME Renewal Information certificate identifier
28/// (RFC 9773 §4.1), when it has an Authority Key Identifier.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct CertInfo {
31    pub not_before: i64,
32    pub not_after: i64,
33    /// `base64url(AKI keyIdentifier) "." base64url(serial)`; `None` without
34    /// an Authority Key Identifier extension.
35    pub cert_id: Option<String>,
36}
37
38/// [`CertInfo`] of the first certificate in the PEM file at `cert_path`;
39/// `None` when it can't be read or parsed.
40/// [`CertInfo`] of the first certificate in PEM text.
41pub fn cert_info_from_pem(pem: &[u8]) -> Option<CertInfo> {
42    cert_info_from_der(&pem_to_first_cert_der(std::str::from_utf8(pem).ok()?)?)
43}
44
45/// The DNS names in the subjectAltName of the first certificate in the PEM
46/// file at `cert_path`; `None` when it can't be read or parsed.
47pub fn read_cert_dns_names(cert_path: &str) -> Option<Vec<String>> {
48    let pem = std::fs::read_to_string(cert_path).ok()?;
49    let names = subject_alt_names(&pem_to_first_cert_der(&pem)?)?;
50    Some(names.into_iter().filter_map(|n| match n { SubjectAltName::Dns(d) => Some(d), SubjectAltName::Uri(_) => None }).collect())
51}
52
53pub fn read_cert_info(cert_path: &str) -> Option<CertInfo> {
54    let pem = std::fs::read_to_string(cert_path).ok()?;
55    cert_info_from_der(&pem_to_first_cert_der(&pem)?)
56}
57
58/// [`CertInfo`] from a DER certificate. CA-supplied bytes: every length is
59/// checked by `read_tlv`, nothing panics on malformed input.
60pub fn cert_info_from_der(cert_der: &[u8]) -> Option<CertInfo> {
61    let (_, cert_body, _) = read_tlv(cert_der, 0)?;
62    let (_, tbs, _) = read_tlv(cert_body, 0)?;
63    let mut pos = 0;
64    if tbs.get(pos) == Some(&0xa0) {
65        pos = read_tlv(tbs, pos)?.2;
66    }
67    let (serial_tag, serial, next) = read_tlv(tbs, pos)?;
68    if serial_tag != 0x02 || serial.is_empty() {
69        return None;
70    }
71    pos = skip_tlvs(tbs, next, 2)?; // signature, issuer
72    let (_, validity, next) = read_tlv(tbs, pos)?;
73    let (nb_tag, nb_val, nb_end) = read_tlv(validity, 0)?;
74    let (na_tag, na_val, _) = read_tlv(validity, nb_end)?;
75    let is_time = |tag: u8| tag == 0x17 || tag == 0x18;
76    if !is_time(nb_tag) || !is_time(na_tag) {
77        return None;
78    }
79    let not_before = parse_asn1_time(nb_val)?;
80    let not_after = parse_asn1_time(na_val)?;
81    pos = skip_tlvs(tbs, next, 2)?; // subject, subjectPublicKeyInfo
82
83    // Optional issuerUniqueID [1], subjectUniqueID [2], then extensions [3].
84    let mut aki: Option<&[u8]> = None;
85    while pos < tbs.len() {
86        let (tag, value, next) = read_tlv(tbs, pos)?;
87        if tag == 0xa3 {
88            aki = authority_key_identifier(value);
89        }
90        pos = next;
91    }
92    let cert_id = aki.map(|key_id| format!("{}.{}", base64url(key_id), base64url(serial)));
93    Some(CertInfo { not_before, not_after, cert_id })
94}
95
96/// The keyIdentifier of the Authority Key Identifier extension
97/// (OID 2.5.29.35) in an `[3] EXPLICIT Extensions` value.
98fn authority_key_identifier(explicit: &[u8]) -> Option<&[u8]> {
99    const AKI_OID: &[u8] = &[0x55, 0x1d, 0x23];
100    let (_, extensions, _) = read_tlv(explicit, 0)?;
101    let mut pos = 0;
102    while pos < extensions.len() {
103        let (_, extension, next) = read_tlv(extensions, pos)?;
104        pos = next;
105        let (oid_tag, oid, mut inner) = read_tlv(extension, 0)?;
106        if oid_tag != 0x06 || oid != AKI_OID {
107            continue;
108        }
109        let (mut tag, mut value, mut after) = read_tlv(extension, inner)?;
110        if tag == 0x01 {
111            // critical BOOLEAN
112            inner = after;
113            (tag, value, after) = read_tlv(extension, inner)?;
114        }
115        let _ = after;
116        if tag != 0x04 {
117            return None;
118        }
119        // OCTET STRING → AuthorityKeyIdentifier SEQUENCE → [0] IMPLICIT keyIdentifier
120        let (_, aki, _) = read_tlv(value, 0)?;
121        let (key_tag, key_id, _) = read_tlv(aki, 0)?;
122        return (key_tag == 0x80 && !key_id.is_empty()).then_some(key_id);
123    }
124    None
125}
126
127/// A subjectAltName entry rws can match.
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum SubjectAltName {
130    Dns(String),
131    Uri(String),
132}
133
134/// The DNS and URI names of a DER certificate's subjectAltName extension
135/// (OID 2.5.29.17); `None` when the certificate can't be read. Backend
136/// certificates are untrusted input: loops only, every read bounded.
137pub fn subject_alt_names(cert_der: &[u8]) -> Option<Vec<SubjectAltName>> {
138    const SAN_OID: &[u8] = &[0x55, 0x1d, 0x11];
139    let (_, cert_body, _) = read_tlv(cert_der, 0)?;
140    let (_, tbs, _) = read_tlv(cert_body, 0)?;
141    let mut pos = 0;
142    let mut names = Vec::new();
143    while pos < tbs.len() {
144        let (tag, value, next) = read_tlv(tbs, pos)?;
145        pos = next;
146        if tag != 0xa3 {
147            continue;
148        }
149        let (_, extensions, _) = read_tlv(value, 0)?;
150        let mut at = 0;
151        while at < extensions.len() {
152            let (_, extension, next) = read_tlv(extensions, at)?;
153            at = next;
154            let (oid_tag, oid, mut inner) = read_tlv(extension, 0)?;
155            if oid_tag != 0x06 || oid != SAN_OID {
156                continue;
157            }
158            let (mut tag, mut value, after) = read_tlv(extension, inner)?;
159            if tag == 0x01 {
160                inner = after; // critical BOOLEAN
161                (tag, value, _) = read_tlv(extension, inner)?;
162            }
163            if tag != 0x04 {
164                return None;
165            }
166            // OCTET STRING → GeneralNames SEQUENCE → [2] dNSName / [6] URI (IMPLICIT IA5String)
167            let (_, general_names, _) = read_tlv(value, 0)?;
168            let mut n = 0;
169            while n < general_names.len() {
170                let (name_tag, name, next) = read_tlv(general_names, n)?;
171                n = next;
172                let text = || std::str::from_utf8(name).ok().map(str::to_string);
173                match name_tag {
174                    0x82 => names.extend(text().map(SubjectAltName::Dns)),
175                    0x86 => names.extend(text().map(SubjectAltName::Uri)),
176                    _ => {}
177                }
178            }
179        }
180    }
181    Some(names)
182}
183
184fn pem_to_first_cert_der(pem: &str) -> Option<Vec<u8>> {
185    let start = pem.find("-----BEGIN CERTIFICATE-----")?;
186    let after = &pem[start + 27..];
187    let end = after.find("-----END CERTIFICATE-----")?;
188    let b64: String = after[..end].chars().filter(|c| !c.is_whitespace()).collect();
189    base64_decode_std(&b64)
190}
191
192// ── minimal DER TLV parser ────────────────────────────────────────────────────
193
194fn read_tlv(data: &[u8], pos: usize) -> Option<(u8, &[u8], usize)> {
195    if pos >= data.len() { return None; }
196    let tag = data[pos];
197    let mut p = pos + 1;
198    let len = {
199        let b = *data.get(p)?; p += 1;
200        if b < 0x80 { b as usize } else {
201            // At most 4 length bytes: more would overflow the shift (a
202            // panic in debug builds) and no certificate is that large.
203            let n = (b & 0x7f) as usize;
204            if n == 0 || n > 4 { return None; }
205            let mut l = 0usize;
206            for _ in 0..n { l = (l << 8) | *data.get(p)? as usize; p += 1; }
207            l
208        }
209    };
210    let end = p.checked_add(len)?;
211    if end > data.len() { return None; }
212    Some((tag, &data[p..end], end))
213}
214
215fn skip_tlvs(data: &[u8], mut pos: usize, count: usize) -> Option<usize> {
216    for _ in 0..count {
217        let (_, _, next) = read_tlv(data, pos)?;
218        pos = next;
219    }
220    Some(pos)
221}
222
223fn parse_asn1_time(t: &[u8]) -> Option<i64> {
224    // Digits then `Z`: anything else (and any non-ASCII byte, which the
225    // slicing below would split) is not a time rws can read.
226    if t.len() < 2 || !t[..t.len() - 1].iter().all(u8::is_ascii_digit) {
227        return None;
228    }
229    let s = std::str::from_utf8(t).ok()?;
230    if !s.ends_with('Z') { return None; }
231    let s = &s[..s.len() - 1]; // strip 'Z'
232    let (year, rest) = if s.len() == 12 {
233        let yy: i64 = s[..2].parse().ok()?;
234        let year = if yy < 50 { 2000 + yy } else { 1900 + yy };
235        (year, &s[2..])
236    } else if s.len() == 14 {
237        (s[..4].parse().ok()?, &s[4..])
238    } else {
239        return None;
240    };
241    let mo: i64 = rest[0..2].parse().ok()?;
242    let da: i64 = rest[2..4].parse().ok()?;
243    let hr: i64 = rest[4..6].parse().ok()?;
244    let mi: i64 = rest[6..8].parse().ok()?;
245    let se: i64 = rest[8..10].parse().ok()?;
246    let days = days_since_epoch(year, mo, da)?;
247    Some(days * 86400 + hr * 3600 + mi * 60 + se)
248}
249
250pub fn days_since_epoch(year: i64, month: i64, day: i64) -> Option<i64> {
251    // https://howardhinnant.github.io/date_algorithms.html "days_from_civil"
252    let y = if month <= 2 { year - 1 } else { year };
253    let m = if month <= 2 { month + 9 } else { month - 3 };
254    let era = y.div_euclid(400);
255    let yoe = y - era * 400;
256    let doy = (153 * m + 2) / 5 + (day - 1);
257    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
258    Some(era * 146097 + doe - 719468)
259}
260
261// ── standard base64 decoder (no padding required) ────────────────────────────
262
263pub fn base64_decode_std(s: &str) -> Option<Vec<u8>> {
264    let digits: Vec<u8> = s.bytes().filter_map(|b| match b {
265        b'A'..=b'Z' => Some(b - b'A'),
266        b'a'..=b'z' => Some(b - b'a' + 26),
267        b'0'..=b'9' => Some(b - b'0' + 52),
268        b'+' | b'-' => Some(62),
269        b'/' | b'_' => Some(63),
270        b'=' => None,
271        _ => None,
272    }).collect();
273    let mut out = Vec::with_capacity(digits.len() * 3 / 4);
274    for chunk in digits.chunks(4) {
275        let v = chunk.iter().enumerate()
276            .fold(0u32, |acc, (i, &d)| acc | ((d as u32) << (18 - i * 6)));
277        if chunk.len() >= 2 { out.push((v >> 16) as u8); }
278        if chunk.len() >= 3 { out.push((v >> 8) as u8); }
279        if chunk.len() >= 4 { out.push(v as u8); }
280    }
281    Some(out)
282}
283
284#[cfg(test)]
285mod subject_alt_name_tests {
286    use super::*;
287
288    #[test]
289    fn dns_and_uri_names_are_read_others_skipped() {
290        let mut params = rcgen::CertificateParams::new(vec!["abc.example.com".to_string()]).unwrap();
291        params.subject_alt_names.push(rcgen::SanType::URI("spiffe://cluster.local/ns/x/sa/y".try_into().unwrap()));
292        params.subject_alt_names.push(rcgen::SanType::IpAddress("10.0.0.1".parse().unwrap()));
293        let cert = params.self_signed(&rcgen::KeyPair::generate().unwrap()).unwrap();
294        let der: &[u8] = cert.der();
295        assert_eq!(
296            Some(vec![SubjectAltName::Dns("abc.example.com".to_string()), SubjectAltName::Uri("spiffe://cluster.local/ns/x/sa/y".to_string())]),
297            subject_alt_names(der),
298            "the IP name is skipped"
299        );
300        for cut in [0, 1, 10, der.len() / 2, der.len() - 1] {
301            assert!(subject_alt_names(&der[..cut]).is_none_or(|n| n.len() <= 2), "a truncated certificate never panics");
302        }
303        assert_eq!(None, subject_alt_names(b"not DER"));
304    }
305}