Skip to main content

xml_sec/xmldsig/
x509.rs

1//! X.509 certificate path and revocation validation.
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use x509_parser::{
6    certificate::X509Certificate, extensions::ParsedExtension, prelude::FromDer,
7    revocation_list::CertificateRevocationList, time::ASN1Time,
8};
9
10use super::X509DataInfo;
11
12/// Inputs controlling X.509 certificate-chain validation.
13#[derive(Debug, Clone)]
14pub struct X509ChainOptions<'a> {
15    /// DER-encoded certificates accepted as trust anchors.
16    pub trusted_certs: &'a [Vec<u8>],
17    /// Time used for certificate, CRL, and revocation checks.
18    pub verification_time: SystemTime,
19    /// Maximum number of certificates in the validated path, including the anchor.
20    pub max_chain_depth: usize,
21    /// Whether parsed `<X509CRL>` entries are enforced.
22    pub check_crls: bool,
23}
24
25/// Certificate-chain validation failure.
26#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
27#[non_exhaustive]
28pub enum X509ChainError {
29    /// The configured path limit cannot contain a certificate.
30    #[error("maximum certificate chain depth must be greater than zero")]
31    InvalidDepth,
32    /// A certificate or CRL is malformed DER.
33    #[error("invalid {kind} DER: {message}")]
34    InvalidDer {
35        /// Object type being parsed.
36        kind: &'static str,
37        /// Parser diagnostic.
38        message: String,
39    },
40    /// The ordered embedded path cannot be completed to a configured anchor.
41    #[error("certificate chain does not terminate at a trusted certificate")]
42    UntrustedRoot,
43    /// The path contains more certificates than allowed.
44    #[error("certificate chain exceeds maximum depth of {0}")]
45    DepthExceeded(usize),
46    /// A certificate is outside its validity period.
47    #[error("certificate at chain position {0} is expired or not yet valid")]
48    CertificateNotValid(usize),
49    /// An issuer certificate is not authorized to issue certificates.
50    #[error("certificate at chain position {0} is not a CA")]
51    IssuerNotCa(usize),
52    /// A CA path-length constraint is violated.
53    #[error("certificate at chain position {position} exceeds path length constraint {limit}")]
54    PathLengthExceeded {
55        /// Position of the constraining CA certificate.
56        position: usize,
57        /// Maximum permitted subordinate CA count.
58        limit: u32,
59    },
60    /// A certificate key usage extension forbids the required operation.
61    #[error("certificate at chain position {position} does not permit {required}")]
62    InvalidKeyUsage {
63        /// Position of the certificate in the validated path.
64        position: usize,
65        /// RFC 5280 key usage required for the operation.
66        required: &'static str,
67    },
68    /// A certificate signature does not verify under its issuer key.
69    #[error("certificate signature at chain position {0} is invalid or unsupported")]
70    InvalidSignature(usize),
71    /// A CRL is not valid for the selected verification time or issuer.
72    #[error("CRL {0} is invalid or cannot be authenticated")]
73    InvalidCrl(usize),
74    /// A path certificate was revoked by an applicable CRL.
75    #[error("certificate at chain position {0} is revoked")]
76    Revoked(usize),
77}
78
79/// Verify the ordered certificate path parsed from one `<X509Data>` element.
80pub fn verify_x509_certificate_chain(
81    info: &X509DataInfo,
82    options: &X509ChainOptions<'_>,
83) -> Result<(), X509ChainError> {
84    if options.max_chain_depth == 0 {
85        return Err(X509ChainError::InvalidDepth);
86    }
87    if info.certificate_chain.is_empty() {
88        return Err(X509ChainError::UntrustedRoot);
89    }
90
91    let path_der = info
92        .certificate_chain
93        .iter()
94        .map(|&idx| {
95            info.certificates
96                .get(idx)
97                .map(Vec::as_slice)
98                .ok_or(X509ChainError::UntrustedRoot)
99        })
100        .collect::<Result<Vec<_>, _>>()?;
101
102    let last = parse_certificate(
103        path_der
104            .last()
105            .copied()
106            .ok_or(X509ChainError::UntrustedRoot)?,
107    )?;
108    let trusted_anchors = options
109        .trusted_certs
110        .iter()
111        .map(|der| parse_certificate(der).map(|cert| (der.as_slice(), cert)))
112        .collect::<Result<Vec<_>, _>>()?;
113    let verification_time = system_time_to_asn1(options.verification_time)?;
114    let embedded_anchor = trusted_anchors.iter().any(|(der, _)| *der == last.as_raw());
115    if embedded_anchor {
116        return validate_path(&path_der, info, options, verification_time);
117    }
118
119    let replace_untrusted_root = if path_der.len() > 1
120        && last.subject() == last.issuer()
121        && last.verify_signature(None).is_ok()
122    {
123        let child = parse_certificate(path_der[path_der.len() - 2])?;
124        child.issuer() == last.subject() && child.verify_signature(Some(last.public_key())).is_ok()
125    } else {
126        false
127    };
128    let candidate_base = if replace_untrusted_root {
129        &path_der[..path_der.len() - 1]
130    } else {
131        path_der.as_slice()
132    };
133    let candidate_child = parse_certificate(
134        candidate_base
135            .last()
136            .copied()
137            .ok_or(X509ChainError::UntrustedRoot)?,
138    )?;
139
140    let mut first_validation_error = None;
141    for (anchor_der, _) in trusted_anchors.iter().filter(|(_, cert)| {
142        cert.subject() == candidate_child.issuer()
143            && candidate_child
144                .verify_signature(Some(cert.public_key()))
145                .is_ok()
146    }) {
147        let mut candidate_path = candidate_base.to_vec();
148        candidate_path.push(anchor_der);
149        match validate_path(&candidate_path, info, options, verification_time) {
150            Ok(()) => return Ok(()),
151            Err(error) => first_validation_error.get_or_insert(error),
152        };
153    }
154
155    Err(first_validation_error.unwrap_or(X509ChainError::UntrustedRoot))
156}
157
158fn validate_path(
159    path_der: &[&[u8]],
160    info: &X509DataInfo,
161    options: &X509ChainOptions<'_>,
162    verification_time: ASN1Time,
163) -> Result<(), X509ChainError> {
164    if path_der.len() > options.max_chain_depth {
165        return Err(X509ChainError::DepthExceeded(options.max_chain_depth));
166    }
167
168    let path = path_der
169        .iter()
170        .map(|der| parse_certificate(der))
171        .collect::<Result<Vec<_>, _>>()?;
172
173    for (position, cert) in path.iter().enumerate() {
174        if !cert.validity().is_valid_at(verification_time) {
175            return Err(X509ChainError::CertificateNotValid(position));
176        }
177        if position == 0 {
178            validate_leaf_key_usage(cert)?;
179        } else {
180            validate_ca_constraints(cert, position)?;
181        }
182    }
183
184    for (position, pair) in path.windows(2).enumerate() {
185        let [child, issuer] = pair else {
186            unreachable!()
187        };
188        if child.issuer() != issuer.subject()
189            || child.verify_signature(Some(issuer.public_key())).is_err()
190        {
191            return Err(X509ChainError::InvalidSignature(position));
192        }
193    }
194
195    if options.check_crls {
196        verify_crls(&path, &info.crls, verification_time)?;
197    }
198    Ok(())
199}
200
201fn validate_leaf_key_usage(cert: &X509Certificate<'_>) -> Result<(), X509ChainError> {
202    // RFC 5280 section 4.2.1.3 restricts key purpose only when KeyUsage is present.
203    if cert
204        .key_usage()
205        .map_err(|error| X509ChainError::InvalidDer {
206            kind: "certificate KeyUsage",
207            message: error.to_string(),
208        })?
209        .is_some_and(|usage| !usage.value.digital_signature() && !usage.value.non_repudiation())
210    {
211        return Err(X509ChainError::InvalidKeyUsage {
212            position: 0,
213            required: "digitalSignature or nonRepudiation",
214        });
215    }
216    Ok(())
217}
218
219fn parse_certificate(der: &[u8]) -> Result<X509Certificate<'_>, X509ChainError> {
220    let (rest, cert) =
221        X509Certificate::from_der(der).map_err(|error| X509ChainError::InvalidDer {
222            kind: "certificate",
223            message: error.to_string(),
224        })?;
225    if !rest.is_empty() {
226        return Err(X509ChainError::InvalidDer {
227            kind: "certificate",
228            message: "trailing data".into(),
229        });
230    }
231    Ok(cert)
232}
233
234fn system_time_to_asn1(time: SystemTime) -> Result<ASN1Time, X509ChainError> {
235    let seconds = time
236        .duration_since(UNIX_EPOCH)
237        .map_err(|_| X509ChainError::CertificateNotValid(0))?
238        .as_secs();
239    let timestamp = i64::try_from(seconds).map_err(|_| X509ChainError::CertificateNotValid(0))?;
240    ASN1Time::from_timestamp(timestamp).map_err(|error| X509ChainError::InvalidDer {
241        kind: "verification time",
242        message: error.to_string(),
243    })
244}
245
246fn validate_ca_constraints(
247    cert: &X509Certificate<'_>,
248    position: usize,
249) -> Result<(), X509ChainError> {
250    let constraints = cert
251        .extensions()
252        .iter()
253        .find_map(|extension| match extension.parsed_extension() {
254            ParsedExtension::BasicConstraints(value) => Some(value),
255            _ => None,
256        })
257        .filter(|constraints| constraints.ca)
258        .ok_or(X509ChainError::IssuerNotCa(position))?;
259
260    if cert
261        .key_usage()
262        .map_err(|error| X509ChainError::InvalidDer {
263            kind: "certificate KeyUsage",
264            message: error.to_string(),
265        })?
266        .is_some_and(|usage| !usage.value.key_cert_sign())
267    {
268        return Err(X509ChainError::InvalidKeyUsage {
269            position,
270            required: "keyCertSign",
271        });
272    }
273
274    if let Some(limit) = constraints.path_len_constraint {
275        let subordinate_ca_count = position.saturating_sub(1);
276        if subordinate_ca_count > limit as usize {
277            return Err(X509ChainError::PathLengthExceeded { position, limit });
278        }
279    }
280    Ok(())
281}
282
283fn verify_crls(
284    path: &[X509Certificate<'_>],
285    crl_der: &[Vec<u8>],
286    verification_time: ASN1Time,
287) -> Result<(), X509ChainError> {
288    let crls = crl_der
289        .iter()
290        .enumerate()
291        .map(|(idx, der)| {
292            let (rest, crl) = CertificateRevocationList::from_der(der).map_err(|error| {
293                X509ChainError::InvalidDer {
294                    kind: "CRL",
295                    message: error.to_string(),
296                }
297            })?;
298            if !rest.is_empty() {
299                return Err(X509ChainError::InvalidDer {
300                    kind: "CRL",
301                    message: "trailing data".into(),
302                });
303            }
304            Ok((idx, crl))
305        })
306        .collect::<Result<Vec<_>, _>>()?;
307
308    for (position, cert) in path.iter().enumerate().take(path.len().saturating_sub(1)) {
309        let issuer = &path[position + 1];
310        for (crl_index, crl) in crls.iter().filter(|(_, crl)| crl.issuer() == cert.issuer()) {
311            if issuer
312                .key_usage()
313                .map_err(|error| X509ChainError::InvalidDer {
314                    kind: "certificate KeyUsage",
315                    message: error.to_string(),
316                })?
317                .is_some_and(|usage| !usage.value.crl_sign())
318            {
319                return Err(X509ChainError::InvalidKeyUsage {
320                    position: position + 1,
321                    required: "cRLSign",
322                });
323            }
324            let time_valid = crl.last_update() <= verification_time
325                && crl
326                    .next_update()
327                    .is_none_or(|next| verification_time <= next);
328            if !time_valid || crl.verify_signature(issuer.public_key()).is_err() {
329                return Err(X509ChainError::InvalidCrl(*crl_index));
330            }
331            if crl.iter_revoked_certificates().any(|revoked| {
332                revoked.raw_serial() == cert.raw_serial()
333                    && revoked.revocation_date <= verification_time
334            }) {
335                return Err(X509ChainError::Revoked(position));
336            }
337        }
338    }
339    Ok(())
340}