Skip to main content

ntex_tls/x509/
cert.rs

1//! Parsed X.509 certificate.
2//!
3//! Certificates are parsed with `x509-parser` (pure Rust DER parser, no crypto).
4//! The DER encoding is kept as [`Bytes`] (cheap clone, zero-copy from ntex-tls) and is
5//! the source for trust verification, thumbprints and PEM encoding.
6//!
7//! Field rules (parity with the previous OpenSSL based implementation):
8//! - subject CN: first `commonName` attribute, decoded like OpenSSL `ASN1_STRING_to_UTF8`
9//!   (UTF8/Printable/IA5/Numeric/Visible strings as UTF-8, T61 as Latin-1, BMP as UCS-2,
10//!   Universal as UCS-4). Values that cannot be decoded are treated as missing.
11//! - SAN DNS / URI / email: values are kept as encoded; entries with invalid encoding
12//!   are skipped. A certificate with a malformed or duplicated SAN extension has no SANs.
13//! - SAN IP: raw octets are kept; [`SanIp::addr`] returns `None` unless 4 or 16 bytes.
14//! - case folding is done by callers (see [`crate::x509::auth`]), values are returned as encoded.
15//! - thumbprints are computed lazily over the DER encoding and rendered as lowercase hex.
16use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
17use std::sync::{Arc, OnceLock};
18use std::time::{Duration, SystemTime};
19use std::{fmt, ops::Range};
20
21use base64::Engine;
22use ntex_bytes::Bytes;
23use x509_parser::asn1_rs::{Any, Tag};
24use x509_parser::extensions::GeneralName;
25use x509_parser::prelude::{FromDer, X509Certificate};
26use x509_parser::time::ASN1Time;
27
28use crate::x509::{BASE64, Error, digest};
29
30const PEM_BEGIN: &str = "-----BEGIN ";
31const PEM_END: &str = "-----END ";
32const PEM_LABELS: [&str; 2] = ["CERTIFICATE", "X509 CERTIFICATE"];
33
34/// Parsed X.509 certificate. Cheap to clone (`Arc`), `Send + Sync`.
35#[derive(Clone)]
36pub struct Certificate(Arc<Inner>);
37
38struct Inner {
39    der: Bytes,
40    spki: Range<usize>,
41    subject_raw: Range<usize>,
42    issuer_raw: Range<usize>,
43    key_type: Option<KeyType>,
44    version: u32,
45    serial: String,
46    subject: String,
47    issuer: String,
48    subject_cn: Option<String>,
49    san: SubjectAltNames,
50    not_before: SystemTime,
51    not_after: SystemTime,
52    sha1: OnceLock<[u8; digest::SHA1_LEN]>,
53    sha256: OnceLock<[u8; digest::SHA256_LEN]>,
54}
55
56/// Subject alternative names of a certificate.
57#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct SubjectAltNames {
59    /// `dNSName` entries.
60    pub dns: Vec<String>,
61    /// `uniformResourceIdentifier` entries.
62    pub uri: Vec<String>,
63    /// `rfc822Name` entries.
64    pub email: Vec<String>,
65    /// `iPAddress` entries.
66    pub ip: Vec<SanIp>,
67}
68
69/// Public key type of a certificate, see [`Certificate::key_type`].
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum KeyType {
72    /// `rsaEncryption`.
73    Rsa,
74    /// `id-ecPublicKey` on `prime256v1`.
75    EcP256,
76    /// `id-ecPublicKey` on `secp384r1`.
77    EcP384,
78    /// `id-ecPublicKey` on `secp521r1`.
79    EcP521,
80}
81
82/// Raw `iPAddress` SAN entry.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SanIp(pub Vec<u8>);
85
86impl SanIp {
87    /// Address for 4 (IPv4) or 16 (IPv6) octets, `None` for malformed entries.
88    #[must_use]
89    pub fn addr(&self) -> Option<IpAddr> {
90        if let Ok(b) = <[u8; 4]>::try_from(self.0.as_slice()) {
91            Some(IpAddr::V4(Ipv4Addr::from(b)))
92        } else if let Ok(b) = <[u8; 16]>::try_from(self.0.as_slice()) {
93            Some(IpAddr::V6(Ipv6Addr::from(b)))
94        } else {
95            None
96        }
97    }
98}
99
100impl Certificate {
101    /// Parses a DER encoded certificate, taking ownership of the buffer (no copy).
102    ///
103    /// Trailing data after the certificate is ignored (and dropped from [`Self::der`]).
104    ///
105    /// # Errors
106    /// [`Error::InvalidDer`] if the data is not a valid X.509 certificate, its version is
107    /// unsupported, or its validity dates cannot be represented by [`SystemTime`].
108    pub fn from_der(der: impl Into<Bytes>) -> Result<Self, Error> {
109        let mut der = der.into();
110        let (rest, cert) =
111            X509Certificate::from_der(&der).map_err(|e| Error::InvalidDer(e.to_string()))?;
112        let len = der.len() - rest.len();
113        let version = cert.version().0;
114        if version > 2 {
115            return Err(Error::InvalidDer(format!(
116                "unsupported X.509 version: {version}"
117            )));
118        }
119
120        let spki = cert.tbs_certificate.subject_pki.raw;
121        let spki_start = spki.as_ptr() as usize - der.as_ptr() as usize;
122        let spki = spki_start..spki_start + spki.len();
123        let range = |raw: &[u8]| {
124            let start = raw.as_ptr() as usize - der.as_ptr() as usize;
125            start..start + raw.len()
126        };
127        let subject_raw = range(cert.subject().as_raw());
128        let issuer_raw = range(cert.issuer().as_raw());
129        let key_type = key_type(cert.public_key());
130
131        let subject_cn = cert
132            .subject()
133            .iter_common_name()
134            .next()
135            .and_then(|cn| decode_string(cn.attr_value()));
136
137        let mut san = SubjectAltNames::default();
138        if let Ok(Some(ext)) = cert.subject_alternative_name() {
139            for name in &ext.value.general_names {
140                match name {
141                    GeneralName::DNSName(v) => san.dns.push((*v).to_string()),
142                    GeneralName::URI(v) => san.uri.push((*v).to_string()),
143                    GeneralName::RFC822Name(v) => san.email.push((*v).to_string()),
144                    GeneralName::IPAddress(v) => san.ip.push(SanIp(v.to_vec())),
145                    _ => {}
146                }
147            }
148        }
149
150        let inner = Inner {
151            spki,
152            subject_raw,
153            issuer_raw,
154            key_type,
155            version: version + 1,
156            serial: serial_hex(cert.raw_serial()),
157            subject: cert.subject().to_string(),
158            issuer: cert.issuer().to_string(),
159            subject_cn,
160            san,
161            not_before: to_system_time(cert.validity().not_before)?,
162            not_after: to_system_time(cert.validity().not_after)?,
163            sha1: OnceLock::new(),
164            sha256: OnceLock::new(),
165            der: Bytes::new(),
166        };
167        drop(cert);
168        der.truncate(len);
169        Ok(Certificate(Arc::new(Inner { der, ..inner })))
170    }
171
172    /// Parses the first `CERTIFICATE` (or `X509 CERTIFICATE`) block of a PEM document.
173    ///
174    /// # Errors
175    /// [`Error::InvalidPem`] if no certificate block is found or base64 is invalid,
176    /// [`Error::InvalidDer`] if the decoded block is not a valid certificate.
177    pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
178        let der = pem_blocks(pem)
179            .next()
180            .ok_or_else(|| Error::InvalidPem("no CERTIFICATE block found".into()))??;
181        Self::from_der(der)
182    }
183
184    /// Parses all `CERTIFICATE` blocks of a PEM document, in order.
185    ///
186    /// # Errors
187    /// See [`Self::from_pem`]. An empty document returns an empty list.
188    pub fn from_pem_multi(pem: &[u8]) -> Result<Vec<Self>, Error> {
189        pem_blocks(pem)
190            .map(|der| der.and_then(Self::from_der))
191            .collect()
192    }
193
194    /// DER encoding.
195    #[must_use]
196    pub fn der(&self) -> &Bytes {
197        &self.0.der
198    }
199
200    /// DER encoded `SubjectPublicKeyInfo`.
201    #[must_use]
202    pub fn public_key_der(&self) -> &[u8] {
203        &self.0.der[self.0.spki.clone()]
204    }
205
206    /// X.509 version (1, 2 or 3).
207    #[must_use]
208    pub fn version(&self) -> u32 {
209        self.0.version
210    }
211
212    /// Serial number, uppercase hex without leading zero bytes (OpenSSL `BN_bn2hex` style).
213    #[must_use]
214    pub fn serial_hex(&self) -> &str {
215        &self.0.serial
216    }
217
218    /// Subject distinguished name (RFC 4514 style), for logging.
219    #[must_use]
220    pub fn subject(&self) -> &str {
221        &self.0.subject
222    }
223
224    /// Issuer distinguished name (RFC 4514 style), for logging.
225    #[must_use]
226    pub fn issuer(&self) -> &str {
227        &self.0.issuer
228    }
229
230    /// DER encoded subject name.
231    #[must_use]
232    pub fn subject_der(&self) -> &[u8] {
233        &self.0.der[self.0.subject_raw.clone()]
234    }
235
236    /// DER encoded issuer name.
237    #[must_use]
238    pub fn issuer_der(&self) -> &[u8] {
239        &self.0.der[self.0.issuer_raw.clone()]
240    }
241
242    /// Whether subject and issuer names are equal (byte comparison of the DER names); the
243    /// signature is not checked.
244    #[must_use]
245    pub fn is_self_issued(&self) -> bool {
246        self.subject_der() == self.issuer_der()
247    }
248
249    /// Public key type, `None` for unsupported key types or curves.
250    #[must_use]
251    pub fn key_type(&self) -> Option<KeyType> {
252        self.0.key_type
253    }
254
255    /// First subject `commonName`, as encoded (no case folding).
256    #[must_use]
257    pub fn subject_cn(&self) -> Option<&str> {
258        self.0.subject_cn.as_deref()
259    }
260
261    /// Subject alternative names.
262    #[must_use]
263    pub fn san(&self) -> &SubjectAltNames {
264        &self.0.san
265    }
266
267    /// Start of the validity period.
268    #[must_use]
269    pub fn not_before(&self) -> SystemTime {
270        self.0.not_before
271    }
272
273    /// End of the validity period.
274    #[must_use]
275    pub fn not_after(&self) -> SystemTime {
276        self.0.not_after
277    }
278
279    /// SHA-1 digest of the DER encoding (computed once).
280    #[must_use]
281    pub fn sha1(&self) -> &[u8; digest::SHA1_LEN] {
282        self.0.sha1.get_or_init(|| digest::sha1(&self.0.der))
283    }
284
285    /// SHA-256 digest of the DER encoding (computed once).
286    #[must_use]
287    pub fn sha256(&self) -> &[u8; digest::SHA256_LEN] {
288        self.0.sha256.get_or_init(|| digest::sha256(&self.0.der))
289    }
290
291    /// Digest of the DER encoding (computed once per algorithm).
292    #[must_use]
293    pub fn digest(&self, alg: digest::Algorithm) -> &[u8] {
294        match alg {
295            digest::Algorithm::Sha1 => self.sha1(),
296            digest::Algorithm::Sha256 => self.sha256(),
297        }
298    }
299
300    /// Thumbprint as lowercase hex.
301    #[must_use]
302    pub fn thumbprint(&self, alg: digest::Algorithm) -> String {
303        hex::encode(self.digest(alg))
304    }
305
306    /// PEM encoding (`CERTIFICATE` label, 64 column lines, trailing newline).
307    #[must_use]
308    pub fn to_pem(&self) -> String {
309        pem_encode("CERTIFICATE", &self.0.der)
310    }
311}
312
313/// PEM encodes `der` with `label` (64 column lines, trailing newline).
314fn key_type(spki: &x509_parser::x509::SubjectPublicKeyInfo<'_>) -> Option<KeyType> {
315    use x509_parser::oid_registry::{
316        OID_EC_P256, OID_KEY_TYPE_EC_PUBLIC_KEY, OID_NIST_EC_P384, OID_NIST_EC_P521,
317        OID_PKCS1_RSAENCRYPTION,
318    };
319
320    let alg = &spki.algorithm;
321    if alg.algorithm == OID_PKCS1_RSAENCRYPTION {
322        Some(KeyType::Rsa)
323    } else if alg.algorithm == OID_KEY_TYPE_EC_PUBLIC_KEY {
324        let curve = alg.parameters.as_ref()?.as_oid().ok()?;
325        if curve == OID_EC_P256 {
326            Some(KeyType::EcP256)
327        } else if curve == OID_NIST_EC_P384 {
328            Some(KeyType::EcP384)
329        } else if curve == OID_NIST_EC_P521 {
330            Some(KeyType::EcP521)
331        } else {
332            None
333        }
334    } else {
335        None
336    }
337}
338
339/// Encodes DER as PEM with the given `label` (64 column base64 lines, trailing newline).
340#[must_use]
341pub fn pem_encode(label: &str, der: &[u8]) -> String {
342    let b64 = BASE64.encode(der);
343    let mut pem = String::with_capacity(b64.len() + b64.len() / 64 + 2 * label.len() + 32);
344    pem.push_str("-----BEGIN ");
345    pem.push_str(label);
346    pem.push_str("-----\n");
347    for line in b64.as_bytes().chunks(64) {
348        // base64 output is ASCII
349        pem.push_str(std::str::from_utf8(line).unwrap_or_default());
350        pem.push('\n');
351    }
352    pem.push_str("-----END ");
353    pem.push_str(label);
354    pem.push_str("-----\n");
355    pem
356}
357
358impl PartialEq for Certificate {
359    fn eq(&self, other: &Self) -> bool {
360        self.0.der == other.0.der
361    }
362}
363
364impl Eq for Certificate {}
365
366impl fmt::Debug for Certificate {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        let mut d = f.debug_struct("X509");
369        d.field("serial_number", &self.0.serial)
370            .field("issuer", &self.0.issuer)
371            .field("subject", &self.0.subject);
372        let san = &self.0.san;
373        if !(san.dns.is_empty() && san.uri.is_empty() && san.email.is_empty() && san.ip.is_empty())
374        {
375            let ips: Vec<_> = san
376                .ip
377                .iter()
378                .map(|ip| {
379                    ip.addr()
380                        .map_or_else(|| hex::encode(&ip.0), |a| a.to_string())
381                })
382                .collect();
383            d.field("san_dns", &san.dns)
384                .field("san_uri", &san.uri)
385                .field("san_email", &san.email)
386                .field("san_ip", &ips);
387        }
388        d.field("not_before", &DisplayTime(self.0.not_before))
389            .field("not_after", &DisplayTime(self.0.not_after))
390            .finish()
391    }
392}
393
394struct DisplayTime(SystemTime);
395
396impl fmt::Debug for DisplayTime {
397    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398        let ts = match self.0.duration_since(SystemTime::UNIX_EPOCH) {
399            Ok(d) => i64::try_from(d.as_secs()).unwrap_or(i64::MAX),
400            Err(e) => -i64::try_from(e.duration().as_secs()).unwrap_or(i64::MAX),
401        };
402        match ASN1Time::from_timestamp(ts) {
403            Ok(t) => write!(f, "{t}"),
404            Err(_) => write!(f, "{ts}"),
405        }
406    }
407}
408
409fn to_system_time(t: ASN1Time) -> Result<SystemTime, Error> {
410    let ts = t.timestamp();
411    if ts >= 0 {
412        SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(ts.unsigned_abs()))
413    } else {
414        SystemTime::UNIX_EPOCH.checked_sub(Duration::from_secs(ts.unsigned_abs()))
415    }
416    .ok_or_else(|| Error::InvalidDer(format!("certificate time is out of range: {t}")))
417}
418
419fn serial_hex(raw: &[u8]) -> String {
420    let start = raw.iter().position(|b| *b != 0).unwrap_or(raw.len());
421    if start == raw.len() {
422        "0".to_string()
423    } else {
424        hex::encode_upper(&raw[start..])
425    }
426}
427
428/// Decodes an ASN.1 string like OpenSSL `ASN1_STRING_to_UTF8`.
429fn decode_string(value: &Any<'_>) -> Option<String> {
430    let data = value.data;
431    match value.tag() {
432        Tag::Utf8String
433        | Tag::PrintableString
434        | Tag::Ia5String
435        | Tag::NumericString
436        | Tag::VisibleString => std::str::from_utf8(data).ok().map(ToString::to_string),
437        // OpenSSL treats T61String as Latin-1
438        Tag::T61String => Some(data.iter().map(|b| char::from(*b)).collect()),
439        Tag::BmpString => {
440            if !data.len().is_multiple_of(2) {
441                return None;
442            }
443            char::decode_utf16(
444                data.as_chunks::<2>()
445                    .0
446                    .iter()
447                    .map(|&c| u16::from_be_bytes(c)),
448            )
449            .collect::<Result<String, _>>()
450            .ok()
451        }
452        Tag::UniversalString => {
453            if !data.len().is_multiple_of(4) {
454                return None;
455            }
456            data.as_chunks::<4>()
457                .0
458                .iter()
459                .map(|&c| char::from_u32(u32::from_be_bytes(c)))
460                .collect()
461        }
462        _ => None,
463    }
464}
465
466/// Iterates over DER payloads of certificate PEM blocks.
467fn pem_blocks(pem: &[u8]) -> impl Iterator<Item = Result<Bytes, Error>> + '_ {
468    let text = String::from_utf8_lossy(pem);
469    let mut blocks = Vec::new();
470    let mut rest: &str = &text;
471    while let Some(start) = rest.find(PEM_BEGIN) {
472        let after = &rest[start + PEM_BEGIN.len()..];
473        let Some(label_end) = after.find("-----") else {
474            blocks.push(Err(Error::InvalidPem("malformed BEGIN line".into())));
475            break;
476        };
477        let label = &after[..label_end];
478        let body = &after[label_end + 5..];
479        let end_marker = format!("{PEM_END}{label}-----");
480        let Some(end) = body.find(&end_marker) else {
481            blocks.push(Err(Error::InvalidPem(format!(
482                "missing END line for {label}"
483            ))));
484            break;
485        };
486        if PEM_LABELS.contains(&label) {
487            let b64: Vec<u8> = body[..end]
488                .bytes()
489                .filter(|b| !b.is_ascii_whitespace())
490                .collect();
491            blocks.push(
492                BASE64
493                    .decode(&b64)
494                    .map(Bytes::from)
495                    .map_err(|e| Error::InvalidPem(e.to_string())),
496            );
497        }
498        rest = &body[end + end_marker.len()..];
499    }
500    blocks.into_iter()
501}
502
503#[cfg(test)]
504pub(crate) mod tests {
505    use super::*;
506    #[cfg(all(unix, feature = "openssl"))]
507    use crate::x509::digest::Algorithm;
508
509    use crate::x509::testgen::{self, San};
510
511    fn sample() -> Certificate {
512        let ips = ["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()];
513        let san = San {
514            dns: &["Host.Example"],
515            uri: &["https://Uri"],
516            email: &["[email protected]"],
517            ip: &ips,
518        };
519        testgen::cert("Client1", false, None, &testgen::key(), &san, (-1, 1))
520    }
521
522    /// STATEMENT: Certificate metadata is parsed on every backend.
523    ///
524    /// HOW:
525    /// - Generate a certificate with multiple SAN kinds with the platform generator.
526    /// - Check version, serial, names, SANs, validity order and the debug output.
527    #[test]
528    fn metadata() {
529        let c = sample();
530        assert_eq!(c.version(), 3);
531        assert_eq!(c.serial_hex(), "0A0B");
532        assert_eq!(c.subject_cn(), Some("Client1"));
533        assert_eq!(c.subject(), "CN=Client1");
534        assert_eq!(c.issuer(), "CN=Client1");
535        assert!(c.is_self_issued());
536        assert_eq!(c.san().dns, vec!["Host.Example"]);
537        assert_eq!(c.san().uri, vec!["https://Uri"]);
538        assert_eq!(c.san().email, vec!["[email protected]"]);
539        let ips: Vec<_> = c
540            .san()
541            .ip
542            .iter()
543            .map(|i| i.addr().unwrap().to_string())
544            .collect();
545        assert_eq!(ips, vec!["127.0.0.1", "::1"]);
546        assert!(c.not_before() < SystemTime::now());
547        assert!(c.not_before() < c.not_after());
548        let dbg = format!("{c:?}");
549        assert!(dbg.contains("serial_number: \"0A0B\""), "{dbg}");
550        assert!(dbg.contains("127.0.0.1"), "{dbg}");
551    }
552
553    /// STATEMENT: Parsed certificate digests and encodings agree with OpenSSL.
554    ///
555    /// HOW:
556    /// - Compare digests, public key, PEM encoding, and validity of a generated certificate
557    ///   against OpenSSL.
558    #[cfg(all(unix, feature = "openssl"))]
559    #[test]
560    fn parity_with_openssl() {
561        let c = sample();
562        let x = tls_openssl::x509::X509::from_der(c.der()).unwrap();
563        assert_eq!(c.sha1(), &tls_openssl::sha::sha1(&x.to_der().unwrap()));
564        assert_eq!(
565            c.thumbprint(Algorithm::Sha1),
566            hex::encode(x.digest(tls_openssl::hash::MessageDigest::sha1()).unwrap())
567        );
568        assert_eq!(
569            c.thumbprint(Algorithm::Sha256),
570            hex::encode(
571                x.digest(tls_openssl::hash::MessageDigest::sha256())
572                    .unwrap()
573            )
574        );
575        assert_eq!(c.to_pem().as_bytes(), x.to_pem().unwrap());
576        assert_eq!(
577            c.public_key_der(),
578            x.public_key().unwrap().public_key_to_der().unwrap()
579        );
580        let epoch = tls_openssl::asn1::Asn1Time::from_unix(0).unwrap();
581        let d = epoch.diff(x.not_after()).unwrap();
582        let secs = u64::try_from(d.days).unwrap() * 86400 + u64::try_from(d.secs).unwrap();
583        assert_eq!(
584            c.not_after(),
585            SystemTime::UNIX_EPOCH + Duration::from_secs(secs)
586        );
587    }
588
589    /// STATEMENT: Certificate parsing supports PEM bundles and excludes trailing DER data.
590    ///
591    /// HOW:
592    /// - Round-trip a generated certificate, parse a bundle, and check malformed inputs fail.
593    /// - Append bytes to DER and check the parsed certificate retains only its encoding.
594    #[test]
595    fn pem_roundtrip_and_trailing_data() {
596        let c = sample();
597        let mut doc = b"junk\n".to_vec();
598        doc.extend_from_slice(c.to_pem().as_bytes());
599        doc.extend_from_slice(c.to_pem().as_bytes());
600        assert_eq!(Certificate::from_pem(&doc).unwrap(), c);
601        assert_eq!(Certificate::from_pem_multi(&doc).unwrap().len(), 2);
602        assert_eq!(
603            Certificate::from_pem_multi(b"").unwrap(),
604            Vec::<Certificate>::new()
605        );
606        assert!(matches!(
607            Certificate::from_pem(b"nothing"),
608            Err(Error::InvalidPem(_))
609        ));
610        assert!(matches!(
611            Certificate::from_pem(b"-----BEGIN CERTIFICATE-----\n!!!\n-----END CERTIFICATE-----\n"),
612            Err(Error::InvalidPem(_))
613        ));
614
615        let mut der = c.der().to_vec();
616        let len = der.len();
617        der.extend_from_slice(b"trailing");
618        let c2 = Certificate::from_der(der).unwrap();
619        assert_eq!(c2.der().len(), len);
620        assert_eq!(c2, c);
621        assert!(matches!(
622            Certificate::from_der(&b"garbage"[..]),
623            Err(Error::InvalidDer(_))
624        ));
625    }
626
627    fn sequence(body: &[u8]) -> Vec<u8> {
628        let mut der = vec![0x30];
629        if body.len() < 128 {
630            der.push(u8::try_from(body.len()).unwrap());
631        } else {
632            let len = body.len().to_be_bytes();
633            let start = len.iter().position(|b| *b != 0).unwrap();
634            der.push(0x80 | u8::try_from(len.len() - start).unwrap());
635            der.extend_from_slice(&len[start..]);
636        }
637        der.extend_from_slice(body);
638        der
639    }
640
641    fn edit_tbs(edit: impl FnOnce(&[u8]) -> Vec<u8>) -> Vec<u8> {
642        let cert = Certificate::from_pem(include_bytes!("../../tests/testdata/root.pem")).unwrap();
643        let (_, outer) = Any::from_der(cert.der()).unwrap();
644        let (rest, tbs) = Any::from_der(outer.data).unwrap();
645        let mut body = sequence(&edit(tbs.data));
646        body.extend_from_slice(rest);
647        sequence(&body)
648    }
649
650    /// STATEMENT: Unsupported certificate versions return `InvalidDer` without overflowing.
651    ///
652    /// HOW:
653    /// - Replace the fixture's version with 3 and `u32::MAX` while preserving valid DER.
654    /// - Confirm the underlying parser accepts the encodings but `Certificate` rejects them.
655    #[test]
656    fn unsupported_versions() {
657        for version in [
658            &[0xa0, 3, 2, 1, 3][..],
659            &[0xa0, 7, 2, 5, 0, 0xff, 0xff, 0xff, 0xff][..],
660        ] {
661            let der = edit_tbs(|tbs| {
662                let (rest, _) = Any::from_der(tbs).unwrap();
663                let mut body = version.to_vec();
664                body.extend_from_slice(rest);
665                body
666            });
667            assert!(X509Certificate::from_der(&der).is_ok());
668            assert!(matches!(
669                Certificate::from_der(der),
670                Err(Error::InvalidDer(_))
671            ));
672        }
673    }
674
675    /// STATEMENT: Certificate dates outside the platform's `SystemTime` range return `InvalidDer`.
676    ///
677    /// HOW:
678    /// - Replace the fixture's `notBefore` with dates in 1500, 1601, and 1960.
679    /// - Check pre-1601 dates are rejected on Windows and representable dates retain their value.
680    #[test]
681    fn validity_time_range() {
682        for date in [b"15000101000000Z", b"16010101000000Z", b"19600101000000Z"] {
683            let der = edit_tbs(|tbs| {
684                let mut rest = tbs;
685                for _ in 0..4 {
686                    rest = Any::from_der(rest).unwrap().0;
687                }
688                let prefix = &tbs[..tbs.len() - rest.len()];
689                let (suffix, validity) = Any::from_der(rest).unwrap();
690                let (not_after, _) = Any::from_der(validity.data).unwrap();
691                let mut times = vec![0x18, 15];
692                times.extend_from_slice(date);
693                times.extend_from_slice(not_after);
694                let mut body = prefix.to_vec();
695                body.extend_from_slice(&sequence(&times));
696                body.extend_from_slice(suffix);
697                body
698            });
699            let (_, parsed) = X509Certificate::from_der(&der).unwrap();
700            let seconds = parsed.validity().not_before.timestamp().unsigned_abs();
701            let result = Certificate::from_der(der);
702            if cfg!(windows) && date == b"15000101000000Z" {
703                assert!(matches!(result, Err(Error::InvalidDer(_))));
704            } else {
705                assert_eq!(
706                    result.unwrap().not_before(),
707                    SystemTime::UNIX_EPOCH - Duration::from_secs(seconds)
708                );
709            }
710        }
711    }
712
713    /// STATEMENT: ASN.1 string decoding handles supported encodings and rejects malformed values.
714    ///
715    /// HOW:
716    /// - Decode BMP, T61, and Universal strings and reject malformed or unsupported values.
717    /// - Check malformed SAN IPs, zero serials, and pre-epoch time conversion.
718    #[test]
719    fn string_decoding() {
720        let any = |tag, data: &'static [u8]| Any::from_tag_and_data(tag, data);
721        assert_eq!(
722            decode_string(&any(Tag::BmpString, b"\x00A\x00b")).as_deref(),
723            Some("Ab")
724        );
725        assert_eq!(
726            decode_string(&any(Tag::T61String, b"\xe9")).as_deref(),
727            Some("\u{e9}")
728        );
729        assert_eq!(
730            decode_string(&any(Tag::UniversalString, b"\x00\x00\x00A")).as_deref(),
731            Some("A")
732        );
733        assert_eq!(decode_string(&any(Tag::BmpString, b"\x00")), None);
734        assert_eq!(decode_string(&any(Tag::Integer, b"\x01")), None);
735        assert_eq!(SanIp(vec![1, 2, 3]).addr(), None);
736        assert_eq!(serial_hex(&[0, 0]), "0");
737        assert_eq!(
738            to_system_time(ASN1Time::from_timestamp(-10).unwrap()).unwrap(),
739            SystemTime::UNIX_EPOCH - Duration::from_secs(10)
740        );
741    }
742}