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