Skip to main content

pkix_path/
lib.rs

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