Skip to main content

noxtls_x509/certs/
validate.rs

1// Copyright (c) 2019-2026, Argenox Technologies LLC
2// All rights reserved.
3//
4// SPDX-License-Identifier: GPL-2.0-only OR LicenseRef-Argenox-Commercial-License
5//
6// This file is part of the NoxTLS Library.
7//
8// This program is free software: you can redistribute it and/or modify
9// it under the terms of the GNU General Public License as published by the
10// Free Software Foundation; version 2 of the License.
11//
12// Alternatively, this file may be used under the terms of a commercial
13// license from Argenox Technologies LLC.
14//
15// See `noxtls/LICENSE` and `noxtls/LICENSE.md` in this repository for full details.
16// CONTACT: info@argenox.com
17
18use core::fmt::{Display, Formatter};
19
20#[cfg(not(feature = "std"))]
21use crate::internal_alloc::ToOwned;
22use crate::internal_alloc::{String, Vec};
23
24use noxtls_crypto::{
25    ed25519_verify, mldsa_verify, p256_ecdsa_verify_sha256, rsassa_pss_sha256_verify,
26    rsassa_pss_sha384_verify, rsassa_sha256_verify, rsassa_sha384_verify, rsassa_sha512_verify,
27    Ed25519PublicKey, MlDsaPublicKey, P256PublicKey, RsaPublicKey, OID_ID_MLDSA65,
28};
29
30use super::{parse_der_node, Certificate};
31
32/// Describes why certificate path validation failed.
33#[derive(Debug, Clone, Eq, PartialEq)]
34pub enum ValidationError {
35    InvalidNowTimeFormat,
36    CertificateNotYetValid,
37    CertificateExpired,
38    IssuerNotFound,
39    IssuerNotCa,
40    IssuerMissingKeyCertSign,
41    PathLenExceeded,
42    UntrustedRoot,
43    ChainLoopDetected,
44    MaxChainDepthExceeded,
45    SignatureAlgorithmMismatch,
46    UnsupportedSignatureAlgorithm,
47    UnsupportedPublicKeyAlgorithm,
48    PublicKeyDecodeFailed,
49    SignatureVerificationFailed,
50    MissingRequiredPolicy,
51    MissingRequiredExtendedKeyUsage,
52    ExplicitPolicyRequired,
53    PolicyMappingInhibited,
54    NameConstraintsViolation,
55    MissingRevocationInfo,
56    MissingRevocationLocator,
57}
58
59impl Display for ValidationError {
60    // Formats validation errors as stable human-readable messages.
61    // Parameter: `f` formatter sink for error string output.
62    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
63        match self {
64            Self::InvalidNowTimeFormat => f.write_str("invalid now timestamp format"),
65            Self::CertificateNotYetValid => f.write_str("certificate is not yet valid"),
66            Self::CertificateExpired => f.write_str("certificate is expired"),
67            Self::IssuerNotFound => f.write_str("certificate issuer not found"),
68            Self::IssuerNotCa => f.write_str("issuer certificate is not a CA"),
69            Self::IssuerMissingKeyCertSign => {
70                f.write_str("issuer certificate key usage missing keyCertSign")
71            }
72            Self::PathLenExceeded => f.write_str("issuer pathLenConstraint exceeded"),
73            Self::UntrustedRoot => {
74                f.write_str("certificate chain does not terminate at trust anchor")
75            }
76            Self::ChainLoopDetected => f.write_str("certificate chain loop detected"),
77            Self::MaxChainDepthExceeded => f.write_str("certificate chain depth exceeded"),
78            Self::SignatureAlgorithmMismatch => {
79                f.write_str("certificate signature algorithm mismatch")
80            }
81            Self::UnsupportedSignatureAlgorithm => {
82                f.write_str("certificate signature algorithm is unsupported")
83            }
84            Self::UnsupportedPublicKeyAlgorithm => {
85                f.write_str("issuer public key algorithm is unsupported")
86            }
87            Self::PublicKeyDecodeFailed => f.write_str("issuer public key decode failed"),
88            Self::SignatureVerificationFailed => {
89                f.write_str("certificate signature verification failed")
90            }
91            Self::MissingRequiredPolicy => f.write_str("certificate missing required policy OID"),
92            Self::MissingRequiredExtendedKeyUsage => {
93                f.write_str("certificate missing required extended key usage")
94            }
95            Self::ExplicitPolicyRequired => {
96                f.write_str("effective certificate policy set is empty")
97            }
98            Self::PolicyMappingInhibited => {
99                f.write_str("certificate policyMappings present while policy mapping is inhibited")
100            }
101            Self::NameConstraintsViolation => {
102                f.write_str("certificate subject violates issuer name constraints")
103            }
104            Self::MissingRevocationInfo => {
105                f.write_str("certificate missing revocation distribution info")
106            }
107            Self::MissingRevocationLocator => {
108                f.write_str("certificate missing CRL distribution point and AIA locator")
109            }
110        }
111    }
112}
113
114#[cfg(feature = "std")]
115impl std::error::Error for ValidationError {}
116
117/// Summarizes key properties of a validated certificate chain.
118#[derive(Debug, Clone, Eq, PartialEq)]
119pub struct ValidationReport {
120    pub chain_len: usize,
121    pub trust_anchor_index: usize,
122    pub effective_policy_oids: Vec<Vec<u8>>,
123}
124
125/// Controls optional policy and revocation-related path validation requirements.
126#[derive(Debug, Clone, Eq, PartialEq, Default)]
127pub struct ValidationOptions {
128    pub required_policy_oid: Option<Vec<u8>>,
129    pub required_extended_key_usage_oid: Option<Vec<u8>>,
130    pub require_explicit_policy: bool,
131    pub require_crl_distribution_points: bool,
132    pub require_revocation_locator: bool,
133    pub inhibit_policy_mapping: bool,
134}
135
136/// Validates certificate chain with signature enforcement at each hop.
137///
138/// # Arguments
139/// * `leaf`: End-entity certificate to validate.
140/// * `intermediates`: Candidate intermediate issuer certificates.
141/// * `trust_anchors`: Trusted root certificates.
142/// * `now`: Validation time string (UTCTime or GeneralizedTime).
143///
144/// # Returns
145/// `ValidationReport` when path building and checks succeed.
146pub fn validate_certificate_chain<'a>(
147    leaf: &Certificate<'a>,
148    intermediates: &[Certificate<'a>],
149    trust_anchors: &[Certificate<'a>],
150    now: &str,
151) -> core::result::Result<ValidationReport, ValidationError> {
152    validate_certificate_chain_with_options(
153        leaf,
154        intermediates,
155        trust_anchors,
156        now,
157        &ValidationOptions::default(),
158    )
159}
160
161/// Validates certificate chain with caller-provided policy/revocation options.
162///
163/// # Arguments
164/// * `leaf`: End-entity certificate to validate.
165/// * `intermediates`: Candidate intermediate issuer certificates.
166/// * `trust_anchors`: Trusted root certificates.
167/// * `now`: Validation time string (UTCTime or GeneralizedTime).
168/// * `options`: Additional policy and revocation requirements.
169///
170/// # Returns
171/// `ValidationReport` when validation succeeds under `options`.
172pub fn validate_certificate_chain_with_options<'a>(
173    leaf: &Certificate<'a>,
174    intermediates: &[Certificate<'a>],
175    trust_anchors: &[Certificate<'a>],
176    now: &str,
177    options: &ValidationOptions,
178) -> core::result::Result<ValidationReport, ValidationError> {
179    validate_certificate_chain_internal(leaf, intermediates, trust_anchors, now, true, options)
180}
181
182/// Validates certificate path constraints without enforcing signature checks.
183///
184/// # Arguments
185/// * `leaf`: End-entity certificate to validate.
186/// * `intermediates`: Candidate intermediate issuer certificates.
187/// * `trust_anchors`: Trusted root certificates.
188/// * `now`: Validation time string (UTCTime or GeneralizedTime).
189///
190/// # Returns
191/// `ValidationReport` when constraint checks succeed.
192pub fn validate_certificate_chain_constraints_only<'a>(
193    leaf: &Certificate<'a>,
194    intermediates: &[Certificate<'a>],
195    trust_anchors: &[Certificate<'a>],
196    now: &str,
197) -> core::result::Result<ValidationReport, ValidationError> {
198    validate_certificate_chain_internal(
199        leaf,
200        intermediates,
201        trust_anchors,
202        now,
203        false,
204        &ValidationOptions::default(),
205    )
206}
207
208/// Validates certificate chain with explicit strict-signature naming for callers.
209///
210/// # Arguments
211/// * `leaf`: End-entity certificate to validate.
212/// * `intermediates`: Candidate intermediate issuer certificates.
213/// * `trust_anchors`: Trusted root certificates.
214/// * `now`: Validation time string (UTCTime or GeneralizedTime).
215///
216/// # Returns
217/// `ValidationReport` when strict chain validation succeeds.
218pub fn validate_certificate_chain_strict<'a>(
219    leaf: &Certificate<'a>,
220    intermediates: &[Certificate<'a>],
221    trust_anchors: &[Certificate<'a>],
222    now: &str,
223) -> core::result::Result<ValidationReport, ValidationError> {
224    validate_certificate_chain(leaf, intermediates, trust_anchors, now)
225}
226
227/// Shared X.509 chain validation implementation with optional signature enforcement.
228///
229/// # Arguments
230///
231/// * `leaf` — End-entity certificate being validated.
232/// * `intermediates` — Candidate intermediate certificates.
233/// * `trust_anchors` — Trusted anchor certificates (must be non-empty).
234/// * `now` — Validation time string (UTCTime or GeneralizedTime text).
235/// * `enforce_signatures` — When `true`, issuer signatures are verified while walking the chain.
236/// * `options` — Policy, EKU, and revocation knobs applied during validation.
237///
238/// # Returns
239///
240/// On success, a populated [`ValidationReport`].
241///
242/// # Errors
243///
244/// Returns [`ValidationError`] when the chain cannot be built, policy checks fail, or signatures do not verify.
245///
246/// # Panics
247///
248/// This function does not panic.
249fn validate_certificate_chain_internal<'a>(
250    leaf: &Certificate<'a>,
251    intermediates: &[Certificate<'a>],
252    trust_anchors: &[Certificate<'a>],
253    now: &str,
254    enforce_signatures: bool,
255    options: &ValidationOptions,
256) -> core::result::Result<ValidationReport, ValidationError> {
257    if trust_anchors.is_empty() {
258        return Err(ValidationError::UntrustedRoot);
259    }
260
261    let now_canonical = canonical_time(now).ok_or(ValidationError::InvalidNowTimeFormat)?;
262    validate_chain_step(
263        leaf,
264        intermediates,
265        trust_anchors,
266        &now_canonical,
267        enforce_signatures,
268        options,
269        1,
270        0,
271        0,
272        ChainValidationState {
273            visited_serials: Vec::new(),
274            effective_policy_oids: None,
275            explicit_policy_skip_certs: if options.require_explicit_policy {
276                Some(0)
277            } else {
278                None
279            },
280            inhibit_any_policy_skip_certs: None,
281            inhibit_policy_mapping_skip_certs: if options.inhibit_policy_mapping {
282                Some(0)
283            } else {
284                None
285            },
286        },
287    )
288}
289
290#[derive(Clone, Debug, Default)]
291struct ChainValidationState {
292    visited_serials: Vec<Vec<u8>>,
293    effective_policy_oids: Option<Vec<Vec<u8>>>,
294    explicit_policy_skip_certs: Option<u32>,
295    inhibit_any_policy_skip_certs: Option<u32>,
296    inhibit_policy_mapping_skip_certs: Option<u32>,
297}
298
299/// Recursively validates one chain step and backtracks across issuer candidates.
300///
301/// # Arguments
302///
303/// * `current` — Certificate under inspection at this depth.
304/// * `intermediates` — Pool of intermediate issuers still available.
305/// * `trust_anchors` — Trusted anchors for terminal issuer resolution.
306/// * `now_canonical` — Canonicalized comparison time (`YYYYMMDDHHMMSSZ`).
307/// * `enforce_signatures` — Whether to verify `current` against its selected issuer.
308/// * `options` — Validation options controlling policies and revocation checks.
309/// * `chain_len` — 1-based depth of `current` from the leaf.
310/// * `ca_hops_below_issuer` — Remaining CA hops permitted under `pathLenConstraint` for the pending issuer.
311/// * `hop_count` — Recursion guard counting traversal steps.
312/// * `state` — Mutable policy and visited-serial state carried through recursion.
313///
314/// # Returns
315///
316/// On success, a [`ValidationReport`] once a trusted anchor is reached.
317///
318/// # Errors
319///
320/// Returns [`ValidationError`] on depth limits, time window violations, unsupported algorithms, or exhausted candidates.
321///
322/// # Panics
323///
324/// This function does not panic.
325fn validate_chain_step<'a>(
326    current: &Certificate<'a>,
327    intermediates: &[Certificate<'a>],
328    trust_anchors: &[Certificate<'a>],
329    now_canonical: &str,
330    enforce_signatures: bool,
331    options: &ValidationOptions,
332    chain_len: usize,
333    ca_hops_below_issuer: usize,
334    hop_count: usize,
335    mut state: ChainValidationState,
336) -> core::result::Result<ValidationReport, ValidationError> {
337    const OID_ANY_POLICY: &[u8] = &[0x55, 0x1d, 0x20, 0x00];
338    if hop_count > 16 {
339        return Err(ValidationError::MaxChainDepthExceeded);
340    }
341
342    validate_time(current, now_canonical)?;
343    validate_policy_and_revocation(current, options, chain_len == 1)?;
344    update_inhibit_any_policy_skip_certs(&mut state.inhibit_any_policy_skip_certs, current);
345    update_effective_policies(
346        &mut state.effective_policy_oids,
347        current,
348        explicit_policy_is_active(state.inhibit_any_policy_skip_certs),
349        OID_ANY_POLICY,
350    );
351    update_explicit_policy_skip_certs(&mut state.explicit_policy_skip_certs, current);
352    enforce_explicit_policy_progress(
353        &state.effective_policy_oids,
354        state.explicit_policy_skip_certs,
355    )?;
356    update_inhibit_policy_mapping_skip_certs(&mut state.inhibit_policy_mapping_skip_certs, current);
357    enforce_policy_mapping(current, state.inhibit_policy_mapping_skip_certs)?;
358    if state
359        .visited_serials
360        .iter()
361        .any(|serial| serial.as_slice() == current.serial.as_slice())
362    {
363        return Err(ValidationError::ChainLoopDetected);
364    }
365    state.visited_serials.push(current.serial.clone());
366
367    if current.subject_raw == current.issuer_raw {
368        let anchor_idx = trust_anchors
369            .iter()
370            .position(|anchor| anchor.subject_raw == current.subject_raw)
371            .ok_or(ValidationError::UntrustedRoot)?;
372        let policies = finalize_effective_policies(
373            &state.effective_policy_oids,
374            state.explicit_policy_skip_certs,
375        )?;
376        return Ok(ValidationReport {
377            chain_len,
378            trust_anchor_index: anchor_idx,
379            effective_policy_oids: policies,
380        });
381    }
382
383    if let Some(anchor_idx) = trust_anchors
384        .iter()
385        .position(|anchor| anchor.subject_raw == current.issuer_raw)
386    {
387        let issuer = &trust_anchors[anchor_idx];
388        validate_time(issuer, now_canonical)?;
389        validate_policy_and_revocation(issuer, options, false)?;
390        apply_policy_mappings_for_issuer(&mut state.effective_policy_oids, issuer);
391        update_inhibit_any_policy_skip_certs(&mut state.inhibit_any_policy_skip_certs, issuer);
392        update_effective_policies(
393            &mut state.effective_policy_oids,
394            issuer,
395            explicit_policy_is_active(state.inhibit_any_policy_skip_certs),
396            OID_ANY_POLICY,
397        );
398        update_explicit_policy_skip_certs(&mut state.explicit_policy_skip_certs, issuer);
399        enforce_explicit_policy_progress(
400            &state.effective_policy_oids,
401            state.explicit_policy_skip_certs,
402        )?;
403        update_inhibit_policy_mapping_skip_certs(
404            &mut state.inhibit_policy_mapping_skip_certs,
405            issuer,
406        );
407        enforce_policy_mapping(current, state.inhibit_policy_mapping_skip_certs)?;
408        enforce_policy_mapping(issuer, state.inhibit_policy_mapping_skip_certs)?;
409        validate_issuer_constraints(issuer, ca_hops_below_issuer)?;
410        validate_name_constraints(issuer, current)?;
411        if enforce_signatures {
412            verify_certificate_signature(current, issuer)?;
413        }
414        let policies = finalize_effective_policies(
415            &state.effective_policy_oids,
416            state.explicit_policy_skip_certs,
417        )?;
418        return Ok(ValidationReport {
419            chain_len: chain_len + 1,
420            trust_anchor_index: anchor_idx,
421            effective_policy_oids: policies,
422        });
423    }
424
425    let issuer_candidates: Vec<&Certificate<'a>> = intermediates
426        .iter()
427        .filter(|candidate| candidate.subject_raw == current.issuer_raw)
428        .collect();
429    if issuer_candidates.is_empty() {
430        return Err(ValidationError::IssuerNotFound);
431    }
432
433    let mut last_error = ValidationError::IssuerNotFound;
434    for issuer in issuer_candidates {
435        let mut next_state = state.clone();
436        let candidate_result = (|| -> core::result::Result<ValidationReport, ValidationError> {
437            validate_time(issuer, now_canonical)?;
438            validate_policy_and_revocation(issuer, options, false)?;
439            apply_policy_mappings_for_issuer(&mut next_state.effective_policy_oids, issuer);
440            update_inhibit_any_policy_skip_certs(
441                &mut next_state.inhibit_any_policy_skip_certs,
442                issuer,
443            );
444            update_effective_policies(
445                &mut next_state.effective_policy_oids,
446                issuer,
447                explicit_policy_is_active(next_state.inhibit_any_policy_skip_certs),
448                OID_ANY_POLICY,
449            );
450            update_explicit_policy_skip_certs(&mut next_state.explicit_policy_skip_certs, issuer);
451            enforce_explicit_policy_progress(
452                &next_state.effective_policy_oids,
453                next_state.explicit_policy_skip_certs,
454            )?;
455            update_inhibit_policy_mapping_skip_certs(
456                &mut next_state.inhibit_policy_mapping_skip_certs,
457                issuer,
458            );
459            enforce_policy_mapping(current, next_state.inhibit_policy_mapping_skip_certs)?;
460            enforce_policy_mapping(issuer, next_state.inhibit_policy_mapping_skip_certs)?;
461            validate_issuer_constraints(issuer, ca_hops_below_issuer)?;
462            validate_name_constraints(issuer, current)?;
463            if enforce_signatures {
464                verify_certificate_signature(current, issuer)?;
465            }
466            decrement_skip_certs_counter(&mut next_state.inhibit_any_policy_skip_certs, current);
467            decrement_skip_certs_counter(&mut next_state.explicit_policy_skip_certs, current);
468            decrement_skip_certs_counter(
469                &mut next_state.inhibit_policy_mapping_skip_certs,
470                current,
471            );
472
473            validate_chain_step(
474                issuer,
475                intermediates,
476                trust_anchors,
477                now_canonical,
478                enforce_signatures,
479                options,
480                chain_len + 1,
481                ca_hops_below_issuer + 1,
482                hop_count + 1,
483                next_state,
484            )
485        })();
486        match candidate_result {
487            Ok(report) => return Ok(report),
488            Err(err) => last_error = err,
489        }
490    }
491    Err(last_error)
492}
493
494/// Verifies one certificate signature using issuer public key material.
495///
496/// # Arguments
497/// * `certificate`: Certificate whose signature should be verified.
498/// * `issuer`: Issuer certificate providing public key material.
499///
500/// # Returns
501/// `Ok(())` when signature verification succeeds.
502pub fn verify_certificate_signature(
503    certificate: &Certificate<'_>,
504    issuer: &Certificate<'_>,
505) -> core::result::Result<(), ValidationError> {
506    const OID_RSA_ENCRYPTION: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
507    const OID_EC_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01];
508    const OID_SHA256_WITH_RSA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b];
509    const OID_SHA384_WITH_RSA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0c];
510    const OID_SHA512_WITH_RSA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0d];
511    const OID_RSASSA_PSS: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a];
512    const OID_ECDSA_WITH_SHA256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02];
513    const OID_ED25519: &[u8] = &[0x2b, 0x65, 0x70];
514
515    if certificate.tbs_signature_algorithm_oid != certificate.certificate_signature_algorithm_oid {
516        return Err(ValidationError::SignatureAlgorithmMismatch);
517    }
518    if issuer.subject_public_key_algorithm_oid == OID_RSA_ENCRYPTION {
519        let (n, e) = parse_rsa_public_key_der(&issuer.subject_public_key)?;
520        let public_key = RsaPublicKey::from_be_bytes(&n, &e)
521            .map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
522        if certificate.certificate_signature_algorithm_oid == OID_SHA256_WITH_RSA {
523            return rsassa_sha256_verify(
524                &public_key,
525                certificate.raw_tbs_der,
526                &certificate.signature_value,
527            )
528            .map_err(|_| ValidationError::SignatureVerificationFailed);
529        }
530        if certificate.certificate_signature_algorithm_oid == OID_SHA384_WITH_RSA {
531            return rsassa_sha384_verify(
532                &public_key,
533                certificate.raw_tbs_der,
534                &certificate.signature_value,
535            )
536            .map_err(|_| ValidationError::SignatureVerificationFailed);
537        }
538        if certificate.certificate_signature_algorithm_oid == OID_SHA512_WITH_RSA {
539            return rsassa_sha512_verify(
540                &public_key,
541                certificate.raw_tbs_der,
542                &certificate.signature_value,
543            )
544            .map_err(|_| ValidationError::SignatureVerificationFailed);
545        }
546        if certificate.certificate_signature_algorithm_oid == OID_RSASSA_PSS {
547            // Until RSASSA-PSS parameters are parsed from AlgorithmIdentifier, accept SHA-256
548            // and SHA-384 common profiles by trying the expected default salt lengths.
549            if rsassa_pss_sha256_verify(
550                &public_key,
551                certificate.raw_tbs_der,
552                &certificate.signature_value,
553                32,
554            )
555            .is_ok()
556            {
557                return Ok(());
558            }
559            return rsassa_pss_sha384_verify(
560                &public_key,
561                certificate.raw_tbs_der,
562                &certificate.signature_value,
563                48,
564            )
565            .map_err(|_| ValidationError::SignatureVerificationFailed);
566        }
567        return Err(ValidationError::UnsupportedSignatureAlgorithm);
568    }
569
570    if issuer.subject_public_key_algorithm_oid == OID_EC_PUBLIC_KEY {
571        if certificate.certificate_signature_algorithm_oid != OID_ECDSA_WITH_SHA256 {
572            return Err(ValidationError::UnsupportedSignatureAlgorithm);
573        }
574        let public_key = P256PublicKey::from_uncompressed(&issuer.subject_public_key)
575            .map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
576        let (r, s) = parse_ecdsa_signature_der(&certificate.signature_value)?;
577        return p256_ecdsa_verify_sha256(&public_key, certificate.raw_tbs_der, &r, &s)
578            .map_err(|_| ValidationError::SignatureVerificationFailed);
579    }
580
581    if issuer.subject_public_key_algorithm_oid.as_slice() == OID_ED25519 {
582        if certificate.certificate_signature_algorithm_oid.as_slice() != OID_ED25519 {
583            return Err(ValidationError::UnsupportedSignatureAlgorithm);
584        }
585        if certificate.signature_value.len() != 64 {
586            return Err(ValidationError::SignatureVerificationFailed);
587        }
588        let key_bytes: [u8; 32] = issuer
589            .subject_public_key
590            .as_slice()
591            .try_into()
592            .map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
593        let public_key = Ed25519PublicKey::from_bytes(&key_bytes)
594            .map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
595        return ed25519_verify(
596            &public_key,
597            certificate.raw_tbs_der,
598            certificate.signature_value.as_slice(),
599        )
600        .map_err(|_| ValidationError::SignatureVerificationFailed);
601    }
602
603    if issuer.subject_public_key_algorithm_oid.as_slice() == OID_ID_MLDSA65 {
604        if certificate.certificate_signature_algorithm_oid.as_slice() != OID_ID_MLDSA65 {
605            return Err(ValidationError::UnsupportedSignatureAlgorithm);
606        }
607        let public_key = MlDsaPublicKey::from_bytes(&issuer.subject_public_key)
608            .map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
609        return mldsa_verify(
610            &public_key,
611            certificate.raw_tbs_der,
612            certificate.signature_value.as_slice(),
613        )
614        .map_err(|_| ValidationError::SignatureVerificationFailed);
615    }
616
617    Err(ValidationError::UnsupportedPublicKeyAlgorithm)
618}
619
620/// Validates issuer CA basic constraints, `keyCertSign`, and `pathLenConstraint` against the pending path.
621///
622/// # Arguments
623///
624/// * `issuer` — Issuer certificate that must act as a CA for this hop.
625/// * `ca_hops_below_issuer` — Number of additional CA certificates below this issuer on the built path.
626///
627/// # Returns
628///
629/// `Ok(())` when issuer constraints allow signing the child certificate.
630///
631/// # Errors
632///
633/// Returns [`ValidationError`] when the issuer is not a CA, lacks `keyCertSign`, or violates `pathLenConstraint`.
634///
635/// # Panics
636///
637/// This function does not panic.
638fn validate_issuer_constraints(
639    issuer: &Certificate<'_>,
640    ca_hops_below_issuer: usize,
641) -> core::result::Result<(), ValidationError> {
642    if issuer.basic_constraints_ca != Some(true) {
643        return Err(ValidationError::IssuerNotCa);
644    }
645    if let Some(key_usage) = issuer.key_usage_bits {
646        // KeyUsage bit 5 (keyCertSign) maps to mask 0x0004 in DER bit-order.
647        if (key_usage & 0x0004) == 0 {
648            return Err(ValidationError::IssuerMissingKeyCertSign);
649        }
650    }
651    if let Some(path_len) = issuer.basic_constraints_path_len {
652        if ca_hops_below_issuer > path_len as usize {
653            return Err(ValidationError::PathLenExceeded);
654        }
655    }
656    Ok(())
657}
658
659/// Validates the certificate validity interval against a canonical `YYYYMMDDHHMMSSZ` comparison string.
660///
661/// # Arguments
662///
663/// * `cert` — Certificate whose `notBefore` / `notAfter` fields are checked.
664/// * `now_canonical` — Current time in canonical form for lexicographic comparison.
665///
666/// # Returns
667///
668/// `Ok(())` when `now_canonical` lies within the certificate validity window.
669///
670/// # Errors
671///
672/// Returns [`ValidationError`] when time fields cannot be canonicalized or the certificate is not yet valid / expired.
673///
674/// # Panics
675///
676/// This function does not panic.
677fn validate_time(
678    cert: &Certificate<'_>,
679    now_canonical: &str,
680) -> core::result::Result<(), ValidationError> {
681    let not_before =
682        canonical_time(&cert.not_before).ok_or(ValidationError::InvalidNowTimeFormat)?;
683    let not_after = canonical_time(&cert.not_after).ok_or(ValidationError::InvalidNowTimeFormat)?;
684    if now_canonical < not_before.as_str() {
685        return Err(ValidationError::CertificateNotYetValid);
686    }
687    if now_canonical > not_after.as_str() {
688        return Err(ValidationError::CertificateExpired);
689    }
690    Ok(())
691}
692
693/// Converts UTCTime or GeneralizedTime text into canonical `YYYYMMDDHHMMSSZ`.
694///
695/// # Arguments
696///
697/// * `input` — `&str`.
698///
699/// # Returns
700///
701/// `Option<String>` produced by `canonical_time` (see implementation).
702///
703/// # Panics
704///
705/// This function does not panic unless otherwise noted.
706fn canonical_time(input: &str) -> Option<String> {
707    if input.len() == 15 && input.ends_with('Z') {
708        let body = &input[..14];
709        if body.chars().all(|c| c.is_ascii_digit()) {
710            return Some(input.to_owned());
711        }
712        return None;
713    }
714    if input.len() == 13 && input.ends_with('Z') {
715        let yy = &input[..2];
716        let rest = &input[2..12];
717        if !yy.chars().all(|c| c.is_ascii_digit()) || !rest.chars().all(|c| c.is_ascii_digit()) {
718            return None;
719        }
720        let yy_value = yy.parse::<u32>().ok()?;
721        let century = if yy_value >= 50 { "19" } else { "20" };
722        return Some(format!("{century}{yy}{rest}Z"));
723    }
724    None
725}
726
727/// Parses a PKCS#1 `RSAPublicKey` SEQUENCE into raw modulus and exponent bytes.
728///
729/// # Arguments
730///
731/// * `public_key_der` — DER `SubjectPublicKeyInfo` public key BIT STRING payload for RSA.
732///
733/// # Returns
734///
735/// On success, `(modulus, exponent)` big-endian integer bodies without extra length padding beyond DER rules.
736///
737/// # Errors
738///
739/// Returns [`ValidationError::PublicKeyDecodeFailed`] when the SEQUENCE layout is not a two-INTEGER PKCS#1 key.
740///
741/// # Panics
742///
743/// This function does not panic.
744fn parse_rsa_public_key_der(
745    public_key_der: &[u8],
746) -> core::result::Result<(Vec<u8>, Vec<u8>), ValidationError> {
747    let (rsa_seq, rem) =
748        parse_der_node(public_key_der).map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
749    if rsa_seq.tag != 0x30 || !rem.is_empty() {
750        return Err(ValidationError::PublicKeyDecodeFailed);
751    }
752    let (modulus_node, rest) =
753        parse_der_node(rsa_seq.body).map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
754    let (exponent_node, tail) =
755        parse_der_node(rest).map_err(|_| ValidationError::PublicKeyDecodeFailed)?;
756    if modulus_node.tag != 0x02 || exponent_node.tag != 0x02 || !tail.is_empty() {
757        return Err(ValidationError::PublicKeyDecodeFailed);
758    }
759    Ok((modulus_node.body.to_vec(), exponent_node.body.to_vec()))
760}
761
762/// Parses an ECDSA signature `SEQUENCE { r INTEGER, s INTEGER }` into fixed 32-byte P-256 scalars.
763///
764/// # Arguments
765///
766/// * `signature_der` — DER-encoded ECDSA signature bytes.
767///
768/// # Returns
769///
770/// On success, `(r, s)` each 32 bytes for use with the in-crypto P-256 verifier.
771///
772/// # Errors
773///
774/// Returns [`ValidationError::SignatureVerificationFailed`] when DER structure or integer normalization is invalid.
775///
776/// # Panics
777///
778/// This function does not panic.
779fn parse_ecdsa_signature_der(
780    signature_der: &[u8],
781) -> core::result::Result<([u8; 32], [u8; 32]), ValidationError> {
782    let (seq, rem) =
783        parse_der_node(signature_der).map_err(|_| ValidationError::SignatureVerificationFailed)?;
784    if seq.tag != 0x30 || !rem.is_empty() {
785        return Err(ValidationError::SignatureVerificationFailed);
786    }
787    let (r_node, rest) =
788        parse_der_node(seq.body).map_err(|_| ValidationError::SignatureVerificationFailed)?;
789    let (s_node, tail) =
790        parse_der_node(rest).map_err(|_| ValidationError::SignatureVerificationFailed)?;
791    if r_node.tag != 0x02 || s_node.tag != 0x02 || !tail.is_empty() {
792        return Err(ValidationError::SignatureVerificationFailed);
793    }
794    let r = ecdsa_integer_to_scalar32(r_node.body)?;
795    let s = ecdsa_integer_to_scalar32(s_node.body)?;
796    Ok((r, s))
797}
798
799/// Converts one DER INTEGER to a 32-byte unsigned scalar for P-256 signature verification.
800///
801/// # Arguments
802///
803/// * `value` — `&[u8]`.
804///
805/// # Returns
806///
807/// On success, a 32-byte big-endian unsigned scalar with leading zero padding when the integer is shorter than 32 bytes.
808///
809/// # Errors
810///
811/// Returns [`ValidationError::SignatureVerificationFailed`] when the INTEGER is empty, negatively signed, non-minimally encoded, or longer than 32 bytes.
812///
813/// # Panics
814///
815/// This function does not panic.
816fn ecdsa_integer_to_scalar32(value: &[u8]) -> core::result::Result<[u8; 32], ValidationError> {
817    if value.is_empty() {
818        return Err(ValidationError::SignatureVerificationFailed);
819    }
820    if value[0] & 0x80 != 0 {
821        return Err(ValidationError::SignatureVerificationFailed);
822    }
823    if value.len() > 1 && value[0] == 0x00 && value[1] & 0x80 == 0 {
824        return Err(ValidationError::SignatureVerificationFailed);
825    }
826    let normalized = if value.len() > 1 && value[0] == 0x00 {
827        &value[1..]
828    } else {
829        value
830    };
831    if normalized.len() > 32 {
832        return Err(ValidationError::SignatureVerificationFailed);
833    }
834    let mut out = [0_u8; 32];
835    out[32 - normalized.len()..].copy_from_slice(normalized);
836    Ok(out)
837}
838
839/// Validates configured policy OIDs, extended key usage, and revocation locator requirements.
840///
841/// # Arguments
842///
843/// * `cert` — Certificate being checked at this chain position.
844/// * `options` — Caller-selected validation options.
845/// * `is_leaf` — `true` when `cert` is the end-entity (leaf) certificate.
846///
847/// # Returns
848///
849/// `Ok(())` when optional policy and revocation requirements are satisfied.
850///
851/// # Errors
852///
853/// Returns [`ValidationError`] when a required policy, EKU, CRL/AIA locator, or similar constraint is missing.
854///
855/// # Panics
856///
857/// This function does not panic.
858fn validate_policy_and_revocation(
859    cert: &Certificate<'_>,
860    options: &ValidationOptions,
861    is_leaf: bool,
862) -> core::result::Result<(), ValidationError> {
863    if let Some(required_policy) = &options.required_policy_oid {
864        if cert
865            .certificate_policies
866            .iter()
867            .all(|policy| policy != required_policy)
868        {
869            return Err(ValidationError::MissingRequiredPolicy);
870        }
871    }
872    if options.require_crl_distribution_points
873        && cert.crl_distribution_uris.is_empty()
874        && cert.subject_raw != cert.issuer_raw
875    {
876        return Err(ValidationError::MissingRevocationInfo);
877    }
878    if options.require_revocation_locator
879        && cert.crl_distribution_uris.is_empty()
880        && cert.authority_info_access_uris.is_empty()
881        && cert.subject_raw != cert.issuer_raw
882    {
883        return Err(ValidationError::MissingRevocationLocator);
884    }
885    if is_leaf {
886        if let Some(required_eku) = &options.required_extended_key_usage_oid {
887            if cert
888                .extended_key_usage_oids
889                .iter()
890                .all(|usage| usage != required_eku)
891            {
892                return Err(ValidationError::MissingRequiredExtendedKeyUsage);
893            }
894        }
895    }
896    Ok(())
897}
898
899/// Updates the effective policy OID set by intersecting with the current certificate's policies when present.
900///
901/// # Arguments
902///
903/// * `effective_policy_oids` — Accumulated policy OID set carried through the walk.
904/// * `cert` — Certificate whose `certificatePolicies` extension contributes to the intersection.
905/// * `inhibit_any_policy_active` — When `true`, `anyPolicy` is not treated as a wildcard.
906/// * `any_policy_oid` — Raw OID bytes for `anyPolicy`.
907///
908/// # Returns
909///
910/// This function returns nothing; it mutates `effective_policy_oids` in place.
911///
912/// # Panics
913///
914/// This function does not panic.
915fn update_effective_policies(
916    effective_policy_oids: &mut Option<Vec<Vec<u8>>>,
917    cert: &Certificate<'_>,
918    inhibit_any_policy_active: bool,
919    any_policy_oid: &[u8],
920) {
921    if cert.certificate_policies.is_empty() {
922        return;
923    }
924    let current = unique_policies(&cert.certificate_policies);
925    let has_any_policy = current.iter().any(|policy| policy == any_policy_oid);
926    if has_any_policy && !inhibit_any_policy_active {
927        // Treat anyPolicy as wildcard in this simplified model.
928        return;
929    }
930    match effective_policy_oids {
931        None => *effective_policy_oids = Some(current),
932        Some(existing) => {
933            existing.retain(|policy| current.iter().any(|candidate| candidate == policy));
934        }
935    }
936}
937
938/// Updates the `inhibitAnyPolicy` skip-certs counter from the current certificate extension state.
939///
940/// # Arguments
941///
942/// * `inhibit_any_policy_skip_certs` — Running counter carried through the walk.
943/// * `cert` — Certificate that may carry an `inhibitAnyPolicy` extension.
944///
945/// # Returns
946///
947/// This function returns nothing; it mutates `inhibit_any_policy_skip_certs` in place.
948///
949/// # Panics
950///
951/// This function does not panic.
952fn update_inhibit_any_policy_skip_certs(
953    inhibit_any_policy_skip_certs: &mut Option<u32>,
954    cert: &Certificate<'_>,
955) {
956    if let Some(inhibit_any_policy) = cert.inhibit_any_policy_skip_certs {
957        let next_value = inhibit_any_policy_skip_certs.map_or(inhibit_any_policy, |existing| {
958            existing.min(inhibit_any_policy)
959        });
960        *inhibit_any_policy_skip_certs = Some(next_value);
961    }
962}
963
964/// Applies the issuer's `policyMappings` to remap subject-domain policy OIDs back toward issuer-domain OIDs.
965///
966/// # Arguments
967///
968/// * `effective_policy_oids` — Effective policy set before issuer intersection.
969/// * `issuer` — Issuer certificate whose mapping table is consulted.
970///
971/// # Returns
972///
973/// This function returns nothing; it may replace `effective_policy_oids` with a remapped vector.
974///
975/// # Panics
976///
977/// This function does not panic.
978fn apply_policy_mappings_for_issuer(
979    effective_policy_oids: &mut Option<Vec<Vec<u8>>>,
980    issuer: &Certificate<'_>,
981) {
982    let Some(existing) = effective_policy_oids.as_ref() else {
983        return;
984    };
985    if issuer.policy_mappings.is_empty() {
986        return;
987    }
988
989    let mut remapped = Vec::new();
990    for policy in existing {
991        let mut mapped = false;
992        for (issuer_policy, subject_policy) in &issuer.policy_mappings {
993            // Traversal is leaf->root, so map subject-domain policy back to issuer-domain policy.
994            if policy == subject_policy {
995                remapped.push(issuer_policy.clone());
996                mapped = true;
997            }
998        }
999        if !mapped {
1000            remapped.push(policy.clone());
1001        }
1002    }
1003    *effective_policy_oids = Some(unique_policies(&remapped));
1004}
1005
1006/// Deduplicates policy OID byte-vectors while preserving first-seen order.
1007///
1008/// # Arguments
1009///
1010/// * `policies` — `&[Vec<u8>]`.
1011///
1012/// # Returns
1013///
1014/// `Vec<Vec<u8>>` produced by `unique_policies` (see implementation).
1015///
1016/// # Panics
1017///
1018/// This function does not panic unless otherwise noted.
1019fn unique_policies(policies: &[Vec<u8>]) -> Vec<Vec<u8>> {
1020    let mut out = Vec::new();
1021    for policy in policies {
1022        if out.iter().all(|existing| existing != policy) {
1023            out.push(policy.clone());
1024        }
1025    }
1026    out
1027}
1028
1029/// Finalizes the effective policy OID list and enforces explicit-policy requirements when active.
1030///
1031/// # Arguments
1032///
1033/// * `effective_policy_oids` — Optional accumulated policy set after walking the chain.
1034/// * `explicit_policy_skip_certs` — Remaining explicit-policy skip counter, if any.
1035///
1036/// # Returns
1037///
1038/// On success, the finalized policy OID vector (possibly empty when explicit policy is not required).
1039///
1040/// # Errors
1041///
1042/// Returns [`ValidationError::ExplicitPolicyRequired`] when explicit policy is active but no policies remain.
1043///
1044/// # Panics
1045///
1046/// This function does not panic.
1047fn finalize_effective_policies(
1048    effective_policy_oids: &Option<Vec<Vec<u8>>>,
1049    explicit_policy_skip_certs: Option<u32>,
1050) -> core::result::Result<Vec<Vec<u8>>, ValidationError> {
1051    let policies = effective_policy_oids.clone().unwrap_or_default();
1052    if explicit_policy_is_active(explicit_policy_skip_certs) && policies.is_empty() {
1053        return Err(ValidationError::ExplicitPolicyRequired);
1054    }
1055    Ok(policies)
1056}
1057
1058/// Fails early when explicit-policy mode would otherwise proceed with an empty effective policy set.
1059///
1060/// # Arguments
1061///
1062/// * `effective_policy_oids` — Optional accumulated policy set.
1063/// * `explicit_policy_skip_certs` — Remaining explicit-policy skip counter, if any.
1064///
1065/// # Returns
1066///
1067/// `Ok(())` when explicit policy is inactive or at least one policy OID is present.
1068///
1069/// # Errors
1070///
1071/// Returns [`ValidationError::ExplicitPolicyRequired`] when explicit policy is active with no effective policies.
1072///
1073/// # Panics
1074///
1075/// This function does not panic.
1076fn enforce_explicit_policy_progress(
1077    effective_policy_oids: &Option<Vec<Vec<u8>>>,
1078    explicit_policy_skip_certs: Option<u32>,
1079) -> core::result::Result<(), ValidationError> {
1080    let has_effective_policies = effective_policy_oids
1081        .as_ref()
1082        .is_some_and(|value| !value.is_empty());
1083    if explicit_policy_is_active(explicit_policy_skip_certs) && !has_effective_policies {
1084        return Err(ValidationError::ExplicitPolicyRequired);
1085    }
1086    Ok(())
1087}
1088
1089/// Updates the explicit-policy skip-certs counter using `policyConstraints.requireExplicitPolicy` from `cert`.
1090///
1091/// # Arguments
1092///
1093/// * `explicit_policy_skip_certs` — Running counter carried through the walk.
1094/// * `cert` — Certificate that may carry `policyConstraints`.
1095///
1096/// # Returns
1097///
1098/// This function returns nothing; it mutates `explicit_policy_skip_certs` in place.
1099///
1100/// # Panics
1101///
1102/// This function does not panic.
1103fn update_explicit_policy_skip_certs(
1104    explicit_policy_skip_certs: &mut Option<u32>,
1105    cert: &Certificate<'_>,
1106) {
1107    if let Some(require_explicit_policy) = cert.policy_constraints_require_explicit_policy {
1108        let next_value = explicit_policy_skip_certs.map_or(require_explicit_policy, |existing| {
1109            existing.min(require_explicit_policy)
1110        });
1111        *explicit_policy_skip_certs = Some(next_value);
1112    }
1113}
1114
1115/// Decrements the explicit-policy skip-certs counter when the certificate is not self-issued.
1116///
1117/// # Arguments
1118///
1119/// * `explicit_policy_skip_certs` — Running counter carried through the walk.
1120/// * `cert` — Certificate whose self-issuance status gates the decrement.
1121///
1122/// # Returns
1123///
1124/// This function returns nothing; it mutates `explicit_policy_skip_certs` in place.
1125///
1126/// # Panics
1127///
1128/// This function does not panic.
1129fn decrement_skip_certs_counter(
1130    explicit_policy_skip_certs: &mut Option<u32>,
1131    cert: &Certificate<'_>,
1132) {
1133    if cert.subject_raw == cert.issuer_raw {
1134        return;
1135    }
1136    if let Some(counter) = explicit_policy_skip_certs.as_mut() {
1137        *counter = counter.saturating_sub(1);
1138    }
1139}
1140
1141/// Returns true when explicit policy requirements are currently in force.
1142///
1143/// # Arguments
1144///
1145/// * `explicit_policy_skip_certs` — `Option<u32>`.
1146///
1147/// # Returns
1148///
1149/// `bool` produced by `explicit_policy_is_active` (see implementation).
1150///
1151/// # Panics
1152///
1153/// This function does not panic unless otherwise noted.
1154fn explicit_policy_is_active(explicit_policy_skip_certs: Option<u32>) -> bool {
1155    matches!(explicit_policy_skip_certs, Some(0))
1156}
1157
1158/// Updates the inhibit-policy-mapping skip-certs counter using `policyConstraints.inhibitPolicyMapping`.
1159///
1160/// # Arguments
1161///
1162/// * `inhibit_policy_mapping_skip_certs` — Running counter carried through the walk.
1163/// * `cert` — Certificate that may carry `policyConstraints`.
1164///
1165/// # Returns
1166///
1167/// This function returns nothing; it mutates `inhibit_policy_mapping_skip_certs` in place.
1168///
1169/// # Panics
1170///
1171/// This function does not panic.
1172fn update_inhibit_policy_mapping_skip_certs(
1173    inhibit_policy_mapping_skip_certs: &mut Option<u32>,
1174    cert: &Certificate<'_>,
1175) {
1176    if let Some(inhibit_policy_mapping) = cert.policy_constraints_inhibit_policy_mapping {
1177        let next_value = inhibit_policy_mapping_skip_certs
1178            .map_or(inhibit_policy_mapping, |existing| {
1179                existing.min(inhibit_policy_mapping)
1180            });
1181        *inhibit_policy_mapping_skip_certs = Some(next_value);
1182    }
1183}
1184
1185/// Rejects certificates that carry `policyMappings` when inhibit-policy-mapping is active at depth zero.
1186///
1187/// # Arguments
1188///
1189/// * `cert` — Certificate under inspection.
1190/// * `inhibit_policy_mapping_skip_certs` — Remaining inhibit-policy-mapping skip counter, if any.
1191///
1192/// # Returns
1193///
1194/// `Ok(())` when policy mappings are allowed or absent.
1195///
1196/// # Errors
1197///
1198/// Returns [`ValidationError::PolicyMappingInhibited`] when mappings are present while inhibition is active.
1199///
1200/// # Panics
1201///
1202/// This function does not panic.
1203fn enforce_policy_mapping(
1204    cert: &Certificate<'_>,
1205    inhibit_policy_mapping_skip_certs: Option<u32>,
1206) -> core::result::Result<(), ValidationError> {
1207    if matches!(inhibit_policy_mapping_skip_certs, Some(0)) && !cert.policy_mappings.is_empty() {
1208        return Err(ValidationError::PolicyMappingInhibited);
1209    }
1210    Ok(())
1211}
1212
1213/// Ensures subject DNS SAN values satisfy the issuer's permitted and excluded DNS name constraints.
1214///
1215/// # Arguments
1216///
1217/// * `issuer` — Issuer certificate carrying optional `nameConstraints` DNS subtrees.
1218/// * `subject` — Subject certificate whose DNS SAN entries are checked.
1219///
1220/// # Returns
1221///
1222/// `Ok(())` when no DNS constraints apply, no DNS SANs are present, or every SAN matches permitted subtrees.
1223///
1224/// # Errors
1225///
1226/// Returns [`ValidationError::NameConstraintsViolation`] when a SAN matches an excluded subtree or misses permitted subtrees.
1227///
1228/// # Panics
1229///
1230/// This function does not panic.
1231fn validate_name_constraints(
1232    issuer: &Certificate<'_>,
1233    subject: &Certificate<'_>,
1234) -> core::result::Result<(), ValidationError> {
1235    let has_dns_constraints = !issuer.name_constraints_permitted_dns.is_empty()
1236        || !issuer.name_constraints_excluded_dns.is_empty();
1237    if !has_dns_constraints || subject.subject_alt_dns_names.is_empty() {
1238        return Ok(());
1239    }
1240
1241    for dns_name in &subject.subject_alt_dns_names {
1242        if issuer
1243            .name_constraints_excluded_dns
1244            .iter()
1245            .any(|constraint| dns_name_matches_constraint(dns_name, constraint))
1246        {
1247            return Err(ValidationError::NameConstraintsViolation);
1248        }
1249        if !issuer.name_constraints_permitted_dns.is_empty()
1250            && issuer
1251                .name_constraints_permitted_dns
1252                .iter()
1253                .all(|constraint| !dns_name_matches_constraint(dns_name, constraint))
1254        {
1255            return Err(ValidationError::NameConstraintsViolation);
1256        }
1257    }
1258    Ok(())
1259}
1260
1261/// Matches DNS name against a name-constraints dNSName suffix constraint.
1262///
1263/// # Arguments
1264///
1265/// * `dns_name` — `&str`.
1266/// * `constraint` — `&str`.
1267///
1268/// # Returns
1269///
1270/// `bool` produced by `dns_name_matches_constraint` (see implementation).
1271///
1272/// # Panics
1273///
1274/// This function does not panic unless otherwise noted.
1275fn dns_name_matches_constraint(dns_name: &str, constraint: &str) -> bool {
1276    let normalized_name = dns_name.to_ascii_lowercase();
1277    let normalized_constraint = constraint.trim_start_matches('.').to_ascii_lowercase();
1278    if normalized_constraint.is_empty() {
1279        return false;
1280    }
1281    normalized_name == normalized_constraint
1282        || normalized_name
1283            .strip_suffix(&normalized_constraint)
1284            .is_some_and(|prefix| prefix.ends_with('.'))
1285}