//! X.509 certificate parsing and chain validation (RFC 5280). //! //! Parses PEM and DER-encoded X.509v3 certificates, extracts subject/issuer //! distinguished names, validity periods, extensions, and public keys. //! Supports certificate chain validation with RSA and ECDSA signature //! verification against an embedded root CA store. use crate::asn1::{ self, Asn1Error, DerParser, Tlv, OID_AUTHORITY_KEY_IDENTIFIER, OID_BASIC_CONSTRAINTS, OID_COMMON_NAME, OID_COUNTRY_NAME, OID_ECDSA_WITH_SHA256, OID_ECDSA_WITH_SHA384, OID_KEY_USAGE, OID_ORGANIZATIONAL_UNIT_NAME, OID_ORGANIZATION_NAME, OID_SHA256_WITH_RSA, OID_SHA384_WITH_RSA, OID_SHA512_WITH_RSA, OID_SUBJECT_ALT_NAME, OID_SUBJECT_KEY_IDENTIFIER, TAG_BOOLEAN, TAG_CLASS_CONTEXT, TAG_CONSTRUCTED, TAG_GENERALIZED_TIME, TAG_INTEGER, TAG_SEQUENCE, TAG_SET, TAG_UTC_TIME, }; use crate::ecdsa::{EcdsaPublicKey, EcdsaSignature}; use crate::rsa::{HashAlgorithm, RsaPublicKey}; use crate::sha2::{sha256, sha384}; use core::fmt; // --------------------------------------------------------------------------- // Error type // --------------------------------------------------------------------------- #[derive(Debug, Clone, PartialEq, Eq)] pub enum X509Error { /// ASN.1 parsing error. Asn1(Asn1Error), /// Invalid PEM encoding. InvalidPem, /// Invalid Base64 encoding. InvalidBase64, /// Invalid certificate structure. InvalidCertificate, /// Unsupported certificate version. UnsupportedVersion, /// Unsupported signature algorithm. UnsupportedAlgorithm, /// Certificate signature verification failed. SignatureVerificationFailed, /// Certificate has expired or is not yet valid. CertificateExpired, /// Certificate is not yet valid. CertificateNotYetValid, /// Certificate chain is incomplete or invalid. InvalidChain, /// Basic Constraints violation (non-CA cert used as issuer). NotCaCertificate, /// Issuer/subject mismatch in chain. IssuerMismatch, /// No trusted root found for the chain. UntrustedRoot, /// RSA verification error. Rsa(String), /// ECDSA verification error. Ecdsa(String), } impl fmt::Display for X509Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Asn1(e) => write!(f, "ASN.1 error: {e}"), Self::InvalidPem => write!(f, "invalid PEM encoding"), Self::InvalidBase64 => write!(f, "invalid Base64 encoding"), Self::InvalidCertificate => write!(f, "invalid certificate structure"), Self::UnsupportedVersion => write!(f, "unsupported certificate version"), Self::UnsupportedAlgorithm => write!(f, "unsupported signature algorithm"), Self::SignatureVerificationFailed => write!(f, "signature verification failed"), Self::CertificateExpired => write!(f, "certificate has expired"), Self::CertificateNotYetValid => write!(f, "certificate is not yet valid"), Self::InvalidChain => write!(f, "invalid certificate chain"), Self::NotCaCertificate => write!(f, "certificate is not a CA"), Self::IssuerMismatch => write!(f, "issuer/subject mismatch in chain"), Self::UntrustedRoot => write!(f, "no trusted root found"), Self::Rsa(e) => write!(f, "RSA error: {e}"), Self::Ecdsa(e) => write!(f, "ECDSA error: {e}"), } } } impl From for X509Error { fn from(e: Asn1Error) -> Self { Self::Asn1(e) } } pub type Result = core::result::Result; // --------------------------------------------------------------------------- // DateTime — simple comparable time representation // --------------------------------------------------------------------------- /// A simple date-time representation for certificate validity checking. /// Supports comparison without system clock access. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DateTime { pub year: u16, pub month: u8, pub day: u8, pub hour: u8, pub minute: u8, pub second: u8, } impl DateTime { /// Create a new DateTime. pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self { Self { year, month, day, hour, minute, second, } } /// Convert to a comparable tuple for ordering. fn as_tuple(&self) -> (u16, u8, u8, u8, u8, u8) { ( self.year, self.month, self.day, self.hour, self.minute, self.second, ) } } impl PartialOrd for DateTime { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for DateTime { fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.as_tuple().cmp(&other.as_tuple()) } } impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", self.year, self.month, self.day, self.hour, self.minute, self.second ) } } /// Parse a UTCTime string (YYMMDDHHMMSSZ) into a DateTime. fn parse_utc_time(s: &str) -> Result { if s.len() != 13 || !s.ends_with('Z') { return Err(X509Error::InvalidCertificate); } let bytes = s.as_bytes(); let yy = parse_decimal_2(&bytes[0..2])?; let month = parse_decimal_2(&bytes[2..4])?; let day = parse_decimal_2(&bytes[4..6])?; let hour = parse_decimal_2(&bytes[6..8])?; let minute = parse_decimal_2(&bytes[8..10])?; let second = parse_decimal_2(&bytes[10..12])?; // RFC 5280: YY >= 50 means 19YY, YY < 50 means 20YY let year = if yy >= 50 { 1900 + yy as u16 } else { 2000 + yy as u16 }; Ok(DateTime::new( year, month as u8, day as u8, hour as u8, minute as u8, second as u8, )) } /// Parse a GeneralizedTime string (YYYYMMDDHHMMSSZ) into a DateTime. fn parse_generalized_time(s: &str) -> Result { if s.len() != 15 || !s.ends_with('Z') { return Err(X509Error::InvalidCertificate); } let bytes = s.as_bytes(); let year = parse_decimal_4(&bytes[0..4])?; let month = parse_decimal_2(&bytes[4..6])?; let day = parse_decimal_2(&bytes[6..8])?; let hour = parse_decimal_2(&bytes[8..10])?; let minute = parse_decimal_2(&bytes[10..12])?; let second = parse_decimal_2(&bytes[12..14])?; Ok(DateTime::new( year, month as u8, day as u8, hour as u8, minute as u8, second as u8, )) } fn parse_decimal_2(bytes: &[u8]) -> Result { if bytes.len() != 2 { return Err(X509Error::InvalidCertificate); } let d1 = digit(bytes[0])?; let d2 = digit(bytes[1])?; Ok(d1 * 10 + d2) } fn parse_decimal_4(bytes: &[u8]) -> Result { if bytes.len() != 4 { return Err(X509Error::InvalidCertificate); } let d1 = digit(bytes[0])? as u16; let d2 = digit(bytes[1])? as u16; let d3 = digit(bytes[2])? as u16; let d4 = digit(bytes[3])? as u16; Ok(d1 * 1000 + d2 * 100 + d3 * 10 + d4) } fn digit(b: u8) -> Result { if b.is_ascii_digit() { Ok((b - b'0') as u32) } else { Err(X509Error::InvalidCertificate) } } // --------------------------------------------------------------------------- // Base64 decoder // --------------------------------------------------------------------------- fn base64_decode(input: &str) -> Result> { const DECODE_TABLE: [u8; 256] = { let mut table = [0xFFu8; 256]; let mut i = 0u8; // A-Z while i < 26 { table[(b'A' + i) as usize] = i; i += 1; } // a-z i = 0; while i < 26 { table[(b'a' + i) as usize] = 26 + i; i += 1; } // 0-9 i = 0; while i < 10 { table[(b'0' + i) as usize] = 52 + i; i += 1; } table[b'+' as usize] = 62; table[b'/' as usize] = 63; table[b'=' as usize] = 0xFE; // padding marker table }; // Strip whitespace and collect valid base64 characters. let clean: Vec = input .bytes() .filter(|&b| !b.is_ascii_whitespace()) .collect(); if clean.is_empty() { return Ok(Vec::new()); } // Must be a multiple of 4. if !clean.len().is_multiple_of(4) { return Err(X509Error::InvalidBase64); } let mut out = Vec::with_capacity(clean.len() * 3 / 4); let chunks = clean.chunks_exact(4); for chunk in chunks { let a = DECODE_TABLE[chunk[0] as usize]; let b = DECODE_TABLE[chunk[1] as usize]; let c = DECODE_TABLE[chunk[2] as usize]; let d = DECODE_TABLE[chunk[3] as usize]; // Check for invalid characters (0xFF). if a == 0xFF || b == 0xFF || c == 0xFF || d == 0xFF { return Err(X509Error::InvalidBase64); } // Mask out padding markers for decoding. let a_val = if a == 0xFE { 0 } else { a }; let b_val = if b == 0xFE { 0 } else { b }; let c_val = if c == 0xFE { 0 } else { c }; let d_val = if d == 0xFE { 0 } else { d }; // First two characters must not be padding. if a == 0xFE || b == 0xFE { return Err(X509Error::InvalidBase64); } let triple = ((a_val as u32) << 18) | ((b_val as u32) << 12) | ((c_val as u32) << 6) | (d_val as u32); out.push((triple >> 16) as u8); if c != 0xFE { out.push((triple >> 8) as u8); } if d != 0xFE { out.push(triple as u8); } } Ok(out) } // --------------------------------------------------------------------------- // PEM decoding // --------------------------------------------------------------------------- const PEM_CERT_BEGIN: &str = "-----BEGIN CERTIFICATE-----"; const PEM_CERT_END: &str = "-----END CERTIFICATE-----"; /// Decode a single PEM-encoded certificate, returning DER bytes. pub fn pem_decode(pem: &str) -> Result> { let certs = pem_decode_multi(pem)?; if certs.len() != 1 { return Err(X509Error::InvalidPem); } Ok(certs.into_iter().next().unwrap()) } /// Decode multiple PEM-encoded certificates from a bundle. pub fn pem_decode_multi(pem: &str) -> Result>> { let mut results = Vec::new(); let mut remaining = pem; while let Some(begin_pos) = remaining.find(PEM_CERT_BEGIN) { remaining = &remaining[begin_pos + PEM_CERT_BEGIN.len()..]; let end_pos = remaining.find(PEM_CERT_END).ok_or(X509Error::InvalidPem)?; let b64_data = &remaining[..end_pos]; remaining = &remaining[end_pos + PEM_CERT_END.len()..]; let der = base64_decode(b64_data)?; if der.is_empty() { return Err(X509Error::InvalidPem); } results.push(der); } if results.is_empty() { return Err(X509Error::InvalidPem); } Ok(results) } // --------------------------------------------------------------------------- // Distinguished Name (DN) // --------------------------------------------------------------------------- /// A parsed X.500 Distinguished Name. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct DistinguishedName { pub common_name: Option, pub organization: Option, pub organizational_unit: Option, pub country: Option, /// The raw DER-encoded bytes of the Name for comparison. pub raw: Vec, } impl DistinguishedName { fn parse(tlv: &Tlv<'_>) -> Result { let mut dn = DistinguishedName { raw: tlv.raw.to_vec(), ..Default::default() }; // Name ::= SEQUENCE OF RelativeDistinguishedName // RelativeDistinguishedName ::= SET OF AttributeTypeAndValue // AttributeTypeAndValue ::= SEQUENCE { type OID, value ANY } let rdns = tlv.sequence_items()?; for rdn_set in &rdns { if rdn_set.tag != TAG_SET { continue; } let attrs = rdn_set.sequence_items()?; for attr in &attrs { if attr.tag != TAG_SEQUENCE { continue; } let items = attr.sequence_items()?; if items.len() < 2 { continue; } let oid = match items[0].as_oid() { Ok(o) => o, Err(_) => continue, }; let value = match items[1].as_string() { Ok(s) => s.to_string(), Err(_) => continue, }; if oid.matches(OID_COMMON_NAME) { dn.common_name = Some(value); } else if oid.matches(OID_ORGANIZATION_NAME) { dn.organization = Some(value); } else if oid.matches(OID_ORGANIZATIONAL_UNIT_NAME) { dn.organizational_unit = Some(value); } else if oid.matches(OID_COUNTRY_NAME) { dn.country = Some(value); } } } Ok(dn) } } impl fmt::Display for DistinguishedName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut parts = Vec::new(); if let Some(cn) = &self.common_name { parts.push(format!("CN={cn}")); } if let Some(o) = &self.organization { parts.push(format!("O={o}")); } if let Some(ou) = &self.organizational_unit { parts.push(format!("OU={ou}")); } if let Some(c) = &self.country { parts.push(format!("C={c}")); } write!(f, "{}", parts.join(", ")) } } // --------------------------------------------------------------------------- // Signature Algorithm // --------------------------------------------------------------------------- /// Supported signature algorithms for X.509 certificates. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SignatureAlgorithm { Sha256WithRsa, Sha384WithRsa, Sha512WithRsa, EcdsaWithSha256, EcdsaWithSha384, } fn parse_signature_algorithm(tlv: &Tlv<'_>) -> Result { let items = tlv.sequence_items()?; if items.is_empty() { return Err(X509Error::InvalidCertificate); } let oid = items[0].as_oid()?; if oid.matches(OID_SHA256_WITH_RSA) { Ok(SignatureAlgorithm::Sha256WithRsa) } else if oid.matches(OID_SHA384_WITH_RSA) { Ok(SignatureAlgorithm::Sha384WithRsa) } else if oid.matches(OID_SHA512_WITH_RSA) { Ok(SignatureAlgorithm::Sha512WithRsa) } else if oid.matches(OID_ECDSA_WITH_SHA256) { Ok(SignatureAlgorithm::EcdsaWithSha256) } else if oid.matches(OID_ECDSA_WITH_SHA384) { Ok(SignatureAlgorithm::EcdsaWithSha384) } else { Err(X509Error::UnsupportedAlgorithm) } } // --------------------------------------------------------------------------- // Extensions // --------------------------------------------------------------------------- /// Basic Constraints extension. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BasicConstraints { pub ca: bool, pub path_len_constraint: Option, } /// Key Usage flags (bit flags from the KeyUsage BIT STRING). #[derive(Debug, Clone, PartialEq, Eq)] pub struct KeyUsage { pub digital_signature: bool, pub key_encipherment: bool, pub key_cert_sign: bool, pub crl_sign: bool, pub raw_bits: Vec, } /// Subject Alternative Name types. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SubjectAltName { DnsName(String), IpAddress(Vec), Other(u8, Vec), } /// Parsed X.509v3 extensions. #[derive(Debug, Clone, Default)] pub struct Extensions { pub basic_constraints: Option, pub key_usage: Option, pub subject_alt_names: Vec, pub subject_key_identifier: Option>, pub authority_key_identifier: Option>, } fn parse_extensions(ext_data: &[u8]) -> Result { let mut exts = Extensions::default(); let mut parser = DerParser::new(ext_data); while parser.has_more() { let ext_seq = parser.read_tlv()?; if ext_seq.tag != TAG_SEQUENCE { continue; } let items = ext_seq.sequence_items()?; if items.len() < 2 { continue; } let oid = match items[0].as_oid() { Ok(o) => o, Err(_) => continue, }; // The value is either items[1] (OCTET STRING) or items[2] if items[1] is critical BOOLEAN. let value_tlv = if items.len() >= 3 && items[1].tag == TAG_BOOLEAN { &items[2] } else { &items[1] }; // Extension value is wrapped in an OCTET STRING. let ext_value = if value_tlv.tag == 0x04 { // TAG_OCTET_STRING value_tlv.value } else { continue; }; if oid.matches(OID_BASIC_CONSTRAINTS) { exts.basic_constraints = parse_basic_constraints(ext_value).ok(); } else if oid.matches(OID_KEY_USAGE) { exts.key_usage = parse_key_usage(ext_value).ok(); } else if oid.matches(OID_SUBJECT_ALT_NAME) { exts.subject_alt_names = parse_subject_alt_names(ext_value).unwrap_or_default(); } else if oid.matches(OID_SUBJECT_KEY_IDENTIFIER) { exts.subject_key_identifier = parse_subject_key_identifier(ext_value).ok(); } else if oid.matches(OID_AUTHORITY_KEY_IDENTIFIER) { exts.authority_key_identifier = parse_authority_key_identifier(ext_value).ok(); } // Unknown extensions are silently ignored. } Ok(exts) } fn parse_basic_constraints(data: &[u8]) -> Result { let seq = asn1::parse_one(data)?; if seq.tag != TAG_SEQUENCE { // Empty SEQUENCE is valid (ca=false, no path len). return Err(X509Error::InvalidCertificate); } let items = seq.sequence_items()?; let mut ca = false; let mut path_len_constraint = None; if !items.is_empty() && items[0].tag == TAG_BOOLEAN { ca = items[0].as_boolean()?; } if items.len() >= 2 && items[1].tag == TAG_INTEGER { let int_bytes = items[1].as_positive_integer()?; if int_bytes.len() <= 4 { let mut val: u32 = 0; for &b in int_bytes { val = (val << 8) | b as u32; } path_len_constraint = Some(val); } } // Special case: if only one item and it's an INTEGER, it's path_len (ca defaults to false) if items.len() == 1 && items[0].tag == TAG_INTEGER { let int_bytes = items[0].as_positive_integer()?; if int_bytes.len() <= 4 { let mut val: u32 = 0; for &b in int_bytes { val = (val << 8) | b as u32; } path_len_constraint = Some(val); } } Ok(BasicConstraints { ca, path_len_constraint, }) } fn parse_key_usage(data: &[u8]) -> Result { let tlv = asn1::parse_one(data)?; let (unused_bits, bits) = tlv.as_bit_string()?; let byte0 = if bits.is_empty() { 0u8 } else { bits[0] }; let _ = unused_bits; // We use the bits as-is for known positions. Ok(KeyUsage { digital_signature: byte0 & 0x80 != 0, key_encipherment: byte0 & 0x20 != 0, key_cert_sign: byte0 & 0x04 != 0, crl_sign: byte0 & 0x02 != 0, raw_bits: bits.to_vec(), }) } fn parse_subject_alt_names(data: &[u8]) -> Result> { let seq = asn1::parse_one(data)?; let items = seq.sequence_items()?; let mut names = Vec::new(); for item in &items { if !item.is_context_specific() { continue; } match item.context_tag() { Some(2) => { // dNSName [2] IA5String if let Ok(s) = core::str::from_utf8(item.value) { names.push(SubjectAltName::DnsName(s.to_string())); } } Some(7) => { // iPAddress [7] OCTET STRING names.push(SubjectAltName::IpAddress(item.value.to_vec())); } Some(tag) => { names.push(SubjectAltName::Other(tag, item.value.to_vec())); } None => {} } } Ok(names) } fn parse_subject_key_identifier(data: &[u8]) -> Result> { let tlv = asn1::parse_one(data)?; Ok(tlv.as_octet_string()?.to_vec()) } fn parse_authority_key_identifier(data: &[u8]) -> Result> { // AuthorityKeyIdentifier ::= SEQUENCE { // keyIdentifier [0] KeyIdentifier OPTIONAL, // ... // } let seq = asn1::parse_one(data)?; let mut parser = DerParser::new(seq.value); while parser.has_more() { let tlv = parser.read_tlv()?; if tlv.is_context_specific() && tlv.context_tag() == Some(0) { return Ok(tlv.value.to_vec()); } } Err(X509Error::InvalidCertificate) } // --------------------------------------------------------------------------- // X.509 Certificate // --------------------------------------------------------------------------- /// A parsed X.509 certificate. #[derive(Debug, Clone)] pub struct Certificate { /// Certificate version (0 = v1, 1 = v2, 2 = v3). pub version: u8, /// Serial number bytes. pub serial_number: Vec, /// Signature algorithm used by the issuer. pub signature_algorithm: SignatureAlgorithm, /// Issuer distinguished name. pub issuer: DistinguishedName, /// Validity: not before. pub not_before: DateTime, /// Validity: not after. pub not_after: DateTime, /// Subject distinguished name. pub subject: DistinguishedName, /// Subject public key info (raw DER bytes). pub subject_public_key_info: Vec, /// V3 extensions. pub extensions: Extensions, /// The raw DER bytes of the TBSCertificate (for signature verification). pub tbs_certificate_raw: Vec, /// The signature value bytes. pub signature_value: Vec, /// The full DER-encoded certificate. pub raw: Vec, } impl Certificate { /// Parse a certificate from DER-encoded bytes. pub fn from_der(data: &[u8]) -> Result { let outer = asn1::parse_one(data)?; if outer.tag != TAG_SEQUENCE { return Err(X509Error::InvalidCertificate); } let items = outer.sequence_items()?; if items.len() != 3 { return Err(X509Error::InvalidCertificate); } // items[0] = TBSCertificate (SEQUENCE) // items[1] = signatureAlgorithm (SEQUENCE) // items[2] = signatureValue (BIT STRING) let tbs = &items[0]; let sig_alg_outer = &items[1]; let sig_val_tlv = &items[2]; if tbs.tag != TAG_SEQUENCE { return Err(X509Error::InvalidCertificate); } let tbs_certificate_raw = tbs.raw.to_vec(); // Parse the outer signature algorithm (used to verify). let signature_algorithm = parse_signature_algorithm(sig_alg_outer)?; // Parse signature value. let (unused_bits, sig_bytes) = sig_val_tlv.as_bit_string()?; if unused_bits != 0 { return Err(X509Error::InvalidCertificate); } let signature_value = sig_bytes.to_vec(); // Parse TBSCertificate fields. let mut tbs_parser = DerParser::new(tbs.value); // version [0] EXPLICIT INTEGER DEFAULT v1 let version = if tbs_parser.has_more() { let peeked = tbs_parser.peek_tag()?; if peeked == (TAG_CLASS_CONTEXT | TAG_CONSTRUCTED) { let ver_wrapper = tbs_parser.read_tlv()?; let ver_tlv = asn1::parse_one(ver_wrapper.value)?; let ver_bytes = ver_tlv.as_integer()?; if ver_bytes.len() != 1 { return Err(X509Error::InvalidCertificate); } ver_bytes[0] } else { 0 // Default v1 } } else { return Err(X509Error::InvalidCertificate); }; if version > 2 { return Err(X509Error::UnsupportedVersion); } // serialNumber INTEGER let serial_tlv = tbs_parser.read_expect(TAG_INTEGER)?; let serial_number = serial_tlv.value.to_vec(); // signature AlgorithmIdentifier (inner — must match outer) let inner_sig_alg = tbs_parser.read_expect(TAG_SEQUENCE)?; let inner_algorithm = parse_signature_algorithm(&inner_sig_alg)?; if inner_algorithm != signature_algorithm { return Err(X509Error::InvalidCertificate); } // issuer Name let issuer_tlv = tbs_parser.read_expect(TAG_SEQUENCE)?; let issuer = DistinguishedName::parse(&issuer_tlv)?; // validity SEQUENCE { notBefore Time, notAfter Time } let validity_tlv = tbs_parser.read_expect(TAG_SEQUENCE)?; let validity_items = validity_tlv.sequence_items()?; if validity_items.len() != 2 { return Err(X509Error::InvalidCertificate); } let not_before = parse_time(&validity_items[0])?; let not_after = parse_time(&validity_items[1])?; // subject Name let subject_tlv = tbs_parser.read_expect(TAG_SEQUENCE)?; let subject = DistinguishedName::parse(&subject_tlv)?; // subjectPublicKeyInfo SEQUENCE let spki_tlv = tbs_parser.read_expect(TAG_SEQUENCE)?; let subject_public_key_info = spki_tlv.raw.to_vec(); // extensions [3] EXPLICIT SEQUENCE OF Extension (v3 only) let mut extensions = Extensions::default(); if version == 2 { // Look for [3] CONSTRUCTED context tag. if let Some(ext_wrapper) = tbs_parser.read_optional_context(3, true)? { // The wrapper contains a SEQUENCE of Extensions. let ext_seq = asn1::parse_one(ext_wrapper.value)?; if ext_seq.tag == TAG_SEQUENCE { extensions = parse_extensions(ext_seq.value)?; } } } Ok(Certificate { version, serial_number, signature_algorithm, issuer, not_before, not_after, subject, subject_public_key_info, extensions, tbs_certificate_raw, signature_value, raw: data.to_vec(), }) } /// Parse a certificate from PEM-encoded data. pub fn from_pem(pem: &str) -> Result { let der = pem_decode(pem)?; Self::from_der(&der) } /// Check if this certificate is valid at the given time. pub fn is_valid_at(&self, now: &DateTime) -> bool { now >= &self.not_before && now <= &self.not_after } /// Check if this certificate is a CA (has Basic Constraints with ca=true). pub fn is_ca(&self) -> bool { self.extensions .basic_constraints .as_ref() .is_some_and(|bc| bc.ca) } /// Check if this certificate is self-signed (subject == issuer by raw DER). pub fn is_self_signed(&self) -> bool { self.subject.raw == self.issuer.raw } /// Verify that this certificate's signature was produced by `issuer_cert`. pub fn verify_signature(&self, issuer_cert: &Certificate) -> Result<()> { let tbs_data = &self.tbs_certificate_raw; let sig_data = &self.signature_value; match self.signature_algorithm { SignatureAlgorithm::Sha256WithRsa | SignatureAlgorithm::Sha384WithRsa | SignatureAlgorithm::Sha512WithRsa => { let hash_alg = match self.signature_algorithm { SignatureAlgorithm::Sha256WithRsa => HashAlgorithm::Sha256, SignatureAlgorithm::Sha384WithRsa => HashAlgorithm::Sha384, SignatureAlgorithm::Sha512WithRsa => HashAlgorithm::Sha512, _ => unreachable!(), }; let rsa_key = RsaPublicKey::from_der(&issuer_cert.subject_public_key_info) .map_err(|e| X509Error::Rsa(format!("{e}")))?; rsa_key .verify_pkcs1v15(hash_alg, tbs_data, sig_data) .map_err(|e| X509Error::Rsa(format!("{e}")))?; Ok(()) } SignatureAlgorithm::EcdsaWithSha256 | SignatureAlgorithm::EcdsaWithSha384 => { let ec_key = EcdsaPublicKey::from_spki_der(&issuer_cert.subject_public_key_info) .map_err(|e| X509Error::Ecdsa(format!("{e}")))?; let ecdsa_sig = EcdsaSignature::from_der(sig_data) .map_err(|e| X509Error::Ecdsa(format!("{e}")))?; // Hash the TBS data with the appropriate algorithm and verify. let hash = match self.signature_algorithm { SignatureAlgorithm::EcdsaWithSha256 => sha256(tbs_data).to_vec(), SignatureAlgorithm::EcdsaWithSha384 => sha384(tbs_data).to_vec(), _ => unreachable!(), }; ec_key .verify_prehashed(&hash, &ecdsa_sig) .map_err(|e| X509Error::Ecdsa(format!("{e}")))?; Ok(()) } } } /// Verify a self-signed certificate's own signature. pub fn verify_self_signed(&self) -> Result<()> { self.verify_signature(self) } } fn parse_time(tlv: &Tlv<'_>) -> Result { match tlv.tag { TAG_UTC_TIME => { let s = tlv.as_utc_time()?; parse_utc_time(s) } TAG_GENERALIZED_TIME => { let s = tlv.as_generalized_time()?; parse_generalized_time(s) } _ => Err(X509Error::InvalidCertificate), } } // --------------------------------------------------------------------------- // Certificate chain validation // --------------------------------------------------------------------------- /// Validate a certificate chain. /// /// `chain` should be ordered leaf-first: chain[0] is the leaf, chain[last] is closest to root. /// `trust_anchors` are the trusted root CA certificates. /// `now` is the current time for validity checking. /// /// Returns Ok(()) if the chain is valid. pub fn validate_chain( chain: &[Certificate], trust_anchors: &[Certificate], now: &DateTime, ) -> Result<()> { if chain.is_empty() { return Err(X509Error::InvalidChain); } // Validate each certificate in the chain is within its validity period. for cert in chain { if now < &cert.not_before { return Err(X509Error::CertificateNotYetValid); } if now > &cert.not_after { return Err(X509Error::CertificateExpired); } } // Verify the chain of signatures. // For each cert[i], verify its signature using cert[i+1]. for i in 0..chain.len() - 1 { let cert = &chain[i]; let issuer = &chain[i + 1]; // Check issuer/subject linkage. if cert.issuer.raw != issuer.subject.raw { return Err(X509Error::IssuerMismatch); } // Non-leaf certs must be CAs. if !issuer.is_ca() { return Err(X509Error::NotCaCertificate); } // Verify the signature. cert.verify_signature(issuer) .map_err(|_| X509Error::SignatureVerificationFailed)?; } // The last cert in the chain should either be a trust anchor itself, or // its issuer should be a trust anchor. let top_cert = &chain[chain.len() - 1]; // Check if the top cert is itself a trust anchor. for anchor in trust_anchors { if top_cert.raw == anchor.raw { return Ok(()); } } // Check if the top cert is signed by a trust anchor. for anchor in trust_anchors { if top_cert.issuer.raw == anchor.subject.raw { // Verify signature. if top_cert.verify_signature(anchor).is_ok() { // Also check the anchor's validity. if now < &anchor.not_before { return Err(X509Error::CertificateNotYetValid); } if now > &anchor.not_after { return Err(X509Error::CertificateExpired); } return Ok(()); } } } Err(X509Error::UntrustedRoot) } // --------------------------------------------------------------------------- // Embedded Root CA Store // --------------------------------------------------------------------------- /// ISRG Root X1 (Let's Encrypt) — RSA 4096-bit, valid 2015-2035. pub const ISRG_ROOT_X1_PEM: &str = "-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE-----"; /// DigiCert Global Root G2 — RSA 2048-bit, valid 2013-2038. pub const DIGICERT_GLOBAL_ROOT_G2_PEM: &str = "-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI 2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx 1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV 5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY 1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl MrY= -----END CERTIFICATE-----"; /// GlobalSign Root CA - R3 — RSA 2048-bit, valid 2009-2029. pub const GLOBALSIGN_ROOT_R3_PEM: &str = "-----BEGIN CERTIFICATE----- MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH WD9f -----END CERTIFICATE-----"; /// Load the embedded root CA certificates. pub fn root_ca_store() -> Result> { let mut roots = Vec::new(); for pem in &[ ISRG_ROOT_X1_PEM, DIGICERT_GLOBAL_ROOT_G2_PEM, GLOBALSIGN_ROOT_R3_PEM, ] { roots.push(Certificate::from_pem(pem)?); } Ok(roots) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; // ------------------------------------------------------------------- // Base64 tests // ------------------------------------------------------------------- #[test] fn base64_decode_basic() { assert_eq!(base64_decode("").ok(), Some(vec![])); // Padding results still produce correct output. assert_eq!(base64_decode("YQ==").unwrap(), b"a"); assert_eq!(base64_decode("YWI=").unwrap(), b"ab"); assert_eq!(base64_decode("YWJj").unwrap(), b"abc"); assert_eq!(base64_decode("YWJjZA==").unwrap(), b"abcd"); } #[test] fn base64_decode_with_whitespace() { // "YWJj" + "ZA==" = base64("abcd"), with whitespace inserted. let encoded = "YWJj\nZA=="; assert_eq!(base64_decode(encoded).unwrap(), b"abcd"); } #[test] fn base64_decode_invalid() { // Not a multiple of 4 after stripping whitespace. assert!(base64_decode("YWJ").is_err()); // Invalid character. assert!(base64_decode("YW@j").is_err()); } // ------------------------------------------------------------------- // PEM tests // ------------------------------------------------------------------- #[test] fn pem_decode_single_cert() { // Use ISRG Root X1. let der = pem_decode(ISRG_ROOT_X1_PEM).unwrap(); // DER should start with SEQUENCE tag 0x30. assert_eq!(der[0], 0x30); assert!(der.len() > 100); } #[test] fn pem_decode_multi_cert() { let bundle = format!("{}\n{}", ISRG_ROOT_X1_PEM, DIGICERT_GLOBAL_ROOT_G2_PEM); let certs = pem_decode_multi(&bundle).unwrap(); assert_eq!(certs.len(), 2); assert_eq!(certs[0][0], 0x30); assert_eq!(certs[1][0], 0x30); } #[test] fn pem_decode_invalid_no_markers() { assert!(pem_decode("not a certificate").is_err()); } #[test] fn pem_decode_invalid_missing_end() { let bad = "-----BEGIN CERTIFICATE-----\nYWJj\n"; assert!(pem_decode(bad).is_err()); } // ------------------------------------------------------------------- // DateTime tests // ------------------------------------------------------------------- #[test] fn datetime_ordering() { let d1 = DateTime::new(2020, 1, 1, 0, 0, 0); let d2 = DateTime::new(2020, 1, 1, 0, 0, 1); let d3 = DateTime::new(2021, 1, 1, 0, 0, 0); let d4 = DateTime::new(2020, 6, 15, 12, 30, 0); assert!(d1 < d2); assert!(d2 < d4); assert!(d4 < d3); assert!(d1 < d3); assert_eq!(d1, d1.clone()); } #[test] fn parse_utc_time_valid() { let dt = parse_utc_time("150604110438Z").unwrap(); assert_eq!(dt.year, 2015); assert_eq!(dt.month, 6); assert_eq!(dt.day, 4); assert_eq!(dt.hour, 11); assert_eq!(dt.minute, 4); assert_eq!(dt.second, 38); } #[test] fn parse_utc_time_1900s() { // YY >= 50 means 19YY. let dt = parse_utc_time("980901120000Z").unwrap(); assert_eq!(dt.year, 1998); } #[test] fn parse_generalized_time_valid() { let dt = parse_generalized_time("20350604110438Z").unwrap(); assert_eq!(dt.year, 2035); assert_eq!(dt.month, 6); assert_eq!(dt.day, 4); } // ------------------------------------------------------------------- // Certificate parsing tests // ------------------------------------------------------------------- #[test] fn parse_isrg_root_x1() { let cert = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); assert_eq!(cert.version, 2); // v3 assert_eq!(cert.subject.common_name.as_deref(), Some("ISRG Root X1")); assert_eq!( cert.subject.organization.as_deref(), Some("Internet Security Research Group") ); assert_eq!(cert.subject.country.as_deref(), Some("US")); // Self-signed: issuer == subject. assert!(cert.is_self_signed()); assert_eq!(cert.issuer.common_name, cert.subject.common_name); // Validity. assert_eq!(cert.not_before.year, 2015); assert_eq!(cert.not_after.year, 2035); // Signature algorithm. assert_eq!(cert.signature_algorithm, SignatureAlgorithm::Sha256WithRsa); // CA cert. assert!(cert.is_ca()); // Extensions. assert!(cert.extensions.basic_constraints.is_some()); let bc = cert.extensions.basic_constraints.as_ref().unwrap(); assert!(bc.ca); assert!(cert.extensions.key_usage.is_some()); let ku = cert.extensions.key_usage.as_ref().unwrap(); assert!(ku.key_cert_sign); assert!(ku.crl_sign); assert!(cert.extensions.subject_key_identifier.is_some()); } #[test] fn parse_digicert_global_root_g2() { let cert = Certificate::from_pem(DIGICERT_GLOBAL_ROOT_G2_PEM).unwrap(); assert_eq!(cert.version, 2); assert_eq!( cert.subject.common_name.as_deref(), Some("DigiCert Global Root G2") ); assert_eq!(cert.subject.organization.as_deref(), Some("DigiCert Inc")); assert_eq!(cert.subject.country.as_deref(), Some("US")); assert!(cert.is_self_signed()); assert!(cert.is_ca()); assert_eq!(cert.not_before.year, 2013); assert_eq!(cert.not_after.year, 2038); assert_eq!(cert.signature_algorithm, SignatureAlgorithm::Sha256WithRsa); } #[test] fn parse_globalsign_root_r3() { let cert = Certificate::from_pem(GLOBALSIGN_ROOT_R3_PEM).unwrap(); assert_eq!(cert.version, 2); assert_eq!(cert.subject.common_name.as_deref(), Some("GlobalSign")); assert_eq!(cert.subject.organization.as_deref(), Some("GlobalSign")); assert!(cert.is_self_signed()); assert!(cert.is_ca()); assert_eq!(cert.not_before.year, 2009); assert_eq!(cert.not_after.year, 2029); assert_eq!(cert.signature_algorithm, SignatureAlgorithm::Sha256WithRsa); } // ------------------------------------------------------------------- // Signature verification tests // ------------------------------------------------------------------- #[test] fn verify_isrg_root_x1_self_signed() { let cert = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); cert.verify_self_signed().unwrap(); } #[test] fn verify_digicert_global_root_g2_self_signed() { let cert = Certificate::from_pem(DIGICERT_GLOBAL_ROOT_G2_PEM).unwrap(); cert.verify_self_signed().unwrap(); } #[test] fn verify_globalsign_root_r3_self_signed() { let cert = Certificate::from_pem(GLOBALSIGN_ROOT_R3_PEM).unwrap(); cert.verify_self_signed().unwrap(); } // ------------------------------------------------------------------- // Validity checks // ------------------------------------------------------------------- #[test] fn certificate_validity_check() { let cert = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); // Valid in 2025. let now = DateTime::new(2025, 6, 15, 12, 0, 0); assert!(cert.is_valid_at(&now)); // Before notBefore (June 4, 2015). let before = DateTime::new(2015, 1, 1, 0, 0, 0); assert!(!cert.is_valid_at(&before)); // After notAfter (June 4, 2035). let after = DateTime::new(2036, 1, 1, 0, 0, 0); assert!(!cert.is_valid_at(&after)); } // ------------------------------------------------------------------- // Chain validation tests // ------------------------------------------------------------------- #[test] fn chain_validation_self_signed_root() { let roots = root_ca_store().unwrap(); let isrg = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); // A chain of just the root should validate against the trust store. let now = DateTime::new(2025, 6, 15, 12, 0, 0); validate_chain(&[isrg], &roots, &now).unwrap(); } #[test] fn chain_validation_reject_expired() { let roots = root_ca_store().unwrap(); // GlobalSign R3 expires in 2029, so check at 2030. let globalsign = Certificate::from_pem(GLOBALSIGN_ROOT_R3_PEM).unwrap(); let future = DateTime::new(2030, 1, 1, 0, 0, 0); let result = validate_chain(&[globalsign], &roots, &future); assert!(result.is_err()); } #[test] fn chain_validation_reject_untrusted() { // Create a minimal "self-signed" scenario where the cert isn't in the trust store. // Use a trust store that doesn't contain our cert. let isrg = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); let digicert = Certificate::from_pem(DIGICERT_GLOBAL_ROOT_G2_PEM).unwrap(); // Use only DigiCert as trust anchor, try to validate ISRG root. let now = DateTime::new(2025, 6, 15, 12, 0, 0); let result = validate_chain(&[isrg], &[digicert], &now); assert_eq!(result, Err(X509Error::UntrustedRoot)); } #[test] fn chain_validation_two_cert_chain() { // We construct a two-cert chain: [DigiCert G2, ISRG X1] with ISRG as "issuer". // This won't actually validate because DigiCert isn't issued by ISRG, // but we test the issuer mismatch detection. let isrg = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); let digicert = Certificate::from_pem(DIGICERT_GLOBAL_ROOT_G2_PEM).unwrap(); let now = DateTime::new(2025, 6, 15, 12, 0, 0); let result = validate_chain(&[digicert, isrg.clone()], &[isrg], &now); assert_eq!(result, Err(X509Error::IssuerMismatch)); } #[test] fn chain_validation_empty_chain() { let roots = root_ca_store().unwrap(); let now = DateTime::new(2025, 6, 15, 12, 0, 0); let result = validate_chain(&[], &roots, &now); assert_eq!(result, Err(X509Error::InvalidChain)); } // ------------------------------------------------------------------- // Root CA store // ------------------------------------------------------------------- #[test] fn root_ca_store_loads() { let roots = root_ca_store().unwrap(); assert_eq!(roots.len(), 3); // All should be self-signed CAs. for root in &roots { assert!(root.is_self_signed()); assert!(root.is_ca()); } } #[test] fn root_ca_store_all_valid_now() { let roots = root_ca_store().unwrap(); let now = DateTime::new(2025, 6, 15, 12, 0, 0); for root in &roots { assert!(root.is_valid_at(&now), "root {} not valid", root.subject); } } // ------------------------------------------------------------------- // Display impls // ------------------------------------------------------------------- #[test] fn distinguished_name_display() { let cert = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); let display = format!("{}", cert.subject); assert!(display.contains("CN=ISRG Root X1")); assert!(display.contains("O=Internet Security Research Group")); assert!(display.contains("C=US")); } #[test] fn datetime_display() { let dt = DateTime::new(2025, 3, 12, 14, 30, 0); assert_eq!(format!("{dt}"), "2025-03-12T14:30:00Z"); } // ------------------------------------------------------------------- // Subject public key info parsing // ------------------------------------------------------------------- #[test] fn spki_bytes_parse_as_rsa_key() { let cert = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); // The SPKI should parse as an RSA key. let key = RsaPublicKey::from_der(&cert.subject_public_key_info); assert!(key.is_ok(), "Failed to parse RSA key from SPKI"); } // ------------------------------------------------------------------- // Serial number // ------------------------------------------------------------------- #[test] fn serial_number_not_empty() { let cert = Certificate::from_pem(ISRG_ROOT_X1_PEM).unwrap(); assert!(!cert.serial_number.is_empty()); } // ------------------------------------------------------------------- // Edge cases // ------------------------------------------------------------------- #[test] fn reject_truncated_der() { let der = pem_decode(ISRG_ROOT_X1_PEM).unwrap(); // Truncate the DER. let truncated = &der[..100]; let result = Certificate::from_der(truncated); assert!(result.is_err()); } #[test] fn reject_garbage_der() { let result = Certificate::from_der(&[0xFF, 0xFF, 0xFF]); assert!(result.is_err()); } }