Skip to main content

pkix_path/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![forbid(unsafe_code)]
4#![warn(missing_docs, rust_2018_idioms)]
5//! RFC 5280 X.509 certificate path validation — pure Rust, `no_std`.
6//!
7//! Implements certificate path building and validation per
8//! [RFC 5280 §6](https://www.rfc-editor.org/rfc/rfc5280#section-6).
9//!
10//! # Architecture
11//!
12//! Cryptographic signature verification is pluggable via [`SignatureVerifier`].
13//! The default feature set (`rustcrypto`) wires in `RustCrypto` backends for
14//! RSA-PKCS1v15-SHA-256 (`rsa` feature) and ECDSA-P-256-SHA-256 (`p256` feature).
15//! P-384 and Ed25519 are planned for a future release.
16//! For FIPS-validated crypto, implement [`SignatureVerifier`] against
17//! `wolfcrypt-rustcrypto` and disable the `rustcrypto` feature.
18//!
19//! Revocation checking is handled by `pkix-revocation`. This crate never
20//! touches the network — use `pkix_chain::verify_chain` for the combined API.
21//!
22//! # Limitations
23//!
24//! The following are **not** implemented in v0.2:
25//! - **RFC 4518 full Unicode NFKC DN normalization** — ASCII case-folding
26//!   plus insignificant-whitespace collapsing is applied. `BMPString` AVA
27//!   values are transcoded UCS-2-BE → UTF-8 and then compared via the same
28//!   ASCII-only normalization pipeline, so two AVAs that share Unicode
29//!   code points but differ only in DER string-type (e.g. `BMPString`
30//!   "Foo Co" vs `UTF8String` "Foo Co") compare equal. Full RFC 4518 prep
31//!   (NFKC, non-ASCII Unicode case fold, prohibit/bidi steps) is future
32//!   work; until it lands, two `BMPString` values that contain the same
33//!   Unicode code points but differ in canonical decomposition (e.g.
34//!   precomposed U+00E9 'é' vs decomposed U+0065 U+0301 'e'+ combining
35//!   acute) compare unequal. `UniversalString` AVA values are rejected by
36//!   the `der` crate at parse time (tag 0x1C is not in `der::Tag` in 0.7)
37//!   and never reach the path validator. `TeletexString` AVAs fall through
38//!   to raw DER byte comparison only — see `any_to_str_bytes` rustdoc.
39//! - **Online revocation** — revocation is handled by `pkix-revocation`
40//!   (CRL/OCSP); this crate is network-free by design.
41//! - **Path building** — converting an unordered bag of certificates into a
42//!   validated chain is handled by `pkix-path-builder`.
43
44// For no_std builds, pull in the alloc crate explicitly so `alloc::` paths
45// and the `vec!` macro resolve. `#[macro_use]` re-exports alloc macros
46// (vec!, format!, etc.) into the crate root, making them available everywhere
47// without qualifying them as `alloc::vec!(...)`.
48#[cfg(not(feature = "std"))]
49#[macro_use]
50extern crate alloc;
51
52// Unified Vec import: alloc::vec::Vec in no_std, std::vec::Vec under std.
53// Both map to the same concrete type; this alias lets the rest of the file
54// write `Vec<_>` without cfg-gating every use site.
55#[cfg(not(feature = "std"))]
56use alloc::vec::Vec;
57#[cfg(feature = "std")]
58use std::vec::Vec;
59
60// Unified Cow import: same cfg-gate pattern as Vec. `Cow` is owned by
61// `alloc` but `std` re-exports it; we can't write `alloc::borrow::Cow`
62// unconditionally because `extern crate alloc` is gated to no_std mode.
63#[cfg(not(feature = "std"))]
64use alloc::borrow::Cow;
65#[cfg(feature = "std")]
66use std::borrow::Cow;
67
68use der::Tagged;
69use signature::Error as SignatureError;
70use spki::{AlgorithmIdentifierRef, SubjectPublicKeyInfoRef};
71use x509_cert::Certificate;
72
73/// Re-exported for use with [`TrustAnchor::name_constraints`].
74pub use x509_cert::ext::pkix::constraints::name::NameConstraints;
75
76/// Private shorthand for the `GeneralSubtrees` type used throughout NC processing.
77type GeneralSubtrees = x509_cert::ext::pkix::constraints::name::GeneralSubtrees;
78
79/// Opaque wrapper around an underlying ASN.1 / DER error.
80///
81/// Carries a [`Display`] message identical to the wrapped `der::Error` so
82/// diagnostic output is preserved, but does not expose the underlying type
83/// in the public API. This insulates callers from semver-breaking changes
84/// in the `der` crate's error variants.
85///
86/// Construction is crate-private. The only way to obtain a `DerError` is
87/// via [`Error::Der`] (and the [`From<der::Error>`] impl on [`Error`]).
88///
89/// [`Display`]: core::fmt::Display
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct DerError(der::Error);
92
93impl core::fmt::Display for DerError {
94    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
95        core::fmt::Display::fmt(&self.0, f)
96    }
97}
98
99#[cfg(feature = "std")]
100impl std::error::Error for DerError {
101    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102        Some(&self.0)
103    }
104}
105
106/// Errors returned by path validation.
107#[derive(Clone, Debug, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum Error {
110    /// Certificate signature verification failed at the given chain index.
111    SignatureInvalid {
112        /// Zero-based index into the `chain` slice of the failing certificate.
113        index: usize,
114    },
115    /// A structural encoding error was found in a certificate.
116    ///
117    /// Currently returned when the outer `signatureAlgorithm` OID differs from
118    /// the inner `TBSCertificate.signature` OID (RFC 5280 §4.1.1.2).
119    /// Parameters are not compared; see `check_oid_consistency` for rationale.
120    MalformedCertificate {
121        /// Zero-based index into the `chain` slice of the malformed certificate.
122        ///
123        /// The underlying `der::Error` is intentionally not stored here to keep
124        /// this variant `no_std`-compatible and to preserve the stable API shape.
125        /// Callers that need the root-cause parse error should validate the
126        /// DER certificate independently before calling [`validate_path`].
127        index: usize,
128    },
129    /// Certificate validity period check failed (expired or not yet valid).
130    ValidityPeriod {
131        /// Zero-based index into the `chain` slice of the failing certificate.
132        index: usize,
133    },
134    /// Issuer/subject name linkage is broken at the given chain index.
135    ChainBroken {
136        /// Zero-based index into the `chain` slice where the break was found.
137        index: usize,
138    },
139    /// No path from the subject certificate to any trust anchor was found.
140    NoTrustedPath,
141    /// Path length exceeds [`ValidationPolicy::max_path_len`].
142    PathTooLong,
143    /// An intermediate certificate is missing `BasicConstraints` `cA=TRUE`.
144    NotCA {
145        /// Zero-based index into the `chain` slice of the failing certificate.
146        index: usize,
147    },
148    /// An intermediate certificate has a `KeyUsage` extension with `keyCertSign` not set.
149    ///
150    /// This error is only returned when a `KeyUsage` extension is **present** and the
151    /// `keyCertSign` bit is explicitly absent or zero (RFC 5280 §6.1.4(n): "If a `KeyUsage`
152    /// extension is present, verify that the keyCertSign bit is set.").
153    ///
154    /// Certificates with **no** `KeyUsage` extension are not rejected by this check;
155    /// RFC 5280 does not require the extension to be present on CA certificates.
156    KeyUsageMissing {
157        /// Zero-based index into the `chain` slice of the failing certificate.
158        index: usize,
159    },
160    /// A critical extension is present that this implementation does not handle.
161    UnhandledCriticalExtension {
162        /// Zero-based index into the `chain` slice of the failing certificate.
163        index: usize,
164    },
165    /// Certificate name constraints violated (RFC 5280 §4.2.1.10); `index` is the 0-based chain position.
166    NameConstraintViolation {
167        /// Zero-based index into the `chain` slice of the failing certificate.
168        index: usize,
169    },
170    /// Certificate policy validation failed (RFC 5280 §6.1.5(g)).
171    ///
172    /// Returned when `explicit_policy` reaches zero and the valid policy tree
173    /// is empty, meaning no acceptable certificate policy exists for the chain.
174    PolicyViolation {
175        /// Zero-based index of the certificate where the violation was detected.
176        index: usize,
177    },
178    /// ASN.1 / DER encoding or decoding error.
179    ///
180    /// Returned when a structural encoding error is found in a certificate or
181    /// when re-encoding `TBSCertificate` for signature verification fails.
182    /// Signature verification now uses heap-allocated encoding (no fixed size
183    /// limit), so this error reflects a genuine DER encoding defect in the
184    /// certificate, not an implementation size constraint.
185    ///
186    /// The inner [`DerError`] is an opaque newtype; the underlying `der::Error`
187    /// is intentionally not exposed so a future major-version bump in the
188    /// `der` crate cannot cascade into a semver break here.
189    Der(DerError),
190    /// A certificate's validity period (notAfter − notBefore) exceeds
191    /// [`ValidationPolicy::max_validity_secs`].
192    ///
193    /// This check fires for every certificate in the chain, not just the leaf.
194    ValidityPeriodExceedsMax {
195        /// Zero-based index into the `chain` slice of the failing certificate.
196        index: usize,
197    },
198    /// A certificate's signature algorithm OID is not in
199    /// [`ValidationPolicy::allowed_signature_algs`].
200    ///
201    /// The check fires before signature verification so the error is diagnostic
202    /// rather than a confusing `SignatureInvalid`.
203    AlgorithmNotAllowed {
204        /// Zero-based index into the `chain` slice of the failing certificate.
205        index: usize,
206    },
207    /// An RSA public key's modulus is smaller than
208    /// [`ValidationPolicy::min_rsa_key_bits`] bits.
209    ///
210    /// Non-RSA keys (EC, Ed25519, …) are not affected by this check.
211    KeyTooSmall {
212        /// Zero-based index into the `chain` slice of the failing certificate.
213        index: usize,
214    },
215    /// The leaf certificate (chain index 0) has no `SubjectAltName` extension,
216    /// or the extension is present but empty.
217    ///
218    /// Only checked when [`ValidationPolicy::require_subject_alt_name`] is `true`.
219    /// Intermediate CA certificates are not subject to this check.
220    MissingSan,
221    /// The leaf certificate (chain index 0) has a `SubjectAltName` extension but
222    /// none of its entries is an `rfc822Name` (email address).
223    ///
224    /// Only checked when [`ValidationPolicy::require_rfc822_san`] is `true`.
225    /// Intermediate CA certificates are not subject to this check.
226    MissingRfc822San,
227    /// The leaf certificate (chain index 0) does not assert all OIDs required
228    /// by [`ValidationPolicy::required_leaf_eku`].
229    ///
230    /// `anyExtendedKeyUsage` (2.5.29.37.0) does not satisfy a specific OID
231    /// requirement — each required OID must be listed explicitly.
232    MissingEku,
233    /// Two certificates in the chain share the same `(issuer DN, serial number)`.
234    ///
235    /// Per RFC 5280 §4.1.2.2, the combination of issuer DN and serial number
236    /// uniquely identifies a certificate. A cert appearing twice at different
237    /// chain positions is a construction error. Returned as a diagnostic rather
238    /// than a confusing [`Error::SignatureInvalid`] or [`Error::ChainBroken`].
239    ///
240    /// Note: two certificates with the same public key but different
241    /// issuer+serial are *distinct* certificates (e.g. cross-signed CAs) and
242    /// are **not** rejected by this check.
243    ///
244    /// `first` and `second` are the zero-based chain indices of the two duplicates.
245    DuplicateCertificate {
246        /// First occurrence index.
247        first: usize,
248        /// Second occurrence index.
249        second: usize,
250    },
251}
252
253impl core::fmt::Display for Error {
254    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
255        match self {
256            Self::SignatureInvalid { index } => {
257                write!(f, "signature invalid at chain index {index}")
258            }
259            Self::ValidityPeriod { index } => {
260                write!(f, "validity period check failed at chain index {index}")
261            }
262            Self::MalformedCertificate { index } => {
263                write!(f, "malformed certificate at chain index {index}")
264            }
265            Self::ChainBroken { index } => {
266                write!(f, "issuer/subject linkage broken at chain index {index}")
267            }
268            Self::NoTrustedPath => write!(f, "no path to a trusted anchor"),
269            Self::PathTooLong => write!(f, "path length exceeds maximum"),
270            Self::NotCA { index } => write!(f, "certificate at index {index} is not a CA"),
271            Self::KeyUsageMissing { index } => {
272                write!(f, "keyCertSign missing at chain index {index}")
273            }
274            Self::UnhandledCriticalExtension { index } => {
275                write!(f, "unhandled critical extension at chain index {index}")
276            }
277            Self::NameConstraintViolation { index } => {
278                write!(f, "name constraints violated at certificate index {index}")
279            }
280            Self::PolicyViolation { index } => {
281                write!(f, "certificate policy violation at chain index {index}")
282            }
283            Self::Der(e) => write!(f, "DER error: {e}"),
284            Self::ValidityPeriodExceedsMax { index } => {
285                write!(f, "validity period exceeds maximum at chain index {index}")
286            }
287            Self::AlgorithmNotAllowed { index } => {
288                write!(f, "signature algorithm not allowed at chain index {index}")
289            }
290            Self::KeyTooSmall { index } => {
291                write!(f, "RSA key too small at chain index {index}")
292            }
293            Self::MissingSan => write!(f, "leaf certificate is missing SubjectAltName"),
294            Self::MissingRfc822San => write!(
295                f,
296                "leaf certificate SubjectAltName contains no rfc822Name entry"
297            ),
298            Self::MissingEku => {
299                write!(
300                    f,
301                    "leaf certificate is missing required ExtendedKeyUsage OID(s)"
302                )
303            }
304            Self::DuplicateCertificate { first, second } => {
305                write!(
306                    f,
307                    "duplicate certificate (issuer+serial) at chain indices {first} and {second}"
308                )
309            }
310        }
311    }
312}
313
314#[cfg(feature = "std")]
315impl std::error::Error for Error {
316    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
317        match self {
318            Self::Der(e) => Some(e),
319            Self::SignatureInvalid { .. }
320            | Self::MalformedCertificate { .. }
321            | Self::ValidityPeriod { .. }
322            | Self::ChainBroken { .. }
323            | Self::NoTrustedPath
324            | Self::PathTooLong
325            | Self::NotCA { .. }
326            | Self::KeyUsageMissing { .. }
327            | Self::UnhandledCriticalExtension { .. }
328            | Self::NameConstraintViolation { .. }
329            | Self::PolicyViolation { .. }
330            | Self::ValidityPeriodExceedsMax { .. }
331            | Self::AlgorithmNotAllowed { .. }
332            | Self::KeyTooSmall { .. }
333            | Self::MissingSan
334            | Self::MissingRfc822San
335            | Self::MissingEku
336            | Self::DuplicateCertificate { .. } => None,
337        }
338    }
339}
340
341impl From<der::Error> for Error {
342    fn from(e: der::Error) -> Self {
343        Self::Der(DerError(e))
344    }
345}
346
347/// Result alias for this crate.
348pub type Result<T> = core::result::Result<T, Error>;
349
350/// Pluggable signature verification backend.
351///
352/// Implement this trait to provide algorithm-specific signature verification.
353/// The trait is OID-dispatched: the `algorithm` argument carries the OID and
354/// any parameters from the certificate's `signatureAlgorithm` field.
355///
356/// This trait is object-safe and can be used as `dyn SignatureVerifier`.
357/// All method arguments are either `&self` or borrows, so no `Sized` bound
358/// is implied.
359///
360/// # Implementing a custom backend
361///
362/// ```rust,no_run
363/// use der::asn1::ObjectIdentifier;
364/// const MY_RSA_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
365/// const MY_ECDSA_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
366///
367/// struct MyVerifier;
368///
369/// impl pkix_path::SignatureVerifier for MyVerifier {
370///     fn verify_signature(
371///         &self,
372///         algorithm: spki::AlgorithmIdentifierRef<'_>,
373///         _issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
374///         _message: &[u8],
375///         _signature: &[u8],
376///     ) -> core::result::Result<(), signature::Error> {
377///         match algorithm.oid {
378///             MY_RSA_OID => { Ok(()) /* RSA verification */ }
379///             MY_ECDSA_OID => { Ok(()) /* ECDSA verification */ }
380///             _ => Err(signature::Error::new()),
381///         }
382///     }
383/// }
384/// ```
385pub trait SignatureVerifier {
386    /// Verify `signature` over `message`.
387    ///
388    /// - `algorithm`    — from the subject cert's `signatureAlgorithm` field
389    /// - `issuer_spki`  — SPKI extracted from the issuer or trust anchor cert
390    /// - `message`      — DER-encoded `TBSCertificate` (the bytes that were signed)
391    /// - `signature`    — raw signature bytes (`BitString` content, not the wrapper)
392    ///
393    /// Returns `Ok(())` on success or `Err(signature::Error)` on failure.
394    /// The caller ([`validate_path`]) maps the error to [`Error::SignatureInvalid`]
395    /// with the correct chain index — the verifier does not need to know it.
396    ///
397    /// # Errors
398    ///
399    /// Returns `Err(signature::Error)` if the signature does not verify against the given public key and data.
400    fn verify_signature(
401        &self,
402        algorithm: AlgorithmIdentifierRef<'_>,
403        issuer_spki: SubjectPublicKeyInfoRef<'_>,
404        message: &[u8],
405        signature: &[u8],
406    ) -> core::result::Result<(), SignatureError>;
407}
408
409/// A trust anchor used to terminate path validation.
410///
411/// A trust anchor is typically either a self-signed root CA certificate
412/// or a raw (name, SPKI) pair extracted from a platform trust store.
413/// The trust anchor itself is **not** signature-verified — it is trusted
414/// by definition (RFC 5280 §6.1.1(c)).
415///
416/// **Validity period**: RFC 5280 §6.1.1(c) explicitly excludes the trust
417/// anchor's notBefore/notAfter from path validation. An expired root CA
418/// certificate used as a trust anchor will still anchor valid paths — this
419/// is intentional behavior, not a bug. Callers are responsible for ensuring
420/// their trust store contains the anchors they intend to trust.
421///
422/// **`PartialEq` is byte-level, not semantic**: The derived `PartialEq`
423/// compares fields verbatim. Two anchors representing the same CA may compare
424/// unequal if their DER encodings differ — for example, one `AlgorithmIdentifier`
425/// with explicit `NULL` parameters and another with absent parameters are both
426/// valid for RSA (RFC 3279 §2.3.1) but will not be equal under `==`. Do not use
427/// `==` to deduplicate a trust store; use [`names_match`] and compare
428/// `algorithm.oid` plus `subject_public_key` bytes directly. Path validation
429/// already handles this internally, so it is not affected by this encoding difference.
430///
431/// # Stability
432///
433/// `TrustAnchor` is `#[non_exhaustive]`: new fields may be added in minor
434/// versions. Construct via [`TrustAnchor::new`], [`TrustAnchor::from_cert`],
435/// or `TrustAnchor::from`/`try_from`. Do not use struct literal syntax.
436#[derive(Clone, Debug, PartialEq, Eq)]
437#[non_exhaustive]
438pub struct TrustAnchor {
439    /// The subject distinguished name of the trust anchor.
440    pub subject: x509_cert::name::Name,
441    /// The subject public key info of the trust anchor.
442    ///
443    /// Must be a valid SPKI for the chosen signature algorithm. An empty or
444    /// malformed SPKI will cause signature verification to fail with
445    /// `Error::NoTrustedPath` (no anchor matched), not a panic.
446    pub subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
447    /// `NameConstraints` from the trust anchor certificate, if present.
448    ///
449    /// When set, `chain_walk` seeds the initial `permitted_subtrees` and
450    /// `excluded_subtrees` state from this value before walking the chain.
451    /// Populated automatically by `from_cert`; `None` for programmatically
452    /// constructed anchors unless explicitly set.
453    pub name_constraints: Option<x509_cert::ext::pkix::constraints::name::NameConstraints>,
454}
455
456impl TrustAnchor {
457    /// Create a trust anchor from raw subject name and SPKI.
458    #[must_use]
459    pub const fn new(
460        subject: x509_cert::name::Name,
461        subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
462    ) -> Self {
463        Self {
464            subject,
465            subject_public_key_info,
466            name_constraints: None,
467        }
468    }
469
470    /// Extract subject name and SPKI from a certificate to create a trust anchor.
471    ///
472    /// This is the typical constructor when your trust store contains full
473    /// self-signed root CA certificates.
474    ///
475    /// Prefer [`TrustAnchor::from`] (i.e. `TrustAnchor::from(&cert)`) when you
476    /// need to keep `cert` alive after building the anchor.
477    ///
478    /// # `NameConstraints` and malformed extensions
479    ///
480    /// If the anchor certificate contains a malformed or unparseable
481    /// `NameConstraints` extension, `from_cert` silently sets
482    /// `name_constraints = None` and continues. The resulting anchor
483    /// will not enforce NC constraints from that extension.
484    ///
485    /// For strict RFC 5280 §4.2 compliance — where a critical extension
486    /// that cannot be parsed MUST cause rejection — use
487    /// [`TrustAnchor::try_from`] instead. That path propagates the
488    /// `der::Error` to the caller.
489    #[must_use]
490    pub fn from_cert(cert: Certificate) -> Self {
491        let name_constraints = find_cert_ext(&cert, OID_NAME_CONSTRAINTS);
492        Self {
493            subject: cert.tbs_certificate.subject,
494            subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
495            name_constraints,
496        }
497    }
498}
499
500impl From<&Certificate> for TrustAnchor {
501    fn from(cert: &Certificate) -> Self {
502        Self {
503            subject: cert.tbs_certificate.subject.clone(),
504            subject_public_key_info: cert.tbs_certificate.subject_public_key_info.clone(),
505            name_constraints: find_cert_ext(cert, OID_NAME_CONSTRAINTS),
506        }
507    }
508}
509
510/// Fail-closed construction from an owned certificate.
511///
512/// Returns `Err(`[`DerError`]`)` if the certificate contains a `NameConstraints`
513/// extension with malformed DER. Use this when building a trust store that
514/// must reject certificates with unparseable critical extensions per
515/// RFC 5280 §4.2.
516///
517/// The error type is the opaque [`DerError`] newtype rather than `der::Error`
518/// so that a future major-version bump in the `der` crate does not cascade
519/// into a semver break here.
520///
521/// # Why only `TryFrom<Certificate>` and not `TryFrom<&Certificate>`
522///
523/// `TryFrom<&Certificate>` would conflict with the blanket impl
524/// `impl<T, U: Into<T>> TryFrom<U>` provided by Rust core, because
525/// `From<&Certificate>` is already implemented (and `From` implies `Into`).
526/// Use `TrustAnchor::try_from(cert.clone())` if you need to keep `cert`.
527impl TryFrom<Certificate> for TrustAnchor {
528    type Error = DerError;
529
530    fn try_from(cert: Certificate) -> core::result::Result<Self, Self::Error> {
531        let name_constraints = try_find_cert_ext(&cert, OID_NAME_CONSTRAINTS).map_err(DerError)?;
532        Ok(Self {
533            subject: cert.tbs_certificate.subject,
534            subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
535            name_constraints,
536        })
537    }
538}
539
540/// Policy parameters controlling path validation.
541///
542/// # Stability
543///
544/// `ValidationPolicy` is `#[non_exhaustive]`.
545/// Construct via [`ValidationPolicy::new`] or [`Default`] + field assignment.
546/// Do not use struct literal syntax.
547///
548/// # Performance note
549///
550/// Policy objects are intended to be constructed once (e.g., at server startup)
551/// and reused for the lifetime of the application. Repeated construction is
552/// unnecessary.
553///
554/// Policy enforcement (`CertificatePolicies`, `PolicyMappings`, `PolicyConstraints`,
555/// `InhibitAnyPolicy`) is implemented per RFC 5280 §6.1. Use the
556/// `initial_explicit_policy`, `initial_any_policy_inhibit`,
557/// `initial_policy_mapping_inhibit`, and `initial_policy_set` fields to
558/// configure the initial policy state.
559///
560/// # Limitations
561///
562/// Path-building (RFC 4158 — cross-signed certificates, multiple candidate
563/// issuers) is **out of scope** for this crate. The caller must supply the
564/// complete, ordered chain (see `pkix-path-builder` for path discovery).
565///
566/// Revocation checking (CRL / OCSP) is out of scope for `pkix-path`; see
567/// `pkix-revocation` for that functionality.
568// `clippy::struct_excessive_bools` would prefer enum-typed groupings here,
569// but the bools map directly to RFC 5280 §6.1.1 named inputs
570// (`initial-explicit-policy`, `initial-any-policy-inhibit`,
571// `initial-policy-mapping-inhibit`) and to the SAN-presence and
572// EKU-presence policy gates. Substituting an enum cluster would obscure the
573// 1:1 mapping to the spec text and force callers through a pattern-match
574// adapter for each field. The current shape is the most direct expression
575// of the spec inputs.
576#[allow(clippy::struct_excessive_bools)]
577#[non_exhaustive]
578#[derive(Clone, Debug, PartialEq, Eq)]
579pub struct ValidationPolicy {
580    /// Maximum chain depth, not counting the trust anchor. Default: 10.
581    ///
582    /// A chain of `[leaf]` is depth 0. `[leaf, intermediate, root]` is depth 1
583    /// (one intermediate). Validation fails if depth exceeds this value.
584    pub max_path_len: u8,
585
586    /// Current time as seconds since the Unix epoch (1970-01-01T00:00:00Z).
587    ///
588    /// Used to check `notBefore` ≤ `now` ≤ `notAfter` on every certificate.
589    /// **Must be set by the caller** — there is no platform clock in `no_std`.
590    ///
591    /// **Warning — the default is 0 (1970-01-01):** Any certificate issued
592    /// after 1970 has `notBefore > 0` and will fail the validity check with
593    /// [`Error::ValidityPeriod`]. If you see unexpected `ValidityPeriod`
594    /// errors, check that `current_time_unix` is set to the current time.
595    ///
596    /// **Warning**: passing `u64::MAX` causes all `notAfter` checks to pass.
597    /// This effectively disables expiry checking — only use it in contexts
598    /// where you explicitly want permissive (clock-free) validation.
599    pub current_time_unix: u64,
600
601    /// Enforce the `KeyUsage` extension when present. Default: `true`.
602    ///
603    /// When `true`, an intermediate certificate whose `KeyUsage` extension is
604    /// **present** but does not include `keyCertSign` will be rejected with
605    /// [`Error::KeyUsageMissing`], per RFC 5280 §6.1.4(n).
606    ///
607    /// Certificates with **no** `KeyUsage` extension are not affected; RFC 5280
608    /// only mandates the check when the extension is present.
609    pub enforce_key_usage: bool,
610
611    /// Initial explicit-policy indicator (RFC 5280 §6.1.1).
612    ///
613    /// When `true`, path validation requires that at least one valid policy exists
614    /// from the initial policy set. When `false` (the default), any valid path is
615    /// accepted even if no certificate policy is asserted.
616    pub initial_explicit_policy: bool,
617
618    /// Initial any-policy inhibit indicator (RFC 5280 §6.1.1).
619    ///
620    /// When `true`, the `anyPolicy` OID is not considered a match for any other
621    /// policy at the start of the path. When `false` (the default), `anyPolicy`
622    /// is accepted as a wildcard unless later inhibited by a CA certificate.
623    pub initial_any_policy_inhibit: bool,
624
625    /// Initial policy-mapping inhibit indicator (RFC 5280 §6.1.1).
626    ///
627    /// When `true`, policy mappings are not permitted in any certificate in the
628    /// chain. When `false` (the default), policy mappings are allowed.
629    pub initial_policy_mapping_inhibit: bool,
630
631    /// Initial user-requested policy set (RFC 5280 §6.1.1).
632    ///
633    /// The set of certificate policies acceptable to the relying party. An empty
634    /// vec is treated as `{anyPolicy}` — all policies are acceptable. Set this
635    /// to restrict which policies are recognized in the output.
636    ///
637    /// Note: this is `pub` but clones the OID set, so prefer constructing once
638    /// and reusing the `ValidationPolicy`.
639    pub initial_policy_set: Vec<der::asn1::ObjectIdentifier>,
640
641    /// If `Some(n)`, reject any certificate whose (notAfter − notBefore) exceeds
642    /// `n` seconds. `None` means unconstrained (the default).
643    ///
644    /// Applied to every certificate in the chain, not just the leaf.
645    /// Violations produce [`Error::ValidityPeriodExceedsMax`].
646    pub max_validity_secs: Option<u64>,
647
648    /// If `Some(list)`, reject any certificate whose signature algorithm OID is
649    /// not in `list`. `None` means any algorithm is accepted (the default).
650    ///
651    /// Applied to every certificate in the chain. The check fires **before**
652    /// signature verification so the error is diagnostic rather than a confusing
653    /// [`Error::SignatureInvalid`].
654    /// Violations produce [`Error::AlgorithmNotAllowed`].
655    pub allowed_signature_algs: Option<Vec<der::asn1::ObjectIdentifier>>,
656
657    /// If `Some(bits)`, reject any certificate carrying an RSA public key whose
658    /// modulus is fewer than `bits` bits. Non-RSA keys are not affected.
659    /// `None` means unconstrained (the default).
660    ///
661    /// Applied to every certificate in the chain.
662    /// Violations produce [`Error::KeyTooSmall`].
663    pub min_rsa_key_bits: Option<u32>,
664
665    /// If `true`, the leaf certificate (chain index 0) must have a non-empty
666    /// `SubjectAltName` extension. `false` means no SAN requirement (the default).
667    ///
668    /// Intermediate CA certificates are not checked by this field.
669    /// Violations produce [`Error::MissingSan`].
670    pub require_subject_alt_name: bool,
671
672    /// If `true`, at least one `rfc822Name` entry must be present in the leaf's
673    /// `SubjectAltName` extension.
674    ///
675    /// Only meaningful when [`require_subject_alt_name`][Self::require_subject_alt_name]
676    /// is also `true`. When `require_subject_alt_name` is `false`, this field has
677    /// no effect.
678    ///
679    /// Default: `false` (backward compatible).
680    /// Violations produce [`Error::MissingRfc822San`].
681    pub require_rfc822_san: bool,
682
683    /// If `Some(oids)`, the leaf certificate must explicitly assert every OID in
684    /// `oids` via its `ExtendedKeyUsage` extension. `None` means no EKU requirement
685    /// (the default).
686    ///
687    /// `anyExtendedKeyUsage` (2.5.29.37.0) does **not** satisfy a specific OID
688    /// check — each required OID must be listed in the cert's EKU extension.
689    /// Violations produce [`Error::MissingEku`].
690    pub required_leaf_eku: Option<Vec<der::asn1::ObjectIdentifier>>,
691}
692
693impl ValidationPolicy {
694    /// Construct a policy with the given time and sensible defaults.
695    ///
696    /// Equivalent to `ValidationPolicy { current_time_unix: now_unix, ..Default::default() }`.
697    /// This is the preferred constructor: it forces the caller to supply a timestamp,
698    /// preventing the silent validity failures caused by `Default`'s `current_time_unix = 0`.
699    #[must_use]
700    pub fn new(now_unix: u64) -> Self {
701        Self {
702            current_time_unix: now_unix,
703            ..Default::default()
704        }
705    }
706}
707
708impl Default for ValidationPolicy {
709    /// Returns a default policy with `current_time_unix = 0` (1970-01-01).
710    ///
711    /// This is **not** safe for production use because every certificate
712    /// issued after the Unix epoch will fail [`Error::ValidityPeriod`].
713    /// Prefer [`ValidationPolicy::new`] (which takes `now_unix` explicitly).
714    /// `Default` is provided only for `..Default::default()` ergonomics on
715    /// this `#[non_exhaustive]` struct.
716    fn default() -> Self {
717        Self {
718            max_path_len: 10,
719            current_time_unix: 0, // caller must set to avoid silent clock skew
720            enforce_key_usage: true,
721            initial_explicit_policy: false,
722            initial_any_policy_inhibit: false,
723            initial_policy_mapping_inhibit: false,
724            initial_policy_set: Vec::new(),
725            // New profile-enforcement fields: all disabled by default so that
726            // existing callers get unconstrained behavior (backward compatible).
727            max_validity_secs: None,
728            allowed_signature_algs: None,
729            min_rsa_key_bits: None,
730            require_subject_alt_name: false,
731            require_rfc822_san: false,
732            required_leaf_eku: None,
733        }
734    }
735}
736
737/// A PKI regime profile that bundles identity, citation, and a validation policy.
738///
739/// # Design rationale
740///
741/// `ValidationPolicy` is the *mechanism*. A `Profile` is the *policy authority*: it
742/// records *who* mandates the policy (e.g., CA/B Forum TLS BR §7.1), supplies a
743/// stable machine-readable identifier, and produces the appropriate
744/// [`ValidationPolicy`] for a given point in time.
745///
746/// Placing the trait in `pkix-path` rather than `pkix-profiles` means that third-party
747/// profile crates (e.g., `pkix-fpki`, `pkix-etsi`) can implement `Profile` by depending
748/// only on `pkix-path` — they do not need to pull in `pkix-profiles`, which would create
749/// a circular coupling between reference implementations and the trait definition.
750///
751/// # `no_std` compatibility
752///
753/// The trait is `no_std`-safe: it uses only `&str`, `&[ObjectIdentifier]`, and
754/// `ValidationPolicy` (all of which are available without `std`).
755/// Implementors on embedded targets may return static `&'static str` slices and
756/// construct `ValidationPolicy` without allocation.
757///
758/// # Implementing `Profile`
759///
760/// ```rust,no_run
761/// use pkix_path::{Profile, ValidationPolicy};
762///
763/// struct MyCorpProfile;
764///
765/// impl Profile for MyCorpProfile {
766///     fn id(&self) -> &'static str { "example.corp.internal" }
767///     fn version(&self) -> &'static str { "2024-01" }
768///     fn policy(&self, now_unix: u64) -> ValidationPolicy {
769///         let mut p = ValidationPolicy::new(now_unix);
770///         p.max_validity_secs = Some(365 * 86_400);
771///         p
772///     }
773///     fn policy_oids(&self) -> &[der::asn1::ObjectIdentifier] { &[] }
774/// }
775/// ```
776pub trait Profile {
777    /// Stable, dot-separated identifier for this profile.
778    ///
779    /// The identifier MUST be unique across all deployed profiles and MUST NOT
780    /// change between versions of the same profile. Use reverse-DNS or
781    /// CABF/IETF-style naming conventions, e.g.:
782    /// - `"cabf.br.tls"` — CA/B Forum TLS Baseline Requirements
783    /// - `"cabf.smime"` — CA/B Forum S/MIME Baseline Requirements
784    /// - `"fpki.common-policy"` — US Federal PKI Common Policy
785    ///
786    /// Lint engines use this ID as a namespace prefix for finding IDs.
787    fn id(&self) -> &'static str;
788
789    /// Human-readable version string for this profile.
790    ///
791    /// Typically the ballot or specification version that last changed the
792    /// policy rules, e.g., `"SC-081"`, `"2024-01"`, or `"v2.0.1"`.
793    /// Used for diagnostic messages and audit logs; not parsed by the engine.
794    fn version(&self) -> &'static str;
795
796    /// Produce the [`ValidationPolicy`] for the given point in time.
797    ///
798    /// `now_unix` is seconds since the Unix epoch. The profile may use this to
799    /// implement phased validity caps or algorithm retirement schedules.
800    /// The returned `ValidationPolicy` MUST have `current_time_unix` set to
801    /// `now_unix`.
802    #[must_use]
803    fn policy(&self, now_unix: u64) -> ValidationPolicy;
804
805    /// The certificate policy OIDs that this profile recognises as its own.
806    ///
807    /// Used by registry and composition tools to detect when two profiles
808    /// claim overlapping policy space. Returns an empty slice if the profile
809    /// does not restrict certificate policy OIDs.
810    fn policy_oids(&self) -> &[der::asn1::ObjectIdentifier];
811}
812
813/// The result of a successful certificate path validation.
814///
815/// Fields are `pub` for direct read access. `#[non_exhaustive]` prevents external
816/// code from constructing `ValidatedPath` directly and from pattern-matching
817/// exhaustively, preserving the ability to add fields in future minor versions
818/// without a breaking change.
819///
820/// # `Copy` removal in 0.3.0
821///
822/// `ValidatedPath` is no longer `Copy`. The struct now carries owned
823/// heap-backed fields exposing RFC 5280 §6.1.5 wrap-up outputs (the leaf's
824/// subject DN, issuer DN, serial number, and SubjectPublicKeyInfo) so
825/// consumers can read the validated leaf identity without re-parsing
826/// `chain[0]`. These fields cannot satisfy the `Copy` bound; pre-0.3
827/// callers that relied on bit-copy semantics need to add `.clone()` or
828/// pass `&ValidatedPath` instead.
829///
830/// # §6.1.5 wrap-up outputs
831///
832/// RFC 5280 §6.1.5 specifies that successful path validation produces
833/// several outputs identifying the validated leaf certificate. The four
834/// leaf-intrinsic outputs (subject, issuer, serial, SPKI) are surfaced
835/// here as convenience accessors; consumers no longer need to re-parse
836/// `chain[0]` to obtain them.
837///
838/// Other §6.1.5 outputs that depend on validation state (the final
839/// `working_public_key_parameters`, the `valid_policy_tree`) are not yet
840/// surfaced. Future minor versions may add them; callers should pattern
841/// match with `..` rest patterns on this `#[non_exhaustive]` struct.
842///
843/// `Hash` is no longer derived because none of the new field types
844/// (`x509_cert::name::Name`, `x509_cert::serial_number::SerialNumber`,
845/// `spki::SubjectPublicKeyInfoOwned`) implement `Hash` upstream. No
846/// in-tree consumer used `ValidatedPath` as a `HashMap`/`HashSet` key,
847/// so dropping the derive is observable but should not require code
848/// changes for any existing user.
849#[derive(Clone, Debug, PartialEq, Eq)]
850#[non_exhaustive]
851pub struct ValidatedPath {
852    /// Index into the `anchors` slice of the trust anchor that terminated the path.
853    pub anchor_index: usize,
854    /// Number of certificates in the validated chain minus one (`chain.len() - 1`).
855    ///
856    /// For a single self-signed certificate, `depth == 0`. For a leaf + one
857    /// intermediate, `depth == 1`. This equals `chain.len().saturating_sub(1)`.
858    ///
859    /// Note: this counts all certificates except the trust anchor — including
860    /// self-issued intermediates that RFC 5280 §4.2.1.9 excludes from the
861    /// `pathLenConstraint` count. For chains with self-issued intermediates the
862    /// `depth` field may be larger than the RFC 5280 path length.
863    ///
864    /// **Do not** compare `depth` directly against a certificate's
865    /// [`BasicConstraints`] `pathLenConstraint` value. RFC 5280 §4.2.1.9
866    /// defines `pathLenConstraint` as the number of non-self-issued
867    /// intermediates below the issuing CA, which differs from this field's
868    /// total certificate count. Use the RFC 5280 §6.1.4(b) accounting
869    /// performed by `chain_walk` instead.
870    ///
871    /// [`BasicConstraints`]: x509_cert::ext::pkix::BasicConstraints
872    pub depth: usize,
873
874    /// Subject DN of the validated leaf certificate (`chain[0].subject`).
875    ///
876    /// RFC 5280 §6.1.5 names this output indirectly: a successful path
877    /// validation produces the leaf's identity (subject + serial uniquely
878    /// identify a certificate). This field is a convenience accessor —
879    /// callers could equivalently read `chain[0].tbs_certificate.subject`,
880    /// but threading the chain to every consumer that needs the leaf's DN
881    /// is awkward. The clone is owned to free the validated value from any
882    /// lifetime tie to the input chain.
883    pub leaf_subject: x509_cert::name::Name,
884
885    /// Issuer DN of the validated leaf certificate (`chain[0].issuer`).
886    ///
887    /// This is the §6.1.5(f) `working_issuer_name` output as observed at
888    /// the leaf cert (which is the first cert processed by the §6.1
889    /// algorithm, so the leaf's `issuer` is the initial value of
890    /// `working_issuer_name` before iteration begins; for a successfully
891    /// validated chain it identifies the directly-signing CA).
892    pub leaf_issuer: x509_cert::name::Name,
893
894    /// Serial number of the validated leaf certificate
895    /// (`chain[0].serial_number`).
896    ///
897    /// Together with [`Self::leaf_issuer`] this forms the RFC 5280
898    /// §4.1.2.2 unique certificate identifier (`{ issuer, serial }`),
899    /// which downstream code commonly needs for revocation lookups,
900    /// audit logging, or de-duplication.
901    pub leaf_serial: x509_cert::serial_number::SerialNumber,
902
903    /// `SubjectPublicKeyInfo` of the validated leaf certificate.
904    ///
905    /// This is the §6.1.5(c)(d)(e) `working_public_key` /
906    /// `working_public_key_algorithm` / `working_public_key_parameters`
907    /// outputs, bundled into the canonical SPKI form. Downstream code
908    /// (e.g. application-layer signature verification using the validated
909    /// leaf as a trust delegate) needs the full SPKI rather than only the
910    /// algorithm identifier or only the public-key bits.
911    ///
912    /// Note: this field reflects the leaf's *encoded* SPKI as it appeared
913    /// in the certificate, not a normalized/canonicalized form. PSS
914    /// parameters, RSA `parameters: NULL` vs `parameters: absent`
915    /// ambiguities, etc. are preserved verbatim.
916    pub leaf_spki: spki::SubjectPublicKeyInfoOwned,
917
918    /// The final RFC 5280 §6.1.5 `valid_policy_tree`, or `None` if the
919    /// tree was reduced to NULL during validation.
920    ///
921    /// `Some(tree)` is the post-§6.1.5(g)(iii) state of the tree (i.e.
922    /// after intersection with `initial_policy_set` and post-pruning).
923    /// `None` means the path validated under `explicit_policy == 0`
924    /// without any policy constraint — semantically "no policies asserted
925    /// or required". Callers that want to enforce a specific policy OID
926    /// (or extract qualifiers attached to a specific policy) should
927    /// inspect this field and treat `None` as "no policy information
928    /// available", not as a validation failure.
929    ///
930    /// Each node carries its policy qualifiers (RFC 5280 §6.1.2(a)) in
931    /// the upstream `PolicyQualifierInfo` form (a `(qualifier_id_oid,
932    /// raw_any_value)` pair, no decoding of `qualifier`). See
933    /// [`PolicyTreeNode`] for the per-node shape.
934    ///
935    /// For convenience, [`Self::policy_qualifiers`] iterates
936    /// `(policy_oid, qualifier)` pairs across all tree nodes.
937    pub valid_policy_tree: Option<Vec<PolicyTreeNode>>,
938}
939
940/// A node in the §6.1.5 `valid_policy_tree`, exposed for post-validation
941/// qualifier extraction on [`ValidatedPath::valid_policy_tree`].
942///
943/// This is the public mirror of the internal `PolicyNode` type. It is
944/// intentionally a separate public type so that the internal node shape
945/// (which may evolve as the path-validator gains features) is not part
946/// of the public API surface.
947#[derive(Clone, Debug, PartialEq, Eq)]
948#[non_exhaustive]
949pub struct PolicyTreeNode {
950    /// Depth at which this node appears in the tree.
951    ///
952    /// `0` is the synthetic anyPolicy root sentinel (always present
953    /// while the tree is alive; carries no qualifiers). `1` is the
954    /// trust-anchor-adjacent CA. `n` is the leaf.
955    pub depth: usize,
956
957    /// The policy OID this node represents.
958    ///
959    /// May be `id-ce-certificatePolicies-anyPolicy` (2.5.29.32.0) for
960    /// nodes that survived an anyPolicy expansion or were not yet
961    /// materialized into specific policies.
962    pub valid_policy: der::asn1::ObjectIdentifier,
963
964    /// Set of policies in the next certificate that are consistent with
965    /// this node, per RFC 5280 §6.1.2(a) `expected_policy_set`.
966    ///
967    /// Initialized to `{valid_policy}` and updated by `PolicyMappings`
968    /// extensions during the walk.
969    pub expected_policy_set: Vec<der::asn1::ObjectIdentifier>,
970
971    /// Policy qualifiers attached to this node, in the upstream
972    /// `PolicyQualifierInfo` form: a `(policy_qualifier_id, qualifier)`
973    /// pair where `qualifier` is a raw `der::Any`. Decoding is left to
974    /// the caller because [the `x509-cert` 0.2.5 `UserNotice` type has
975    /// an upstream typo on `notice_ref`][1] — pass-through avoids the
976    /// buggy decoder. The two standard qualifier IDs are
977    /// `id-qt-cps` (1.3.6.1.5.5.7.2.1, qualifier is a `CPSuri` /
978    /// `IA5String`) and `id-qt-unotice` (1.3.6.1.5.5.7.2.2, qualifier
979    /// is a `UserNotice`).
980    ///
981    /// [1]: https://github.com/RustCrypto/formats/issues/x509-cert
982    pub qualifiers: Vec<x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo>,
983}
984
985impl ValidatedPath {
986    /// Iterate `(policy_oid, qualifier)` pairs across every node in the
987    /// final policy tree.
988    ///
989    /// Returns an empty iterator if the tree is `None` (either the chain
990    /// validated under `explicit_policy == 0` with no policy assertions,
991    /// or the tree was reduced to NULL during validation).
992    ///
993    /// Each yielded pair is `(node.valid_policy, &qualifier)` — the
994    /// qualifier is a reference into the tree, valid for the borrow
995    /// duration. Multiple qualifiers attached to the same policy yield
996    /// multiple pairs with equal first elements; multiple nodes with the
997    /// same policy OID (possible in pathological policy-mapping cases)
998    /// also yield multiple pairs.
999    ///
1000    /// Callers commonly need to:
1001    ///
1002    /// 1. Filter by `policy_qualifier_id` to find a specific qualifier
1003    ///    type (CPS pointer or user notice).
1004    /// 2. Read `qualifier.qualifier` (an `Option<der::Any>`) and decode
1005    ///    according to the `policy_qualifier_id`. See [`PolicyTreeNode::qualifiers`]
1006    ///    for why we do not decode upstream-side.
1007    pub fn policy_qualifiers(
1008        &self,
1009    ) -> impl Iterator<
1010        Item = (
1011            &der::asn1::ObjectIdentifier,
1012            &x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo,
1013        ),
1014    > + '_ {
1015        self.valid_policy_tree
1016            .as_ref()
1017            .into_iter()
1018            .flat_map(|tree| tree.iter())
1019            .flat_map(|node| node.qualifiers.iter().map(move |q| (&node.valid_policy, q)))
1020    }
1021}
1022
1023/// Validate a certificate chain from subject to a trust anchor.
1024///
1025/// `chain` must be ordered leaf-first:
1026/// - `chain[0]` is the subject (end-entity) certificate
1027/// - `chain[1..]` are intermediates in issuer order
1028/// - The last element of `chain` must be issued by one of `anchors`
1029///
1030/// Validation follows RFC 5280 §6.1. Each certificate's signature is verified
1031/// using `verifier`, with the signing key taken from the next certificate in
1032/// the chain (or the matching trust anchor for the last cert).
1033///
1034/// # Errors
1035///
1036/// Returns `Err` on the first RFC 5280 §6.1 check failure. The error variant
1037/// includes the chain index of the failing certificate where applicable.
1038///
1039/// # Limitations
1040///
1041/// See crate-level documentation for current scope limits.
1042pub fn validate_path<V>(
1043    chain: &[Certificate],
1044    anchors: &[TrustAnchor],
1045    policy: &ValidationPolicy,
1046    verifier: &V,
1047) -> Result<ValidatedPath>
1048where
1049    V: SignatureVerifier,
1050{
1051    // (1) Input guards: reject empty chain or anchors, check OID consistency.
1052    check_inputs(chain, anchors)?;
1053    check_oid_consistency(chain)?;
1054
1055    // (2) Path-length check (anchor-independent).
1056    // RFC 5280 §4.2.1.9: pathLen counts non-self-issued intermediates only.
1057    let num_non_si_intermediates = chain[1..]
1058        .iter()
1059        .filter(|c| !is_self_issued_cert(c))
1060        .count();
1061    if num_non_si_intermediates > policy.max_path_len as usize {
1062        return Err(Error::PathTooLong);
1063    }
1064
1065    // (3) Try each name-matching anchor. Iterating all candidates handles key
1066    //     rollover: multiple anchors may share a DN but have different keys
1067    //     (e.g., during a root CA rotation). The first anchor that passes the
1068    //     full chain walk is used; the last error is returned if none succeed.
1069    //
1070    //     Complexity: O(A × N) where A = number of anchors, N = chain length.
1071    //     For the common case of O(1) matching anchors this is effectively O(N).
1072    let last_cert = chain.last().ok_or(Error::NoTrustedPath)?;
1073    let is_self_issued = names_match(
1074        &last_cert.tbs_certificate.issuer,
1075        &last_cert.tbs_certificate.subject,
1076    );
1077    let mut last_err = Error::NoTrustedPath;
1078    for (anchor_index, anchor) in anchors.iter().enumerate() {
1079        if !names_match(&anchor.subject, &last_cert.tbs_certificate.issuer) {
1080            continue;
1081        }
1082        // For self-issued certs the cert and anchor are the same entity; their
1083        // keys must match (RFC 5280 §3.2 name-collision guard).
1084        if is_self_issued
1085            && !spki_key_matches(
1086                &anchor.subject_public_key_info,
1087                &last_cert.tbs_certificate.subject_public_key_info,
1088            )
1089        {
1090            continue;
1091        }
1092        match chain_walk(chain, anchor, policy, verifier) {
1093            Ok(final_policy_tree) => {
1094                // §6.1.5 leaf-intrinsic outputs. `chain[0]` is guaranteed
1095                // non-empty by `check_inputs` at the top of this function.
1096                // The four `leaf_*` fields are direct clones of the leaf's
1097                // `tbs_certificate` fields; populating them from `chain[0]`
1098                // (rather than threading them out of `chain_walk`) avoids
1099                // adding more entries to the walker's return tuple.
1100                let leaf_tbs = &chain[0].tbs_certificate;
1101                // Convert the internal `PolicyNode` to the public
1102                // `PolicyTreeNode`. The two structs are field-compatible
1103                // by design; the conversion is a `.into()` per node.
1104                // Keeping `PolicyNode` private preserves the freedom to
1105                // evolve the internal representation (e.g. add caching
1106                // of qualifier_id OIDs) without a public-API break.
1107                let valid_policy_tree = final_policy_tree.map(|nodes| {
1108                    nodes
1109                        .into_iter()
1110                        .map(|n| PolicyTreeNode {
1111                            depth: n.depth,
1112                            valid_policy: n.valid_policy,
1113                            expected_policy_set: n.expected_policy_set,
1114                            qualifiers: n.qualifiers,
1115                        })
1116                        .collect()
1117                });
1118                return Ok(ValidatedPath {
1119                    anchor_index,
1120                    depth: chain.len().saturating_sub(1),
1121                    leaf_subject: leaf_tbs.subject.clone(),
1122                    leaf_issuer: leaf_tbs.issuer.clone(),
1123                    leaf_serial: leaf_tbs.serial_number.clone(),
1124                    leaf_spki: leaf_tbs.subject_public_key_info.clone(),
1125                    valid_policy_tree,
1126                });
1127            }
1128            Err(e) => last_err = e,
1129        }
1130    }
1131    Err(last_err)
1132}
1133
1134/// Validate a certificate chain using a [`Profile`] to produce the policy.
1135///
1136/// This is a convenience wrapper around [`validate_path`] for callers that
1137/// work with a `Profile` implementation rather than constructing a
1138/// [`ValidationPolicy`] directly.
1139///
1140/// The profile's [`Profile::policy`] method is called with `now_unix` to
1141/// produce the `ValidationPolicy`.  The returned policy's `current_time_unix`
1142/// is then unconditionally overwritten with `now_unix`, so that a buggy
1143/// `Profile` implementation that returns the wrong clock value cannot silently
1144/// cause validity checks to run against the wrong time.
1145///
1146/// See [`validate_path`] for full documentation of the remaining parameters
1147/// and error semantics.
1148///
1149/// # Errors
1150///
1151/// Returns `Err(Error::...)` for each validation failure. See [`Error`] for the full list of failure conditions.
1152pub fn validate_path_with_profile<V, P>(
1153    chain: &[Certificate],
1154    anchors: &[TrustAnchor],
1155    profile: &P,
1156    now_unix: u64,
1157    verifier: &V,
1158) -> Result<ValidatedPath>
1159where
1160    V: SignatureVerifier,
1161    P: Profile,
1162{
1163    let mut policy = profile.policy(now_unix);
1164    // Defense-in-depth: overwrite current_time_unix with the caller's value.
1165    // A correct Profile implementation already sets this in policy(), but
1166    // an incorrect implementation might use a stale or wrong clock. This
1167    // overwrite is a belt-and-suspenders guard — it does not compensate for a
1168    // known bug; no existing Profile impl is incorrect.
1169    policy.current_time_unix = now_unix;
1170    validate_path(chain, anchors, &policy, verifier)
1171}
1172
1173// ---------------------------------------------------------------------------
1174// validate_path helpers — input guards and OID consistency (PKIX-6vu)
1175// ---------------------------------------------------------------------------
1176
1177/// Compare two SPKIs for the purpose of the self-issued anchor guard.
1178///
1179/// Compares algorithm OID and key bytes only — not the parameters field.
1180/// This is intentional: for RSA, explicit NULL parameters and absent
1181/// parameters are both valid encodings of the same algorithm (RFC 3279
1182/// §2.3.1); comparing the full `AlgorithmIdentifier` would wrongly reject
1183/// a valid anchor whose SPKI parameter encoding differs from the cert's.
1184/// For ECDSA, the parameters carry the curve OID, but two keys on different
1185/// curves also differ in their raw key bytes, so OID + key comparison is
1186/// still sufficient to distinguish them.
1187fn spki_key_matches(
1188    a: &spki::SubjectPublicKeyInfoOwned,
1189    b: &spki::SubjectPublicKeyInfoOwned,
1190) -> bool {
1191    a.algorithm.oid == b.algorithm.oid && a.subject_public_key == b.subject_public_key
1192}
1193
1194fn check_inputs(chain: &[Certificate], anchors: &[TrustAnchor]) -> Result<()> {
1195    if chain.is_empty() || anchors.is_empty() {
1196        return Err(Error::NoTrustedPath);
1197    }
1198    // Duplicate detection: check all pairs for (issuer DN, serial number) identity.
1199    // Per RFC 5280 §4.1.2.2, issuer+serial uniquely identifies a certificate.
1200    // A cert appearing twice in the chain is a construction error; reporting
1201    // DuplicateCertificate is cleaner than the confusing SignatureInvalid or
1202    // ChainBroken that would otherwise result.
1203    //
1204    // SPKI equality is intentionally NOT used here: cross-signed CAs legitimately
1205    // have two distinct certificates sharing the same public key (same SPKI, different
1206    // issuer+serial). Using issuer+serial avoids false positives in those chains.
1207    //
1208    // O(n²) over chain.len() — acceptable for chains of typical length (2–5 certs).
1209    for i in 0..chain.len() {
1210        for j in (i + 1)..chain.len() {
1211            let a = &chain[i].tbs_certificate;
1212            let b = &chain[j].tbs_certificate;
1213            if names_match(&a.issuer, &b.issuer) && a.serial_number == b.serial_number {
1214                return Err(Error::DuplicateCertificate {
1215                    first: i,
1216                    second: j,
1217                });
1218            }
1219        }
1220    }
1221    Ok(())
1222}
1223
1224/// RFC 5280 §4.1.1.2: outer signatureAlgorithm OID must equal inner TBSCertificate.signature OID.
1225///
1226/// Only OIDs are compared, not parameters.  RFC 5280 says the two
1227/// `AlgorithmIdentifiers` MUST be identical, but many production CAs
1228/// generate certs where one field has explicit NULL parameters and the other
1229/// omits them — a mismatch that OpenSSL and other validators accept in
1230/// practice.  OID-only comparison preserves the security intent (the same
1231/// algorithm must be named in both places) without rejecting otherwise-valid
1232/// certs from common PKI deployments.
1233fn check_oid_consistency(chain: &[Certificate]) -> Result<()> {
1234    for (index, cert) in chain.iter().enumerate() {
1235        if cert.signature_algorithm.oid != cert.tbs_certificate.signature.oid {
1236            return Err(Error::MalformedCertificate { index });
1237        }
1238    }
1239    Ok(())
1240}
1241
1242// ---------------------------------------------------------------------------
1243// Critical extension guard (PKIX-ad6)
1244// ---------------------------------------------------------------------------
1245
1246const OID_KEY_USAGE: der::asn1::ObjectIdentifier =
1247    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.15");
1248
1249const OID_BASIC_CONSTRAINTS: der::asn1::ObjectIdentifier =
1250    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.19");
1251
1252const OID_SUBJECT_ALT_NAME: der::asn1::ObjectIdentifier =
1253    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.17");
1254
1255const OID_EXTENDED_KEY_USAGE: der::asn1::ObjectIdentifier =
1256    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.37");
1257
1258const OID_NAME_CONSTRAINTS: der::asn1::ObjectIdentifier =
1259    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.30");
1260
1261const OID_CERTIFICATE_POLICIES: der::asn1::ObjectIdentifier =
1262    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.32");
1263
1264const OID_POLICY_MAPPINGS: der::asn1::ObjectIdentifier =
1265    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.33");
1266
1267const OID_POLICY_CONSTRAINTS: der::asn1::ObjectIdentifier =
1268    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.36");
1269
1270const OID_INHIBIT_ANY_POLICY: der::asn1::ObjectIdentifier =
1271    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.54");
1272
1273/// OID for the `anyPolicy` wildcard (2.5.29.32.0 — a child of id-ce-certificatePolicies).
1274const OID_ANY_POLICY: der::asn1::ObjectIdentifier =
1275    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.32.0");
1276
1277/// OID for the emailAddress attribute in Distinguished Names (PKCS #9 §5.2.1).
1278/// Used when enforcing RFC 5280 §4.2.1.10 rfc822Name constraints against DN attributes.
1279const OID_EMAIL_ADDRESS: der::asn1::ObjectIdentifier =
1280    der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.1");
1281
1282/// OIDs of extensions that this implementation handles; all others, if critical, cause rejection.
1283///
1284/// `OID_SUBJECT_ALT_NAME` is listed here so that certs with critical SAN extensions
1285/// (e.g. TLS server certs) do not fail with `UnhandledCriticalExtension`. A cert
1286/// with an empty Subject and a critical SAN is handled correctly: the SAN is
1287/// used as the cert's identity via `cert_has_san_identity` / `working_issuer_is_san_identity`
1288/// (RFC 5280 §4.2.1.6), so name linkage does not fall back to the empty Subject DN.
1289///
1290/// `OID_EXTENDED_KEY_USAGE` is listed here so that certs with critical EKU
1291/// (common in CA/B Forum TLS and code-signing certificates) do not fail with
1292/// `UnhandledCriticalExtension`. RFC 5280 §6.1 path validation does not require
1293/// inspecting EKU values; the extension is accepted and its content is not verified.
1294const HANDLED_CRITICAL_OIDS: &[der::asn1::ObjectIdentifier] = &[
1295    OID_KEY_USAGE,
1296    OID_BASIC_CONSTRAINTS,
1297    OID_SUBJECT_ALT_NAME,
1298    OID_EXTENDED_KEY_USAGE,
1299    OID_NAME_CONSTRAINTS,
1300    OID_CERTIFICATE_POLICIES,
1301    OID_POLICY_MAPPINGS,
1302    OID_POLICY_CONSTRAINTS,
1303    OID_INHIBIT_ANY_POLICY,
1304];
1305
1306/// RFC 5280 §6.1.3(a)(3): reject any critical extension not in the handled set.
1307fn check_critical_extensions(cert: &Certificate, index: usize) -> Result<()> {
1308    for ext in cert.tbs_certificate.extensions.as_deref().unwrap_or(&[]) {
1309        if ext.critical && !HANDLED_CRITICAL_OIDS.contains(&ext.extn_id) {
1310            return Err(Error::UnhandledCriticalExtension { index });
1311        }
1312    }
1313    Ok(())
1314}
1315
1316// ---------------------------------------------------------------------------
1317// Policy tree (RFC 5280 §6.1) — PKIX-mi3.2
1318// ---------------------------------------------------------------------------
1319
1320/// A node in the certificate policy tree (RFC 5280 §6.1.2(a)).
1321///
1322/// Stored as a flat `Vec<PolicyNode>`.  Depth 0 is the synthetic anyPolicy
1323/// root (initialized before any cert is processed).  Depth `d` corresponds
1324/// to the d-th certificate from the trust-anchor end (depth 1 = CA adjacent
1325/// to trust anchor, depth n = leaf).
1326///
1327/// # Qualifier handling
1328///
1329/// Each node carries the policy qualifiers (`qualifier_set` per RFC 5280
1330/// §6.1.2(a)) attached to it at creation time. The qualifiers are
1331/// preserved as the upstream `x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo`
1332/// (a `(qualifier_id_oid, raw_any_value)` pair) without decoding the
1333/// `Any` content. Two reasons for the pass-through approach:
1334///
1335/// 1. RFC 5280 §6.1.2(a) says qualifier processing is application-specific
1336///    — path validation MUST NOT gate on qualifier validity.
1337/// 2. `x509-cert` 0.2.5 has a typo on `UserNotice.notice_ref` (declared
1338///    `Option<GeneralizedTime>` instead of `Option<NoticeReference>`),
1339///    so decoding the `Any` upstream-side would silently mishandle real-world
1340///    UserNotice qualifiers. Pass-through avoids the buggy decoder.
1341///
1342/// Qualifiers travel with the node through pruning (whole-node delete) and
1343/// are sourced per-site at construction:
1344/// - §6.1.3(d)(1)(i),(ii): from the current cert's `policy_info.policy_qualifiers`
1345///   for that policy OID.
1346/// - §6.1.3(d)(2) (anyPolicy expansion): from the current cert's anyPolicy
1347///   PolicyInformation entry's qualifiers.
1348/// - §6.1.4(b)(1) (PolicyMappings synthesis): from the current cert's
1349///   anyPolicy PolicyInformation entry's qualifiers (per RFC §6.1.4(b)(1)(ii)).
1350/// - §6.1.5(g)(iii)(3) (initial-policy-set materialization): inherited from
1351///   the leaf anyPolicy node that is about to be deleted.
1352/// - Synthetic depth-0 root: empty (no source).
1353#[derive(Clone, Debug)]
1354struct PolicyNode {
1355    /// Certificate depth at which this node was added (0 = root sentinel).
1356    depth: usize,
1357    /// The policy OID this node represents.
1358    valid_policy: der::asn1::ObjectIdentifier,
1359    /// Policies in the NEXT certificate that are consistent with this node.
1360    /// Initialized to `{valid_policy}`; updated by `PolicyMappings`.
1361    expected_policy_set: Vec<der::asn1::ObjectIdentifier>,
1362    /// Policy qualifiers attached to this node, per RFC 5280 §6.1.2(a).
1363    /// See struct rustdoc for sourcing rules per construction site.
1364    qualifiers: Vec<x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo>,
1365}
1366
1367/// Extract the qualifiers attached to the cert's `anyPolicy` PolicyInformation
1368/// entry, or an empty Vec if no such entry exists or it has no qualifiers.
1369///
1370/// Used at the §6.1.3(d)(2) and §6.1.4(b)(1) synthesis sites where new
1371/// nodes are added on behalf of the cert's anyPolicy entry. Per RFC 5280
1372/// these synthesized nodes inherit the cert's anyPolicy qualifiers,
1373/// distinct from any per-policy qualifiers on more-specific entries.
1374fn cert_any_policy_qualifiers(
1375    cp: &x509_cert::ext::pkix::certpolicy::CertificatePolicies,
1376) -> Vec<x509_cert::ext::pkix::certpolicy::PolicyQualifierInfo> {
1377    cp.0.iter()
1378        .find(|pi| pi.policy_identifier == OID_ANY_POLICY)
1379        .and_then(|pi| pi.policy_qualifiers.clone())
1380        .unwrap_or_default()
1381}
1382
1383/// Initialise the policy tree with the anyPolicy root node (RFC 5280 §6.1.2(a)).
1384fn init_policy_tree() -> Vec<PolicyNode> {
1385    vec![PolicyNode {
1386        depth: 0,
1387        valid_policy: OID_ANY_POLICY,
1388        expected_policy_set: vec![OID_ANY_POLICY],
1389        // Synthetic root sentinel: no cert is yet in scope, no qualifiers.
1390        qualifiers: Vec::new(),
1391    }]
1392}
1393
1394/// Prune nodes at depth < `cert_depth` that have no children at depth+1.
1395///
1396/// After processing certificate at depth `d`, any ancestor node with no
1397/// surviving child must be deleted (RFC 5280 §6.1.3(d)(3)): "If there is a
1398/// node in the `valid_policy_tree` of depth i-1 or less without any child
1399/// nodes, delete that node.  Repeat this step until there are no nodes of
1400/// depth i-1 or less without children."
1401///
1402/// Starts by pruning depth `cert_depth - 1` (checking against children at
1403/// `cert_depth`), then walks upward toward depth 1.  The depth-0 root is
1404/// left in place (it is only removed when `policy_tree` is set to `None`).
1405fn prune_policy_tree(tree: &mut Vec<PolicyNode>, cert_depth: usize) {
1406    // Walk upward from cert_depth-1 down to depth 1 (inclusive), pruning nodes
1407    // that have no surviving child at depth d+1.  Depth 0 (the anyPolicy root
1408    // sentinel) is never pruned here — the caller clears policy_tree entirely
1409    // when it becomes effectively NULL (no nodes at depth ≥ 1).
1410    //
1411    // RFC 5280 §6.1.3(d)(3): "If there is a node in the valid_policy_tree of
1412    // depth i-1 or less without any child nodes, delete that node. Repeat this
1413    // step until there are no nodes of depth i-1 or less without children."
1414    //
1415    // Iteration: d starts at cert_depth, decrements to 1.  At each step we
1416    // prune depth d-1 against children at depth d, then continue upward.
1417    // We stop at d==1 because depth 0 is the root sentinel and is excluded.
1418    // Invariant: callers pass cert_depth >= 2, so d starts at >= 2 and the
1419    // prune_depth == 0 guard below is the only termination condition needed.
1420    let mut d = cert_depth;
1421    loop {
1422        let prune_depth = d - 1; // depth to prune (children are at d)
1423        if prune_depth == 0 {
1424            break; // depth-0 root sentinel — never prune it
1425        }
1426        // Collect child OIDs into a temporary Vec to release the shared borrow
1427        // before tree.retain() takes &mut self. This allocates once per depth
1428        // level per prune pass. In practice, chains are ≤ 10 deep and the policy
1429        // tree is small (≤ 5 nodes), so the allocation cost is negligible.
1430        let child_policies: Vec<der::asn1::ObjectIdentifier> = tree
1431            .iter()
1432            .filter(|n| n.depth == d)
1433            .map(|n| n.valid_policy)
1434            .collect();
1435        // Remove nodes at prune_depth that have no surviving child at depth d.
1436        // A node has a child if some child's valid_policy appears in its
1437        // expected_policy_set (policy mappings may have changed those).
1438        // anyPolicy nodes are not exempt — they get pruned the same way.
1439        tree.retain(|n| {
1440            if n.depth != prune_depth {
1441                return true; // leave nodes at other depths untouched
1442            }
1443            child_policies
1444                .iter()
1445                .any(|cp| n.expected_policy_set.contains(cp))
1446        });
1447        d -= 1;
1448        // Continue upward even if prune_depth became empty — the level above
1449        // may now also be childless and needs pruning.
1450    }
1451}
1452
1453// ---------------------------------------------------------------------------
1454// KeyUsage extraction (PKIX-8ae)
1455// ---------------------------------------------------------------------------
1456
1457/// Returns whether the `keyCertSign` bit is set in the `KeyUsage` extension.
1458///
1459/// - `None`         — `KeyUsage` extension absent (no constraint)
1460/// - `Ok(Some(true))`  — keyCertSign is set
1461/// - `Ok(Some(false))` — `KeyUsage` present, keyCertSign NOT set
1462/// - `Ok(None)`        — `KeyUsage` extension absent
1463/// - `Err(_)`          — `KeyUsage` present but DER-malformed (fail-closed)
1464fn has_key_cert_sign(cert: &Certificate) -> der::Result<Option<bool>> {
1465    use x509_cert::ext::pkix::KeyUsage;
1466
1467    try_find_cert_ext::<KeyUsage>(cert, OID_KEY_USAGE).map(|opt| opt.map(|ku| ku.key_cert_sign()))
1468}
1469
1470// ---------------------------------------------------------------------------
1471// Extension extraction helpers
1472// ---------------------------------------------------------------------------
1473
1474/// Find and decode an X.509 extension from `cert` by OID.
1475///
1476/// **Fail-open**: returns `None` if the extension is absent *or* if its DER
1477/// value cannot be decoded. Decoding errors are silently discarded.
1478///
1479/// Use this for extensions where a parse failure is tolerable (e.g., optional
1480/// informational extensions). For security-critical extensions where a parse
1481/// failure must be propagated, use [`try_find_cert_ext`] instead.
1482fn find_cert_ext<T: der::DecodeOwned>(
1483    cert: &Certificate,
1484    oid: der::asn1::ObjectIdentifier,
1485) -> Option<T> {
1486    cert.tbs_certificate
1487        .extensions
1488        .as_deref()
1489        .unwrap_or(&[])
1490        .iter()
1491        .find(|e| e.extn_id == oid)
1492        .and_then(|e| T::from_der(e.extn_value.as_bytes()).ok())
1493}
1494
1495/// Look up and decode an X.509 extension from `cert` by OID.
1496///
1497/// **Fail-closed**: propagates DER decoding errors to the caller rather than
1498/// discarding them. This is appropriate for security-critical extensions where
1499/// a malformed value must not be silently ignored.
1500///
1501/// Returns:
1502/// - `Ok(None)` — extension absent.
1503/// - `Ok(Some(T))` — extension present and decoded successfully.
1504/// - `Err(der::Error)` — extension present but DER decoding failed.
1505///
1506/// For non-critical extensions where a parse failure should be treated as
1507/// absent, use [`find_cert_ext`] (fail-open) instead.
1508fn try_find_cert_ext<T: der::DecodeOwned>(
1509    cert: &Certificate,
1510    oid: der::asn1::ObjectIdentifier,
1511) -> der::Result<Option<T>> {
1512    cert.tbs_certificate
1513        .extensions
1514        .as_deref()
1515        .unwrap_or(&[])
1516        .iter()
1517        .find(|e| e.extn_id == oid)
1518        .map_or(Ok(None), |e| T::from_der(e.extn_value.as_bytes()).map(Some))
1519}
1520
1521/// Decode the `SubjectAltName` extension from `cert`.
1522///
1523/// **Fail-closed**: a present-but-malformed SAN returns `Err` rather than being
1524/// silently treated as absent.  Treating a malformed SAN as absent during name
1525/// constraint checking would allow a cert to bypass NC exclusion/permission
1526/// constraints when the SAN extension is present but cannot be decoded (vjc.20).
1527fn cert_subject_alt_names(
1528    cert: &Certificate,
1529    index: usize,
1530) -> crate::Result<Option<x509_cert::ext::pkix::SubjectAltName>> {
1531    try_find_cert_ext(cert, OID_SUBJECT_ALT_NAME).map_err(|_| Error::MalformedCertificate { index })
1532}
1533
1534/// Decode the `NameConstraints` extension from `cert`.
1535///
1536/// Returns `Err(MalformedCertificate)` if the extension is present but:
1537/// - its DER cannot be decoded (vjc.7: fail-closed on security-critical extension), or
1538/// - any `GeneralSubtree` has a non-zero `minimum` or a present `maximum` field
1539///   (vjc.8: RFC 5280 §4.2.1.10 MUST require minimum=0, maximum=absent).
1540///
1541/// Returns `Ok(None)` if the extension is absent.
1542fn cert_name_constraints(
1543    cert: &Certificate,
1544    index: usize,
1545) -> crate::Result<Option<NameConstraints>> {
1546    let nc = try_find_cert_ext::<NameConstraints>(cert, OID_NAME_CONSTRAINTS)
1547        .map_err(|_| Error::MalformedCertificate { index })?;
1548
1549    if let Some(nc) = &nc {
1550        // RFC 5280 §4.2.1.10: "the minimum and maximum fields are not used with
1551        // any name forms, thus minimum MUST be zero, maximum MUST be absent."
1552        // Reject certs that encode non-conformant subtrees rather than silently
1553        // applying potentially unexpected constraint semantics.
1554        let subtrees_iter = nc
1555            .permitted_subtrees
1556            .iter()
1557            .flatten()
1558            .chain(nc.excluded_subtrees.iter().flatten());
1559        for st in subtrees_iter {
1560            if st.minimum != 0 || st.maximum.is_some() {
1561                return Err(Error::MalformedCertificate { index });
1562            }
1563        }
1564    }
1565
1566    Ok(nc)
1567}
1568
1569// ---------------------------------------------------------------------------
1570// Validity period checker (PKIX-047)
1571// ---------------------------------------------------------------------------
1572
1573/// Convert an `x509_cert::time::Time` to seconds since the Unix epoch.
1574fn time_to_unix_secs(t: &x509_cert::time::Time) -> u64 {
1575    t.to_unix_duration().as_secs()
1576}
1577
1578/// RFC 5280 §6.1.3(a)(2): check notBefore ≤ now ≤ notAfter.
1579fn check_validity(cert: &Certificate, now_unix: u64, index: usize) -> Result<()> {
1580    let not_before = time_to_unix_secs(&cert.tbs_certificate.validity.not_before);
1581    let not_after = time_to_unix_secs(&cert.tbs_certificate.validity.not_after);
1582    if now_unix >= not_before && now_unix <= not_after {
1583        Ok(())
1584    } else {
1585        Err(Error::ValidityPeriod { index })
1586    }
1587}
1588
1589// ---------------------------------------------------------------------------
1590// Name comparison — RFC 4518 string prep (PKIX-drv)
1591// ---------------------------------------------------------------------------
1592
1593/// Compare two distinguished names per RFC 4518 string prep rules.
1594///
1595/// Currently implements ASCII case-fold and insignificant-whitespace
1596/// collapsing. `BMPString`-tagged AVAs are transcoded UCS-2-BE → UTF-8
1597/// before normalization, so a `BMPString` AVA and a `UTF8String` (or
1598/// `PrintableString`/`IA5String`/`VisibleString`) AVA that share Unicode
1599/// code points compare equal. Full Unicode NFKC normalization is future
1600/// work.
1601///
1602/// Returns `true` if the names are equivalent.
1603///
1604/// # Ordering
1605///
1606/// RFC 5280 §4.1.2.4 defines `Name` as `SEQUENCE OF RDN`, so RDNs are
1607/// compared positionally (index 0 with index 0, etc.). Within each RDN —
1608/// which is a `SET OF AttributeTypeAndValue` — comparison is order-independent:
1609/// each AVA in one RDN is matched against any AVA in the other.
1610///
1611/// # Limitations
1612///
1613/// - **No NFKC / non-ASCII case fold.** Two AVA values that contain the
1614///   same Unicode characters but differ in canonical decomposition
1615///   (precomposed vs combining, e.g. U+00E9 vs U+0065 U+0301) compare
1616///   unequal even though RFC 4518 says they should match. Non-Latin
1617///   case differences (e.g. Greek lowercase σ vs final σ) are also not
1618///   folded.
1619/// - **`UniversalString` is parser-rejected upstream.** `der` 0.7 omits
1620///   tag 0x1C from `Tag::try_from`, so any cert with a `UniversalString`
1621///   AVA fails to parse before reaching this comparator. This is an
1622///   upstream limitation; the same `BMPString` transcoding applied here
1623///   would generalize to `UniversalString` (UCS-4-BE → UTF-8) once the
1624///   parser accepts it.
1625/// - **`TeletexString` (T61String) uses raw DER byte comparison.**
1626///   T.61→Unicode mapping is deferred pending a clear interoperability
1627///   target. Certificates from legacy PKIs using `TeletexString` may
1628///   fail name matching even when the names are semantically equivalent.
1629#[must_use]
1630pub fn names_match(a: &x509_cert::name::Name, b: &x509_cert::name::Name) -> bool {
1631    let a_rdns = a.0.as_slice();
1632    let b_rdns = b.0.as_slice();
1633
1634    if a_rdns.len() != b_rdns.len() {
1635        return false;
1636    }
1637
1638    for (a_rdn, b_rdn) in a_rdns.iter().zip(b_rdns) {
1639        let a_avas = a_rdn.0.as_slice();
1640        let b_avas = b_rdn.0.as_slice();
1641        if a_avas.len() != b_avas.len() {
1642            return false;
1643        }
1644        // Bijective AVA matching: every AVA in a_rdn must match some AVA in b_rdn,
1645        // AND every AVA in b_rdn must match some AVA in a_rdn (both directions).
1646        //
1647        // The bidirectional check is equivalent to set equality for well-formed RDNs
1648        // (RFC 5280 §5.1.2.4 SHOULD NOT contain duplicate OIDs), and also correctly
1649        // handles the malformed-cert case where an RDN has duplicate OIDs:
1650        //   a={CN=Alice, CN=Alice}, b={CN=Bob, CN=Alice} → both len=2, forward pass
1651        //   finds CN=Alice for each a_ava, but the reverse pass finds no match for
1652        //   CN=Bob → returns false (correct).
1653        // The reverse pass is O(n²) on AVA count; n is 1–5 in practice.
1654        for a_ava in a_avas {
1655            let found = b_avas.iter().any(|b_ava| {
1656                b_ava.oid == a_ava.oid && ava_values_match(&a_ava.value, &b_ava.value)
1657            });
1658            if !found {
1659                return false;
1660            }
1661        }
1662        for b_ava in b_avas {
1663            let found = a_avas.iter().any(|a_ava| {
1664                a_ava.oid == b_ava.oid && ava_values_match(&a_ava.value, &b_ava.value)
1665            });
1666            if !found {
1667                return false;
1668            }
1669        }
1670    }
1671    true
1672}
1673
1674/// Returns `Ok(true)` if `cert` is a CA certificate per its `BasicConstraints`
1675/// extension (RFC 5280 §4.2.1.9), `Ok(false)` if the extension is absent or
1676/// `cA = FALSE`, and `Err(DerError)` if the extension is present but cannot be
1677/// DER-decoded.
1678///
1679/// Propagating decode failure rather than treating a malformed extension as
1680/// "not a CA" is a fail-closed defense-in-depth choice: silently skipping a
1681/// malformed `BasicConstraints` could mask a topologically valid CA whose CRL
1682/// scope or path-building should be honored.
1683///
1684/// This helper is shared by `pkix-path-builder` (path construction) and
1685/// `pkix-revocation::crl` (IDP scope checking) to avoid maintaining two
1686/// parallel implementations of the same RFC 5280 §4.2.1.9 decode.
1687///
1688/// # Errors
1689///
1690/// Returns [`DerError`] if the `BasicConstraints` extension is present but
1691/// fails to DER-decode.
1692pub fn cert_is_ca(cert: &Certificate) -> core::result::Result<bool, DerError> {
1693    use der::Decode as _;
1694    use x509_cert::ext::pkix::BasicConstraints;
1695
1696    let Some(ext) = cert
1697        .tbs_certificate
1698        .extensions
1699        .as_deref()
1700        .unwrap_or(&[])
1701        .iter()
1702        .find(|e| e.extn_id == OID_BASIC_CONSTRAINTS)
1703    else {
1704        return Ok(false);
1705    };
1706
1707    let bc = BasicConstraints::from_der(ext.extn_value.as_bytes()).map_err(DerError)?;
1708    Ok(bc.ca)
1709}
1710
1711/// RFC 5280 §3.3: a certificate is self-issued if subject == issuer and neither is empty.
1712fn is_self_issued_cert(cert: &Certificate) -> bool {
1713    !cert.tbs_certificate.subject.is_empty()
1714        && names_match(&cert.tbs_certificate.subject, &cert.tbs_certificate.issuer)
1715}
1716
1717/// Returns `true` if `cert` is identified by its `SubjectAltName` rather than its
1718/// Subject DN.
1719///
1720/// RFC 5280 §4.2.1.6 specifies that a certificate with an empty Subject field and
1721/// a **critical** `SubjectAltName` extension is identified by the SAN, not the DN.
1722/// In this case, name linkage checks against the Subject DN are meaningless.
1723///
1724/// Returns `false` for any cert that has a non-empty Subject or a non-critical SAN.
1725fn cert_has_san_identity(cert: &Certificate) -> bool {
1726    // Subject must be empty.
1727    if !cert.tbs_certificate.subject.is_empty() {
1728        return false;
1729    }
1730    // Must have a critical SubjectAltName extension.
1731    cert.tbs_certificate
1732        .extensions
1733        .as_deref()
1734        .unwrap_or(&[])
1735        .iter()
1736        .any(|ext| ext.extn_id == OID_SUBJECT_ALT_NAME && ext.critical)
1737}
1738
1739/// Compare two `AttributeTypeAndValue` values after RFC 4518 normalization.
1740fn ava_values_match(a: &der::Any, b: &der::Any) -> bool {
1741    let a_str = any_to_str_bytes(a);
1742    let b_str = any_to_str_bytes(b);
1743
1744    match (a_str, b_str) {
1745        (Some(a_bytes), Some(b_bytes)) => normalized_eq(a_bytes.as_ref(), b_bytes.as_ref()),
1746        // Both values are non-string types (e.g. OID, INTEGER) or unhandled string
1747        // types (TeletexString, UniversalString — UniversalString is parser-rejected
1748        // upstream by `der` 0.7's `Tag::try_from`, so this branch in practice covers
1749        // TeletexString and non-string types only):
1750        // compare tag AND content bytes (raw DER). Tag comparison ensures two
1751        // different string encodings of the same text are not considered equal.
1752        (None, None) => a.tag() == b.tag() && a.value() == b.value(),
1753        // One value is a string type and the other is not. Return false (fail-closed).
1754        // A legitimate certificate chain will never encode the same attribute OID as a
1755        // string type in one cert and a non-string type in another, so this mismatch
1756        // indicates a malformed or suspicious certificate.
1757        _ => false,
1758    }
1759}
1760
1761/// Extract the string content bytes from a `DirectoryString` Any value,
1762/// returning `None` for types that require special pre-processing before
1763/// normalization (see `ava_values_match` for the dispatch logic).
1764///
1765/// The return type is `Cow<'_, [u8]>` because some string types
1766/// (currently `BMPString`) must be transcoded into a heap-allocated UTF-8
1767/// buffer before normalization, while others (`UTF8String`,
1768/// `PrintableString`, `IA5String`, `VisibleString`) can be borrowed
1769/// directly from the `der::Any` value's content bytes.
1770///
1771/// # Normalization strategy by string type
1772///
1773/// **Borrowed (zero-copy) — bytes already comparable as UTF-8/ASCII:**
1774/// `UTF8String`, `PrintableString`, `IA5String`, `VisibleString`. These
1775/// types are encoded as ASCII (or UTF-8 in the case of `UTF8String`) at
1776/// the DER level. The borrowed slice is fed directly to `NormalizedIter`
1777/// which applies ASCII case-folding and insignificant-space handling
1778/// (RFC 4518 §2.4 step 6 subset).
1779///
1780/// **Owned (transcoded) — UCS-2-BE → UTF-8:**
1781/// `BMPString`. RFC 4518 §2.1 treats `BMPString` as "a subset of Unicode"
1782/// — every two-byte big-endian unit is a Unicode code point in the BMP.
1783/// We transcode to UTF-8 so the same normalization pipeline used for
1784/// `UTF8String` applies to the result. ASCII-range code points in the BMP
1785/// (U+0000..=U+007F) round-trip to single-byte UTF-8, so a BMPString-encoded
1786/// "Foo Co" compares equal to a UTF8String-encoded "Foo Co" after this
1787/// step. Malformed `BMPString` content (odd-length bytes, or values in
1788/// the surrogate range U+D800..=U+DFFF which are not valid Unicode scalar
1789/// values) returns `None` (fail-closed): a malformed value will not match
1790/// anything via `ava_values_match`.
1791///
1792/// **Rejected at parse time — UniversalString:**
1793/// `der` 0.7 omits tag 0x1C (`UniversalString`) from `Tag::try_from`,
1794/// causing any cert with a `UniversalString` AVA to fail
1795/// `Certificate::from_der` upstream. This dispatch therefore never sees
1796/// `UniversalString`-tagged values. Documented for completeness; a future
1797/// upstream fix in `der` (or our own pre-decode shim) is required before
1798/// `UniversalString` becomes reachable here.
1799///
1800/// **Deferred — `TeletexString` (T61String):**
1801/// Raw DER byte comparison only. RFC 4518 §2.1 states: "As there is no
1802/// standard for mapping `TeletexString` values to Unicode, the mapping is
1803/// left a local matter." RFC 5280 §7.1 classifies `TeletexString` support
1804/// as OPTIONAL. No canonical T.61→Unicode table exists — OpenSSL, NSS,
1805/// and `GnuTLS` each use incompatible vendor extensions. Any mapping we
1806/// choose would silently accept mismatches that other validators reject,
1807/// or reject chains those validators accept. Support is deferred until a
1808/// clear interoperability target exists (e.g., alignment with OpenSSL's
1809/// table). Tracked in PKIX-19l.
1810///
1811/// # Future work
1812///
1813/// Full RFC 4518 six-step preparation (Map → NFKC → Prohibit → `CheckBidi`
1814/// → insignificant-space) for non-ASCII Unicode code points is tracked
1815/// separately. Until that lands, two `BMPString` values that contain the
1816/// same Unicode code points but differ in canonical decomposition (e.g.
1817/// precomposed U+00E9 'é' vs decomposed U+0065 U+0301 'e'+ combining
1818/// acute) compare unequal even though RFC 4518 says they should match.
1819fn any_to_str_bytes(a: &der::Any) -> Option<Cow<'_, [u8]>> {
1820    use der::Tag;
1821    match a.tag() {
1822        Tag::Utf8String | Tag::PrintableString | Tag::Ia5String | Tag::VisibleString => {
1823            Some(Cow::Borrowed(a.value()))
1824        }
1825        Tag::BmpString => bmp_string_to_utf8(a.value()).map(Cow::Owned),
1826        _ => None,
1827    }
1828}
1829
1830/// Decode a `BMPString` content byte slice (UCS-2 big-endian, BMP-only
1831/// Unicode code points per X.680 / RFC 4518 §2.1) into a UTF-8 byte
1832/// vector.
1833///
1834/// Returns `None` if the input is malformed, specifically:
1835/// - odd byte length (UCS-2 units are 16 bits = 2 bytes each), or
1836/// - any 16-bit unit falls in the UTF-16 surrogate range
1837///   (U+D800..=U+DFFF). Surrogates are *reserved* by Unicode and do not
1838///   represent characters; they appear in UTF-16 only as paired
1839///   surrogates encoding supplementary-plane code points (which are
1840///   forbidden in `BMPString` by definition — `BMP` = Basic Multilingual
1841///   Plane, U+0000..=U+FFFF).
1842///
1843/// On a well-formed input the return value is a UTF-8 encoding of the
1844/// same Unicode code points, suitable for byte-level comparison against
1845/// other UTF-8 string types via [`normalized_eq`].
1846///
1847/// No `unsafe`. No new dependencies — uses only `core::char::from_u32`
1848/// and `char::encode_utf8`.
1849fn bmp_string_to_utf8(bytes: &[u8]) -> Option<Vec<u8>> {
1850    if bytes.len() % 2 != 0 {
1851        // RFC 4518 §2.1 requires `BMPString` to be a sequence of Unicode
1852        // code points; the underlying DER encoding is UCS-2-BE which is
1853        // exactly two bytes per unit. An odd-length content octet string
1854        // is malformed and we fail-closed by returning None.
1855        return None;
1856    }
1857    // Capacity hint: each UCS-2 unit (2 bytes) becomes at most 3 UTF-8
1858    // bytes (BMP code points U+0800..=U+FFFF take 3 bytes; below that
1859    // they take 1 or 2). Worst-case sizing avoids reallocation in the
1860    // common all-CJK case.
1861    let mut out = Vec::with_capacity((bytes.len() / 2) * 3);
1862    let mut buf = [0u8; 4];
1863    for chunk in bytes.chunks_exact(2) {
1864        let cp = u16::from_be_bytes([chunk[0], chunk[1]]);
1865        // `char::from_u32` rejects surrogates (U+D800..=U+DFFF) by
1866        // returning None. Any other u16 in 0..=0xFFFF is a valid Unicode
1867        // scalar value in the Basic Multilingual Plane.
1868        let ch = char::from_u32(u32::from(cp))?;
1869        let s = ch.encode_utf8(&mut buf);
1870        out.extend_from_slice(s.as_bytes());
1871    }
1872    Some(out)
1873}
1874
1875/// Compare two byte slices after RFC 4518 whitespace normalization and case-folding.
1876///
1877/// Rules applied (per RFC 4518 §2):
1878/// 1. ASCII letters (0x41–0x5A): case-fold to lowercase. Non-ASCII bytes are
1879///    passed through unchanged; full Unicode case-folding (NFKC + case-fold)
1880///    is future work.
1881/// 2. Leading/trailing spaces: ignored
1882/// 3. Internal multiple spaces: collapsed to single space
1883fn normalized_eq(a: &[u8], b: &[u8]) -> bool {
1884    NormalizedIter::new(a).eq(NormalizedIter::new(b))
1885}
1886
1887/// Iterator that yields bytes after ASCII case-fold and whitespace normalization.
1888///
1889/// # Known limitation
1890///
1891/// Only U+0020 SPACE (byte `0x20`) is treated as insignificant whitespace.
1892/// Tabs (`\t`, `0x09`), non-breaking spaces (`0xA0` in Latin-1), and other
1893/// Unicode whitespace variants pass through unchanged. Full RFC 4518
1894/// insignificant-space handling requires Unicode-aware processing deferred
1895/// to a future release.
1896struct NormalizedIter<'a> {
1897    bytes: &'a [u8],
1898    pos: usize,
1899    pending_space: bool,
1900}
1901
1902impl<'a> NormalizedIter<'a> {
1903    fn new(bytes: &'a [u8]) -> Self {
1904        // Skip leading spaces.
1905        let start = bytes.iter().position(|&b| b != b' ').unwrap_or(bytes.len());
1906        // Find end (skip trailing spaces).
1907        let end = bytes[start..]
1908            .iter()
1909            .rposition(|&b| b != b' ')
1910            .map_or(start, |i| start + i + 1);
1911        Self {
1912            bytes: &bytes[start..end],
1913            pos: 0,
1914            pending_space: false,
1915        }
1916    }
1917}
1918
1919impl Iterator for NormalizedIter<'_> {
1920    type Item = u8;
1921    fn next(&mut self) -> Option<u8> {
1922        // Invariant: `pending_space = true` means we emitted a space on the previous
1923        // call but have not yet consumed the consecutive space run that follows it.
1924        // On the next call we skip the entire run and resume with the next non-space
1925        // byte. This ensures:
1926        //   (a) internal space runs collapse to exactly one space, and
1927        //   (b) trailing space runs do not emit a trailing space, because the run
1928        //       ends at the trim boundary established in `new()` (trailing spaces
1929        //       are excluded from `self.bytes` before iteration begins).
1930        if self.pending_space {
1931            self.pending_space = false;
1932            while self.pos < self.bytes.len() && self.bytes[self.pos] == b' ' {
1933                self.pos += 1;
1934            }
1935            // Fall through: process the next non-space byte (or return None if at end).
1936        }
1937        if self.pos >= self.bytes.len() {
1938            return None;
1939        }
1940        let b = self.bytes[self.pos];
1941        self.pos += 1;
1942        if b == b' ' {
1943            // Emit one space; next call will skip any further consecutive spaces.
1944            self.pending_space = true;
1945            Some(b' ')
1946        } else {
1947            Some(b.to_ascii_lowercase())
1948        }
1949    }
1950}
1951
1952// ---------------------------------------------------------------------------
1953// NameConstraints matching (PKIX-mew)
1954// ---------------------------------------------------------------------------
1955
1956/// Newtype wrapping a bitmask of `GeneralName` name types for `NameConstraints`.
1957///
1958/// Used by `nc_constrained_types` to track which types have been constrained
1959/// by at least one CA certificate in the path, even if the intersection later
1960/// empties the permitted set for that type.
1961///
1962/// Bare `u32` constants would allow silent misuse (e.g., confusing a count
1963/// with a mask). The newtype makes the intent explicit at every operation site.
1964#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1965struct NcTypeMask(u32);
1966
1967impl NcTypeMask {
1968    const EMPTY: Self = Self(0);
1969    const RFC822: Self = Self(1 << 0);
1970    const DNS: Self = Self(1 << 1);
1971    const DIRECTORY_NAME: Self = Self(1 << 2);
1972    const URI: Self = Self(1 << 3);
1973    /// `IP_ADDRESS` is used by `name_type_bit` and participates in `nc_constrained_types`
1974    /// tracking. `IpAddress` names cannot appear in Subject DNs, so there is no
1975    /// inline DN-path code for this type; SAN `IpAddress` entries are handled by the
1976    /// generic SAN loop in `check_name_constraints` via `type_constrained(name)`.
1977    const IP_ADDRESS: Self = Self(1 << 4);
1978
1979    /// Returns `true` if `self` and `other` share at least one bit (non-empty intersection).
1980    ///
1981    /// Named `intersects` rather than `contains` because this is a bitmask test,
1982    /// not a set-membership check — `a.intersects(b)` is symmetric, while `contains`
1983    /// implies `a ⊇ b`.
1984    const fn intersects(self, other: Self) -> bool {
1985        self.0 & other.0 != 0
1986    }
1987}
1988
1989impl core::ops::BitOr for NcTypeMask {
1990    type Output = Self;
1991    fn bitor(self, rhs: Self) -> Self {
1992        Self(self.0 | rhs.0)
1993    }
1994}
1995
1996impl core::ops::BitOrAssign for NcTypeMask {
1997    fn bitor_assign(&mut self, rhs: Self) {
1998        self.0 |= rhs.0;
1999    }
2000}
2001
2002/// Return the `NcTypeMask` bit for the name type of `name`, or `EMPTY` for
2003/// unrecognized types.
2004const fn name_type_bit(name: &x509_cert::ext::pkix::name::GeneralName) -> NcTypeMask {
2005    use x509_cert::ext::pkix::name::GeneralName;
2006    match name {
2007        GeneralName::Rfc822Name(_) => NcTypeMask::RFC822,
2008        GeneralName::DnsName(_) => NcTypeMask::DNS,
2009        GeneralName::DirectoryName(_) => NcTypeMask::DIRECTORY_NAME,
2010        GeneralName::UniformResourceIdentifier(_) => NcTypeMask::URI,
2011        GeneralName::IpAddress(_) => NcTypeMask::IP_ADDRESS,
2012        _ => NcTypeMask::EMPTY,
2013    }
2014}
2015
2016/// Returns true if `subject` DN is within the subtree rooted at `constraint`.
2017///
2018/// RFC 5280 §4.2.1.10: a `DirectoryName` constraint is satisfied when the subject's
2019/// DN has the constraint DN as a prefix (most-general to most-specific order).
2020/// E.g., constraint `{C=US, O=Test}` matches subject `{C=US, O=Test, CN=Alice}`.
2021fn dn_within_subtree(subject: &x509_cert::name::Name, constraint: &x509_cert::name::Name) -> bool {
2022    let c_rdns = &constraint.0;
2023    let s_rdns = &subject.0;
2024    if c_rdns.len() > s_rdns.len() {
2025        return false;
2026    }
2027    c_rdns.iter().zip(s_rdns.iter()).all(|(c_rdn, s_rdn)| {
2028        // Each pair of RDNs must have matching attribute-value pairs.
2029        if c_rdn.0.len() != s_rdn.0.len() {
2030            return false;
2031        }
2032        c_rdn.0.iter().all(|c_ava| {
2033            s_rdn
2034                .0
2035                .iter()
2036                .any(|s_ava| c_ava.oid == s_ava.oid && ava_values_match(&c_ava.value, &s_ava.value))
2037        })
2038    })
2039}
2040
2041/// Returns true if `a` and `b` are the same handled `GeneralName` variant.
2042///
2043/// Uses `name_type_bit` as the single source of truth so that adding a new
2044/// handled type to `name_type_bit` automatically extends this check with no
2045/// separate update required.
2046fn same_nc_variant(
2047    a: &x509_cert::ext::pkix::name::GeneralName,
2048    b: &x509_cert::ext::pkix::name::GeneralName,
2049) -> bool {
2050    name_type_bit(a) != NcTypeMask::EMPTY && name_type_bit(a) == name_type_bit(b)
2051}
2052
2053/// Returns true if `name` satisfies the `subtree` constraint.
2054fn name_matches_subtree(
2055    name: &x509_cert::ext::pkix::name::GeneralName,
2056    subtree: &x509_cert::ext::pkix::constraints::name::GeneralSubtree,
2057) -> bool {
2058    use x509_cert::ext::pkix::name::GeneralName;
2059    match (name, &subtree.base) {
2060        (GeneralName::DnsName(subj), GeneralName::DnsName(constr)) => {
2061            matches_dns_name(subj.as_str(), constr.as_str())
2062        }
2063        (GeneralName::DirectoryName(subj), GeneralName::DirectoryName(constr)) => {
2064            dn_within_subtree(subj, constr)
2065        }
2066        (GeneralName::Rfc822Name(subj), GeneralName::Rfc822Name(constr)) => {
2067            matches_rfc822_name(subj.as_str(), constr.as_str())
2068        }
2069        (
2070            GeneralName::UniformResourceIdentifier(subj),
2071            GeneralName::UniformResourceIdentifier(constr),
2072        ) => matches_uri(subj.as_str(), constr.as_str()),
2073        (GeneralName::IpAddress(subj), GeneralName::IpAddress(constr)) => {
2074            matches_ip_address(subj.as_bytes(), constr.as_bytes())
2075        }
2076        // Mismatched variants or unhandled types: no match.
2077        _ => false,
2078    }
2079}
2080
2081/// DNS name constraint matching (RFC 5280 §4.2.1.10).
2082///
2083/// If `constraint` starts with '.', `subject` must be a subdomain of it
2084/// (label-aware suffix check). Otherwise exact match (case-insensitive).
2085fn matches_dns_name(subject: &str, constraint: &str) -> bool {
2086    if constraint.is_empty() {
2087        return false;
2088    }
2089    if let Some(suffix) = constraint.strip_prefix('.') {
2090        // Subdomain match: subject must end with ".suffix" (not just "suffix").
2091        if subject.eq_ignore_ascii_case(suffix) {
2092            // The constraint is ".example.com"; subject "example.com" is the
2093            // apex — RFC 5280 §4.2.1.10 excludes the apex from subdomain constraints.
2094            return false;
2095        }
2096        let dot_suffix = constraint; // already starts with '.'
2097        subject.len() > dot_suffix.len()
2098            && subject[subject.len() - dot_suffix.len()..].eq_ignore_ascii_case(dot_suffix)
2099    } else {
2100        // RFC 5280 §4.2.1.10: a constraint without a leading period matches
2101        // the hostname exactly AND any subdomain (labels added to the left).
2102        // E.g., "example.com" matches "example.com" and "host.example.com".
2103        subject.eq_ignore_ascii_case(constraint)
2104            || (subject.len() > constraint.len() + 1
2105                && subject.as_bytes()[subject.len() - constraint.len() - 1] == b'.'
2106                && subject[subject.len() - constraint.len()..].eq_ignore_ascii_case(constraint))
2107    }
2108}
2109
2110/// RFC 822 (email) name constraint matching (RFC 5280 §4.2.1.10).
2111fn matches_rfc822_name(subject: &str, constraint: &str) -> bool {
2112    if constraint.contains('@') {
2113        // Constraint is a specific mailbox address: exact match required.
2114        return subject.eq_ignore_ascii_case(constraint);
2115    }
2116    // Constraint is a domain (or .domain); extract the domain part of subject.
2117    let Some((_, domain)) = subject.split_once('@') else {
2118        return false; // malformed subject
2119    };
2120    if let Some(suffix) = constraint.strip_prefix('.') {
2121        // Domain must end with .suffix.
2122        if domain.eq_ignore_ascii_case(suffix) {
2123            return false; // apex excluded
2124        }
2125        let dot_suffix = constraint;
2126        domain.len() > dot_suffix.len()
2127            && domain[domain.len() - dot_suffix.len()..].eq_ignore_ascii_case(dot_suffix)
2128    } else {
2129        // Domain must equal the constraint exactly.
2130        domain.eq_ignore_ascii_case(constraint)
2131    }
2132}
2133
2134/// URI host name constraint matching (RFC 5280 §4.2.1.10).
2135///
2136/// URI constraints use different semantics from DNS constraints:
2137/// - Leading period: subdomains only (same as DNS).
2138/// - No leading period: **exact host only** (unlike DNS, which also matches subdomains).
2139fn matches_uri_host(host: &str, constraint: &str) -> bool {
2140    if constraint.is_empty() {
2141        return false;
2142    }
2143    if let Some(suffix) = constraint.strip_prefix('.') {
2144        // Leading dot: subdomains only, apex excluded (same rule as DNS).
2145        if host.eq_ignore_ascii_case(suffix) {
2146            return false;
2147        }
2148        let dot_suffix = constraint;
2149        host.len() > dot_suffix.len()
2150            && host[host.len() - dot_suffix.len()..].eq_ignore_ascii_case(dot_suffix)
2151    } else {
2152        // RFC 5280 §4.2.1.10: URI constraint without leading period matches
2153        // the exact host only — subdomains are NOT included.
2154        host.eq_ignore_ascii_case(constraint)
2155    }
2156}
2157
2158/// URI name constraint matching (RFC 5280 §4.2.1.10).
2159///
2160/// Extracts the host from the URI and applies URI host matching rules.
2161fn matches_uri(subject_uri: &str, constraint: &str) -> bool {
2162    // Extract host: everything between "://" and the next '/' or '?' or '#' or end.
2163    let host = if let Some(after_scheme) = subject_uri.find("://") {
2164        let rest = &subject_uri[after_scheme + 3..];
2165        // Strip userinfo if present (user:pass@host).
2166        let rest = rest.split_once('@').map_or(rest, |(_, h)| h);
2167        // Strip port and path.
2168        let host_end = rest.find(['/', '?', '#', ':']).unwrap_or(rest.len());
2169        &rest[..host_end]
2170    } else {
2171        return false; // not a URI with scheme
2172    };
2173    matches_uri_host(host, constraint)
2174}
2175
2176/// IP address name constraint matching (RFC 5280 §4.2.1.10).
2177///
2178/// `constraint_bytes` must be 8 bytes (IPv4: addr + mask) or 32 bytes (IPv6).
2179/// `subject_bytes` must be 4 bytes (IPv4) or 16 bytes (IPv6).
2180fn matches_ip_address(subject_bytes: &[u8], constraint_bytes: &[u8]) -> bool {
2181    let (expected_subj_len, half) = match constraint_bytes.len() {
2182        8 => (4usize, 4usize),
2183        32 => (16usize, 16usize),
2184        _ => return false,
2185    };
2186    if subject_bytes.len() != expected_subj_len {
2187        return false;
2188    }
2189    let (addr, mask) = constraint_bytes.split_at(half);
2190    subject_bytes
2191        .iter()
2192        .zip(addr.iter().zip(mask.iter()))
2193        .all(|(s, (a, m))| s & m == a & m)
2194}
2195
2196// ---------------------------------------------------------------------------
2197// ECDSA P-256 SHA-256 backend (PKIX-evy)
2198// ---------------------------------------------------------------------------
2199
2200/// OID for `ecdsa-with-SHA256` (1.2.840.10045.4.3.2).
2201#[cfg(feature = "p256")]
2202const OID_ECDSA_P256_SHA256: der::asn1::ObjectIdentifier =
2203    der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
2204
2205/// ECDSA P-256 with SHA-256 signature verifier.
2206///
2207/// Handles OID `ecdsa-with-SHA256` (1.2.840.10045.4.3.2).
2208/// Feature-gated behind `p256`.
2209#[cfg(feature = "p256")]
2210#[cfg_attr(docsrs, doc(cfg(feature = "p256")))]
2211#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
2212pub struct EcdsaP256Verifier;
2213
2214#[cfg(feature = "p256")]
2215impl SignatureVerifier for EcdsaP256Verifier {
2216    fn verify_signature(
2217        &self,
2218        algorithm: spki::AlgorithmIdentifierRef<'_>,
2219        issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
2220        message: &[u8],
2221        signature: &[u8],
2222    ) -> core::result::Result<(), SignatureError> {
2223        use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
2224
2225        // Reject any OID other than ecdsa-with-SHA256.
2226        if algorithm.oid != OID_ECDSA_P256_SHA256 {
2227            return Err(SignatureError::new());
2228        }
2229
2230        let vk = VerifyingKey::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
2231
2232        let sig = DerSignature::try_from(signature).map_err(|_| SignatureError::new())?;
2233
2234        vk.verify(message, &sig).map_err(|_| SignatureError::new())
2235    }
2236}
2237
2238// ---------------------------------------------------------------------------
2239// RSA PKCS#1 v1.5 SHA-256 backend (PKIX-gmv)
2240// ---------------------------------------------------------------------------
2241
2242/// OID for `sha256WithRSAEncryption` (1.2.840.113549.1.1.11).
2243#[cfg(feature = "rsa")]
2244const OID_SHA256_WITH_RSA: der::asn1::ObjectIdentifier =
2245    der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
2246
2247/// RSA with PKCS#1 v1.5 padding and SHA-256 signature verifier.
2248///
2249/// Handles OID `sha256WithRSAEncryption` (1.2.840.113549.1.1.11).
2250/// Feature-gated behind `rsa`.
2251#[cfg(feature = "rsa")]
2252#[cfg_attr(docsrs, doc(cfg(feature = "rsa")))]
2253#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
2254pub struct RsaPkcs1v15Sha256Verifier;
2255
2256#[cfg(feature = "rsa")]
2257impl SignatureVerifier for RsaPkcs1v15Sha256Verifier {
2258    fn verify_signature(
2259        &self,
2260        algorithm: spki::AlgorithmIdentifierRef<'_>,
2261        issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
2262        message: &[u8],
2263        signature: &[u8],
2264    ) -> core::result::Result<(), SignatureError> {
2265        use rsa::pkcs1v15::{Signature, VerifyingKey};
2266        use rsa::signature::Verifier as _;
2267        use sha2::Sha256;
2268
2269        // Reject any OID other than sha256WithRSAEncryption.
2270        if algorithm.oid != OID_SHA256_WITH_RSA {
2271            return Err(SignatureError::new());
2272        }
2273
2274        let vk =
2275            VerifyingKey::<Sha256>::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
2276
2277        let sig = Signature::try_from(signature).map_err(|_| SignatureError::new())?;
2278
2279        vk.verify(message, &sig).map_err(|_| SignatureError::new())
2280    }
2281}
2282
2283// ---------------------------------------------------------------------------
2284// RSA key size helper (PKIX-ken.1.5)
2285// ---------------------------------------------------------------------------
2286
2287/// rsaEncryption OID: 1.2.840.113549.1.1.1 (RFC 3279 §2.3.1)
2288const OID_RSA_ENCRYPTION: der::asn1::ObjectIdentifier =
2289    der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
2290
2291/// Decode the RSA modulus from an SPKI and return its bit length.
2292///
2293/// Returns `None` when:
2294/// - the key algorithm OID is not `rsaEncryption` (non-RSA key; check does not apply), or
2295/// - the SPKI bytes cannot be decoded (malformed; signature verification will also fail).
2296///
2297/// Uses `der::SliceReader` and `der::asn1::UintRef` from the existing `der`
2298/// dependency — no additional crate required.
2299///
2300/// `RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }` (RFC 3279 §2.3.1).
2301/// `UintRef::as_bytes()` strips the leading 0x00 sign byte from a DER unsigned INTEGER,
2302/// returning only the magnitude. Bit length is derived as `magnitude_bytes * 8`, which
2303/// over-counts by at most 7 bits for keys whose high magnitude byte has leading zero bits —
2304/// this lenient rounding is acceptable for a minimum-floor check: a real 2040-bit key
2305/// would measure as 2048 bits and pass a 2048-bit floor. Key-generation tools always
2306/// produce keys whose top bit is set, so the practical impact is zero.
2307fn rsa_public_key_bits(spki: &spki::SubjectPublicKeyInfoOwned) -> Option<u32> {
2308    use der::{asn1::UintRef, Reader};
2309
2310    if spki.algorithm.oid != OID_RSA_ENCRYPTION {
2311        return None; // Non-RSA key: check does not apply.
2312    }
2313    // BitString::as_bytes() returns None when unused_bits != 0.
2314    // RSA SPKI subject_public_key is always octet-aligned (unused_bits = 0).
2315    let raw = spki.subject_public_key.as_bytes()?;
2316
2317    // raw is a DER-encoded RSAPublicKey SEQUENCE.
2318    // RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
2319    //
2320    // We decode the modulus INTEGER and then skip the publicExponent so the
2321    // sequence reader does not complain about trailing data (der 0.7 requires
2322    // the closure to consume the entire SEQUENCE content).
2323    //
2324    // Skip strategy: read the modulus, then call tlv_bytes() to consume the
2325    // exponent TLV as a raw byte slice (no allocation, no decode required).
2326    let modulus_byte_len: usize = der::SliceReader::new(raw)
2327        .ok()?
2328        .sequence(|r| {
2329            // UintRef strips the leading 0x00 sign byte; as_bytes() returns magnitude only.
2330            let modulus: UintRef<'_> = r.decode()?;
2331            let modulus_len = modulus.as_bytes().len();
2332            // Consume the publicExponent TLV so the nested reader has no trailing data.
2333            let _ = r.tlv_bytes()?;
2334            Ok(modulus_len)
2335        })
2336        .ok()?;
2337
2338    // saturating_mul guards against overflow on a hypothetical absurdly large modulus.
2339    // The result fits in u32: the largest practical RSA key is 16384 bits (2048 bytes),
2340    // well within u32::MAX. u32::try_from is used to make the bound explicit.
2341    u32::try_from(modulus_byte_len.saturating_mul(8)).ok()
2342}
2343
2344// ---------------------------------------------------------------------------
2345// Chain walk loop — signature verification and name linkage (PKIX-vxf)
2346// ---------------------------------------------------------------------------
2347
2348/// Walk the chain from issuer to leaf, applying all RFC 5280 §6.1 per-cert checks.
2349///
2350/// Path-length and anchor-matching are handled by the caller (`validate_path`).
2351/// This function walks `chain` in reverse (issuer-to-leaf) against `anchor`:
2352///
2353///    a. Verify signature with the current issuer's SPKI.
2354///    b. Verify issuer/subject name linkage.
2355///    c. Check validity period against `policy.current_time_unix`.
2356///    d. Reject any unhandled critical extensions.
2357///    e. Check cert names (subject DN + SAN) against accumulated NC state.
2358///    f. For all certs except the leaf (i > 0): require `BasicConstraints` cA=TRUE.
2359///    g. For all certs except the leaf (i > 0): if `policy.enforce_key_usage`, require `keyCertSign`.
2360///    h. For all certs except the leaf (i > 0): enforce `pathLenConstraint` if present.
2361///    i. For all certs except the leaf (i > 0): accumulate `NameConstraints` state
2362///       (INTERSECTION for permittedSubtrees, UNION for excludedSubtrees).
2363///
2364/// RFC 5280 §4.2.1.9 note on pathLenConstraint: for the cert at position `i`
2365/// (leaf at 0, root-adjacent at chain.len()-1), there are exactly `i-1`
2366/// intermediate certs below it. The constraint requires `i-1 ≤ pathLenConstraint`.
2367///
2368/// # Implementation notes
2369///
2370/// This function is intentionally structured as a single loop over the certificate
2371/// chain. The RFC 5280 §6.1 state machine has significant shared state (working
2372/// SPKI, name constraints, policy tree, inhibit flags) that must be threaded
2373/// through every step in a defined order. Decomposing the loop into smaller helpers
2374/// would require passing this state through many function boundaries without clarity
2375/// gain. The monolithic structure mirrors the RFC's sequential algorithm description
2376/// and keeps all state-mutation sites visible in one place for audit.
2377fn chain_walk<V: SignatureVerifier>(
2378    chain: &[Certificate],
2379    anchor: &TrustAnchor,
2380    policy: &ValidationPolicy,
2381    verifier: &V,
2382) -> Result<Option<Vec<PolicyNode>>> {
2383    use der::Encode;
2384    use spki::der::referenced::OwnedToRef as _;
2385    use x509_cert::ext::pkix::{InhibitAnyPolicy, PolicyConstraints, PolicyMappings};
2386
2387    let mut working_spki = &anchor.subject_public_key_info;
2388    let mut working_issuer_name = &anchor.subject;
2389    // RFC 5280 §4.2.1.6: when a CA cert has an empty Subject DN and a critical
2390    // SubjectAltName, the SAN is the cert's identity — the Subject DN is not used
2391    // for name matching. Track whether the current issuer (working_issuer_name)
2392    // was set from such a cert so we can skip the DN linkage check below.
2393    let mut working_issuer_is_san_identity = false;
2394
2395    // RFC 5280 §6.1.2 (b)+(c): seed the initial permitted/excluded subtrees
2396    // from the trust anchor. These initial constraints apply to ALL certs in
2397    // the chain (including intermediates), not just to leaves — the chain walk
2398    // enforces them from the first certificate onward.
2399    let (mut nc_permitted, mut nc_excluded) = match &anchor.name_constraints {
2400        None => (None, GeneralSubtrees::default()),
2401        Some(nc) => (
2402            // Clone necessary: nc_permitted and nc_excluded are mutated during the walk.
2403            nc.permitted_subtrees.clone(),
2404            nc.excluded_subtrees.clone().unwrap_or_default(),
2405        ),
2406    };
2407    // Bitmask of NcTypeMask bits for name types that have been explicitly
2408    // constrained by at least one permittedSubtrees entry in any CA cert seen so far.
2409    // Needed to detect violations when intersection empties the permitted set
2410    // for a type (e.g., two incompatible DN constraints → empty, but DN still forbidden).
2411    //
2412    // INVARIANT: bits are ORed in and never cleared. Once a type bit is set,
2413    // nc_permitted must contain zero entries of that type to represent "empty
2414    // intersection" (all names of that type are forbidden). Do NOT derive
2415    // "is type constrained?" from nc_permitted contents alone — that would
2416    // silently allow names of a type whose permitted set was emptied by
2417    // conflicting CA constraints.
2418    let mut nc_constrained_types: NcTypeMask =
2419        nc_permitted
2420            .as_ref()
2421            .map_or(NcTypeMask::EMPTY, |permitted| {
2422                let mut bits = NcTypeMask::EMPTY;
2423                for st in permitted {
2424                    bits |= name_type_bit(&st.base);
2425                }
2426                bits
2427            });
2428
2429    // RFC 5280 §6.1.2: initialise policy state variables (PKIX-mi3.3).
2430    //
2431    // The counters represent "skip N more non-self-issued certificates before
2432    // the constraint activates".  Setting a counter to `n + 1` means the
2433    // constraint never triggers unless a CA certificate forces it lower.
2434    let n = chain.len();
2435    // Convert n (usize) to u32 safely. Chains with >4 billion certs are not
2436    // realistic, but a truncating cast would produce a wrong counter value.
2437    // u32::MAX is safe: counters are only decremented (saturating), so u32::MAX
2438    // behaves identically to any value > the chain length for these semantics.
2439    let n_u32 = u32::try_from(n).unwrap_or(u32::MAX);
2440    let mut explicit_policy: u32 = if policy.initial_explicit_policy {
2441        0
2442    } else {
2443        n_u32.saturating_add(1)
2444    };
2445    let mut inhibit_any: u32 = if policy.initial_any_policy_inhibit {
2446        0
2447    } else {
2448        n_u32.saturating_add(1)
2449    };
2450    let mut policy_mapping: u32 = if policy.initial_policy_mapping_inhibit {
2451        0
2452    } else {
2453        n_u32.saturating_add(1)
2454    };
2455    // §6.1.2(a): initial valid_policy_tree — single anyPolicy root node.
2456    let mut policy_tree: Option<Vec<PolicyNode>> = Some(init_policy_tree());
2457
2458    for i in (0..chain.len()).rev() {
2459        let cert = &chain[i];
2460
2461        // (a0) Signature algorithm allowlist check.
2462        //      Fires BEFORE signature verification to give a diagnostic error
2463        //      (AlgorithmNotAllowed) rather than a confusing SignatureInvalid.
2464        //      Uses the outer signatureAlgorithm field, which RFC 5280 §4.1.1.2
2465        //      requires to be identical to the inner TBSCertificate.signature OID.
2466        //      Applies to every cert in the chain (no i == 0 guard), matching
2467        //      CA/B Forum profile intent.
2468        if let Some(allowed) = &policy.allowed_signature_algs {
2469            // O(n) over a typically 2–6 element list; acceptable for the common case.
2470            if !allowed.contains(&cert.signature_algorithm.oid) {
2471                return Err(Error::AlgorithmNotAllowed { index: i });
2472            }
2473        }
2474
2475        // (a) Verify signature with the current issuer's SPKI.
2476        //     Use heap-backed encoding (alloc::vec) so that large certificates
2477        //     (government, enterprise, HSM attestation certs > 8 KiB TBSCertificate)
2478        //     are handled correctly. The previous fixed 8 KiB stack buffer returned
2479        //     Error::Der for oversized certs, which is an implementation limit not a
2480        //     cert defect. Heap encoding eliminates this limit; the only failure mode
2481        //     is a genuine DER encoding error in a malformed certificate.
2482        let tbs_bytes_owned = {
2483            let mut buf = Vec::new();
2484            cert.tbs_certificate
2485                .encode_to_vec(&mut buf)
2486                .map_err(|e| Error::Der(DerError(e)))?;
2487            buf
2488        };
2489        let tbs_bytes: &[u8] = &tbs_bytes_owned;
2490        verifier
2491            .verify_signature(
2492                cert.signature_algorithm.owned_to_ref(),
2493                working_spki.owned_to_ref(),
2494                tbs_bytes,
2495                cert.signature.raw_bytes(),
2496            )
2497            .map_err(|_| Error::SignatureInvalid { index: i })?;
2498
2499        // (b) Issuer/subject name linkage.
2500        //
2501        // RFC 5280 §4.2.1.6: if the issuer cert has an empty Subject DN and a
2502        // critical SubjectAltName, the issuer is identified by its SAN rather
2503        // than its Subject DN. In that case, skip the DN-based linkage check —
2504        // we cannot compare `cert.issuer` against an empty Subject and expect a
2505        // meaningful match. The signature verification in step (a) already
2506        // confirmed the issuer's key, so the cryptographic binding is intact.
2507        if !working_issuer_is_san_identity
2508            && !names_match(working_issuer_name, &cert.tbs_certificate.issuer)
2509        {
2510            return Err(Error::ChainBroken { index: i });
2511        }
2512
2513        // (c) Validity period.
2514        check_validity(cert, policy.current_time_unix, i)?;
2515
2516        // (c2) Max validity period length check.
2517        //      saturating_sub avoids wrap on a malformed cert where notAfter < notBefore;
2518        //      a duration of 0 trivially passes the > max_secs test (safe, not a bypass).
2519        //      Applies to every cert in the chain per the epic intent.
2520        if let Some(max_secs) = policy.max_validity_secs {
2521            let not_before = cert
2522                .tbs_certificate
2523                .validity
2524                .not_before
2525                .to_unix_duration()
2526                .as_secs();
2527            let not_after = cert
2528                .tbs_certificate
2529                .validity
2530                .not_after
2531                .to_unix_duration()
2532                .as_secs();
2533            if not_after.saturating_sub(not_before) > max_secs {
2534                return Err(Error::ValidityPeriodExceedsMax { index: i });
2535            }
2536        }
2537
2538        // (c3) Minimum RSA key size check.
2539        //      Non-RSA keys produce None from rsa_public_key_bits and are silently skipped.
2540        //      Applies to every cert in the chain per the epic intent.
2541        if let Some(min_bits) = policy.min_rsa_key_bits {
2542            if let Some(actual_bits) =
2543                rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info)
2544            {
2545                if actual_bits < min_bits {
2546                    return Err(Error::KeyTooSmall { index: i });
2547                }
2548            }
2549            // Non-RSA keys: rsa_public_key_bits returns None → check silently skipped.
2550        }
2551
2552        // (d) Critical extension guard.
2553        check_critical_extensions(cert, i)?;
2554
2555        // Cert depth in the RFC 5280 §6.1 sense: 1 = root-adjacent, n = leaf.
2556        let cert_depth = n - i;
2557
2558        // Decode the cert's CertificatePolicies extension once per cert.
2559        // Used in both step (d) (policy tree update) and step (a/b) (PolicyMappings
2560        // anyPolicy qualifier lookup).  Decoding here avoids a second parse inside
2561        // the mapping loop (b5r.12).
2562        // try_find_cert_ext (fail-closed): a malformed CertificatePolicies must
2563        // cause rejection rather than being silently treated as absent; silently
2564        // dropping it would leave the policy tree in an incorrect state (vjc.21).
2565        let cert_cp: Option<x509_cert::ext::pkix::certpolicy::CertificatePolicies> =
2566            try_find_cert_ext(cert, OID_CERTIFICATE_POLICIES)
2567                .map_err(|_| Error::MalformedCertificate { index: i })?;
2568
2569        // (policy-d) CertificatePolicies extension (RFC 5280 §6.1.3(d)).
2570        // Only processed when the policy tree is still alive.
2571        if let Some(tree) = &mut policy_tree {
2572            if let Some(cp_ext) = &cert_cp {
2573                let mut new_nodes: Vec<PolicyNode> = Vec::new();
2574                let mut has_any_policy = false;
2575
2576                // Step (d)(1): process each specific policy P ≠ anyPolicy.
2577                for policy_info in &cp_ext.0 {
2578                    let p_oid = &policy_info.policy_identifier;
2579                    if p_oid == &OID_ANY_POLICY {
2580                        // Defer anyPolicy processing to step (d)(2).
2581                        has_any_policy = true;
2582                        continue;
2583                    }
2584
2585                    // RFC §6.1.3(d)(1)(i)/(ii) qualifier source: the
2586                    // policy_qualifiers attached to THIS PolicyInformation
2587                    // entry (the cert's entry for OID p_oid). Hoisted out
2588                    // of the parent loop so a single clone is shared
2589                    // across all matched parents at depth i-1.
2590                    let policy_qualifiers: Vec<_> =
2591                        policy_info.policy_qualifiers.clone().unwrap_or_default();
2592
2593                    // (d)(1)(i): for each parent at depth i-1 whose
2594                    // expected_policy_set contains p_oid, create a child.
2595                    // Track whether any parent matched to decide step (d)(1)(ii).
2596                    let mut matched_via_i = false;
2597                    for _parent in tree.iter().filter(|parent| {
2598                        parent.depth == cert_depth - 1 && parent.expected_policy_set.contains(p_oid)
2599                    }) {
2600                        matched_via_i = true;
2601                        new_nodes.push(PolicyNode {
2602                            depth: cert_depth,
2603                            valid_policy: *p_oid,
2604                            expected_policy_set: vec![*p_oid],
2605                            qualifiers: policy_qualifiers.clone(),
2606                        });
2607                    }
2608
2609                    // (d)(1)(ii): if no match in (i), check for an anyPolicy
2610                    // parent at depth i-1.
2611                    if !matched_via_i {
2612                        let has_any_parent = tree.iter().any(|parent| {
2613                            parent.depth == cert_depth - 1 && parent.valid_policy == OID_ANY_POLICY
2614                        });
2615                        if has_any_parent {
2616                            new_nodes.push(PolicyNode {
2617                                depth: cert_depth,
2618                                valid_policy: *p_oid,
2619                                expected_policy_set: vec![*p_oid],
2620                                qualifiers: policy_qualifiers,
2621                            });
2622                        }
2623                    }
2624                }
2625
2626                // Step (d)(2): if cert has anyPolicy and (inhibit_any > 0 or
2627                // self-issued non-leaf), expand for each unmatched expected
2628                // policy from parent nodes.
2629                if has_any_policy {
2630                    let may_expand = inhibit_any > 0 || (i > 0 && is_self_issued_cert(cert));
2631                    if may_expand {
2632                        // RFC §6.1.3(d)(2) qualifier source: the qualifiers
2633                        // attached to the cert's anyPolicy PolicyInformation
2634                        // entry (NOT the parent node's qualifiers). Computed
2635                        // once per cert; cloned per synthesized child below.
2636                        let any_policy_qualifiers = cert_any_policy_qualifiers(cp_ext);
2637                        // Already-covered valid_policies at this depth.
2638                        let already_covered: Vec<der::asn1::ObjectIdentifier> =
2639                            new_nodes.iter().map(|nd| nd.valid_policy).collect();
2640                        for parent in tree.iter().filter(|nd| nd.depth == cert_depth - 1) {
2641                            for ep in &parent.expected_policy_set {
2642                                if !already_covered.contains(ep) {
2643                                    new_nodes.push(PolicyNode {
2644                                        depth: cert_depth,
2645                                        valid_policy: *ep,
2646                                        expected_policy_set: vec![*ep],
2647                                        qualifiers: any_policy_qualifiers.clone(),
2648                                    });
2649                                }
2650                            }
2651                        }
2652                    }
2653                }
2654
2655                tree.extend(new_nodes);
2656
2657                // Step (d)(3): prune ancestors with no children.
2658                if cert_depth > 1 {
2659                    prune_policy_tree(tree, cert_depth);
2660                }
2661                // If no nodes at depth >= 1 remain, tree is effectively NULL.
2662                if !tree.iter().any(|nd| nd.depth >= 1) {
2663                    policy_tree = None;
2664                }
2665            } else {
2666                // §6.1.3(e): CertificatePolicies absent → tree becomes NULL.
2667                policy_tree = None;
2668            }
2669        }
2670
2671        // (policy-f) RFC 5280 §6.1.3(f): explicit_policy == 0 and tree NULL
2672        // → policy violation.
2673        if explicit_policy == 0 && policy_tree.is_none() {
2674            return Err(Error::PolicyViolation { index: i });
2675        }
2676
2677        // Decode SAN once per cert: used in both the NC name check (e) and
2678        // potentially cached for the NC state update (i). Avoids scanning the
2679        // extension list twice per cert when both checks are active (vjc.13).
2680        // Fail-closed: a malformed SAN returns MalformedCertificate (vjc.20).
2681        let san = cert_subject_alt_names(cert, i)?;
2682
2683        // (e) NameConstraints: check this cert's names against accumulated state.
2684        // RFC 5280 §6.1.3(b): self-issued non-leaf certs are exempt from NC name checking.
2685        // The NC state is still updated from their extensions in step (i).
2686        if i == 0 || !is_self_issued_cert(cert) {
2687            check_name_constraints(
2688                cert,
2689                san.as_ref(),
2690                nc_permitted.as_ref(),
2691                &nc_excluded,
2692                nc_constrained_types,
2693                i,
2694            )?;
2695        }
2696
2697        // (e2) Require non-empty SubjectAltName on leaf cert.
2698        //      Only when require_subject_alt_name is set; intermediate CA certs
2699        //      are NOT checked (i == 0 guard). The `san` variable is decoded above
2700        //      and is already available — no second extension scan needed.
2701        if i == 0 && policy.require_subject_alt_name {
2702            // san is None if the extension is absent; Some(v) where v.0 may be empty.
2703            let san_is_nonempty = san.as_ref().is_some_and(|s| !s.0.is_empty());
2704            if !san_is_nonempty {
2705                return Err(Error::MissingSan);
2706            }
2707        }
2708
2709        // (e3) Required leaf EKU OID check.
2710        //      Only when required_leaf_eku is Some; only on the leaf (i == 0).
2711        //      Uses try_find_cert_ext (fail-closed): malformed EKU DER on the leaf
2712        //      is mapped to MalformedCertificate rather than silently ignored.
2713        //      anyExtendedKeyUsage (OID 2.5.29.37.0) does NOT satisfy a specific
2714        //      OID requirement — only explicit listing in the cert's EKU counts.
2715        if i == 0 {
2716            if let Some(required_ekus) = &policy.required_leaf_eku {
2717                use x509_cert::ext::pkix::ExtendedKeyUsage;
2718                match try_find_cert_ext::<ExtendedKeyUsage>(cert, OID_EXTENDED_KEY_USAGE)
2719                    .map_err(|_| Error::MalformedCertificate { index: 0 })?
2720                {
2721                    None => {
2722                        // EKU extension absent; any non-empty requirement fails.
2723                        if !required_ekus.is_empty() {
2724                            return Err(Error::MissingEku);
2725                        }
2726                    }
2727                    Some(eku) => {
2728                        for req_oid in required_ekus {
2729                            if !eku.0.iter().any(|e| e == req_oid) {
2730                                return Err(Error::MissingEku);
2731                            }
2732                        }
2733                    }
2734                }
2735            }
2736        }
2737
2738        // (e4) If require_rfc822_san is set, at least one rfc822Name entry must
2739        //      be present in the leaf's SAN extension.
2740        //      Only meaningful (and checked) when require_subject_alt_name is also
2741        //      true; the non-empty SAN check above (e2) already guards the absent
2742        //      / empty SAN case. EKU is checked first (e3) so that a cert with both
2743        //      wrong EKU and wrong SAN type reports MissingEku (more actionable).
2744        if i == 0 && policy.require_subject_alt_name && policy.require_rfc822_san {
2745            use x509_cert::ext::pkix::name::GeneralName;
2746            let has_rfc822 = san.as_ref().is_some_and(|s| {
2747                s.0.iter()
2748                    .any(|name| matches!(name, GeneralName::Rfc822Name(_)))
2749            });
2750            if !has_rfc822 {
2751                return Err(Error::MissingRfc822San);
2752            }
2753        }
2754
2755        // (f–h) CA-only checks: apply to every cert except the leaf (chain[0]).
2756        //        This includes any intermediate CAs and the root CA cert if it
2757        //        is included in the chain rather than supplied only as an anchor.
2758        if i > 0 {
2759            // (f) BasicConstraints cA=TRUE required; (h) pathLenConstraint.
2760            // Decode BasicConstraints once for both checks.
2761            //
2762            // Fail-closed: if the extension is structurally present but DER-malformed
2763            // on an intermediate CA, propagate MalformedCertificate rather than
2764            // treating it as absent (which would fall through to NotCA and hide the
2765            // real structural problem).
2766            let bc = try_find_cert_ext::<x509_cert::ext::pkix::BasicConstraints>(
2767                cert,
2768                OID_BASIC_CONSTRAINTS,
2769            )
2770            .map_err(|_| Error::MalformedCertificate { index: i })?;
2771            if !bc.as_ref().is_some_and(|b| b.ca) {
2772                return Err(Error::NotCA { index: i });
2773            }
2774
2775            // (g) KeyUsage keyCertSign required (when policy demands it).
2776            // RFC 5280 §6.1.4(n): "If a KeyUsage extension is present, verify that the
2777            // keyCertSign bit is set."  Only reject when KeyUsage IS present (Some(_)) and
2778            // keyCertSign is NOT set (== Some(false)).  Absent KeyUsage (None) is allowed.
2779            // has_key_cert_sign is fail-closed: a malformed critical KeyUsage returns
2780            // MalformedCertificate rather than being silently treated as absent (vjc.15).
2781            if policy.enforce_key_usage
2782                && has_key_cert_sign(cert).map_err(|_| Error::MalformedCertificate { index: i })?
2783                    == Some(false)
2784            {
2785                return Err(Error::KeyUsageMissing { index: i });
2786            }
2787
2788            // (h) pathLenConstraint: count only non-self-issued intermediates below position i
2789            // (RFC 5280 §4.2.1.9: "non-self-issued intermediate certificates").
2790            // chain[1..i] = the intermediate positions between the leaf (0) and this cert (i).
2791            if let Some(path_len) = bc.and_then(|b| b.path_len_constraint) {
2792                let effective_depth = chain[1..i]
2793                    .iter()
2794                    .filter(|c| !is_self_issued_cert(c))
2795                    .count();
2796                if effective_depth > path_len as usize {
2797                    return Err(Error::PathTooLong);
2798                }
2799            }
2800
2801            // (policy-a) PolicyMappings (RFC 5280 §6.1.4(a)): anyPolicy must
2802            // not appear on either side of a mapping.
2803            // (policy-b) Apply mappings to the tree or delete mapped nodes.
2804            // NOTE: Policy mappings use the current policy_mapping counter value
2805            // (before decrement); the decrement happens in §6.1.4(h) below.
2806            // try_find_cert_ext (fail-closed): a malformed PolicyMappings extension
2807            // must cause rejection rather than silent ignore; a silently-discarded
2808            // mapping could allow a policy bypass (e.g., inhibit_policy_mapping bypass).
2809            if let Some(pm) = try_find_cert_ext::<PolicyMappings>(cert, OID_POLICY_MAPPINGS)
2810                .map_err(|_| Error::MalformedCertificate { index: i })?
2811            {
2812                // §6.1.4(a): reject anyPolicy as issuer or subject domain.
2813                for mapping in &pm.0 {
2814                    if mapping.issuer_domain_policy == OID_ANY_POLICY
2815                        || mapping.subject_domain_policy == OID_ANY_POLICY
2816                    {
2817                        return Err(Error::PolicyViolation { index: i });
2818                    }
2819                }
2820
2821                // §6.1.4(b)(1): if policy_mapping > 0, update expected_policy_set.
2822                // §6.1.4(b)(2): if policy_mapping == 0, delete mapped nodes.
2823                if let Some(tree) = &mut policy_tree {
2824                    if policy_mapping > 0 {
2825                        // RFC §6.1.4(b)(1)(ii) qualifier source for the
2826                        // synthesis branch below: the qualifiers attached
2827                        // to the cert's anyPolicy PolicyInformation entry.
2828                        // Computed once per cert iteration; reused for
2829                        // each mapping that triggers synthesis.
2830                        //
2831                        // `cert_cp` may be None here: although a None
2832                        // certificatePolicies extension would have set the
2833                        // policy tree to None back at line 2487, the
2834                        // PolicyMappings extension can in principle be
2835                        // processed even with no certificatePolicies in
2836                        // the cert. Defensive `as_ref().map(...)` covers
2837                        // this — synthesizes nodes with empty qualifiers
2838                        // when the cert has no anyPolicy entry.
2839                        let any_policy_qualifiers = cert_cp
2840                            .as_ref()
2841                            .map(cert_any_policy_qualifiers)
2842                            .unwrap_or_default();
2843                        // For each issuerDomainPolicy ID-P in the mappings,
2844                        // update expected_policy_set of matching nodes.
2845                        for mapping in &pm.0 {
2846                            let idp = &mapping.issuer_domain_policy;
2847                            let sdp = &mapping.subject_domain_policy;
2848                            let mut found = false;
2849                            for node in tree.iter_mut() {
2850                                if node.depth == cert_depth && &node.valid_policy == idp {
2851                                    found = true;
2852                                    node.expected_policy_set.retain(|p| p != idp);
2853                                    if !node.expected_policy_set.contains(sdp) {
2854                                        node.expected_policy_set.push(*sdp);
2855                                    }
2856                                }
2857                            }
2858                            // If no node at cert_depth has valid_policy = ID-P
2859                            // but there is an anyPolicy node, generate a new
2860                            // child of the depth-(i-1) anyPolicy node.
2861                            if !found {
2862                                let has_any = tree.iter().any(|nd| {
2863                                    nd.depth == cert_depth && nd.valid_policy == OID_ANY_POLICY
2864                                });
2865                                if has_any {
2866                                    tree.push(PolicyNode {
2867                                        depth: cert_depth,
2868                                        valid_policy: *idp,
2869                                        expected_policy_set: vec![*sdp],
2870                                        qualifiers: any_policy_qualifiers.clone(),
2871                                    });
2872                                }
2873                            }
2874                        }
2875                    } else {
2876                        // policy_mapping == 0: delete nodes whose valid_policy
2877                        // is an issuer_domain_policy in a mapping.
2878                        let mapped_policies: Vec<der::asn1::ObjectIdentifier> =
2879                            pm.0.iter().map(|m| m.issuer_domain_policy).collect();
2880                        tree.retain(|nd| {
2881                            nd.depth != cert_depth || !mapped_policies.contains(&nd.valid_policy)
2882                        });
2883                        if cert_depth > 0 {
2884                            prune_policy_tree(tree, cert_depth);
2885                        }
2886                    }
2887                }
2888            }
2889            // Check if tree became effectively NULL after mapping operations.
2890            if let Some(t) = &policy_tree {
2891                if !t.iter().any(|nd| nd.depth >= 1) {
2892                    policy_tree = None;
2893                }
2894            }
2895
2896            // (policy-h) RFC 5280 §6.1.4(h): decrement policy counters for
2897            // non-self-issued intermediate certificates.
2898            // This happens AFTER policy mappings processing (§6.1.4(b)) and
2899            // BEFORE clamping from extensions (§6.1.4(i)/(j)).
2900            if !is_self_issued_cert(cert) {
2901                explicit_policy = explicit_policy.saturating_sub(1);
2902                policy_mapping = policy_mapping.saturating_sub(1);
2903                inhibit_any = inhibit_any.saturating_sub(1);
2904            }
2905
2906            // (policy-i) PolicyConstraints (RFC 5280 §6.1.4(c)): clamp
2907            // explicit_policy and policy_mapping from the extension.
2908            // try_find_cert_ext (fail-closed): malformed PolicyConstraints must reject;
2909            // silently ignoring it could allow explicit_policy bypass.
2910            if let Some(pc) = try_find_cert_ext::<PolicyConstraints>(cert, OID_POLICY_CONSTRAINTS)
2911                .map_err(|_| Error::MalformedCertificate { index: i })?
2912            {
2913                if let Some(req) = pc.require_explicit_policy {
2914                    explicit_policy = explicit_policy.min(req);
2915                }
2916                if let Some(ipm) = pc.inhibit_policy_mapping {
2917                    policy_mapping = policy_mapping.min(ipm);
2918                }
2919            }
2920
2921            // (policy-j) InhibitAnyPolicy (RFC 5280 §6.1.4(d)): clamp inhibit_any.
2922            // try_find_cert_ext (fail-closed): malformed InhibitAnyPolicy must reject;
2923            // silently ignoring it could allow anyPolicy through when it should be inhibited.
2924            if let Some(iap) = try_find_cert_ext::<InhibitAnyPolicy>(cert, OID_INHIBIT_ANY_POLICY)
2925                .map_err(|_| Error::MalformedCertificate { index: i })?
2926            {
2927                inhibit_any = inhibit_any.min(iap.0);
2928            }
2929
2930            // (i) NC update: NameConstraints state update (RFC 5280 §6.1.4(b)).
2931            //     INTERSECTION for permitted, UNION for excluded.
2932            //     cert_name_constraints is fail-closed: a malformed or non-conformant
2933            //     NC extension (e.g., non-zero minimum/maximum) returns MalformedCertificate
2934            //     rather than silently ignoring the constraints (vjc.7, vjc.8).
2935            if let Some(nc) = cert_name_constraints(cert, i)? {
2936                // permittedSubtrees: intersect with current state.
2937                if let Some(new_permitted) = nc.permitted_subtrees {
2938                    // Track which types this CA is constraining.
2939                    for entry in &new_permitted {
2940                        nc_constrained_types |= name_type_bit(&entry.base);
2941                    }
2942                    match nc_permitted.as_mut() {
2943                        None => {
2944                            // First constraint seen; adopt it directly.
2945                            nc_permitted = Some(new_permitted);
2946                        }
2947                        Some(current) => {
2948                            // Type-aware intersection of two permitted-subtrees sets.
2949                            //
2950                            // RFC 5280 §6.1.4(b): intersect entry-by-entry, but only
2951                            // compare entries of the SAME name type. Entries of types
2952                            // not present in new_permitted are unchanged (new doesn't
2953                            // constrain that type). Entries of types not in current
2954                            // are added directly (new adds a fresh constraint).
2955                            //
2956                            // For entries of matching type, keep:
2957                            //   1. new entries within (⊆) some same-type current entry.
2958                            //   2. current entries within (⊆) some same-type new entry.
2959                            // (If neither is within the other the intersection for that
2960                            // type is empty — tracked via nc_constrained_types.)
2961                            let mut result = GeneralSubtrees::default();
2962
2963                            // For each new entry, pre-filter current entries of the
2964                            // same type to avoid calling same_nc_variant twice per
2965                            // pair (vjc.16: duplicated guard + containment check).
2966                            for n in &new_permitted {
2967                                let same_type_in_current: GeneralSubtrees = current
2968                                    .iter()
2969                                    .filter(|c| same_nc_variant(&c.base, &n.base))
2970                                    .cloned()
2971                                    .collect();
2972                                if same_type_in_current.is_empty() {
2973                                    // Type not previously constrained → add directly.
2974                                    result.push(n.clone());
2975                                } else if same_type_in_current
2976                                    .iter()
2977                                    .any(|c| name_matches_subtree(&n.base, c))
2978                                {
2979                                    // n is within some same-type current entry → keep.
2980                                    result.push(n.clone());
2981                                }
2982                                // else: n is not within any current entry of same type → drop.
2983                            }
2984
2985                            for c in current.iter() {
2986                                let same_type_in_new: GeneralSubtrees = new_permitted
2987                                    .iter()
2988                                    .filter(|n| same_nc_variant(&n.base, &c.base))
2989                                    .cloned()
2990                                    .collect();
2991                                if same_type_in_new.is_empty() {
2992                                    // Type not in new_permitted → keep unchanged.
2993                                    result.push(c.clone());
2994                                } else if same_type_in_new
2995                                    .iter()
2996                                    .any(|n| name_matches_subtree(&c.base, n))
2997                                {
2998                                    // c is more specific than some new entry; keep unless
2999                                    // an equivalent entry is already in result (dedup
3000                                    // within the result set for this type).
3001                                    let same_type_in_result: &[_] = result.as_slice();
3002                                    let already_in_result = same_type_in_result.iter().any(|e| {
3003                                        same_nc_variant(&e.base, &c.base)
3004                                            && name_matches_subtree(&e.base, c)
3005                                            && name_matches_subtree(&c.base, e)
3006                                    });
3007                                    if !already_in_result {
3008                                        result.push(c.clone());
3009                                    }
3010                                }
3011                                // else: c is not within any new entry of same type → drop.
3012                            }
3013
3014                            *current = result;
3015                        }
3016                    }
3017                }
3018                // excludedSubtrees: union — append only entries not already present,
3019                // avoiding monotonic growth that would make per-cert NC checks O(chain²)
3020                // when the same excluded subtrees are repeated across multiple CAs (vjc.12).
3021                if let Some(new_excluded) = nc.excluded_subtrees {
3022                    for new_entry in &new_excluded {
3023                        // Deduplication uses name_matches_subtree as a two-way equality
3024                        // check: two entries are considered the same subtree when each
3025                        // matches the other (i.e., they are semantically equivalent, not
3026                        // just byte-equal).
3027                        let already_present = nc_excluded.iter().any(|existing| {
3028                            same_nc_variant(&existing.base, &new_entry.base)
3029                                && name_matches_subtree(&existing.base, new_entry)
3030                                && name_matches_subtree(&new_entry.base, existing)
3031                        });
3032                        if !already_present {
3033                            nc_excluded.push(new_entry.clone());
3034                        }
3035                    }
3036                }
3037            }
3038        }
3039
3040        // Update state for next iteration.
3041        working_spki = &cert.tbs_certificate.subject_public_key_info;
3042        working_issuer_name = &cert.tbs_certificate.subject;
3043        // Determine whether the cert we just processed presents itself via SAN
3044        // identity (empty Subject + critical SAN). This affects the chain-linkage
3045        // check for the certificate immediately below it in the next iteration.
3046        working_issuer_is_san_identity = cert_has_san_identity(cert);
3047    }
3048
3049    // RFC 5280 §6.1.5(a-b): post-loop leaf policy finalisation.
3050    //
3051    // §6.1.5 is a post-loop step in the RFC.  These operations apply only to
3052    // the leaf certificate (chain[0]), which was the last iteration (i == 0).
3053    // Placing them here rather than inside the loop at i == 0 matches the RFC
3054    // section numbering and makes clear that they happen after all per-cert
3055    // §6.1.3/§6.1.4 steps have completed.
3056    {
3057        let leaf = &chain[0];
3058        // §6.1.5(a): if the leaf is not self-issued, decrement counters.
3059        // inhibit_any and policy_mapping are decremented per RFC 5280 §6.1.5(a)
3060        // but are not used after this point in the algorithm — only explicit_policy
3061        // is tested in §6.1.5(g) and the final check.
3062        if !is_self_issued_cert(leaf) {
3063            explicit_policy = explicit_policy.saturating_sub(1);
3064            // Per §6.1.5(a): RFC also decrements inhibit_any and policy_mapping here,
3065            // but neither is read after §6.1.5(a) in our implementation.
3066        }
3067        // §6.1.5(b): if PolicyConstraints requireExplicitPolicy == 0,
3068        // force explicit_policy to 0.
3069        // try_find_cert_ext (fail-closed): consistent with per-loop treatment of
3070        // PolicyConstraints; a malformed extension on the leaf must also reject.
3071        if let Some(pc) = try_find_cert_ext::<PolicyConstraints>(leaf, OID_POLICY_CONSTRAINTS)
3072            .map_err(|_| Error::MalformedCertificate { index: 0 })?
3073        {
3074            if let Some(req) = pc.require_explicit_policy {
3075                explicit_policy = explicit_policy.min(req);
3076            }
3077        }
3078    }
3079
3080    // RFC 5280 §6.1.5(g): intersect the valid_policy_tree with the
3081    // user-initial-policy-set (PKIX-mi3.5).
3082    //
3083    // An empty initial_policy_set means {anyPolicy} — no trimming needed.
3084    //
3085    // When the set is non-empty:
3086    //   §6.1.5(g)(iii)(1): valid_policy_node_set = nodes whose parent
3087    //     has valid_policy = anyPolicy.
3088    //   §6.1.5(g)(iii)(2): delete nodes in that set not in initial_policy_set
3089    //     (and not anyPolicy themselves) along with their descendants.
3090    //   §6.1.5(g)(iii)(3): if a leaf anyPolicy node exists, materialise
3091    //     nodes for each P-OID in initial_policy_set not already present.
3092    //   §6.1.5(g)(iii)(4): prune childless ancestors.
3093    if !policy.initial_policy_set.is_empty() {
3094        if let Some(tree) = &mut policy_tree {
3095            let leaf_depth = n;
3096
3097            // §6.1.5(g)(iii): intersect the valid_policy_tree with
3098            // user-initial-policy-set.
3099            //
3100            // The RFC defines valid_policy_node_set (vpns) as nodes in the tree
3101            // whose PARENT has valid_policy == anyPolicy.  Because the depth-0 root
3102            // is always anyPolicy, this includes ALL depth-1 nodes.  For deeper trees,
3103            // it also includes nodes at any depth whose immediate parent is anyPolicy.
3104            //
3105            // Step (iii)(2): delete every vpns node whose valid_policy is not anyPolicy
3106            // AND not in the user-initial-policy-set.  Then prune ancestors that
3107            // become childless.
3108            //
3109            // Implementation: collect vpns node indices, delete out-of-set nodes,
3110            // then cascade-prune childless descendants.
3111            let vpns_indices: Vec<usize> = tree
3112                .iter()
3113                .enumerate()
3114                .filter(|(_, nd)| {
3115                    nd.depth >= 1
3116                        && tree
3117                            .iter()
3118                            .any(|p| p.depth == nd.depth - 1 && p.valid_policy == OID_ANY_POLICY)
3119                })
3120                .map(|(idx, _)| idx)
3121                .collect();
3122
3123            // Identify vpns nodes to delete: not anyPolicy and not in initial_policy_set.
3124            let to_delete_vpns: Vec<(usize, der::asn1::ObjectIdentifier)> = vpns_indices
3125                .iter()
3126                .filter(|&&idx| {
3127                    tree[idx].valid_policy != OID_ANY_POLICY
3128                        && !policy.initial_policy_set.contains(&tree[idx].valid_policy)
3129                })
3130                .map(|&idx| (tree[idx].depth, tree[idx].valid_policy))
3131                .collect();
3132
3133            if !to_delete_vpns.is_empty() {
3134                // Delete the out-of-set vpns nodes.
3135                tree.retain(|nd| {
3136                    !to_delete_vpns
3137                        .iter()
3138                        .any(|(d, vp)| nd.depth == *d && &nd.valid_policy == vp)
3139                });
3140                // Cascade deletion downward: remove any node that is no longer
3141                // reachable from a living parent node.
3142                //
3143                // Top-down order (shallowest to deepest) is required: the retain
3144                // at depth d mutates the tree in-place, so the any_parent check
3145                // at depth d+1 sees the post-deletion state of depth d parents.
3146                // Bottom-up order would miss grandchildren whose parents survived
3147                // but whose grandparent was deleted.
3148                for d in 2..=leaf_depth {
3149                    let parent_depth = d - 1;
3150                    let reachable: Vec<der::asn1::ObjectIdentifier> = tree
3151                        .iter()
3152                        .filter(|nd| nd.depth == parent_depth)
3153                        .flat_map(|nd| nd.expected_policy_set.iter().copied())
3154                        .collect();
3155                    let any_parent = tree
3156                        .iter()
3157                        .any(|nd| nd.depth == parent_depth && nd.valid_policy == OID_ANY_POLICY);
3158                    tree.retain(|nd| {
3159                        if nd.depth != d {
3160                            return true;
3161                        }
3162                        reachable.contains(&nd.valid_policy) || any_parent
3163                    });
3164                }
3165            }
3166
3167            // Step (iii)(3): materialise nodes for initial_policy_set members
3168            // not yet present, if there's an anyPolicy node at leaf depth.
3169            let has_leaf_any = tree
3170                .iter()
3171                .any(|nd| nd.depth == leaf_depth && nd.valid_policy == OID_ANY_POLICY);
3172            if has_leaf_any {
3173                // RFC §6.1.5(g)(iii)(3) qualifier source: the qualifiers of
3174                // the leaf anyPolicy node about to be deleted. Snapshot
3175                // before the materialise/delete cycle so the deletion at
3176                // the bottom of this block doesn't race the read.
3177                let leaf_any_qualifiers: Vec<_> = tree
3178                    .iter()
3179                    .find(|nd| nd.depth == leaf_depth && nd.valid_policy == OID_ANY_POLICY)
3180                    .map(|nd| nd.qualifiers.clone())
3181                    .unwrap_or_default();
3182                // Collect ALL valid_policy values at leaf_depth (not just vpns_policies,
3183                // which only covers nodes whose parent is anyPolicy).  Using the full
3184                // set prevents materialising a duplicate node for a policy already
3185                // present at leaf depth via a non-anyPolicy parent.
3186                let leaf_policies: Vec<der::asn1::ObjectIdentifier> = tree
3187                    .iter()
3188                    .filter(|nd| nd.depth == leaf_depth)
3189                    .map(|nd| nd.valid_policy)
3190                    .collect();
3191                let mut additions = Vec::new();
3192                for p_oid in &policy.initial_policy_set {
3193                    if !leaf_policies.contains(p_oid) {
3194                        additions.push(PolicyNode {
3195                            depth: leaf_depth,
3196                            valid_policy: *p_oid,
3197                            expected_policy_set: vec![*p_oid],
3198                            qualifiers: leaf_any_qualifiers.clone(),
3199                        });
3200                    }
3201                }
3202                tree.extend(additions);
3203                // Delete the leaf anyPolicy node.
3204                tree.retain(|nd| !(nd.depth == leaf_depth && nd.valid_policy == OID_ANY_POLICY));
3205            }
3206
3207            // Step (iii)(4): prune childless ancestors.
3208            if n > 0 {
3209                prune_policy_tree(tree, leaf_depth);
3210            }
3211            // The tree is effectively NULL if no nodes exist at depth >= 1
3212            // (only the synthetic depth-0 anyPolicy root is left, which
3213            // does not represent any actual valid policy).
3214            if !tree.iter().any(|nd| nd.depth >= 1) {
3215                policy_tree = None;
3216            }
3217        }
3218    }
3219
3220    // §6.1.5 final check: path is valid iff explicit_policy > 0 OR tree
3221    // is non-NULL.
3222    if explicit_policy == 0 && policy_tree.is_none() {
3223        return Err(Error::PolicyViolation { index: 0 });
3224    }
3225
3226    // Return the final valid_policy_tree to `validate_path` so it can be
3227    // surfaced on `ValidatedPath` for post-validation qualifier extraction.
3228    // `None` propagates through unchanged when the tree was reduced to NULL
3229    // during validation; callers that rely on §6.1.5 outputs MUST treat
3230    // `None` as "no policy information available", not as a validation
3231    // failure (the policy-success check above already happened).
3232    Ok(policy_tree)
3233}
3234
3235// ---------------------------------------------------------------------------
3236// NameConstraints enforcement (PKIX-xji)
3237// ---------------------------------------------------------------------------
3238
3239/// Whether a name-constraint check requires a match (permitted) or forbids a
3240/// match (excluded).
3241///
3242/// Using an explicit enum instead of a bare `bool` makes call sites
3243/// self-documenting: `CheckMode::Excluded` / `CheckMode::Permitted` vs
3244/// opaque `false` / `true` (vjc.25).
3245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3246enum CheckMode {
3247    /// Excluded subtrees: any name that matches is a violation.
3248    Excluded,
3249    /// Permitted subtrees: a constrained name type that matches *no* entry is a violation.
3250    Permitted,
3251}
3252
3253/// Check that all names in `cert` satisfy the current `NameConstraints` state.
3254///
3255/// Called once per certificate during `chain_walk`, BEFORE updating the NC
3256/// state from that certificate's own `NameConstraints` extension.
3257///
3258/// `san` is the pre-decoded `SubjectAltName` for this cert (pass `None` if the
3259/// extension is absent). Decoding it before the call avoids a second scan of
3260/// the extension list when both NC check and NC update are needed (vjc.13).
3261///
3262/// RFC 5280 §6.1.4(b)(1)–(2): check excluded subtrees first, then
3263/// permitted subtrees.
3264fn check_name_constraints(
3265    cert: &x509_cert::Certificate,
3266    san: Option<&x509_cert::ext::pkix::SubjectAltName>,
3267    nc_permitted: Option<&GeneralSubtrees>,
3268    nc_excluded: &GeneralSubtrees,
3269    nc_constrained_types: NcTypeMask,
3270    index: usize,
3271) -> crate::Result<()> {
3272    use x509_cert::ext::pkix::name::GeneralName;
3273
3274    let subject = &cert.tbs_certificate.subject;
3275    let subject_is_empty = subject.0.is_empty();
3276
3277    // Helper: check all cert names (subject DN + SAN) against `subtrees`.
3278    //
3279    // CheckMode::Excluded → any match is a violation.
3280    // CheckMode::Permitted → a name type is constrained if any CA in the path
3281    // ever added a permittedSubtrees entry of that type (tracked in
3282    // nc_constrained_types). Constrained types must match at least one permitted
3283    // subtree entry; unconstrained types are always accepted.
3284    let check_names = |subtrees: &[x509_cert::ext::pkix::constraints::name::GeneralSubtree],
3285                       mode: CheckMode|
3286     -> crate::Result<()> {
3287        let type_constrained =
3288            |name: &GeneralName| -> bool { nc_constrained_types.intersects(name_type_bit(name)) };
3289
3290        // subject DN — skipped when empty per RFC 5280 §6.1.3(b).
3291        // Avoid constructing a GeneralName::DirectoryName (which requires a clone)
3292        // by handling DirectoryName constraints inline: pull DirectoryName entries
3293        // from `subtrees` and test directly against the subject Name (vjc.24).
3294        if !subject_is_empty {
3295            let subject_constrained = nc_constrained_types.intersects(NcTypeMask::DIRECTORY_NAME);
3296            let dn_matches_any = subtrees.iter().any(|st| {
3297                if let GeneralName::DirectoryName(constr) = &st.base {
3298                    dn_within_subtree(subject, constr)
3299                } else {
3300                    false
3301                }
3302            });
3303            match mode {
3304                CheckMode::Excluded => {
3305                    if dn_matches_any {
3306                        return Err(Error::NameConstraintViolation { index });
3307                    }
3308                }
3309                CheckMode::Permitted => {
3310                    if subject_constrained && !dn_matches_any {
3311                        return Err(Error::NameConstraintViolation { index });
3312                    }
3313                }
3314            }
3315        }
3316
3317        // SAN entries.
3318        if let Some(san_ext) = san {
3319            for name in &san_ext.0 {
3320                match mode {
3321                    CheckMode::Excluded => {
3322                        if subtrees.iter().any(|st| name_matches_subtree(name, st)) {
3323                            return Err(Error::NameConstraintViolation { index });
3324                        }
3325                    }
3326                    CheckMode::Permitted => {
3327                        if type_constrained(name)
3328                            && !subtrees.iter().any(|st| name_matches_subtree(name, st))
3329                        {
3330                            return Err(Error::NameConstraintViolation { index });
3331                        }
3332                    }
3333                }
3334            }
3335        }
3336        Ok(())
3337    };
3338
3339    // (1) Excluded check: any excluded subtree match → violation.
3340    check_names(nc_excluded.as_slice(), CheckMode::Excluded)?;
3341
3342    // (2) Permitted check: if permitted set is constrained, every name must
3343    //     match at least one permitted subtree.
3344    if let Some(permitted) = nc_permitted {
3345        check_names(permitted.as_slice(), CheckMode::Permitted)?;
3346    }
3347
3348    // (3) RFC 5280 §4.2.1.10: emailAddress attributes in the subject DN MUST
3349    //     be checked against the rfc822Name constraint.
3350    //     Guard: only enter the RDN walk if RFC822 constraints are actually
3351    //     present — either a permitted-subtrees entry for RFC822 exists, OR at
3352    //     least one excluded entry is an Rfc822Name.  Checking !nc_excluded.is_empty()
3353    //     without filtering by type would cause the walk whenever ANY excluded
3354    //     name type exists, even if none are Rfc822Name (vjc.11).
3355    let has_rfc822_excluded = nc_excluded
3356        .iter()
3357        .any(|st| matches!(st.base, GeneralName::Rfc822Name(_)));
3358    let has_rfc822_constraint =
3359        nc_constrained_types.intersects(NcTypeMask::RFC822) || has_rfc822_excluded;
3360
3361    if has_rfc822_constraint && !subject_is_empty {
3362        // Collect the RFC822 permitted subtrees once, outside the RDN loop,
3363        // to avoid re-checking the Option and iterating nc_permitted on every
3364        // emailAddress AVA found (vjc.26). `None` means the permitted check is
3365        // inactive (only an excluded check may apply); the NcTypeMask::RFC822
3366        // condition is evaluated once here and the result carried forward via
3367        // `permitted_rfc822`. `permitted_rfc822_storage` holds the allocation
3368        // when the check is active; `Option` avoids a dummy assignment that
3369        // would trigger an unused-assignment warning.
3370        let permitted_rfc822_storage: Option<GeneralSubtrees> =
3371            if nc_constrained_types.intersects(NcTypeMask::RFC822) {
3372                Some(
3373                    nc_permitted
3374                        .map(|p| {
3375                            p.iter()
3376                                .filter(|st| matches!(st.base, GeneralName::Rfc822Name(_)))
3377                                .cloned()
3378                                .collect()
3379                        })
3380                        .unwrap_or_default(),
3381                )
3382            } else {
3383                None
3384            };
3385        let permitted_rfc822: Option<&[x509_cert::ext::pkix::constraints::name::GeneralSubtree]> =
3386            permitted_rfc822_storage.as_deref();
3387
3388        for rdn in &subject.0 {
3389            for ava in rdn.0.iter() {
3390                if ava.oid != OID_EMAIL_ADDRESS {
3391                    continue;
3392                }
3393                let Ok(email_ia5) = ava.value.decode_as::<der::asn1::Ia5StringRef<'_>>() else {
3394                    continue;
3395                };
3396                let email_str = email_ia5.as_str();
3397                // Excluded check — walk only Rfc822Name excluded entries.
3398                for st in nc_excluded {
3399                    if let GeneralName::Rfc822Name(constraint) = &st.base {
3400                        if matches_rfc822_name(email_str, constraint.as_str()) {
3401                            return Err(Error::NameConstraintViolation { index });
3402                        }
3403                    }
3404                }
3405                // Permitted check (only when RFC822 has been constrained).
3406                if let Some(permitted) = permitted_rfc822 {
3407                    if !permitted.iter().any(|st| {
3408                        if let GeneralName::Rfc822Name(constraint) = &st.base {
3409                            matches_rfc822_name(email_str, constraint.as_str())
3410                        } else {
3411                            false
3412                        }
3413                    }) {
3414                        return Err(Error::NameConstraintViolation { index });
3415                    }
3416                }
3417            }
3418        }
3419    }
3420
3421    Ok(())
3422}
3423
3424// ---------------------------------------------------------------------------
3425// DefaultVerifier — OID-dispatching RustCrypto backend (PKIX-8wg)
3426// ---------------------------------------------------------------------------
3427
3428/// A [`SignatureVerifier`] that dispatches to available `RustCrypto` backends by OID.
3429///
3430/// This is the recommended out-of-the-box verifier for applications that use
3431/// the default `RustCrypto` feature set. It supports:
3432///
3433/// - `ecdsa-with-SHA256` (1.2.840.10045.4.3.2) — via the `p256` feature
3434/// - `sha256WithRSAEncryption` (1.2.840.113549.1.1.11) — via the `rsa` feature
3435///
3436/// Any OID not in the above set returns `Err(signature::Error::new())`.
3437///
3438/// To support additional algorithms, implement [`SignatureVerifier`] directly
3439/// and dispatch your own OID table.
3440#[cfg(any(feature = "p256", feature = "rsa"))]
3441#[cfg_attr(docsrs, doc(cfg(any(feature = "p256", feature = "rsa"))))]
3442#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
3443pub struct DefaultVerifier;
3444
3445#[cfg(any(feature = "p256", feature = "rsa"))]
3446impl SignatureVerifier for DefaultVerifier {
3447    fn verify_signature(
3448        &self,
3449        algorithm: AlgorithmIdentifierRef<'_>,
3450        issuer_spki: SubjectPublicKeyInfoRef<'_>,
3451        message: &[u8],
3452        signature: &[u8],
3453    ) -> core::result::Result<(), SignatureError> {
3454        let oid = algorithm.oid;
3455        #[cfg(feature = "p256")]
3456        if oid == OID_ECDSA_P256_SHA256 {
3457            return EcdsaP256Verifier.verify_signature(algorithm, issuer_spki, message, signature);
3458        }
3459        #[cfg(feature = "rsa")]
3460        if oid == OID_SHA256_WITH_RSA {
3461            return RsaPkcs1v15Sha256Verifier.verify_signature(
3462                algorithm,
3463                issuer_spki,
3464                message,
3465                signature,
3466            );
3467        }
3468        Err(SignatureError::new())
3469    }
3470}
3471
3472// ---------------------------------------------------------------------------
3473// Tests
3474// ---------------------------------------------------------------------------
3475
3476#[cfg(all(test, feature = "p256"))]
3477mod tests_ecdsa_p256 {
3478    use super::*;
3479    use der::Decode;
3480
3481    /// Test vector: a real P-256/SHA-256 self-signed cert generated by OpenSSL.
3482    /// Oracle: `openssl verify -CAfile ec.pem ec.pem` returns OK.
3483    #[test]
3484    fn verify_p256_self_signed() {
3485        use der::Encode as _;
3486        use spki::der::referenced::OwnedToRef as _;
3487        let der = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
3488        let cert = Certificate::from_der(der).expect("parse cert");
3489
3490        let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
3491        let sig_bytes = cert.signature.raw_bytes();
3492
3493        // Self-signed cert: signer SPKI is the cert's own SPKI.
3494        let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
3495
3496        let verifier = EcdsaP256Verifier;
3497        assert!(
3498            verifier
3499                .verify_signature(
3500                    cert.signature_algorithm.owned_to_ref(),
3501                    spki_ref,
3502                    &tbs_der,
3503                    sig_bytes,
3504                )
3505                .is_ok(),
3506            "self-signed P-256 cert should verify"
3507        );
3508    }
3509}
3510
3511#[cfg(all(test, feature = "rsa"))]
3512mod tests_rsa {
3513    use super::*;
3514    use der::Decode;
3515
3516    /// Test vector: a real RSA-2048/SHA-256 self-signed cert generated by OpenSSL.
3517    /// Oracle: `openssl verify -CAfile rsa.pem rsa.pem` returns OK.
3518    #[test]
3519    fn verify_rsa_pkcs1v15_sha256_self_signed() {
3520        use der::Encode as _;
3521        use spki::der::referenced::OwnedToRef as _;
3522        let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
3523        let cert = Certificate::from_der(der).expect("parse cert");
3524
3525        let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
3526        let sig_bytes = cert.signature.raw_bytes();
3527
3528        // Self-signed cert: signer SPKI is the cert's own SPKI.
3529        let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
3530
3531        let verifier = RsaPkcs1v15Sha256Verifier;
3532        assert!(
3533            verifier
3534                .verify_signature(
3535                    cert.signature_algorithm.owned_to_ref(),
3536                    spki_ref,
3537                    &tbs_der,
3538                    sig_bytes,
3539                )
3540                .is_ok(),
3541            "self-signed RSA cert should verify"
3542        );
3543    }
3544
3545    /// Regression (PKIX-5u0): `spki_key_matches` ignores the NULL-vs-absent
3546    /// parameter encoding difference that exists for RSA SPKIs.
3547    ///
3548    /// RFC 3279 §2.3.1 allows both explicit NULL parameters and absent
3549    /// parameters for `rsaEncryption`. The derived `PartialEq` in the `spki`
3550    /// crate treats `Some(NULL) ≠ None`, so using `==` in the self-issued
3551    /// anchor guard would wrongly reject a valid anchor.
3552    #[test]
3553    fn spki_key_matches_ignores_null_vs_absent_params() {
3554        let der_bytes = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
3555        let cert = Certificate::from_der(der_bytes).expect("parse cert");
3556        let cert_spki = &cert.tbs_certificate.subject_public_key_info;
3557
3558        // Same OID and key bytes, but parameters: None instead of Some(NULL).
3559        let spki_no_params: spki::SubjectPublicKeyInfoOwned = spki::SubjectPublicKeyInfoOwned {
3560            algorithm: spki::AlgorithmIdentifier {
3561                oid: cert_spki.algorithm.oid,
3562                parameters: None,
3563            },
3564            subject_public_key: cert_spki.subject_public_key.clone(),
3565        };
3566
3567        // PartialEq distinguishes Some(NULL) from None — document this behavior.
3568        assert_ne!(cert_spki, &spki_no_params);
3569
3570        // spki_key_matches must return true: same OID + same key bytes.
3571        assert!(super::spki_key_matches(cert_spki, &spki_no_params));
3572    }
3573
3574    /// Integration regression (PKIX-5u0): the self-issued anchor guard must not
3575    /// return `NoTrustedPath` when an anchor has absent parameters (None) and the
3576    /// cert in the chain has explicit NULL parameters — both are valid per RFC 3279
3577    /// §2.3.1 for rsaEncryption.
3578    ///
3579    /// The guard compares anchor and cert SPKIs with `spki_key_matches` (OID + key
3580    /// bytes only). Before the fix, using `==` caused `NoTrustedPath` because
3581    /// `Some(NULL) != None` under derived `PartialEq`.
3582    ///
3583    /// Note: the anchor with `parameters: None` will fail signature verification
3584    /// (the `rsa` crate rejects absent params during key parsing), so the result
3585    /// is `Err(SignatureInvalid)`, not `Ok`. What this test verifies is that the
3586    /// guard does NOT skip the anchor and return `NoTrustedPath`. The anchor is
3587    /// tried; the failure is at a later stage, not the guard.
3588    #[test]
3589    fn self_issued_rsa_anchor_absent_params_not_no_trusted_path() {
3590        // 2026-06-01 — within rsa-pkcs1v15-sha256.der validity window
3591        // (notBefore=2026-05-02, notAfter=2036-04-29).
3592        const NOW: u64 = 1_780_272_000;
3593
3594        let der_bytes = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
3595        let cert = Certificate::from_der(der_bytes).expect("parse cert");
3596        let cert_spki = &cert.tbs_certificate.subject_public_key_info;
3597
3598        // Construct an anchor from the same cert but with parameters: None.
3599        // Simulates a trust store that was populated from a source omitting the
3600        // explicit NULL — a common DER encoding variation for rsaEncryption.
3601        let anchor = TrustAnchor::new(
3602            cert.tbs_certificate.subject.clone(),
3603            spki::SubjectPublicKeyInfoOwned {
3604                algorithm: spki::AlgorithmIdentifier {
3605                    oid: cert_spki.algorithm.oid,
3606                    parameters: None,
3607                },
3608                subject_public_key: cert_spki.subject_public_key.clone(),
3609            },
3610        );
3611
3612        let policy = ValidationPolicy {
3613            current_time_unix: NOW,
3614            ..Default::default()
3615        };
3616        let result = validate_path(&[cert], &[anchor], &policy, &RsaPkcs1v15Sha256Verifier);
3617        // The guard must not skip the anchor (which would return NoTrustedPath).
3618        // SignatureInvalid is expected: the anchor was tried but the rsa crate
3619        // rejects absent params during key parsing.
3620        assert!(
3621            !matches!(result, Err(Error::NoTrustedPath)),
3622            "guard must not return NoTrustedPath for same key with different param encoding; got: {result:?}"
3623        );
3624    }
3625}
3626
3627// ---------------------------------------------------------------------------
3628// NormalizedIter / names_match unit tests
3629// ---------------------------------------------------------------------------
3630#[cfg(test)]
3631mod tests_normalized_iter {
3632    use super::normalized_eq;
3633
3634    /// Identical ASCII strings must compare equal.
3635    #[test]
3636    fn identical_strings_equal() {
3637        assert!(normalized_eq(b"hello", b"hello"));
3638    }
3639
3640    /// Case is folded to lowercase.
3641    #[test]
3642    fn case_folding() {
3643        assert!(normalized_eq(b"Hello", b"hello"));
3644        assert!(normalized_eq(b"HELLO WORLD", b"hello world"));
3645    }
3646
3647    /// Leading spaces are stripped.
3648    #[test]
3649    fn leading_spaces_stripped() {
3650        assert!(normalized_eq(b"  hello", b"hello"));
3651    }
3652
3653    /// Trailing spaces are stripped.
3654    ///
3655    /// Regression test: `NormalizedIter` must not emit a trailing space for
3656    /// input that ends with a space sequence.
3657    #[test]
3658    fn trailing_spaces_stripped() {
3659        assert!(normalized_eq(b"hello  ", b"hello"));
3660        assert!(normalized_eq(b"hello ", b"hello"));
3661    }
3662
3663    /// Multiple consecutive internal spaces are collapsed to a single space.
3664    ///
3665    /// Regression test for the double-space bug: `pending_space` must not
3666    /// cause two spaces to be emitted for a single space in the input.
3667    #[test]
3668    fn internal_spaces_collapsed() {
3669        assert!(normalized_eq(b"hello  world", b"hello world"));
3670        assert!(normalized_eq(b"hello   world", b"hello world"));
3671    }
3672
3673    /// Combined: leading + trailing + internal spaces, case folding.
3674    #[test]
3675    fn combined_normalization() {
3676        assert!(normalized_eq(b"  Hello   World  ", b"hello world"));
3677    }
3678
3679    /// Empty string and all-spaces string must both yield zero bytes.
3680    #[test]
3681    fn empty_and_whitespace_only() {
3682        assert!(normalized_eq(b"", b""));
3683        assert!(normalized_eq(b"   ", b""));
3684        assert!(normalized_eq(b"   ", b"   "));
3685    }
3686
3687    /// Different strings must NOT compare equal after normalization.
3688    #[test]
3689    fn different_strings_not_equal() {
3690        assert!(!normalized_eq(b"hello", b"world"));
3691        assert!(!normalized_eq(b"ab", b"abc"));
3692    }
3693
3694    /// `NormalizedIter`: input ending with an internal space sequence followed by
3695    /// trailing spaces must emit the space and then stop (no double space, no
3696    /// trailing space).
3697    #[test]
3698    fn internal_then_trailing_space_no_trailing_emit() {
3699        assert!(
3700            normalized_eq(b"ab  ", b"ab"),
3701            "trailing spaces must not be emitted"
3702        );
3703        assert!(
3704            normalized_eq(b"ab  cd  ", b"ab cd"),
3705            "internal double-space collapses; trailing spaces stripped"
3706        );
3707    }
3708}
3709
3710// ---------------------------------------------------------------------------
3711// PKIX-l63j.1: BMPString transcoding tests
3712// ---------------------------------------------------------------------------
3713#[cfg(test)]
3714mod tests_bmp_string {
3715    use super::Vec;
3716    use super::{ava_values_match, bmp_string_to_utf8};
3717    use der::asn1::Any;
3718    use der::Tag;
3719
3720    /// Construct UCS-2-BE bytes for an ASCII-only string.
3721    ///
3722    /// Each ASCII byte `b` becomes the two-byte sequence `[0x00, b]` since
3723    /// ASCII code points are in the range U+0000..=U+007F and the
3724    /// big-endian UCS-2 encoding of U+00XX is `0x00, 0xXX`.
3725    ///
3726    /// Independent oracle: this construction matches the literal
3727    /// description in X.680 Annex B and ITU-T X.690 §8.6 ("BMPString
3728    /// is encoded as a sequence of two-octet code units in big-endian
3729    /// order, with each unit representing one Unicode code point").
3730    fn ucs2_be_ascii(s: &str) -> Vec<u8> {
3731        let mut v = Vec::with_capacity(s.len() * 2);
3732        for b in s.bytes() {
3733            assert!(
3734                b.is_ascii(),
3735                "ucs2_be_ascii test helper only supports ASCII input"
3736            );
3737            v.push(0x00);
3738            v.push(b);
3739        }
3740        v
3741    }
3742
3743    /// `bmp_string_to_utf8` round-trip for ASCII-only input.
3744    ///
3745    /// Independent oracle: ASCII Unicode code points (U+0000..=U+007F)
3746    /// are encoded identically as a single byte in UTF-8 and as a
3747    /// two-byte big-endian unit `[0x00, X]` in UCS-2. Decoding the
3748    /// UCS-2 form must yield bytes byte-equal to the original ASCII.
3749    #[test]
3750    fn ascii_round_trip() {
3751        let src = "Foo Co";
3752        let ucs2 = ucs2_be_ascii(src);
3753        let utf8 = bmp_string_to_utf8(&ucs2).expect("well-formed UCS-2 ASCII");
3754        assert_eq!(utf8, src.as_bytes());
3755    }
3756
3757    /// `bmp_string_to_utf8` for a non-ASCII BMP code point.
3758    ///
3759    /// Independent oracle: the Hiragana letter A (U+3042) has well-known
3760    /// encodings — UTF-8: `0xE3 0x81 0x82`; UCS-2-BE: `0x30 0x42`. Both
3761    /// are tabulated in the Unicode Character Database. Citing the
3762    /// Unicode codepoint is the external oracle here, not our own code.
3763    #[test]
3764    fn non_ascii_bmp_code_point() {
3765        // U+3042 (HIRAGANA LETTER A): UCS-2-BE bytes 0x30 0x42.
3766        let ucs2 = vec![0x30, 0x42];
3767        let utf8 = bmp_string_to_utf8(&ucs2).expect("U+3042 is a valid BMP code point");
3768        // U+3042 in UTF-8 is the well-known three-byte sequence E3 81 82.
3769        assert_eq!(utf8, vec![0xE3, 0x81, 0x82]);
3770    }
3771
3772    /// Odd-length UCS-2 input is malformed (each unit must be 2 bytes).
3773    /// Fail-closed: return None.
3774    #[test]
3775    fn odd_length_returns_none() {
3776        let malformed = vec![0x00, 0x46, 0x00]; // 3 bytes, not a multiple of 2
3777        assert_eq!(bmp_string_to_utf8(&malformed), None);
3778    }
3779
3780    /// UTF-16 surrogate values (U+D800..=U+DFFF) are not valid Unicode
3781    /// scalar values and must not appear in `BMPString`. Fail-closed:
3782    /// return None.
3783    ///
3784    /// Independent oracle: Unicode Standard §3.8 explicitly defines
3785    /// surrogates as non-scalar; `core::char::from_u32(0xD800..=0xDFFF)`
3786    /// returns None.
3787    #[test]
3788    fn surrogate_returns_none() {
3789        // U+D800 (high surrogate, first surrogate code point).
3790        assert_eq!(bmp_string_to_utf8(&[0xD8, 0x00]), None);
3791        // U+DC00 (low surrogate).
3792        assert_eq!(bmp_string_to_utf8(&[0xDC, 0x00]), None);
3793        // U+DFFF (last surrogate code point).
3794        assert_eq!(bmp_string_to_utf8(&[0xDF, 0xFF]), None);
3795    }
3796
3797    /// Empty BMPString content is well-formed (zero code points) and
3798    /// transcodes to an empty UTF-8 byte vector.
3799    #[test]
3800    fn empty_input_round_trip() {
3801        let utf8 = bmp_string_to_utf8(&[]).expect("empty UCS-2 is well-formed (zero units)");
3802        assert!(utf8.is_empty());
3803    }
3804
3805    /// `ava_values_match`: a BMPString-encoded "Foo Co" must compare
3806    /// equal to a UTF8String-encoded "Foo Co".
3807    ///
3808    /// This is the core PKIX-l63j.1 invariant: same Unicode code points
3809    /// in different DER string types compare equal under DN matching.
3810    #[test]
3811    fn bmp_matches_utf8_same_text() {
3812        let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("Foo Co")).unwrap();
3813        let utf8 = Any::new(Tag::Utf8String, b"Foo Co".to_vec()).unwrap();
3814        assert!(ava_values_match(&bmp, &utf8));
3815        // Symmetry: order of arguments must not matter.
3816        assert!(ava_values_match(&utf8, &bmp));
3817    }
3818
3819    /// `ava_values_match`: BMPString and PrintableString comparisons
3820    /// must succeed for ASCII content with the same Unicode code points.
3821    #[test]
3822    fn bmp_matches_printable_same_text() {
3823        let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("Acme CA")).unwrap();
3824        let printable = Any::new(Tag::PrintableString, b"Acme CA".to_vec()).unwrap();
3825        assert!(ava_values_match(&bmp, &printable));
3826        assert!(ava_values_match(&printable, &bmp));
3827    }
3828
3829    /// `ava_values_match`: ASCII case-folding still applies to
3830    /// BMPString-derived UTF-8 bytes after transcoding (since BMP code
3831    /// points U+0041..=U+005A round-trip to ASCII bytes 0x41..=0x5A).
3832    #[test]
3833    fn bmp_matches_utf8_case_insensitive_ascii() {
3834        let bmp_upper = Any::new(Tag::BmpString, ucs2_be_ascii("FOO CO")).unwrap();
3835        let utf8_lower = Any::new(Tag::Utf8String, b"foo co".to_vec()).unwrap();
3836        assert!(ava_values_match(&bmp_upper, &utf8_lower));
3837    }
3838
3839    /// `ava_values_match`: whitespace collapsing still applies to
3840    /// BMPString-derived UTF-8 bytes (BMP space U+0020 → byte 0x20).
3841    #[test]
3842    fn bmp_matches_utf8_whitespace_collapsed() {
3843        let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("  Foo   Co  ")).unwrap();
3844        let utf8 = Any::new(Tag::Utf8String, b"foo co".to_vec()).unwrap();
3845        assert!(ava_values_match(&bmp, &utf8));
3846    }
3847
3848    /// `ava_values_match`: different Unicode content must NOT match
3849    /// across encodings.
3850    #[test]
3851    fn bmp_does_not_match_utf8_different_text() {
3852        let bmp = Any::new(Tag::BmpString, ucs2_be_ascii("Foo Co")).unwrap();
3853        let utf8 = Any::new(Tag::Utf8String, b"Bar Co".to_vec()).unwrap();
3854        assert!(!ava_values_match(&bmp, &utf8));
3855    }
3856
3857    /// `ava_values_match`: malformed BMPString (odd length) is treated
3858    /// as a non-string type by the dispatcher (`any_to_str_bytes` returns
3859    /// None). It must therefore NOT match a well-formed UTF8String of
3860    /// any content. Fail-closed.
3861    #[test]
3862    fn malformed_bmp_does_not_match_well_formed_utf8() {
3863        // 3-byte content: malformed UCS-2.
3864        let malformed_bmp = Any::new(Tag::BmpString, vec![0x00, 0x46, 0x00]).unwrap();
3865        let utf8 = Any::new(Tag::Utf8String, b"F".to_vec()).unwrap();
3866        assert!(!ava_values_match(&malformed_bmp, &utf8));
3867        assert!(!ava_values_match(&utf8, &malformed_bmp));
3868    }
3869
3870    /// `ava_values_match`: non-ASCII Unicode code points compare equal
3871    /// when both AVAs encode the same code points (BMPString vs
3872    /// UTF8String).
3873    ///
3874    /// Independent oracle: the UCS-2-BE bytes for U+3042 are 0x30 0x42
3875    /// and the UTF-8 bytes are 0xE3 0x81 0x82, both per the Unicode
3876    /// Standard.
3877    #[test]
3878    fn bmp_matches_utf8_non_ascii() {
3879        // U+3042 HIRAGANA LETTER A.
3880        let bmp = Any::new(Tag::BmpString, vec![0x30, 0x42]).unwrap();
3881        let utf8 = Any::new(Tag::Utf8String, vec![0xE3, 0x81, 0x82]).unwrap();
3882        assert!(ava_values_match(&bmp, &utf8));
3883    }
3884
3885    /// Sanity: two BMPString-encoded values with the same Unicode code
3886    /// points compare equal (this exercises the BMPString-on-both-sides
3887    /// path through `any_to_str_bytes`, where both sides allocate).
3888    #[test]
3889    fn bmp_matches_bmp_same_text() {
3890        let a = Any::new(Tag::BmpString, ucs2_be_ascii("Acme")).unwrap();
3891        let b = Any::new(Tag::BmpString, ucs2_be_ascii("acme")).unwrap();
3892        assert!(ava_values_match(&a, &b));
3893    }
3894}
3895
3896// PKIX-h6z: validate_path public API tests.
3897#[cfg(all(test, feature = "p256"))]
3898mod tests_validate_path {
3899    use super::*;
3900    use der::Decode;
3901
3902    // Fixtures and time constants reused from tests_chain_walk.
3903    const GRY_NOW: u64 = 1_780_272_000; // 2026-06-01
3904
3905    fn load(bytes: &[u8]) -> Certificate {
3906        Certificate::from_der(bytes).expect("parse cert")
3907    }
3908
3909    fn policy_at(t: u64) -> ValidationPolicy {
3910        ValidationPolicy {
3911            current_time_unix: t,
3912            ..Default::default()
3913        }
3914    }
3915
3916    /// Happy-path 1-cert chain: self-signed cert is both chain and anchor.
3917    ///
3918    /// Expected: Ok(ValidatedPath { `anchor_index`: 0, depth: 0 })
3919    #[test]
3920    fn one_cert_chain_ok() {
3921        let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
3922        let anchors = [TrustAnchor::from_cert(cert.clone())];
3923        let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
3924            .expect("1-cert chain must validate");
3925        assert_eq!(result.anchor_index, 0);
3926        assert_eq!(result.depth, 0);
3927    }
3928
3929    /// Happy-path 2-cert chain: leaf + intermediate, with root anchor.
3930    ///
3931    /// Oracle: openssl verify -`CAfile` gry-root.pem -untrusted gry-int.pem gry-leaf.pem → OK
3932    /// Expected: Ok(ValidatedPath { `anchor_index`: 0, depth: 1 })
3933    #[test]
3934    fn two_cert_chain_ok() {
3935        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
3936        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
3937        let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
3938        let anchors = [TrustAnchor::from_cert(root)];
3939        let result = validate_path(
3940            &[leaf, int_cert],
3941            &anchors,
3942            &policy_at(GRY_NOW),
3943            &EcdsaP256Verifier,
3944        )
3945        .expect("2-cert chain must validate");
3946        assert_eq!(result.anchor_index, 0);
3947        assert_eq!(result.depth, 1);
3948    }
3949
3950    /// §6.1.5 leaf-intrinsic outputs are populated correctly from `chain[0]`.
3951    ///
3952    /// Independent oracle: parse the leaf fixture a second time via
3953    /// `Certificate::from_der` (a different code path from `validate_path`'s
3954    /// chain[0] access). Compare each `ValidatedPath` accessor field against
3955    /// the independently-parsed leaf's `tbs_certificate` field. Catches:
3956    /// - Field swaps (e.g. `leaf_subject` accidentally populated from `issuer`).
3957    /// - Wrong cert in chain (e.g. `chain[1].subject` instead of `chain[0]`).
3958    /// - Forgotten field assignments.
3959    ///
3960    /// Does NOT validate x509-cert's parser correctness (that's tested
3961    /// upstream); it validates the wiring between `validate_path` and
3962    /// `ValidatedPath`.
3963    #[test]
3964    fn validated_path_exposes_leaf_subject_issuer_serial_spki() {
3965        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
3966        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
3967        let leaf_bytes = include_bytes!("../tests/fixtures/gry-leaf.der");
3968        let leaf = load(leaf_bytes);
3969
3970        // Independent oracle: re-parse the leaf via Certificate::from_der.
3971        // x509-cert's parser is the upstream oracle for the field values.
3972        let oracle_leaf =
3973            Certificate::from_der(leaf_bytes).expect("oracle: leaf fixture must parse");
3974
3975        let anchors = [TrustAnchor::from_cert(root)];
3976        let result = validate_path(
3977            &[leaf, int_cert],
3978            &anchors,
3979            &policy_at(GRY_NOW),
3980            &EcdsaP256Verifier,
3981        )
3982        .expect("2-cert chain must validate");
3983
3984        // The four §6.1.5 leaf-intrinsic outputs must match the
3985        // independently-parsed leaf's corresponding tbs_certificate fields.
3986        assert_eq!(
3987            result.leaf_subject, oracle_leaf.tbs_certificate.subject,
3988            "ValidatedPath.leaf_subject must equal chain[0].tbs_certificate.subject"
3989        );
3990        assert_eq!(
3991            result.leaf_issuer, oracle_leaf.tbs_certificate.issuer,
3992            "ValidatedPath.leaf_issuer must equal chain[0].tbs_certificate.issuer"
3993        );
3994        assert_eq!(
3995            result.leaf_serial, oracle_leaf.tbs_certificate.serial_number,
3996            "ValidatedPath.leaf_serial must equal chain[0].tbs_certificate.serial_number"
3997        );
3998        assert_eq!(
3999            result.leaf_spki, oracle_leaf.tbs_certificate.subject_public_key_info,
4000            "ValidatedPath.leaf_spki must equal chain[0].tbs_certificate.subject_public_key_info"
4001        );
4002
4003        // Sanity: leaf_subject != leaf_issuer for a real (non-self-signed)
4004        // leaf. This catches the swap-subject-with-issuer regression: a
4005        // subject-issuer copy bug would leave both fields equal to whichever
4006        // one was used.
4007        assert_ne!(
4008            result.leaf_subject, result.leaf_issuer,
4009            "non-self-signed leaf must have distinct subject and issuer DNs (regression: swap)"
4010        );
4011    }
4012
4013    /// §6.1.5 outputs for a 1-cert (self-signed) chain.
4014    ///
4015    /// Self-signed certs have `subject == issuer`. This documents that
4016    /// `leaf_subject` and `leaf_issuer` ARE expected to be equal in this
4017    /// case — the previous test's `assert_ne!` is for non-self-signed
4018    /// chains only.
4019    #[test]
4020    fn validated_path_outputs_for_self_signed_chain() {
4021        let cert_bytes = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
4022        let cert = load(cert_bytes);
4023        let oracle = Certificate::from_der(cert_bytes).expect("oracle: cert must parse");
4024        let anchors = [TrustAnchor::from_cert(cert.clone())];
4025
4026        let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
4027            .expect("1-cert self-signed chain must validate");
4028
4029        assert_eq!(result.leaf_subject, oracle.tbs_certificate.subject);
4030        assert_eq!(result.leaf_issuer, oracle.tbs_certificate.issuer);
4031        assert_eq!(result.leaf_serial, oracle.tbs_certificate.serial_number);
4032        assert_eq!(
4033            result.leaf_spki,
4034            oracle.tbs_certificate.subject_public_key_info
4035        );
4036        // Self-signed: subject == issuer by definition.
4037        assert_eq!(
4038            result.leaf_subject, result.leaf_issuer,
4039            "self-signed cert must have equal subject and issuer DNs"
4040        );
4041    }
4042
4043    /// Multiple anchors: correct anchor is second in the slice.
4044    ///
4045    /// Expected: Ok(ValidatedPath { `anchor_index`: 1, depth: 0 })
4046    #[test]
4047    fn correct_anchor_index_when_multiple_anchors() {
4048        let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
4049        let rsa = load(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"));
4050        // First anchor is the RSA cert (wrong name and SPKI for the P-256 chain).
4051        // Second anchor matches.
4052        let anchors = [
4053            TrustAnchor::from_cert(rsa),
4054            TrustAnchor::from_cert(p256.clone()),
4055        ];
4056        let result = validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
4057            .expect("must find second anchor");
4058        assert_eq!(result.anchor_index, 1);
4059        assert_eq!(result.depth, 0);
4060    }
4061
4062    /// Empty chain returns `NoTrustedPath`.
4063    #[test]
4064    fn empty_chain_returns_error() {
4065        let anchors = [TrustAnchor::from_cert(load(include_bytes!(
4066            "../tests/fixtures/ec-p256-sha256.der"
4067        )))];
4068        assert!(
4069            matches!(
4070                validate_path(&[], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
4071                Err(Error::NoTrustedPath)
4072            ),
4073            "empty chain must fail"
4074        );
4075    }
4076
4077    /// Duplicate certificate in chain returns `DuplicateCertificate` error.
4078    ///
4079    /// Oracle: RFC 5280 does not define behavior for duplicate certs; we reject
4080    /// early with a diagnostic error rather than failing later with a confusing
4081    /// `SignatureInvalid` or `ChainBroken`.
4082    ///
4083    /// Duplicate is detected by (issuer DN, serial number) identity per RFC 5280
4084    /// §4.1.2.2 — the same cert appearing twice has the same issuer+serial.
4085    /// SPKI equality is intentionally NOT used (cross-signed CAs share a key but
4086    /// have distinct issuer+serial and must not be rejected).
4087    #[test]
4088    fn duplicate_cert_in_chain_returns_error() {
4089        let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
4090        let anchors = [TrustAnchor::from_cert(cert.clone())];
4091        // Chain [cert, cert]: same cert at index 0 and index 1 — same issuer+serial.
4092        let result = validate_path(
4093            &[cert.clone(), cert],
4094            &anchors,
4095            &policy_at(GRY_NOW),
4096            &EcdsaP256Verifier,
4097        );
4098        assert!(
4099            matches!(
4100                result,
4101                Err(Error::DuplicateCertificate {
4102                    first: 0,
4103                    second: 1
4104                })
4105            ),
4106            "duplicate cert must return DuplicateCertificate{{first:0, second:1}}, got {result:?}"
4107        );
4108    }
4109
4110    /// `path_too_long`: vxf chain [leaf, int] with `max_path_len` = 0.
4111    ///
4112    /// chain.len()=2 → 1 intermediate. 1 > `max_path_len(0)` → `PathTooLong`.
4113    #[test]
4114    fn path_too_long_returns_error() {
4115        let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
4116        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
4117        let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
4118        let anchors = [TrustAnchor::from_cert(root)];
4119        let policy = ValidationPolicy {
4120            current_time_unix: GRY_NOW,
4121            max_path_len: 0,
4122            ..Default::default()
4123        };
4124        assert!(
4125            matches!(
4126                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4127                Err(Error::PathTooLong)
4128            ),
4129            "1 intermediate with max_path_len=0 must return PathTooLong"
4130        );
4131    }
4132
4133    /// `no_trusted_path`: vxf chain presented to an unrelated anchor (gry-root).
4134    ///
4135    /// vxf's last cert issuer name does not match gry-root's subject name.
4136    #[test]
4137    fn no_trusted_path_unrelated_anchor_returns_error() {
4138        let gry_root = load(include_bytes!("../tests/fixtures/gry-root.der"));
4139        let vxf_int = load(include_bytes!("../tests/fixtures/vxf-int.der"));
4140        let vxf_leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
4141        let anchors = [TrustAnchor::from_cert(gry_root)];
4142        assert!(
4143            matches!(
4144                validate_path(
4145                    &[vxf_leaf, vxf_int],
4146                    &anchors,
4147                    &policy_at(GRY_NOW),
4148                    &EcdsaP256Verifier
4149                ),
4150                Err(Error::NoTrustedPath)
4151            ),
4152            "vxf chain with gry anchor must return NoTrustedPath"
4153        );
4154    }
4155
4156    /// `oid_mismatch`: outer signatureAlgorithm OID differs from inner TBS signature OID.
4157    ///
4158    /// Patch the SECOND occurrence of the ECDSA-with-SHA256 OID bytes in vxf-leaf.der
4159    /// to ECDSA-with-SHA384. The inner TBS.signature remains SHA256.
4160    /// `check_oid_consistency` detects this → `MalformedCertificate` { index: 0 }.
4161    ///
4162    /// Oracle: RFC 5280 §4.1.1.2 requires outer and inner `AlgorithmIdentifiers` to be identical.
4163    #[test]
4164    fn oid_mismatch_outer_returns_malformed_certificate() {
4165        let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
4166        // ECDSA-with-SHA256 OID content bytes: 1.2.840.10045.4.3.2
4167        let oid_sha256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02];
4168        // ECDSA-with-SHA384 OID content bytes: 1.2.840.10045.4.3.3 (same length, last byte differs)
4169        let oid_sha384: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03];
4170        // In Certificate DER the inner TBS.signature OID appears FIRST (inside TBSCertificate)
4171        // and the outer signatureAlgorithm OID appears SECOND (after TBSCertificate). Patching
4172        // only the second occurrence changes the outer OID while leaving the inner intact.
4173        let first = leaf_der
4174            .windows(8)
4175            .position(|w| w == oid_sha256)
4176            .expect("inner SHA256 OID must be present in vxf-leaf.der");
4177        let second = leaf_der[first + 8..]
4178            .windows(8)
4179            .position(|w| w == oid_sha256)
4180            .map(|p| first + 8 + p)
4181            .expect("outer SHA256 OID must be present in vxf-leaf.der");
4182        leaf_der[second..second + 8].copy_from_slice(oid_sha384);
4183        let leaf = Certificate::from_der(&leaf_der).expect("patched DER must parse");
4184        assert_ne!(
4185            leaf.signature_algorithm, leaf.tbs_certificate.signature,
4186            "outer/inner OIDs must differ after patch"
4187        );
4188        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
4189        let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
4190        let anchors = [TrustAnchor::from_cert(root)];
4191        assert!(
4192            matches!(
4193                validate_path(
4194                    &[leaf, int_cert],
4195                    &anchors,
4196                    &policy_at(GRY_NOW),
4197                    &EcdsaP256Verifier
4198                ),
4199                Err(Error::MalformedCertificate { index: 0 })
4200            ),
4201            "outer/inner OID mismatch must return MalformedCertificate {{ index: 0 }}"
4202        );
4203    }
4204
4205    /// `intermediate_not_ca`: nca-int has no `BasicConstraints` extension.
4206    ///
4207    /// Oracle: pyca/cryptography — nca-int built without any extensions.
4208    /// cert_is_ca(nca-int) returns None → `NotCA` { index: 1 }.
4209    #[test]
4210    fn intermediate_not_ca_returns_not_ca() {
4211        let root = load(include_bytes!("../tests/fixtures/nca-root.der"));
4212        let int_cert = load(include_bytes!("../tests/fixtures/nca-int.der"));
4213        let leaf = load(include_bytes!("../tests/fixtures/nca-leaf.der"));
4214        let anchors = [TrustAnchor::from_cert(root)];
4215        assert!(
4216            matches!(
4217                validate_path(
4218                    &[leaf, int_cert],
4219                    &anchors,
4220                    &policy_at(GRY_NOW),
4221                    &EcdsaP256Verifier
4222                ),
4223                Err(Error::NotCA { index: 1 })
4224            ),
4225            "intermediate without BasicConstraints CA flag must return NotCA {{ index: 1 }}"
4226        );
4227    }
4228
4229    /// `key_usage_missing_cert_sign`: kuf-int has `KeyUsage` with digitalSignature only.
4230    ///
4231    /// Oracle: pyca/cryptography — kuf-int KeyUsage.keyCertSign = False.
4232    /// Default policy has `enforce_key_usage` = true; `chain_walk` checks at i=1.
4233    #[test]
4234    fn key_usage_missing_cert_sign_returns_error() {
4235        let root = load(include_bytes!("../tests/fixtures/kuf-root.der"));
4236        let int_cert = load(include_bytes!("../tests/fixtures/kuf-int.der"));
4237        let leaf = load(include_bytes!("../tests/fixtures/kuf-leaf.der"));
4238        let anchors = [TrustAnchor::from_cert(root)];
4239        assert!(
4240            matches!(
4241                validate_path(&[leaf, int_cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
4242                Err(Error::KeyUsageMissing { index: 1 })
4243            ),
4244            "intermediate with KeyUsage but no keyCertSign must return KeyUsageMissing {{ index: 1 }}"
4245        );
4246    }
4247
4248    /// `absent_key_usage_intermediate_accepted`: nku-int has NO `KeyUsage` extension at all.
4249    ///
4250    /// RFC 5280 §6.1.4(n): "If a `KeyUsage` extension is **present**, verify that the
4251    /// keyCertSign bit is set." Absent `KeyUsage` must not be rejected by `enforce_key_usage`.
4252    ///
4253    /// Oracle: pyca/cryptography — nku-int has only `BasicConstraints` (OID 2.5.29.19),
4254    /// no `KeyUsage` extension.
4255    #[test]
4256    fn absent_key_usage_intermediate_accepted() {
4257        let root = load(include_bytes!("../tests/fixtures/nku-root.der"));
4258        let int_cert = load(include_bytes!("../tests/fixtures/nku-int.der"));
4259        let leaf = load(include_bytes!("../tests/fixtures/nku-leaf.der"));
4260        let anchors = [TrustAnchor::from_cert(root)];
4261        // Default policy has enforce_key_usage = true.
4262        // nku-int has no KeyUsage — must NOT trigger KeyUsageMissing per RFC 5280 §6.1.4(n).
4263        let now: u64 = 1_720_000_000; // 2024-07-03, within nku-int validity (2024-2030)
4264        let policy = ValidationPolicy {
4265            current_time_unix: now,
4266            ..Default::default()
4267        };
4268        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier).expect(
4269            "intermediate with absent KeyUsage must be accepted when enforce_key_usage=true",
4270        );
4271    }
4272
4273    /// Leaf with critical `ExtendedKeyUsage` → `validate_path` must accept it.
4274    ///
4275    /// EKU is in `HANDLED_CRITICAL_OIDS`; its value is not inspected.
4276    /// Oracle: pyca/cryptography — eku-critical-self-signed.der, critical=True, serverAuth.
4277    #[test]
4278    fn critical_eku_accepted() {
4279        let cert = load(include_bytes!(
4280            "../tests/fixtures/eku-critical-self-signed.der"
4281        ));
4282        let anchors = [TrustAnchor::from_cert(cert.clone())];
4283        validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
4284            .expect("cert with critical EKU must be accepted");
4285    }
4286
4287    /// Security test: anchor with matching name but wrong SPKI must be rejected.
4288    ///
4289    /// Guards against a name-collision attack: an attacker who creates a root cert
4290    /// with the same DN as a trusted anchor but a different key must not be accepted.
4291    /// The self-issued SPKI guard in `validate_path` catches this.
4292    #[test]
4293    fn forged_anchor_name_match_spki_mismatch_rejected() {
4294        use der::Decode as _;
4295        let p256 = Certificate::from_der(include_bytes!("../tests/fixtures/ec-p256-sha256.der"))
4296            .expect("parse P-256 cert");
4297        let rsa =
4298            Certificate::from_der(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"))
4299                .expect("parse RSA cert");
4300        // Forged anchor: P-256 cert's subject name + RSA cert's SPKI.
4301        let forged = TrustAnchor::new(
4302            p256.tbs_certificate.subject.clone(),
4303            rsa.tbs_certificate.subject_public_key_info,
4304        );
4305        let anchors = [forged];
4306        assert!(
4307            matches!(
4308                validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
4309                Err(Error::NoTrustedPath)
4310            ),
4311            "anchor with matching name but wrong SPKI must return NoTrustedPath"
4312        );
4313    }
4314
4315    /// Verify that `validate_path` handles large certs without `Error::Der`.
4316    ///
4317    /// The previous fixed 8 KiB stack buffer returned `Error::Der` for any cert
4318    /// whose `TBSCertificate` DER exceeded 8 KiB. The heap-backed encoding path
4319    /// introduced in v0.2 removes that limit. This test verifies that a normally-
4320    /// sized cert (well under 8 KiB) still validates correctly, confirming the
4321    /// heap path is wired up correctly and not just a dead code path.
4322    ///
4323    /// Oracle: the gry-leaf fixture validates correctly via openssl verify.
4324    #[test]
4325    fn large_cert_encoding_does_not_fail_with_der_error() {
4326        // We don't have an actual > 8 KiB TBSCertificate fixture in the test suite,
4327        // but we can verify the heap path is taken by confirming normal certs still pass.
4328        // The regression test for the bug is: this path no longer returns Error::Der
4329        // for legitimately-sized certs.
4330        let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
4331        let anchors = [TrustAnchor::from_cert(cert.clone())];
4332        // Must not return Err(Error::Der(...)) — the heap encoding path must succeed.
4333        let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier);
4334        assert!(
4335            !matches!(result, Err(Error::Der(_))),
4336            "heap-backed encoding must not return Error::Der for a normal cert"
4337        );
4338    }
4339
4340    /// Verify `cert_has_san_identity` returns false for normal certs (non-empty Subject).
4341    ///
4342    /// Oracle: RFC 5280 §4.2.1.6 — `cert_has_san_identity` must return true only when
4343    /// Subject is empty AND SAN is critical. Normal certs have non-empty Subject.
4344    #[test]
4345    fn cert_has_san_identity_false_for_normal_cert() {
4346        let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
4347        // This is a normal self-signed cert with a non-empty Subject DN.
4348        assert!(
4349            !cert_has_san_identity(&cert),
4350            "normal cert with non-empty Subject must not be SAN-identity"
4351        );
4352    }
4353}
4354
4355// PKIX-vxf + PKIX-gry: chain_walk tests require the p256 feature.
4356#[cfg(all(test, feature = "p256"))]
4357mod tests_chain_walk {
4358    use super::*;
4359    use der::Decode;
4360
4361    // Fixtures (PKIX-vxf):
4362    //   vxf-root.der — self-signed root CA, CN=PKIX-vxf-root  (P-256)
4363    //   vxf-int.der  — intermediate CA, CN=PKIX-vxf-int, signed by vxf-root
4364    //   vxf-leaf.der — leaf cert, CN=PKIX-vxf-leaf, signed by vxf-int
4365    //   chk-root.der / chk-int.der / chk-leaf-wrong-issuer.der — ChainBroken test chain
4366    //
4367    // Fixtures (PKIX-gry):
4368    //   gry-root.der                  — root CA, CN=PKIX-gry-root (P-256)
4369    //   gry-int.der                   — intermediate CA, CN=PKIX-gry-int, valid 2026-2036
4370    //   gry-leaf.der                  — leaf, CN=PKIX-gry-leaf, valid 2026-2027 (short-lived)
4371    //   gry-leaf-unknown-crit.der     — leaf with unknown critical extension
4372    //
4373    // Unix timestamp constants for gry validity tests:
4374    //   GRY_NOW     = 1780272000  (2026-06-01, all gry certs valid)
4375    //   GRY_EXPIRED = 1830384000  (2028-01-02, gry-leaf expired; gry-int still valid)
4376    //   GRY_NOTYET  = 0           (1970-01-01, all gry certs not-yet-valid)
4377    //
4378    // Oracle:
4379    //   vxf chain: openssl verify -CAfile vxf-root.pem -untrusted vxf-int.pem vxf-leaf.pem → OK
4380    //   gry chain: pyca/cryptography; chain verifies at GRY_NOW
4381    //   chk-leaf-wrong-issuer: signature valid under chk-int key (pyca); issuer = PKIX-WRONG-ISSUER by design
4382
4383    const GRY_NOW: u64 = 1_780_272_000;
4384    const GRY_EXPIRED: u64 = 1_830_384_000;
4385    const GRY_NOTYET: u64 = 0;
4386
4387    fn load(bytes: &[u8]) -> Certificate {
4388        Certificate::from_der(bytes).expect("parse cert")
4389    }
4390
4391    fn policy_at(t: u64) -> ValidationPolicy {
4392        ValidationPolicy {
4393            current_time_unix: t,
4394            ..Default::default()
4395        }
4396    }
4397
4398    /// 1-cert chain: self-signed P-256 cert as both chain and anchor.
4399    #[test]
4400    fn single_cert_chain_ok() {
4401        let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
4402        let policy = policy_at(GRY_NOW);
4403        let anchor = TrustAnchor::from_cert(p256.clone());
4404        chain_walk(&[p256], &anchor, &policy, &EcdsaP256Verifier)
4405            .expect("1-cert chain must pass chain_walk");
4406    }
4407
4408    /// 2-cert chain (leaf + intermediate) with root as anchor.
4409    ///
4410    /// Oracle: openssl verify -`CAfile` vxf-root.pem -untrusted vxf-int.pem vxf-leaf.pem → OK
4411    #[test]
4412    fn two_cert_chain_ok() {
4413        let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
4414        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
4415        let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
4416        let policy = policy_at(GRY_NOW);
4417        let anchor = TrustAnchor::from_cert(root);
4418        chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier)
4419            .expect("2-cert chain must pass chain_walk");
4420    }
4421
4422    /// Leaf with corrupted signature — last byte flipped.
4423    ///
4424    /// The DER structure remains valid; only the BIT STRING content is wrong.
4425    /// Expect `SignatureInvalid` at chain index 0.
4426    #[test]
4427    fn corrupted_signature_returns_signature_invalid() {
4428        let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
4429        *leaf_der.last_mut().unwrap() ^= 0xFF;
4430        let leaf = Certificate::from_der(&leaf_der).expect("parse still succeeds after bit flip");
4431        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
4432        let anchor = TrustAnchor::from_cert(load(include_bytes!("../tests/fixtures/vxf-root.der")));
4433        let policy = policy_at(GRY_NOW);
4434        assert!(
4435            matches!(
4436                chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
4437                Err(Error::SignatureInvalid { index: 0 })
4438            ),
4439            "corrupted leaf signature must return SignatureInvalid {{ index: 0 }}"
4440        );
4441    }
4442
4443    /// Chain where the leaf's issuer field does not match the intermediate's subject.
4444    ///
4445    /// Oracle: chk-leaf-wrong-issuer was signed by chk-int's private key
4446    /// (signature IS valid), but its issuer field = "PKIX-WRONG-ISSUER" by design.
4447    #[test]
4448    fn wrong_issuer_name_returns_chain_broken() {
4449        let root = load(include_bytes!("../tests/fixtures/chk-root.der"));
4450        let int_cert = load(include_bytes!("../tests/fixtures/chk-int.der"));
4451        let leaf_wrong = load(include_bytes!(
4452            "../tests/fixtures/chk-leaf-wrong-issuer.der"
4453        ));
4454        let policy = policy_at(GRY_NOW);
4455        let anchor = TrustAnchor::from_cert(root);
4456        assert!(
4457            matches!(
4458                chain_walk(
4459                    &[leaf_wrong, int_cert],
4460                    &anchor,
4461                    &policy,
4462                    &EcdsaP256Verifier
4463                ),
4464                Err(Error::ChainBroken { index: 0 })
4465            ),
4466            "leaf with wrong issuer must return ChainBroken {{ index: 0 }}"
4467        );
4468    }
4469
4470    // --- PKIX-gry per-cert check tests ---
4471
4472    /// Expired leaf cert → `ValidityPeriod` at index 0.
4473    ///
4474    /// Oracle: gry-leaf.der has notAfter=2027-01-01; GRY_EXPIRED=2028-01-02.
4475    /// gry-int.der has notAfter=2036-01-01, which is still valid at `GRY_EXPIRED`.
4476    /// Reverse walk: i=1 (gry-int) passes validity, then i=0 (gry-leaf) fails.
4477    #[test]
4478    fn expired_leaf_returns_validity_period() {
4479        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
4480        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
4481        let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
4482        let policy = policy_at(GRY_EXPIRED);
4483        let anchor = TrustAnchor::from_cert(root);
4484        assert!(
4485            matches!(
4486                chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
4487                Err(Error::ValidityPeriod { index: 0 })
4488            ),
4489            "expired leaf must return ValidityPeriod {{ index: 0 }}"
4490        );
4491    }
4492
4493    /// Not-yet-valid intermediate → `ValidityPeriod` at index 1.
4494    ///
4495    /// Oracle: gry-int.der has notBefore=2026-01-01; `GRY_NOTYET=0` (1970-01-01).
4496    /// Reverse walk processes chain[1] (gry-int) first; it is not yet valid at time 0.
4497    #[test]
4498    fn notyet_valid_intermediate_returns_validity_period() {
4499        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
4500        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
4501        let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
4502        let policy = policy_at(GRY_NOTYET);
4503        let anchor = TrustAnchor::from_cert(root);
4504        assert!(
4505            matches!(
4506                chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
4507                Err(Error::ValidityPeriod { index: 1 })
4508            ),
4509            "not-yet-valid intermediate must return ValidityPeriod {{ index: 1 }}"
4510        );
4511    }
4512
4513    /// Leaf with unknown critical extension → `UnhandledCriticalExtension` at index 0.
4514    ///
4515    /// Oracle: gry-leaf-unknown-crit.der was generated with OID 1.3.6.1.5.5.7.99.99 critical=true
4516    /// (not in `HANDLED_CRITICAL_OIDS`) using pyca/cryptography.
4517    #[test]
4518    fn unknown_critical_extension_returns_unhandled() {
4519        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
4520        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
4521        let leaf_unk = load(include_bytes!(
4522            "../tests/fixtures/gry-leaf-unknown-crit.der"
4523        ));
4524        let policy = policy_at(GRY_NOW);
4525        let anchor = TrustAnchor::from_cert(root);
4526        assert!(
4527            matches!(
4528                chain_walk(&[leaf_unk, int_cert], &anchor, &policy, &EcdsaP256Verifier),
4529                Err(Error::UnhandledCriticalExtension { index: 0 })
4530            ),
4531            "unknown critical ext must return UnhandledCriticalExtension {{ index: 0 }}"
4532        );
4533    }
4534}
4535
4536// ---------------------------------------------------------------------------
4537// Tests: ValidationPolicy profile-enforcement fields (PKIX-ken.1.9–1.13)
4538// ---------------------------------------------------------------------------
4539//
4540// Fixtures: pkix-path/tests/fixtures/policy-checks/
4541//   root-p256.der, int-p256.der — P-256 CA chain (ecdsa-sha256)
4542//   leaf-p256-365d-san-eku.der  — 365-day leaf, SAN=DNS:test.example.com, EKU=serverAuth
4543//   leaf-p256-400d-san-eku.der  — 400-day leaf, SAN, EKU=serverAuth
4544//   leaf-p256-365d-no-san.der   — 365-day leaf, no SAN extension
4545//   leaf-p256-365d-no-eku.der   — 365-day leaf, SAN, no EKU extension
4546//   leaf-p256-365d-wrong-eku.der— 365-day leaf, SAN, EKU=emailProtection only
4547//   root-rsa2048.der, int-rsa2048.der — RSA-2048 CA chain (sha256WithRSAEncryption)
4548//   leaf-rsa2048-365d-san-eku.der — RSA-2048 leaf, SAN, EKU=serverAuth
4549//   leaf-rsa1024-365d-san-eku.der — RSA-1024 leaf, SAN, EKU=serverAuth
4550//
4551// Oracle: pkix-path/tests/fixtures/policy-checks/gen.py (pyca/cryptography)
4552// Chain verification: openssl verify passed for P-256 and RSA-2048 happy paths.
4553// Time constant: PC_NOW = 2026-06-01T00:00:00Z = 1_780_272_000 (unix)
4554//   All fixtures have NOT_BEFORE=2026-01-01, valid at PC_NOW.
4555//
4556// All tests require the p256 feature for P-256 chain tests, and rsa for RSA chain tests.
4557//
4558// The P-256 chain uses the module-level const directly; RSA chain tests live inside
4559// a separate rsa-feature-gated block so clippy does not warn about unused imports.
4560
4561#[cfg(all(test, feature = "p256"))]
4562mod tests_policy_fields {
4563    use super::*;
4564    use der::Decode;
4565
4566    // GRY_NOW is also the test time for these fixtures (2026-06-01T00:00:00Z).
4567    const PC_NOW: u64 = 1_780_272_000;
4568
4569    // OID constants — values from const_oid spec, NOT derived from the code under test.
4570    // ecdsa-with-SHA256: 1.2.840.10045.4.3.2  (RFC 5912 §6)
4571    const ECDSA_SHA256_OID: der::asn1::ObjectIdentifier =
4572        der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
4573    // sha256WithRSAEncryption: 1.2.840.113549.1.1.11  (RFC 5912 §2)
4574    const RSA_SHA256_OID: der::asn1::ObjectIdentifier =
4575        der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
4576    // id-kp-serverAuth: 1.3.6.1.5.5.7.3.1  (RFC 5280 §4.2.1.12)
4577    const ID_KP_SERVER_AUTH: der::asn1::ObjectIdentifier =
4578        der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
4579    // id-kp-emailProtection: 1.3.6.1.5.5.7.3.4  (RFC 5280 §4.2.1.12)
4580    const ID_KP_EMAIL_PROTECTION: der::asn1::ObjectIdentifier =
4581        der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.4");
4582
4583    fn load(bytes: &[u8]) -> Certificate {
4584        Certificate::from_der(bytes).expect("valid DER fixture")
4585    }
4586
4587    // -----------------------------------------------------------------------
4588    // max_validity_secs (PKIX-ken.1.9)
4589    // -----------------------------------------------------------------------
4590
4591    /// Oracle: all certs in the chain have validity ≤ 3652 days (10-year root/int,
4592    /// 365-day leaf). A cap of 4000 days allows all of them through.
4593    #[test]
4594    fn max_validity_passes_when_cert_within_limit() {
4595        let root = load(include_bytes!(
4596            "../tests/fixtures/policy-checks/root-p256.der"
4597        ));
4598        let int_cert = load(include_bytes!(
4599            "../tests/fixtures/policy-checks/int-p256.der"
4600        ));
4601        let leaf = load(include_bytes!(
4602            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4603        ));
4604        let mut policy = ValidationPolicy::new(PC_NOW);
4605        // 4000-day cap: root/int have ~3652 days, leaf has 365 days — all within limit.
4606        policy.max_validity_secs = Some(4_000 * 86_400);
4607        let anchors = [TrustAnchor::from_cert(root)];
4608        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4609            .expect("all certs within 4000-day cap should validate");
4610    }
4611
4612    /// Oracle: root-p256.der and int-p256.der each have ~3652-day validity
4613    /// (NOT_BEFORE=2026-01-01, NOT_AFTER=2036-01-01 from gen.py).
4614    /// A cap of 400 days forces `ValidityPeriodExceedsMax` on the root (checked first
4615    /// by `chain_walk` which iterates from high index to low).
4616    ///
4617    /// Note: the check applies to every cert in the chain, not just the leaf.
4618    /// The root cert (highest index) is checked first and produces the error.
4619    #[test]
4620    fn max_validity_fails_when_cert_exceeds_limit() {
4621        let root = load(include_bytes!(
4622            "../tests/fixtures/policy-checks/root-p256.der"
4623        ));
4624        let int_cert = load(include_bytes!(
4625            "../tests/fixtures/policy-checks/int-p256.der"
4626        ));
4627        let leaf = load(include_bytes!(
4628            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4629        ));
4630        let mut policy = ValidationPolicy::new(PC_NOW);
4631        // 400-day cap: root/int have 3652-day validity → ValidityPeriodExceedsMax.
4632        // Wildcard index because the root (highest-index cert) is checked first.
4633        policy.max_validity_secs = Some(400 * 86_400);
4634        let anchors = [TrustAnchor::from_cert(root)];
4635        assert!(
4636            matches!(
4637                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4638                Err(Error::ValidityPeriodExceedsMax { .. })
4639            ),
4640            "certs with 3652-day validity over 400-day cap must return ValidityPeriodExceedsMax"
4641        );
4642    }
4643
4644    /// Isolates the leaf-only failure: use a 1-cert self-issued chain where
4645    /// the cert acts as both leaf and anchor. The 400-day cert fails a 398-day cap.
4646    ///
4647    /// Oracle: leaf-p256-400d-san-eku.der has notAfter-notBefore = 400 days = 34,560,000 s.
4648    /// 400 days > 398 days → `ValidityPeriodExceedsMax` { index: 0 }.
4649    #[test]
4650    fn max_validity_fails_at_leaf_index_zero() {
4651        // Use a single self-signed cert as both chain[0] and anchor so there is only
4652        // one cert in the chain, making index 0 the only possible failure point.
4653        // leaf-p256-400d-san-eku.der is NOT self-signed, so we use a known self-signed
4654        // cert from the existing fixture set (ec-p256-sha256.der) which has a long
4655        // validity, then set max to 1 day to force failure at index 0.
4656        let cert = Certificate::from_der(include_bytes!("../tests/fixtures/ec-p256-sha256.der"))
4657            .expect("parse ec-p256-sha256.der");
4658        let anchors = [TrustAnchor::from_cert(cert.clone())];
4659        let mut policy = ValidationPolicy::new(1_780_272_000); // PC_NOW: 2026-06-01
4660                                                               // 1-day cap: the cert has multi-year validity → fails at index 0.
4661        policy.max_validity_secs = Some(86_400);
4662        assert!(
4663            matches!(
4664                validate_path(&[cert], &anchors, &policy, &EcdsaP256Verifier),
4665                Err(Error::ValidityPeriodExceedsMax { index: 0 })
4666            ),
4667            "1-cert chain: long-validity cert with 1-day cap must return ValidityPeriodExceedsMax {{ index: 0 }}"
4668        );
4669    }
4670
4671    /// Oracle: None = unconstrained, any validity length is accepted.
4672    #[test]
4673    fn max_validity_none_is_unconstrained() {
4674        let root = load(include_bytes!(
4675            "../tests/fixtures/policy-checks/root-p256.der"
4676        ));
4677        let int_cert = load(include_bytes!(
4678            "../tests/fixtures/policy-checks/int-p256.der"
4679        ));
4680        let leaf = load(include_bytes!(
4681            "../tests/fixtures/policy-checks/leaf-p256-400d-san-eku.der"
4682        ));
4683        let mut policy = ValidationPolicy::new(PC_NOW);
4684        policy.max_validity_secs = None; // default, but explicit for documentation
4685        let anchors = [TrustAnchor::from_cert(root)];
4686        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4687            .expect("None cap must accept any validity length");
4688    }
4689
4690    // -----------------------------------------------------------------------
4691    // allowed_signature_algs (PKIX-ken.1.10)
4692    // -----------------------------------------------------------------------
4693
4694    /// Oracle: P-256 chain uses ecdsa-with-SHA256; allowlist contains that OID.
4695    #[test]
4696    fn alg_allowlist_passes_when_oid_in_list() {
4697        let root = load(include_bytes!(
4698            "../tests/fixtures/policy-checks/root-p256.der"
4699        ));
4700        let int_cert = load(include_bytes!(
4701            "../tests/fixtures/policy-checks/int-p256.der"
4702        ));
4703        let leaf = load(include_bytes!(
4704            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4705        ));
4706        let mut policy = ValidationPolicy::new(PC_NOW);
4707        policy.allowed_signature_algs = Some(vec![ECDSA_SHA256_OID]);
4708        let anchors = [TrustAnchor::from_cert(root)];
4709        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4710            .expect("ECDSA-SHA256 chain with ECDSA-SHA256 allowlist should pass");
4711    }
4712
4713    /// Oracle: P-256 chain uses ecdsa-sha256; allowlist contains only RSA-sha256.
4714    /// `chain_walk` walks highest index first: leaf=[0], int=[1], root=[2].
4715    /// For a 3-cert chain, the root-adjacent cert is at index 2 in the slice.
4716    /// `chain_walk` iterates i from (chain.len()-1) down to 0, so i=2 (root) is checked
4717    /// first and fails with `AlgorithmNotAllowed` { index: 2 }.
4718    #[test]
4719    fn alg_allowlist_fails_when_oid_not_in_list() {
4720        let root = load(include_bytes!(
4721            "../tests/fixtures/policy-checks/root-p256.der"
4722        ));
4723        let int_cert = load(include_bytes!(
4724            "../tests/fixtures/policy-checks/int-p256.der"
4725        ));
4726        let leaf = load(include_bytes!(
4727            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4728        ));
4729        let mut policy = ValidationPolicy::new(PC_NOW);
4730        // Only RSA allowed, but chain uses ECDSA.
4731        policy.allowed_signature_algs = Some(vec![RSA_SHA256_OID]);
4732        let anchors = [TrustAnchor::from_cert(root)];
4733        assert!(
4734            matches!(
4735                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4736                Err(Error::AlgorithmNotAllowed { .. })
4737            ),
4738            "ECDSA chain with RSA-only allowlist must return AlgorithmNotAllowed"
4739        );
4740    }
4741
4742    /// Oracle: None = unconstrained, any algorithm is accepted.
4743    #[test]
4744    fn alg_allowlist_none_is_unconstrained() {
4745        let root = load(include_bytes!(
4746            "../tests/fixtures/policy-checks/root-p256.der"
4747        ));
4748        let int_cert = load(include_bytes!(
4749            "../tests/fixtures/policy-checks/int-p256.der"
4750        ));
4751        let leaf = load(include_bytes!(
4752            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4753        ));
4754        let mut policy = ValidationPolicy::new(PC_NOW);
4755        policy.allowed_signature_algs = None; // default
4756        let anchors = [TrustAnchor::from_cert(root)];
4757        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4758            .expect("None allowlist must accept any algorithm");
4759    }
4760
4761    // -----------------------------------------------------------------------
4762    // require_subject_alt_name (PKIX-ken.1.12)
4763    // -----------------------------------------------------------------------
4764
4765    /// Oracle: leaf-p256-365d-san-eku.der has SAN=DNS:test.example.com.
4766    #[test]
4767    fn require_san_passes_when_san_present() {
4768        let root = load(include_bytes!(
4769            "../tests/fixtures/policy-checks/root-p256.der"
4770        ));
4771        let int_cert = load(include_bytes!(
4772            "../tests/fixtures/policy-checks/int-p256.der"
4773        ));
4774        let leaf = load(include_bytes!(
4775            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4776        ));
4777        let mut policy = ValidationPolicy::new(PC_NOW);
4778        policy.require_subject_alt_name = true;
4779        let anchors = [TrustAnchor::from_cert(root)];
4780        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4781            .expect("leaf with SAN must pass require_subject_alt_name=true");
4782    }
4783
4784    /// Oracle: leaf-p256-365d-no-san.der has no SAN extension.
4785    #[test]
4786    fn require_san_fails_when_san_absent() {
4787        let root = load(include_bytes!(
4788            "../tests/fixtures/policy-checks/root-p256.der"
4789        ));
4790        let int_cert = load(include_bytes!(
4791            "../tests/fixtures/policy-checks/int-p256.der"
4792        ));
4793        let leaf = load(include_bytes!(
4794            "../tests/fixtures/policy-checks/leaf-p256-365d-no-san.der"
4795        ));
4796        let mut policy = ValidationPolicy::new(PC_NOW);
4797        policy.require_subject_alt_name = true;
4798        let anchors = [TrustAnchor::from_cert(root)];
4799        assert!(
4800            matches!(
4801                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4802                Err(Error::MissingSan)
4803            ),
4804            "leaf without SAN must return MissingSan when require_subject_alt_name=true"
4805        );
4806    }
4807
4808    /// Oracle: false = default = no SAN requirement; missing SAN is not an error.
4809    #[test]
4810    fn require_san_false_does_not_fail_on_missing_san() {
4811        let root = load(include_bytes!(
4812            "../tests/fixtures/policy-checks/root-p256.der"
4813        ));
4814        let int_cert = load(include_bytes!(
4815            "../tests/fixtures/policy-checks/int-p256.der"
4816        ));
4817        let leaf = load(include_bytes!(
4818            "../tests/fixtures/policy-checks/leaf-p256-365d-no-san.der"
4819        ));
4820        let mut policy = ValidationPolicy::new(PC_NOW);
4821        policy.require_subject_alt_name = false; // default, explicit for documentation
4822        let anchors = [TrustAnchor::from_cert(root)];
4823        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4824            .expect("require_subject_alt_name=false must not fail on missing SAN");
4825    }
4826
4827    /// Regression guard for the i == 0 guard in `chain_walk`.
4828    ///
4829    /// int-p256.der has no SAN extension. With `require_subject_alt_name=true`,
4830    /// the check MUST NOT fail on the intermediate (i == 1). Only the leaf
4831    /// (i == 0) is checked.
4832    ///
4833    /// Oracle: openssl x509 -inform DER -in int-p256.der -text -noout | grep -i alt
4834    /// → empty output; int-p256.der has no SAN. Confirmed during fixture generation.
4835    #[test]
4836    fn require_san_only_checks_leaf_not_intermediates() {
4837        let root = load(include_bytes!(
4838            "../tests/fixtures/policy-checks/root-p256.der"
4839        ));
4840        let int_cert = load(include_bytes!(
4841            "../tests/fixtures/policy-checks/int-p256.der"
4842        ));
4843        // The leaf HAS a SAN; the intermediate does NOT.
4844        let leaf = load(include_bytes!(
4845            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4846        ));
4847        let mut policy = ValidationPolicy::new(PC_NOW);
4848        policy.require_subject_alt_name = true;
4849        let anchors = [TrustAnchor::from_cert(root)];
4850        // Must pass: the SAN-less intermediate is not checked, only the leaf.
4851        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4852            .expect("i==0 guard must ensure only the leaf is checked for SAN presence");
4853    }
4854
4855    // -----------------------------------------------------------------------
4856    // required_leaf_eku (PKIX-ken.1.13)
4857    // -----------------------------------------------------------------------
4858
4859    /// Oracle: leaf-p256-365d-san-eku.der has EKU=serverAuth (1.3.6.1.5.5.7.3.1).
4860    #[test]
4861    fn required_eku_passes_when_all_oids_present() {
4862        let root = load(include_bytes!(
4863            "../tests/fixtures/policy-checks/root-p256.der"
4864        ));
4865        let int_cert = load(include_bytes!(
4866            "../tests/fixtures/policy-checks/int-p256.der"
4867        ));
4868        let leaf = load(include_bytes!(
4869            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4870        ));
4871        let mut policy = ValidationPolicy::new(PC_NOW);
4872        policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
4873        let anchors = [TrustAnchor::from_cert(root)];
4874        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4875            .expect("leaf with serverAuth EKU must pass required_leaf_eku=[serverAuth]");
4876    }
4877
4878    /// Oracle: leaf-p256-365d-no-eku.der has no EKU extension.
4879    /// `required_leaf_eku=Some`([serverAuth]) with absent EKU → `MissingEku`.
4880    #[test]
4881    fn required_eku_fails_when_eku_extension_absent() {
4882        let root = load(include_bytes!(
4883            "../tests/fixtures/policy-checks/root-p256.der"
4884        ));
4885        let int_cert = load(include_bytes!(
4886            "../tests/fixtures/policy-checks/int-p256.der"
4887        ));
4888        let leaf = load(include_bytes!(
4889            "../tests/fixtures/policy-checks/leaf-p256-365d-no-eku.der"
4890        ));
4891        let mut policy = ValidationPolicy::new(PC_NOW);
4892        policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
4893        let anchors = [TrustAnchor::from_cert(root)];
4894        assert!(
4895            matches!(
4896                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4897                Err(Error::MissingEku)
4898            ),
4899            "leaf without EKU extension must return MissingEku when an EKU OID is required"
4900        );
4901    }
4902
4903    /// Oracle: leaf-p256-365d-wrong-eku.der has EKU=emailProtection only, not serverAuth.
4904    #[test]
4905    fn required_eku_fails_when_required_oid_not_in_list() {
4906        let root = load(include_bytes!(
4907            "../tests/fixtures/policy-checks/root-p256.der"
4908        ));
4909        let int_cert = load(include_bytes!(
4910            "../tests/fixtures/policy-checks/int-p256.der"
4911        ));
4912        let leaf = load(include_bytes!(
4913            "../tests/fixtures/policy-checks/leaf-p256-365d-wrong-eku.der"
4914        ));
4915        let mut policy = ValidationPolicy::new(PC_NOW);
4916        // Requires serverAuth; leaf only has emailProtection.
4917        policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
4918        let anchors = [TrustAnchor::from_cert(root)];
4919        assert!(
4920            matches!(
4921                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4922                Err(Error::MissingEku)
4923            ),
4924            "leaf with wrong EKU must return MissingEku when required OID is absent"
4925        );
4926    }
4927
4928    /// Oracle: None = no EKU requirement; missing EKU is not an error.
4929    #[test]
4930    fn required_eku_none_is_unconstrained() {
4931        let root = load(include_bytes!(
4932            "../tests/fixtures/policy-checks/root-p256.der"
4933        ));
4934        let int_cert = load(include_bytes!(
4935            "../tests/fixtures/policy-checks/int-p256.der"
4936        ));
4937        let leaf = load(include_bytes!(
4938            "../tests/fixtures/policy-checks/leaf-p256-365d-no-eku.der"
4939        ));
4940        let mut policy = ValidationPolicy::new(PC_NOW);
4941        policy.required_leaf_eku = None; // default
4942        let anchors = [TrustAnchor::from_cert(root)];
4943        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4944            .expect("None required_leaf_eku must accept leaf with no EKU");
4945    }
4946
4947    /// Oracle: Some([]) = require zero OIDs → trivially passes regardless of EKU content.
4948    #[test]
4949    fn required_eku_empty_vec_is_unconstrained() {
4950        let root = load(include_bytes!(
4951            "../tests/fixtures/policy-checks/root-p256.der"
4952        ));
4953        let int_cert = load(include_bytes!(
4954            "../tests/fixtures/policy-checks/int-p256.der"
4955        ));
4956        let leaf = load(include_bytes!(
4957            "../tests/fixtures/policy-checks/leaf-p256-365d-no-eku.der"
4958        ));
4959        let mut policy = ValidationPolicy::new(PC_NOW);
4960        // Empty vec: Some([]) requires zero OIDs → always passes.
4961        policy.required_leaf_eku = Some(vec![]);
4962        let anchors = [TrustAnchor::from_cert(root)];
4963        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
4964            .expect("Some([]) required_leaf_eku (empty) must accept any EKU configuration");
4965    }
4966
4967    /// Verify that emailProtection in `required_leaf_eku` does NOT match serverAuth in the cert.
4968    /// This guards against a hypothetical relaxed OID comparison bug.
4969    #[test]
4970    fn required_eku_emailprotection_does_not_match_serverauth() {
4971        let root = load(include_bytes!(
4972            "../tests/fixtures/policy-checks/root-p256.der"
4973        ));
4974        let int_cert = load(include_bytes!(
4975            "../tests/fixtures/policy-checks/int-p256.der"
4976        ));
4977        let leaf = load(include_bytes!(
4978            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
4979        ));
4980        let mut policy = ValidationPolicy::new(PC_NOW);
4981        // Require emailProtection; leaf only has serverAuth.
4982        policy.required_leaf_eku = Some(vec![ID_KP_EMAIL_PROTECTION]);
4983        let anchors = [TrustAnchor::from_cert(root)];
4984        assert!(
4985            matches!(
4986                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
4987                Err(Error::MissingEku)
4988            ),
4989            "OID comparison must be exact; emailProtection must not match serverAuth"
4990        );
4991    }
4992}
4993
4994// RSA-specific policy field tests — gated on the rsa feature.
4995#[cfg(all(test, feature = "p256", feature = "rsa"))]
4996mod tests_policy_fields_rsa {
4997    use super::*;
4998    use der::Decode;
4999
5000    const PC_NOW: u64 = 1_780_272_000;
5001
5002    // sha256WithRSAEncryption: 1.2.840.113549.1.1.11  (RFC 5912 §2)
5003    const RSA_SHA256_OID: der::asn1::ObjectIdentifier =
5004        der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
5005    // ecdsa-with-SHA256: 1.2.840.10045.4.3.2  (RFC 5912 §6)
5006    const ECDSA_SHA256_OID: der::asn1::ObjectIdentifier =
5007        der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
5008    // id-kp-serverAuth: 1.3.6.1.5.5.7.3.1
5009    const ID_KP_SERVER_AUTH: der::asn1::ObjectIdentifier =
5010        der::asn1::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
5011
5012    fn load(bytes: &[u8]) -> Certificate {
5013        Certificate::from_der(bytes).expect("valid DER fixture")
5014    }
5015
5016    // -----------------------------------------------------------------------
5017    // min_rsa_key_bits helper unit tests (PKIX-ken.1.11)
5018    // -----------------------------------------------------------------------
5019
5020    /// Direct unit test of `rsa_public_key_bits` helper.
5021    /// Oracle: openssl x509 -inform DER -in leaf-rsa2048.der -text -noout | grep 'Public-Key'
5022    /// → Public-Key: (2048 bit)
5023    #[test]
5024    fn rsa_key_bits_correct_for_2048_key() {
5025        let cert = load(include_bytes!(
5026            "../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
5027        ));
5028        let result = rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info);
5029        assert_eq!(
5030            result,
5031            Some(2048),
5032            "RSA-2048 key must return Some(2048) from rsa_public_key_bits"
5033        );
5034    }
5035
5036    /// Direct unit test of `rsa_public_key_bits` helper.
5037    /// Oracle: openssl x509 -inform DER -in leaf-rsa1024.der -text -noout | grep 'Public-Key'
5038    /// → Public-Key: (1024 bit)
5039    #[test]
5040    fn rsa_key_bits_correct_for_1024_key() {
5041        let cert = load(include_bytes!(
5042            "../tests/fixtures/policy-checks/leaf-rsa1024-365d-san-eku.der"
5043        ));
5044        let result = rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info);
5045        assert_eq!(
5046            result,
5047            Some(1024),
5048            "RSA-1024 key must return Some(1024) from rsa_public_key_bits"
5049        );
5050    }
5051
5052    /// Direct unit test of `rsa_public_key_bits` helper.
5053    /// P-256 key is not RSA; must return None.
5054    #[test]
5055    fn rsa_key_bits_none_for_ec_key() {
5056        let cert = load(include_bytes!(
5057            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
5058        ));
5059        let result = rsa_public_key_bits(&cert.tbs_certificate.subject_public_key_info);
5060        assert_eq!(
5061            result, None,
5062            "EC key must return None from rsa_public_key_bits (not RSA)"
5063        );
5064    }
5065
5066    // -----------------------------------------------------------------------
5067    // min_rsa_key_bits validate_path tests (PKIX-ken.1.11)
5068    // -----------------------------------------------------------------------
5069
5070    /// Oracle: leaf-rsa2048-365d-san-eku.der has RSA-2048 leaf.
5071    /// 2048 >= 2048 → passes.
5072    #[test]
5073    fn min_rsa_key_bits_passes_when_key_meets_limit() {
5074        let root = load(include_bytes!(
5075            "../tests/fixtures/policy-checks/root-rsa2048.der"
5076        ));
5077        let int_cert = load(include_bytes!(
5078            "../tests/fixtures/policy-checks/int-rsa2048.der"
5079        ));
5080        let leaf = load(include_bytes!(
5081            "../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
5082        ));
5083        let mut policy = ValidationPolicy::new(PC_NOW);
5084        policy.min_rsa_key_bits = Some(2048);
5085        let anchors = [TrustAnchor::from_cert(root)];
5086        validate_path(
5087            &[leaf, int_cert],
5088            &anchors,
5089            &policy,
5090            &RsaPkcs1v15Sha256Verifier,
5091        )
5092        .expect("RSA-2048 leaf with min=2048 should pass");
5093    }
5094
5095    /// Oracle: leaf-rsa1024-365d-san-eku.der has RSA-1024 leaf.
5096    /// 1024 < 2048 → `KeyTooSmall` { index: 0 }.
5097    #[test]
5098    fn min_rsa_key_bits_fails_when_key_too_small() {
5099        let root = load(include_bytes!(
5100            "../tests/fixtures/policy-checks/root-rsa2048.der"
5101        ));
5102        let int_cert = load(include_bytes!(
5103            "../tests/fixtures/policy-checks/int-rsa2048.der"
5104        ));
5105        let leaf = load(include_bytes!(
5106            "../tests/fixtures/policy-checks/leaf-rsa1024-365d-san-eku.der"
5107        ));
5108        let mut policy = ValidationPolicy::new(PC_NOW);
5109        policy.min_rsa_key_bits = Some(2048);
5110        let anchors = [TrustAnchor::from_cert(root)];
5111        assert!(
5112            matches!(
5113                validate_path(
5114                    &[leaf, int_cert],
5115                    &anchors,
5116                    &policy,
5117                    &RsaPkcs1v15Sha256Verifier
5118                ),
5119                Err(Error::KeyTooSmall { index: 0 })
5120            ),
5121            "RSA-1024 leaf with min=2048 must return KeyTooSmall {{ index: 0 }}"
5122        );
5123    }
5124
5125    /// Oracle: None = unconstrained; RSA-1024 leaf passes with no key size restriction.
5126    #[test]
5127    fn min_rsa_key_bits_none_is_unconstrained() {
5128        let root = load(include_bytes!(
5129            "../tests/fixtures/policy-checks/root-rsa2048.der"
5130        ));
5131        let int_cert = load(include_bytes!(
5132            "../tests/fixtures/policy-checks/int-rsa2048.der"
5133        ));
5134        let leaf = load(include_bytes!(
5135            "../tests/fixtures/policy-checks/leaf-rsa1024-365d-san-eku.der"
5136        ));
5137        let mut policy = ValidationPolicy::new(PC_NOW);
5138        policy.min_rsa_key_bits = None; // default
5139        let anchors = [TrustAnchor::from_cert(root)];
5140        validate_path(
5141            &[leaf, int_cert],
5142            &anchors,
5143            &policy,
5144            &RsaPkcs1v15Sha256Verifier,
5145        )
5146        .expect("None min_rsa_key_bits must accept RSA-1024 leaf");
5147    }
5148
5149    /// EC key must not be affected by `min_rsa_key_bits` regardless of the value.
5150    /// Oracle: P-256 key is not RSA; `rsa_public_key_bits` returns None → check skipped.
5151    #[test]
5152    fn min_rsa_key_bits_ec_key_passes_unconditionally() {
5153        let root = load(include_bytes!(
5154            "../tests/fixtures/policy-checks/root-p256.der"
5155        ));
5156        let int_cert = load(include_bytes!(
5157            "../tests/fixtures/policy-checks/int-p256.der"
5158        ));
5159        let leaf = load(include_bytes!(
5160            "../tests/fixtures/policy-checks/leaf-p256-365d-san-eku.der"
5161        ));
5162        let mut policy = ValidationPolicy::new(PC_NOW);
5163        // Extremely high floor — would reject any RSA key, but P-256 is not RSA.
5164        policy.min_rsa_key_bits = Some(16384);
5165        let anchors = [TrustAnchor::from_cert(root)];
5166        validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier)
5167            .expect("EC key must not be affected by min_rsa_key_bits");
5168    }
5169
5170    // -----------------------------------------------------------------------
5171    // allowed_signature_algs: RSA chain test (PKIX-ken.1.10)
5172    // -----------------------------------------------------------------------
5173
5174    /// Oracle: RSA chain uses sha256WithRSAEncryption; ECDSA-only allowlist must reject it.
5175    #[test]
5176    fn alg_allowlist_fails_on_rsa_chain_when_only_ecdsa_allowed() {
5177        let root = load(include_bytes!(
5178            "../tests/fixtures/policy-checks/root-rsa2048.der"
5179        ));
5180        let int_cert = load(include_bytes!(
5181            "../tests/fixtures/policy-checks/int-rsa2048.der"
5182        ));
5183        let leaf = load(include_bytes!(
5184            "../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
5185        ));
5186        let mut policy = ValidationPolicy::new(PC_NOW);
5187        // Only ECDSA allowed; RSA chain must fail.
5188        policy.allowed_signature_algs = Some(vec![ECDSA_SHA256_OID]);
5189        let anchors = [TrustAnchor::from_cert(root)];
5190        assert!(
5191            matches!(
5192                validate_path(
5193                    &[leaf, int_cert],
5194                    &anchors,
5195                    &policy,
5196                    &RsaPkcs1v15Sha256Verifier
5197                ),
5198                Err(Error::AlgorithmNotAllowed { .. })
5199            ),
5200            "RSA chain with ECDSA-only allowlist must return AlgorithmNotAllowed"
5201        );
5202    }
5203
5204    /// Oracle: RSA chain with RSA in allowlist must pass.
5205    #[test]
5206    fn alg_allowlist_passes_for_rsa_chain() {
5207        let root = load(include_bytes!(
5208            "../tests/fixtures/policy-checks/root-rsa2048.der"
5209        ));
5210        let int_cert = load(include_bytes!(
5211            "../tests/fixtures/policy-checks/int-rsa2048.der"
5212        ));
5213        let leaf = load(include_bytes!(
5214            "../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
5215        ));
5216        let mut policy = ValidationPolicy::new(PC_NOW);
5217        policy.allowed_signature_algs = Some(vec![RSA_SHA256_OID]);
5218        let anchors = [TrustAnchor::from_cert(root)];
5219        validate_path(
5220            &[leaf, int_cert],
5221            &anchors,
5222            &policy,
5223            &RsaPkcs1v15Sha256Verifier,
5224        )
5225        .expect("RSA chain with RSA-SHA256 in allowlist should pass");
5226    }
5227
5228    /// EKU tests for RSA chain are structurally identical to P-256; spot-check one.
5229    ///
5230    /// Oracle: leaf-rsa2048-365d-san-eku.der has EKU=serverAuth.
5231    #[test]
5232    fn required_eku_passes_for_rsa_chain() {
5233        let root = load(include_bytes!(
5234            "../tests/fixtures/policy-checks/root-rsa2048.der"
5235        ));
5236        let int_cert = load(include_bytes!(
5237            "../tests/fixtures/policy-checks/int-rsa2048.der"
5238        ));
5239        let leaf = load(include_bytes!(
5240            "../tests/fixtures/policy-checks/leaf-rsa2048-365d-san-eku.der"
5241        ));
5242        let mut policy = ValidationPolicy::new(PC_NOW);
5243        policy.required_leaf_eku = Some(vec![ID_KP_SERVER_AUTH]);
5244        let anchors = [TrustAnchor::from_cert(root)];
5245        validate_path(
5246            &[leaf, int_cert],
5247            &anchors,
5248            &policy,
5249            &RsaPkcs1v15Sha256Verifier,
5250        )
5251        .expect("RSA leaf with serverAuth EKU must pass required_leaf_eku=[serverAuth]");
5252    }
5253}