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