Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491//! 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<Asn1Error> for X509Error { fn from(e: Asn1Error) -> Self { Self::Asn1(e) }}
pub type Result<T> = core::result::Result<T, X509Error>;
// ---------------------------------------------------------------------------// 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<core::cmp::Ordering> { 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<DateTime> { 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<DateTime> { 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<u32> { 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<u16> { 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<u32> { if b.is_ascii_digit() { Ok((b - b'0') as u32) } else { Err(X509Error::InvalidCertificate) }}
// ---------------------------------------------------------------------------// Base64 decoder// ---------------------------------------------------------------------------
fn base64_decode(input: &str) -> Result<Vec<u8>> { 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<u8> = 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<Vec<u8>> { 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<Vec<Vec<u8>>> { 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<String>, pub organization: Option<String>, pub organizational_unit: Option<String>, pub country: Option<String>, /// The raw DER-encoded bytes of the Name for comparison. pub raw: Vec<u8>,}
impl DistinguishedName { fn parse(tlv: &Tlv<'_>) -> Result<Self> { 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<SignatureAlgorithm> { 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<u32>,}
/// 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<u8>,}
/// Subject Alternative Name types.#[derive(Debug, Clone, PartialEq, Eq)]pub enum SubjectAltName { DnsName(String), IpAddress(Vec<u8>), Other(u8, Vec<u8>),}
/// Parsed X.509v3 extensions.#[derive(Debug, Clone, Default)]pub struct Extensions { pub basic_constraints: Option<BasicConstraints>, pub key_usage: Option<KeyUsage>, pub subject_alt_names: Vec<SubjectAltName>, pub subject_key_identifier: Option<Vec<u8>>, pub authority_key_identifier: Option<Vec<u8>>,}
fn parse_extensions(ext_data: &[u8]) -> Result<Extensions> { 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<BasicConstraints> { 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<KeyUsage> { 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<Vec<SubjectAltName>> { 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<Vec<u8>> { let tlv = asn1::parse_one(data)?; Ok(tlv.as_octet_string()?.to_vec())}
fn parse_authority_key_identifier(data: &[u8]) -> Result<Vec<u8>> { // 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<u8>, /// 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<u8>, /// V3 extensions. pub extensions: Extensions, /// The raw DER bytes of the TBSCertificate (for signature verification). pub tbs_certificate_raw: Vec<u8>, /// The signature value bytes. pub signature_value: Vec<u8>, /// The full DER-encoded certificate. pub raw: Vec<u8>,}
impl Certificate { /// Parse a certificate from DER-encoded bytes. pub fn from_der(data: &[u8]) -> Result<Self> { 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<Self> { 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<DateTime> { 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-----MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/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-----MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTflMrY=-----END CERTIFICATE-----";
/// GlobalSign Root CA - R3 — RSA 2048-bit, valid 2009-2029.pub const GLOBALSIGN_ROOT_R3_PEM: &str = "-----BEGIN CERTIFICATE-----MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpHWD9f-----END CERTIFICATE-----";
/// Load the embedded root CA certificates.pub fn root_ca_store() -> Result<Vec<Certificate>> { 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()); }}