1use 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#[derive(Debug, Clone)]
14pub struct X509ChainOptions<'a> {
15 pub trusted_certs: &'a [Vec<u8>],
17 pub verification_time: SystemTime,
19 pub max_chain_depth: usize,
21 pub check_crls: bool,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
27#[non_exhaustive]
28pub enum X509ChainError {
29 #[error("maximum certificate chain depth must be greater than zero")]
31 InvalidDepth,
32 #[error("invalid {kind} DER: {message}")]
34 InvalidDer {
35 kind: &'static str,
37 message: String,
39 },
40 #[error("certificate chain does not terminate at a trusted certificate")]
42 UntrustedRoot,
43 #[error("certificate chain exceeds maximum depth of {0}")]
45 DepthExceeded(usize),
46 #[error("certificate at chain position {0} is expired or not yet valid")]
48 CertificateNotValid(usize),
49 #[error("certificate at chain position {0} is not a CA")]
51 IssuerNotCa(usize),
52 #[error("certificate at chain position {position} exceeds path length constraint {limit}")]
54 PathLengthExceeded {
55 position: usize,
57 limit: u32,
59 },
60 #[error("certificate at chain position {position} does not permit {required}")]
62 InvalidKeyUsage {
63 position: usize,
65 required: &'static str,
67 },
68 #[error("certificate signature at chain position {0} is invalid or unsupported")]
70 InvalidSignature(usize),
71 #[error("CRL {0} is invalid or cannot be authenticated")]
73 InvalidCrl(usize),
74 #[error("certificate at chain position {0} is revoked")]
76 Revoked(usize),
77}
78
79pub 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 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}