Skip to main content

x509_validator/
validator.rs

1use std::fmt;
2
3use crate::crypto::SignatureVerifier;
4use crate::diagnostic::VerificationDiagnostic;
5use crate::store::CertificateStore;
6use crate::unverified_chain::UnverifiedCertificateChain;
7use crate::validated_chain::ValidatedCertificateChain;
8use crate::{Certificate, CertificateExt, PolicyFailureReason, ValidationPolicy};
9
10/// Reports a diagnostic to an optional callback, building it only if a callback
11/// is present.
12macro_rules! diagnose {
13    ($diagnostics:expr, $diagnostic:expr) => {
14        if let Some(callback) = $diagnostics.as_deref_mut() {
15            callback($diagnostic);
16        }
17    };
18}
19
20/// Validates an X.509 certificate chain against a set of root certificates and
21/// a [`ValidationPolicy`], using the crypto backend selected by this crate's
22/// feature flags.
23///
24/// ```ignore
25/// let validator = x509_validator::Validator::with_policy(roots, policy);
26/// ```
27pub struct Validator<'a, P> {
28    /// The trusted root certificates used to anchor chain validation.
29    root_certificates: CertificateStore<'a>,
30    crypto: &'a dyn SignatureVerifier,
31    /// The policy applied to candidate chains during validation.
32    policy: P,
33}
34
35impl<'a, P> Validator<'a, P>
36where
37    P: ValidationPolicy,
38{
39    /// Creates a validator with the given root certificates, policy and backend.
40    ///
41    /// - Parameters:
42    ///   - root_certificates: The trusted root certificates.
43    ///   - policy: The verification policy.
44    pub fn with_policy_and_backend(
45        root_certificates: CertificateStore<'a>,
46        policy: P,
47        crypto: &'a dyn SignatureVerifier,
48    ) -> Self {
49        Self {
50            root_certificates,
51            crypto,
52            policy,
53        }
54    }
55
56    /// Creates a validator with the given root certificates and policy, using
57    /// the crypto backend selected by this crate's feature flags.
58    ///
59    /// - Parameters:
60    ///   - root_certificates: The trusted root certificates.
61    ///   - policy: The verification policy.
62    ///
63    /// # Panics
64    ///
65    /// Validation panics unless exactly one backend feature is enabled, since
66    /// no single default backend can be determined otherwise; see
67    /// [`crate::crypto::default_provider`].
68    pub fn with_policy(root_certificates: CertificateStore<'a>, policy: P) -> Self {
69        Self::with_policy_and_backend(root_certificates, policy, crate::crypto::default_provider())
70    }
71
72    /// Validates a leaf certificate by building chains through intermediate certificates to the root store.
73    ///
74    /// - Parameters:
75    ///   - leaf: The leaf certificate to validate.
76    ///   - intermediates: A store of intermediate certificates that may form part of the chain.
77    /// - Returns: A [`ChainValidationResult`] indicating whether the certificate is valid.
78    pub fn validate(
79        &self,
80        leaf: &Certificate<'a>,
81        intermediates: &CertificateStore<'a>,
82    ) -> ChainValidationResult<'a> {
83        self.validate_inner(leaf, intermediates, None)
84    }
85
86    /// Validates a leaf certificate by building chains through intermediate certificates to the root store,
87    /// reporting each step to a callback.
88    ///
89    /// - Parameters:
90    ///   - leaf: The leaf certificate to validate.
91    ///   - intermediates: A store of intermediate certificates that may form part of the chain.
92    ///   - diagnostic_callback: A closure invoked with diagnostic events during validation.
93    /// - Returns: A [`ChainValidationResult`] indicating whether the certificate is valid.
94    pub fn validate_with_diagnostics(
95        &self,
96        leaf: &Certificate<'a>,
97        intermediates: &CertificateStore<'a>,
98        diagnostic_callback: &mut dyn FnMut(VerificationDiagnostic<'a>),
99    ) -> ChainValidationResult<'a> {
100        self.validate_inner(leaf, intermediates, Some(diagnostic_callback))
101    }
102
103    fn validate_inner(
104        &self,
105        leaf: &Certificate<'a>,
106        intermediates: &CertificateStore<'a>,
107        mut diagnostics: Option<&mut dyn FnMut(VerificationDiagnostic<'a>)>,
108    ) -> ChainValidationResult<'a> {
109        // First check: does this leaf certificate contain critical extensions that are not satisfied by the policy?
110        // If so, reject the chain.
111        if has_unhandled_critical_extensions(leaf, &self.policy) {
112            diagnose!(
113                diagnostics,
114                VerificationDiagnostic::leaf_certificate_has_unhandled_critical_extension(
115                    leaf.clone(),
116                    self.policy
117                        .verifying_critical_extensions(),
118                )
119            );
120            return Err(vec![PolicyFailure::new(
121                UnverifiedCertificateChain::new(vec![leaf.clone()]),
122                PolicyFailureReason::new("leaf certificate has unhandled critical extension"),
123            )]);
124        }
125
126        let mut policy_failures = Vec::new();
127
128        // Second check: is this leaf _already in_ the certificate store? If it is, we can just trust it directly.
129        //
130        // Note that this requires an _exact match_: if there isn't an exact match, we'll fall back to chain building,
131        // which may let us chain through another variant of this certificate and build a valid chain. This is a very
132        // deliberate choice: certificates that assert the same combination of (subject, public key, SAN) but different
133        // extensions or policies should not be tolerated by this check, and will be ignored.
134        let leaf_key = leaf.subject_key();
135        if self
136            .root_certificates
137            .find_by_subject(&leaf_key)
138            .iter()
139            .any(|c| c == leaf)
140        {
141            let chain = UnverifiedCertificateChain::new(vec![leaf.clone()]);
142            match self
143                .policy
144                .chain_meets_policy_requirements(&chain)
145            {
146                Ok(()) => {
147                    // We're good!
148                    diagnose!(
149                        diagnostics,
150                        VerificationDiagnostic::found_valid_certificate_chain(vec![leaf.clone()])
151                    );
152                    return Ok(ValidatedCertificateChain::new_unchecked(vec![leaf.clone()]));
153                }
154                Err(reason) => {
155                    diagnose!(
156                        diagnostics,
157                        VerificationDiagnostic::leaf_certificate_is_in_the_root_store_but_does_not_meet_policy(leaf.clone(), reason.clone())
158                    );
159                    policy_failures.push(PolicyFailure::new(chain, reason));
160                }
161            }
162        }
163
164        let mut stack: Vec<Vec<Certificate<'a>>> = vec![vec![leaf.clone()]];
165
166        // This is essentially a DFS of the certificate tree. We attempt to iteratively build up possible chains.
167        while let Some(partial_chain) = stack.pop() {
168            diagnose!(
169                diagnostics,
170                VerificationDiagnostic::searching_for_issuer_of_partial_chain(
171                    partial_chain.clone(),
172                )
173            );
174
175            let tip = partial_chain.last().unwrap();
176            let issuer_key = tip.issuer_key();
177
178            // We want to search for parents. Our preferred parent comes from the root store, as this will potentially
179            // produce smaller chains.
180            let mut root_candidates = self
181                .root_certificates
182                .find_by_subject(&issuer_key)
183                .to_vec();
184            // We then want to sort by suitability.
185            sort_by_suitability_for_issuing(&mut root_candidates, tip);
186            if !root_candidates.is_empty() {
187                diagnose!(
188                    diagnostics,
189                    VerificationDiagnostic::found_candidate_issuers_of_partial_chain_in_root_store(
190                        partial_chain.clone(),
191                        root_candidates.clone(),
192                    )
193                );
194            }
195            // Each of these is now potentially a valid unverified chain.
196            for candidate in &root_candidates {
197                if should_skip_adding_certificate(
198                    candidate,
199                    &partial_chain,
200                    self.crypto,
201                    &self.policy,
202                    &mut diagnostics,
203                ) {
204                    continue;
205                }
206                let mut chain_certs = partial_chain.clone();
207                chain_certs.push(candidate.clone());
208                let chain = UnverifiedCertificateChain::new(chain_certs.clone());
209                match self
210                    .policy
211                    .chain_meets_policy_requirements(&chain)
212                {
213                    Ok(()) => {
214                        // We're good!
215                        diagnose!(
216                            diagnostics,
217                            VerificationDiagnostic::found_valid_certificate_chain(
218                                chain_certs.clone(),
219                            )
220                        );
221                        return Ok(ValidatedCertificateChain::new_unchecked(chain_certs));
222                    }
223                    Err(reason) => {
224                        diagnose!(
225                            diagnostics,
226                            VerificationDiagnostic::chain_fails_to_meet_policy(
227                                chain_certs,
228                                reason.clone(),
229                            )
230                        );
231                        policy_failures.push(PolicyFailure::new(chain, reason));
232                    }
233                }
234            }
235
236            let mut intermediate_candidates = intermediates
237                .find_by_subject(&issuer_key)
238                .to_vec();
239            // We then want to sort by suitability.
240            sort_by_suitability_for_issuing(&mut intermediate_candidates, tip);
241            if !intermediate_candidates.is_empty() {
242                diagnose!(
243                    diagnostics,
244                    VerificationDiagnostic::found_candidate_issuers_of_partial_chain_in_intermediate_store(
245                        partial_chain.clone(),
246                        intermediate_candidates.clone(),
247                    )
248                );
249            }
250            // we need to reverse the order of the already sorted intermediates because
251            // we will push them on to the `stack` which in turn will
252            // consume them in the reverse order that they have been pushed onto the stack
253            for candidate in intermediate_candidates
254                .into_iter()
255                .rev()
256            {
257                if should_skip_adding_certificate(
258                    &candidate,
259                    &partial_chain,
260                    self.crypto,
261                    &self.policy,
262                    &mut diagnostics,
263                ) {
264                    continue;
265                }
266                let mut next = partial_chain.clone();
267                next.push(candidate);
268                stack.push(next);
269            }
270        }
271
272        diagnose!(
273            diagnostics,
274            VerificationDiagnostic::could_not_validate_leaf_certificate(leaf.clone())
275        );
276        Err(policy_failures)
277    }
278}
279
280fn has_unhandled_critical_extensions(
281    cert: &Certificate<'_>,
282    policy: &impl ValidationPolicy,
283) -> bool {
284    let handled = policy.verifying_critical_extensions();
285    cert.tbs_certificate
286        .iter_extensions()
287        .any(|ext| ext.critical && !handled.contains(&ext.oid))
288}
289
290fn sort_by_suitability_for_issuing<'a>(
291    candidates: &mut [Certificate<'a>],
292    subject: &Certificate<'a>,
293) {
294    // First, an early exit. If the subject doesn't have an AKI extension, we don't need
295    // to do anything.
296    let subject_aki = subject.authority_key_identifier();
297
298    // Medium preference if we have no SKI. The SKI is present: if the two match, this is
299    // higher preference; if they don't match, it's lower.
300    let rank = |candidate: &Certificate<'a>| -> u8 {
301        match (subject_aki, candidate.subject_key_identifier()) {
302            (Some(aki), Some(ski)) if aki == ski => 0,
303            (_, None) => 1,
304            (None, Some(_)) => 1,
305            (Some(_), Some(_)) => 2,
306        }
307    };
308
309    candidates.sort_by_key(rank);
310}
311
312fn should_skip_adding_certificate<'a>(
313    candidate: &Certificate<'a>,
314    partial_chain: &[Certificate<'a>],
315    crypto: &dyn SignatureVerifier,
316    policy: &impl ValidationPolicy,
317    diagnostics: &mut Option<&mut dyn FnMut(VerificationDiagnostic<'a>)>,
318) -> bool {
319    // We want to confirm that the certificate has no unhandled critical extensions. If it does, we can't build the chain.
320    if has_unhandled_critical_extensions(candidate, policy) {
321        diagnose!(
322            diagnostics,
323            VerificationDiagnostic::issuer_has_unhandled_critical_extension(
324                candidate.clone(),
325                partial_chain.to_vec(),
326                policy.verifying_critical_extensions(),
327            )
328        );
329        return true;
330    }
331
332    // We don't want to re-add the same certificate to the chain: that will always produce a chain that
333    // could have been shorter.
334    if partial_chain
335        .iter()
336        .any(|existing| existing.has_same_identity_as(candidate))
337    {
338        diagnose!(
339            diagnostics,
340            VerificationDiagnostic::issuer_is_already_in_the_chain(
341                partial_chain.to_vec(),
342                candidate.clone(),
343            )
344        );
345        return true;
346    }
347
348    // We check the signature here: if the signature isn't valid, don't try to apply policy.
349    let tip = partial_chain.last().unwrap();
350    let signature_verifies = crypto
351        .verify_signature(
352            &tip.signature_algorithm,
353            candidate.public_key(),
354            tip.tbs_certificate.as_ref(),
355            tip.signature_value.as_ref(),
356        )
357        .is_ok();
358
359    if !signature_verifies {
360        diagnose!(
361            diagnostics,
362            VerificationDiagnostic::issuer_has_not_signed_certificate(
363                candidate.clone(),
364                partial_chain.to_vec(),
365            )
366        );
367    }
368
369    !signature_verifies
370}
371
372/// The result of validating a certificate chain.
373///
374/// The error case carries every chain that was built and rejected, each with
375/// the reason it was rejected, in the order the implementation considered them.
376pub type ChainValidationResult<'a> = Result<ValidatedCertificateChain<'a>, Vec<PolicyFailure<'a>>>;
377
378/// A chain that was built but rejected by policy, and why.
379#[derive(Clone)]
380pub struct PolicyFailure<'a> {
381    pub chain: UnverifiedCertificateChain<'a>,
382    pub policy_failure_reason: PolicyFailureReason,
383}
384
385impl<'a> PolicyFailure<'a> {
386    pub fn new(
387        chain: UnverifiedCertificateChain<'a>,
388        policy_failure_reason: PolicyFailureReason,
389    ) -> Self {
390        Self {
391            chain,
392            policy_failure_reason,
393        }
394    }
395}
396
397impl fmt::Display for PolicyFailure<'_> {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        write!(f, "{}", self.policy_failure_reason)
400    }
401}
402
403impl fmt::Debug for PolicyFailure<'_> {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        write!(
406            f,
407            "{} (chain of {})",
408            self.policy_failure_reason,
409            self.chain.len()
410        )
411    }
412}