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