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
6//! RFC 5280 X.509 certificate path validation — pure Rust, `no_std`.
7//!
8//! Implements certificate path building and validation per
9//! [RFC 5280 §6](https://www.rfc-editor.org/rfc/rfc5280#section-6).
10//!
11//! # Architecture
12//!
13//! Cryptographic signature verification is pluggable via [`SignatureVerifier`].
14//! The default feature set (`rustcrypto`) wires in RustCrypto backends for
15//! RSA-PKCS1v15-SHA-256 (`rsa` feature) and ECDSA-P-256-SHA-256 (`p256` feature).
16//! P-384 and Ed25519 are planned for v0.2.
17//! For FIPS-validated crypto, implement [`SignatureVerifier`] against
18//! `wolfcrypt-rustcrypto` and disable the `rustcrypto` feature.
19//!
20//! Revocation checking is handled by `pkix-revocation`. This crate never
21//! touches the network — use `pkix_chain::verify_chain` for the combined API.
22//!
23//! # Limitations
24//!
25//! v0.1 does **not** implement:
26//! - NameConstraints (RFC 5280 §4.2.1.10)
27//! - PolicyConstraints / certificate policy validation (§4.2.1.9, §6.1.5)
28//! - Revocation (use `pkix-revocation`)
29//! - Cross-certificate path building (RFC 4158)
30//!
31//! These are tracked for v0.2+.
32
33use der::Tagged;
34use signature::Error as SignatureError;
35use spki::{AlgorithmIdentifierRef, SubjectPublicKeyInfoRef};
36use x509_cert::Certificate;
37
38/// Errors returned by path validation.
39#[derive(Debug)]
40#[non_exhaustive]
41pub enum Error {
42    /// Certificate signature verification failed at the given chain index.
43    SignatureInvalid {
44        /// Zero-based index into the `chain` slice of the failing certificate.
45        index: usize,
46    },
47    /// A structural encoding error was found in a certificate.
48    ///
49    /// Currently returned when the outer `signatureAlgorithm` field differs from
50    /// the inner `TBSCertificate.signature` field (RFC 5280 §4.1.1.2).
51    MalformedCertificate {
52        /// Zero-based index into the `chain` slice of the malformed certificate.
53        index: usize,
54    },
55    /// Certificate validity period check failed (expired or not yet valid).
56    ValidityPeriod {
57        /// Zero-based index into the `chain` slice of the failing certificate.
58        index: usize,
59    },
60    /// Issuer/subject name linkage is broken at the given chain index.
61    ChainBroken {
62        /// Zero-based index into the `chain` slice where the break was found.
63        index: usize,
64    },
65    /// No path from the subject certificate to any trust anchor was found.
66    NoTrustedPath,
67    /// Path length exceeds [`ValidationPolicy::max_path_len`].
68    PathTooLong,
69    /// An intermediate certificate is missing BasicConstraints cA=TRUE.
70    NotCA {
71        /// Zero-based index into the `chain` slice of the failing certificate.
72        index: usize,
73    },
74    /// An intermediate certificate is missing KeyUsage keyCertSign.
75    KeyUsageMissing {
76        /// Zero-based index into the `chain` slice of the failing certificate.
77        index: usize,
78    },
79    /// A critical extension is present that this implementation does not handle.
80    UnhandledCriticalExtension {
81        /// Zero-based index into the `chain` slice of the failing certificate.
82        index: usize,
83    },
84    /// ASN.1 / DER decoding error.
85    ///
86    /// Returned when DER encoding of a TBS structure fails inside `chain_walk`
87    /// (e.g. the TBS is too large for the internal stack buffer). The inner
88    /// `der::Error` is exposed for diagnostic purposes; callers that want a
89    /// stable match target should check for `Error::Der(_)` without inspecting
90    /// the inner value, as the specific `der::Error` variants are not part of
91    /// the stable API contract.
92    Der(der::Error),
93}
94
95impl core::fmt::Display for Error {
96    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
97        match self {
98            Error::SignatureInvalid { index } => {
99                write!(f, "signature invalid at chain index {index}")
100            }
101            Error::ValidityPeriod { index } => {
102                write!(f, "validity period check failed at chain index {index}")
103            }
104            Error::MalformedCertificate { index } => {
105                write!(f, "malformed certificate at chain index {index}")
106            }
107            Error::ChainBroken { index } => {
108                write!(f, "issuer/subject linkage broken at chain index {index}")
109            }
110            Error::NoTrustedPath => write!(f, "no path to a trusted anchor"),
111            Error::PathTooLong => write!(f, "path length exceeds maximum"),
112            Error::NotCA { index } => write!(f, "certificate at index {index} is not a CA"),
113            Error::KeyUsageMissing { index } => {
114                write!(f, "keyCertSign missing at chain index {index}")
115            }
116            Error::UnhandledCriticalExtension { index } => {
117                write!(f, "unhandled critical extension at chain index {index}")
118            }
119            Error::Der(e) => write!(f, "DER error: {e}"),
120        }
121    }
122}
123
124#[cfg(feature = "std")]
125impl std::error::Error for Error {
126    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127        match self {
128            Error::Der(e) => Some(e),
129            Error::SignatureInvalid { .. }
130            | Error::MalformedCertificate { .. }
131            | Error::ValidityPeriod { .. }
132            | Error::ChainBroken { .. }
133            | Error::NoTrustedPath
134            | Error::PathTooLong
135            | Error::NotCA { .. }
136            | Error::KeyUsageMissing { .. }
137            | Error::UnhandledCriticalExtension { .. } => None,
138        }
139    }
140}
141
142impl From<der::Error> for Error {
143    fn from(e: der::Error) -> Self {
144        Error::Der(e)
145    }
146}
147
148/// Result alias for this crate.
149pub type Result<T> = core::result::Result<T, Error>;
150
151/// Pluggable signature verification backend.
152///
153/// Implement this trait to provide algorithm-specific signature verification.
154/// The trait is OID-dispatched: the `algorithm` argument carries the OID and
155/// any parameters from the certificate's `signatureAlgorithm` field.
156///
157/// # Implementing a custom backend
158///
159/// ```rust,ignore
160/// struct MyVerifier;
161///
162/// impl pkix_path::SignatureVerifier for MyVerifier {
163///     fn verify_signature(
164///         &self,
165///         algorithm: spki::AlgorithmIdentifierRef<'_>,
166///         issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
167///         message: &[u8],
168///         signature: &[u8],
169///     ) -> core::result::Result<(), signature::Error> {
170///         match algorithm.oid {
171///             MY_RSA_OID => { /* ... */ }
172///             MY_ECDSA_OID => { /* ... */ }
173///             _ => Err(signature::Error::new()),
174///         }
175///     }
176/// }
177/// ```
178pub trait SignatureVerifier {
179    /// Verify `signature` over `message`.
180    ///
181    /// - `algorithm`    — from the subject cert's `signatureAlgorithm` field
182    /// - `issuer_spki`  — SPKI extracted from the issuer or trust anchor cert
183    /// - `message`      — DER-encoded TBSCertificate (the bytes that were signed)
184    /// - `signature`    — raw signature bytes (BitString content, not the wrapper)
185    ///
186    /// Returns `Ok(())` on success or `Err(signature::Error)` on failure.
187    /// The caller ([`validate_path`]) maps the error to [`Error::SignatureInvalid`]
188    /// with the correct chain index — the verifier does not need to know it.
189    fn verify_signature(
190        &self,
191        algorithm: AlgorithmIdentifierRef<'_>,
192        issuer_spki: SubjectPublicKeyInfoRef<'_>,
193        message: &[u8],
194        signature: &[u8],
195    ) -> core::result::Result<(), SignatureError>;
196}
197
198/// A trust anchor used to terminate path validation.
199///
200/// A trust anchor is typically either a self-signed root CA certificate
201/// or a raw (name, SPKI) pair extracted from a platform trust store.
202/// The trust anchor itself is **not** signature-verified — it is trusted
203/// by definition.
204#[derive(Clone, Debug)]
205pub struct TrustAnchor {
206    /// The subject distinguished name of the trust anchor.
207    pub subject: x509_cert::name::Name,
208    /// The subject public key info of the trust anchor.
209    ///
210    /// Must be a valid SPKI for the chosen signature algorithm. An empty or
211    /// malformed SPKI will cause signature verification to fail with
212    /// `Error::NoTrustedPath` (no anchor matched), not a panic.
213    pub subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
214}
215
216impl TrustAnchor {
217    /// Create a trust anchor from raw subject name and SPKI.
218    pub fn new(
219        subject: x509_cert::name::Name,
220        subject_public_key_info: spki::SubjectPublicKeyInfoOwned,
221    ) -> Self {
222        Self {
223            subject,
224            subject_public_key_info,
225        }
226    }
227
228    /// Extract subject name and SPKI from a certificate to create a trust anchor.
229    ///
230    /// This is the typical constructor when your trust store contains full
231    /// self-signed root CA certificates.
232    pub fn from_cert(cert: Certificate) -> Self {
233        Self {
234            subject: cert.tbs_certificate.subject,
235            subject_public_key_info: cert.tbs_certificate.subject_public_key_info,
236        }
237    }
238}
239
240/// Policy parameters controlling path validation.
241///
242/// # Limitations
243///
244/// v0.1 does not enforce NameConstraints, CertificatePolicies, or
245/// PolicyMappings. Fields for these will be added in v0.2.
246#[derive(Clone, Debug)]
247pub struct ValidationPolicy {
248    /// Maximum chain depth, not counting the trust anchor. Default: 10.
249    ///
250    /// A chain of `[leaf]` is depth 0. `[leaf, intermediate, root]` is depth 1
251    /// (one intermediate). Validation fails if depth exceeds this value.
252    pub max_path_len: u8,
253
254    /// Current time as seconds since the Unix epoch (1970-01-01T00:00:00Z).
255    ///
256    /// Used to check `notBefore` ≤ `now` ≤ `notAfter` on every certificate.
257    /// **Must be set by the caller** — there is no platform clock in `no_std`.
258    ///
259    /// **Warning — the default is 0 (1970-01-01):** Any certificate issued
260    /// after 1970 has `notBefore > 0` and will fail the validity check with
261    /// [`Error::ValidityPeriod`]. If you see unexpected `ValidityPeriod`
262    /// errors, check that `current_time_unix` is set to the current time.
263    ///
264    /// **Warning**: passing `u64::MAX` causes all `notAfter` checks to pass.
265    /// This effectively disables expiry checking — only use it in contexts
266    /// where you explicitly want permissive (clock-free) validation.
267    pub current_time_unix: u64,
268
269    /// Enforce the KeyUsage extension when present. Default: `true`.
270    ///
271    /// When `true`, an intermediate certificate missing `keyCertSign` in its
272    /// KeyUsage will be rejected even if BasicConstraints cA=TRUE.
273    pub enforce_key_usage: bool,
274}
275
276impl ValidationPolicy {
277    /// Construct a policy with the given time and sensible defaults.
278    ///
279    /// Equivalent to `ValidationPolicy { current_time_unix: now_unix, ..Default::default() }`.
280    /// This is the preferred constructor: it forces the caller to supply a timestamp,
281    /// preventing the silent validity failures caused by `Default`'s `current_time_unix = 0`.
282    pub fn new(now_unix: u64) -> Self {
283        Self {
284            current_time_unix: now_unix,
285            ..Default::default()
286        }
287    }
288}
289
290impl Default for ValidationPolicy {
291    fn default() -> Self {
292        Self {
293            max_path_len: 10,
294            current_time_unix: 0, // caller must set to avoid silent clock skew
295            enforce_key_usage: true,
296        }
297    }
298}
299
300/// The result of a successful certificate path validation.
301///
302/// Fields are `pub` for direct read access. `#[non_exhaustive]` prevents external
303/// code from constructing `ValidatedPath` directly and from pattern-matching
304/// exhaustively, preserving the ability to add fields in future minor versions
305/// without a breaking change.
306#[derive(Clone, Debug, PartialEq, Eq)]
307#[non_exhaustive]
308pub struct ValidatedPath {
309    /// Index into the `anchors` slice of the trust anchor that terminated the path.
310    pub anchor_index: usize,
311    /// Depth of the validated chain (number of intermediates, excluding trust anchor).
312    pub depth: usize,
313}
314
315/// Validate a certificate chain from subject to a trust anchor.
316///
317/// `chain` must be ordered leaf-first:
318/// - `chain[0]` is the subject (end-entity) certificate
319/// - `chain[1..]` are intermediates in issuer order
320/// - The last element of `chain` must be issued by one of `anchors`
321///
322/// Validation follows RFC 5280 §6.1. Each certificate's signature is verified
323/// using `verifier`, with the signing key taken from the next certificate in
324/// the chain (or the matching trust anchor for the last cert).
325///
326/// # Errors
327///
328/// Returns `Err` on the first RFC 5280 §6.1 check failure. The error variant
329/// includes the chain index of the failing certificate where applicable.
330///
331/// # Limitations
332///
333/// See crate-level documentation for v0.1 scope limits.
334///
335/// Duplicate certificates in `chain` (same cert appearing at two indices) are
336/// not detected. They will fail signature verification or name linkage with a
337/// `SignatureInvalid` or `ChainBroken` error rather than a dedicated diagnostic.
338pub fn validate_path<V>(
339    chain: &[Certificate],
340    anchors: &[TrustAnchor],
341    policy: &ValidationPolicy,
342    verifier: &V,
343) -> Result<ValidatedPath>
344where
345    V: SignatureVerifier,
346{
347    // (1) Input guards: reject empty chain or anchors, check OID consistency.
348    check_inputs(chain, anchors)?;
349    check_oid_consistency(chain)?;
350
351    // (2) Path-length check (anchor-independent).
352    let num_intermediates = chain.len().saturating_sub(1);
353    if num_intermediates > policy.max_path_len as usize {
354        return Err(Error::PathTooLong);
355    }
356
357    // (3) Try each name-matching anchor. Iterating all candidates handles key
358    //     rollover: multiple anchors may share a DN but have different keys
359    //     (e.g., during a root CA rotation). The first anchor that passes the
360    //     full chain walk is used; the last error is returned if none succeed.
361    //
362    //     Complexity: O(A × N) where A = number of anchors, N = chain length.
363    //     For the common case of O(1) matching anchors this is effectively O(N).
364    let last_cert = chain.last().ok_or(Error::NoTrustedPath)?;
365    let is_self_issued = names_match(
366        &last_cert.tbs_certificate.issuer,
367        &last_cert.tbs_certificate.subject,
368    );
369    let mut last_err = Error::NoTrustedPath;
370    for (anchor_index, anchor) in anchors.iter().enumerate() {
371        if !names_match(&anchor.subject, &last_cert.tbs_certificate.issuer) {
372            continue;
373        }
374        // For self-issued certs the cert and anchor are the same entity; their
375        // SPKIs must match (RFC 5280 §3.2 name-collision guard).
376        if is_self_issued
377            && anchor.subject_public_key_info != last_cert.tbs_certificate.subject_public_key_info
378        {
379            continue;
380        }
381        match chain_walk(chain, anchor, policy, verifier) {
382            Ok(()) => {
383                return Ok(ValidatedPath {
384                    anchor_index,
385                    depth: chain.len().saturating_sub(1),
386                });
387            }
388            Err(e) => last_err = e,
389        }
390    }
391    Err(last_err)
392}
393
394// ---------------------------------------------------------------------------
395// validate_path helpers — input guards and OID consistency (PKIX-6vu)
396// ---------------------------------------------------------------------------
397
398fn check_inputs(chain: &[Certificate], anchors: &[TrustAnchor]) -> Result<()> {
399    if chain.is_empty() || anchors.is_empty() {
400        return Err(Error::NoTrustedPath);
401    }
402    Ok(())
403}
404
405/// RFC 5280 §4.1.1.2: outer signatureAlgorithm must equal inner TBSCertificate.signature.
406fn check_oid_consistency(chain: &[Certificate]) -> Result<()> {
407    for (index, cert) in chain.iter().enumerate() {
408        if cert.signature_algorithm != cert.tbs_certificate.signature {
409            return Err(Error::MalformedCertificate { index });
410        }
411    }
412    Ok(())
413}
414
415// ---------------------------------------------------------------------------
416// Critical extension guard (PKIX-ad6)
417// ---------------------------------------------------------------------------
418
419const OID_KEY_USAGE: der::asn1::ObjectIdentifier =
420    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.15");
421
422const OID_BASIC_CONSTRAINTS: der::asn1::ObjectIdentifier =
423    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.19");
424
425const OID_SUBJECT_ALT_NAME: der::asn1::ObjectIdentifier =
426    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.17");
427
428/// OIDs of extensions that this implementation handles; all others, if critical, cause rejection.
429///
430/// `OID_SUBJECT_ALT_NAME` is listed here so that certs with critical SAN extensions
431/// (e.g. TLS server certs) do not fail with `UnhandledCriticalExtension`. However,
432/// the SAN *value* is not inspected by path validation — name matching still uses the
433/// Subject DN. **v0.1 limitation**: a cert with an empty Subject and critical SAN
434/// will pass this check but fail name linkage since `names_match` compares against
435/// the empty Subject. This is tracked for v0.2 (RFC 5280 §4.2.1.6).
436const HANDLED_CRITICAL_OIDS: &[der::asn1::ObjectIdentifier] =
437    &[OID_KEY_USAGE, OID_BASIC_CONSTRAINTS, OID_SUBJECT_ALT_NAME];
438
439/// RFC 5280 §6.1.3(a)(3): reject any critical extension not in the handled set.
440fn check_critical_extensions(cert: &Certificate, index: usize) -> Result<()> {
441    if let Some(exts) = cert.tbs_certificate.extensions.as_ref() {
442        for ext in exts.iter() {
443            if ext.critical && !HANDLED_CRITICAL_OIDS.contains(&ext.extn_id) {
444                return Err(Error::UnhandledCriticalExtension { index });
445            }
446        }
447    }
448    Ok(())
449}
450
451// ---------------------------------------------------------------------------
452// KeyUsage extraction (PKIX-8ae)
453// ---------------------------------------------------------------------------
454
455/// Returns whether the `keyCertSign` bit is set in the KeyUsage extension.
456///
457/// - `None`         — KeyUsage extension absent (no constraint)
458/// - `Some(true)`   — keyCertSign is set
459/// - `Some(false)`  — KeyUsage present, keyCertSign NOT set
460fn has_key_cert_sign(cert: &Certificate) -> Option<bool> {
461    use der::Decode;
462    use x509_cert::ext::pkix::KeyUsage;
463
464    let exts = cert.tbs_certificate.extensions.as_ref()?;
465    for ext in exts.iter() {
466        if ext.extn_id == OID_KEY_USAGE {
467            let ku = KeyUsage::from_der(ext.extn_value.as_bytes()).ok()?;
468            return Some(ku.key_cert_sign());
469        }
470    }
471    None
472}
473
474// ---------------------------------------------------------------------------
475// BasicConstraints extraction (PKIX-0q5)
476// ---------------------------------------------------------------------------
477
478/// Decode the `BasicConstraints` extension from a certificate, if present.
479///
480/// Returns `None` if the extension is absent; decoding errors are silently
481/// treated as absent (the caller will then fail the cA=TRUE check).
482fn cert_basic_constraints(cert: &Certificate) -> Option<x509_cert::ext::pkix::BasicConstraints> {
483    use der::Decode;
484    use x509_cert::ext::pkix::BasicConstraints;
485
486    let exts = cert.tbs_certificate.extensions.as_ref()?;
487    for ext in exts.iter() {
488        if ext.extn_id == OID_BASIC_CONSTRAINTS {
489            return BasicConstraints::from_der(ext.extn_value.as_bytes()).ok();
490        }
491    }
492    None
493}
494
495// ---------------------------------------------------------------------------
496// Validity period checker (PKIX-047)
497// ---------------------------------------------------------------------------
498
499/// Convert an `x509_cert::time::Time` to seconds since the Unix epoch.
500fn time_to_unix_secs(t: &x509_cert::time::Time) -> u64 {
501    t.to_unix_duration().as_secs()
502}
503
504/// RFC 5280 §6.1.3(a)(2): check notBefore ≤ now ≤ notAfter.
505fn check_validity(cert: &Certificate, now_unix: u64, index: usize) -> Result<()> {
506    let not_before = time_to_unix_secs(&cert.tbs_certificate.validity.not_before);
507    let not_after = time_to_unix_secs(&cert.tbs_certificate.validity.not_after);
508    if now_unix >= not_before && now_unix <= not_after {
509        Ok(())
510    } else {
511        Err(Error::ValidityPeriod { index })
512    }
513}
514
515// ---------------------------------------------------------------------------
516// Name comparison — RFC 4518 string prep (PKIX-drv)
517// ---------------------------------------------------------------------------
518
519/// Compare two distinguished names per RFC 4518 string prep rules.
520///
521/// For v0.1: implements case-fold and whitespace normalization for ASCII
522/// characters. Full Unicode NFKD normalization is deferred to v0.2.
523///
524/// Returns `true` if the names are equivalent.
525///
526/// # Ordering
527///
528/// RFC 5280 §4.1.2.4 defines `Name` as `SEQUENCE OF RDN`, so RDNs are
529/// compared positionally (index 0 with index 0, etc.). Within each RDN —
530/// which is a `SET OF AttributeTypeAndValue` — comparison is order-independent:
531/// each AVA in one RDN is matched against any AVA in the other.
532pub fn names_match(a: &x509_cert::name::Name, b: &x509_cert::name::Name) -> bool {
533    let a_rdns = a.0.as_slice();
534    let b_rdns = b.0.as_slice();
535
536    if a_rdns.len() != b_rdns.len() {
537        return false;
538    }
539
540    for (a_rdn, b_rdn) in a_rdns.iter().zip(b_rdns.iter()) {
541        let a_avas = a_rdn.0.as_slice();
542        let b_avas = b_rdn.0.as_slice();
543        if a_avas.len() != b_avas.len() {
544            return false;
545        }
546        // For each AVA in a_rdn, find matching AVA in b_rdn (same OID, equal normalized value).
547        for a_ava in a_avas.iter() {
548            let found = b_avas.iter().any(|b_ava| {
549                b_ava.oid == a_ava.oid && ava_values_match(&a_ava.value, &b_ava.value)
550            });
551            if !found {
552                return false;
553            }
554        }
555    }
556    true
557}
558
559/// Compare two AttributeTypeAndValue values after RFC 4518 normalization.
560fn ava_values_match(a: &der::Any, b: &der::Any) -> bool {
561    let a_str = any_to_str_bytes(a);
562    let b_str = any_to_str_bytes(b);
563
564    match (a_str, b_str) {
565        (Some(a_bytes), Some(b_bytes)) => normalized_eq(a_bytes, b_bytes),
566        // Fall back to raw DER byte comparison if we can't decode as a string type.
567        (None, None) => a.value() == b.value(),
568        _ => false,
569    }
570}
571
572/// Extract the string content bytes from a DirectoryString Any value.
573/// Returns None if the tag is not a string type we handle.
574///
575/// **v0.1 limitation**: `TeletexString` (T61String) and `BMPString` (used in
576/// some legacy CA certificates) are not handled here and fall back to raw DER
577/// byte comparison in `ava_values_match`. Name matching against these string
578/// types may fail even when the names are semantically equivalent. Tracked
579/// for v0.2 (RFC 5280 §7.1 / RFC 4518 §2.6 legacy encoding support).
580fn any_to_str_bytes(a: &der::Any) -> Option<&[u8]> {
581    use der::Tag;
582    match a.tag() {
583        Tag::Utf8String | Tag::PrintableString | Tag::Ia5String | Tag::VisibleString => {
584            Some(a.value())
585        }
586        _ => None,
587    }
588}
589
590/// Compare two ASCII byte slices after RFC 4518 whitespace normalization and case-folding.
591///
592/// Rules applied:
593/// 1. ASCII letters: case-fold to lowercase
594/// 2. Leading/trailing spaces: ignored
595/// 3. Internal multiple spaces: collapsed to single space
596fn normalized_eq(a: &[u8], b: &[u8]) -> bool {
597    NormalizedIter::new(a).eq(NormalizedIter::new(b))
598}
599
600/// Iterator that yields bytes after ASCII case-fold and whitespace normalization.
601struct NormalizedIter<'a> {
602    bytes: &'a [u8],
603    pos: usize,
604    pending_space: bool,
605}
606
607impl<'a> NormalizedIter<'a> {
608    fn new(bytes: &'a [u8]) -> Self {
609        // Skip leading spaces.
610        let start = bytes.iter().position(|&b| b != b' ').unwrap_or(bytes.len());
611        // Find end (skip trailing spaces).
612        let end = bytes[start..]
613            .iter()
614            .rposition(|&b| b != b' ')
615            .map(|i| start + i + 1)
616            .unwrap_or(start);
617        Self {
618            bytes: &bytes[start..end],
619            pos: 0,
620            pending_space: false,
621        }
622    }
623}
624
625impl<'a> Iterator for NormalizedIter<'a> {
626    type Item = u8;
627    fn next(&mut self) -> Option<u8> {
628        // A space was already emitted on the previous call; skip any additional
629        // consecutive spaces now without emitting another space character.
630        if self.pending_space {
631            self.pending_space = false;
632            while self.pos < self.bytes.len() && self.bytes[self.pos] == b' ' {
633                self.pos += 1;
634            }
635            // Fall through: process the next non-space byte (or return None if at end).
636        }
637        if self.pos >= self.bytes.len() {
638            return None;
639        }
640        let b = self.bytes[self.pos];
641        self.pos += 1;
642        if b == b' ' {
643            // Emit one space; next call will skip any further consecutive spaces.
644            self.pending_space = true;
645            Some(b' ')
646        } else {
647            Some(b.to_ascii_lowercase())
648        }
649    }
650}
651
652// ---------------------------------------------------------------------------
653// ECDSA P-256 SHA-256 backend (PKIX-evy)
654// ---------------------------------------------------------------------------
655
656/// ECDSA P-256 with SHA-256 signature verifier.
657///
658/// Handles OID `ecdsa-with-SHA256` (1.2.840.10045.4.3.2).
659/// Feature-gated behind `p256`.
660#[cfg(feature = "p256")]
661pub struct EcdsaP256Verifier;
662
663#[cfg(feature = "p256")]
664impl SignatureVerifier for EcdsaP256Verifier {
665    fn verify_signature(
666        &self,
667        algorithm: spki::AlgorithmIdentifierRef<'_>,
668        issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
669        message: &[u8],
670        signature: &[u8],
671    ) -> core::result::Result<(), SignatureError> {
672        // Reject any OID other than ecdsa-with-SHA256.
673        const OID: der::asn1::ObjectIdentifier =
674            der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
675        if algorithm.oid != OID {
676            return Err(SignatureError::new());
677        }
678
679        use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
680
681        let vk = VerifyingKey::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
682
683        let sig = DerSignature::try_from(signature).map_err(|_| SignatureError::new())?;
684
685        vk.verify(message, &sig).map_err(|_| SignatureError::new())
686    }
687}
688
689// ---------------------------------------------------------------------------
690// RSA PKCS#1 v1.5 SHA-256 backend (PKIX-gmv)
691// ---------------------------------------------------------------------------
692
693/// RSA with PKCS#1 v1.5 padding and SHA-256 signature verifier.
694///
695/// Handles OID `sha256WithRSAEncryption` (1.2.840.113549.1.1.11).
696/// Feature-gated behind `rsa`.
697#[cfg(feature = "rsa")]
698pub struct RsaPkcs1v15Sha256Verifier;
699
700#[cfg(feature = "rsa")]
701impl SignatureVerifier for RsaPkcs1v15Sha256Verifier {
702    fn verify_signature(
703        &self,
704        algorithm: spki::AlgorithmIdentifierRef<'_>,
705        issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
706        message: &[u8],
707        signature: &[u8],
708    ) -> core::result::Result<(), SignatureError> {
709        // Reject any OID other than sha256WithRSAEncryption.
710        const OID: der::asn1::ObjectIdentifier =
711            der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
712        if algorithm.oid != OID {
713            return Err(SignatureError::new());
714        }
715
716        use rsa::pkcs1v15::{Signature, VerifyingKey};
717        use rsa::signature::Verifier as _;
718        use sha2::Sha256;
719
720        let vk =
721            VerifyingKey::<Sha256>::try_from(issuer_spki).map_err(|_| SignatureError::new())?;
722
723        let sig = Signature::try_from(signature).map_err(|_| SignatureError::new())?;
724
725        vk.verify(message, &sig).map_err(|_| SignatureError::new())
726    }
727}
728
729// ---------------------------------------------------------------------------
730// Chain walk loop — signature verification and name linkage (PKIX-vxf)
731// ---------------------------------------------------------------------------
732
733/// Walk the chain from issuer to leaf, applying all RFC 5280 §6.1 per-cert checks.
734///
735/// Path-length and anchor-matching are handled by the caller (`validate_path`).
736/// This function walks `chain` in reverse (issuer-to-leaf) against `anchor`:
737///
738///    a. Verify signature with the current issuer's SPKI.
739///    b. Verify issuer/subject name linkage.
740///    c. Check validity period against `policy.current_time_unix`.
741///    d. Reject any unhandled critical extensions.
742///    e. For all certs except the leaf (i > 0): require `BasicConstraints` cA=TRUE.
743///    f. For all certs except the leaf (i > 0): if `policy.enforce_key_usage`, require `keyCertSign`.
744///    g. For all certs except the leaf (i > 0): enforce `pathLenConstraint` if present.
745///
746/// RFC 5280 §4.2.1.9 note on pathLenConstraint: for the cert at position `i`
747/// (leaf at 0, root-adjacent at chain.len()-1), there are exactly `i-1`
748/// intermediate certs below it. The constraint requires `i-1 ≤ pathLenConstraint`.
749fn chain_walk<V: SignatureVerifier>(
750    chain: &[Certificate],
751    anchor: &TrustAnchor,
752    policy: &ValidationPolicy,
753    verifier: &V,
754) -> Result<()> {
755    use der::Encode;
756    use spki::der::referenced::OwnedToRef as _;
757
758    let mut working_spki = &anchor.subject_public_key_info;
759    let mut working_issuer_name = &anchor.subject;
760
761    for i in (0..chain.len()).rev() {
762        let cert = &chain[i];
763
764        // (a) Verify signature with the current issuer's SPKI.
765        //     8 KiB covers every well-formed certificate encountered in practice
766        //     (typical TLS certs are 1–3 KiB). Certificates exceeding this limit
767        //     return Error::Der; tracked for v0.2 with heap-backed encoding.
768        let mut tbs_buf = [0u8; 8192];
769        let tbs_bytes = cert
770            .tbs_certificate
771            .encode_to_slice(&mut tbs_buf)
772            .map_err(Error::Der)?;
773        verifier
774            .verify_signature(
775                cert.signature_algorithm.owned_to_ref(),
776                working_spki.owned_to_ref(),
777                tbs_bytes,
778                cert.signature.raw_bytes(),
779            )
780            .map_err(|_| Error::SignatureInvalid { index: i })?;
781
782        // (b) Issuer/subject name linkage.
783        if !names_match(working_issuer_name, &cert.tbs_certificate.issuer) {
784            return Err(Error::ChainBroken { index: i });
785        }
786
787        // (c) Validity period.
788        check_validity(cert, policy.current_time_unix, i)?;
789
790        // (d) Critical extension guard.
791        check_critical_extensions(cert, i)?;
792
793        // (e–g) CA-only checks: apply to every cert except the leaf (chain[0]).
794        //        This includes any intermediate CAs and the root CA cert if it
795        //        is included in the chain rather than supplied only as an anchor.
796        if i > 0 {
797            // (e) BasicConstraints cA=TRUE required; (g) pathLenConstraint.
798            // Decode BasicConstraints once for both checks.
799            let bc = cert_basic_constraints(cert);
800            if bc.as_ref().map(|b| b.ca) != Some(true) {
801                return Err(Error::NotCA { index: i });
802            }
803
804            // (f) KeyUsage keyCertSign required (when policy demands it).
805            if policy.enforce_key_usage {
806                match has_key_cert_sign(cert) {
807                    Some(true) => {}
808                    _ => return Err(Error::KeyUsageMissing { index: i }),
809                }
810            }
811
812            // (g) pathLenConstraint: the cert at position i has i-1 intermediates
813            // below it in the chain. Enforce the constraint.
814            if let Some(path_len) = bc.and_then(|b| b.path_len_constraint) {
815                if (i - 1) > path_len as usize {
816                    return Err(Error::PathTooLong);
817                }
818            }
819        }
820
821        // Update state for next iteration.
822        working_spki = &cert.tbs_certificate.subject_public_key_info;
823        working_issuer_name = &cert.tbs_certificate.subject;
824    }
825
826    Ok(())
827}
828
829// ---------------------------------------------------------------------------
830// DefaultVerifier — OID-dispatching RustCrypto backend (PKIX-8wg)
831// ---------------------------------------------------------------------------
832
833/// A [`SignatureVerifier`] that dispatches to available RustCrypto backends by OID.
834///
835/// This is the recommended out-of-the-box verifier for applications that use
836/// the default RustCrypto feature set. It supports:
837///
838/// - `ecdsa-with-SHA256` (1.2.840.10045.4.3.2) — via the `p256` feature
839/// - `sha256WithRSAEncryption` (1.2.840.113549.1.1.11) — via the `rsa` feature
840///
841/// Any OID not in the above set returns `Err(signature::Error::new())`.
842///
843/// To support additional algorithms, implement [`SignatureVerifier`] directly
844/// and dispatch your own OID table.
845#[cfg(any(feature = "p256", feature = "rsa"))]
846pub struct DefaultVerifier;
847
848#[cfg(any(feature = "p256", feature = "rsa"))]
849impl SignatureVerifier for DefaultVerifier {
850    fn verify_signature(
851        &self,
852        algorithm: AlgorithmIdentifierRef<'_>,
853        issuer_spki: SubjectPublicKeyInfoRef<'_>,
854        message: &[u8],
855        signature: &[u8],
856    ) -> core::result::Result<(), SignatureError> {
857        let oid = algorithm.oid;
858        #[cfg(feature = "p256")]
859        if oid == OID_ECDSA_P256_SHA256 {
860            return EcdsaP256Verifier.verify_signature(algorithm, issuer_spki, message, signature);
861        }
862        #[cfg(feature = "rsa")]
863        if oid == OID_SHA256_WITH_RSA {
864            return RsaPkcs1v15Sha256Verifier.verify_signature(
865                algorithm,
866                issuer_spki,
867                message,
868                signature,
869            );
870        }
871        Err(SignatureError::new())
872    }
873}
874
875/// OID for `ecdsa-with-SHA256` — used by `DefaultVerifier` dispatch.
876#[cfg(any(feature = "p256", feature = "rsa"))]
877const OID_ECDSA_P256_SHA256: der::asn1::ObjectIdentifier =
878    der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
879
880/// OID for `sha256WithRSAEncryption` — used by `DefaultVerifier` dispatch.
881#[cfg(any(feature = "p256", feature = "rsa"))]
882const OID_SHA256_WITH_RSA: der::asn1::ObjectIdentifier =
883    der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
884
885// ---------------------------------------------------------------------------
886// Tests
887// ---------------------------------------------------------------------------
888
889#[cfg(all(test, feature = "p256"))]
890mod tests_ecdsa_p256 {
891    use super::*;
892    use der::Decode;
893
894    /// Test vector: a real P-256/SHA-256 self-signed cert generated by OpenSSL.
895    /// Oracle: `openssl verify -CAfile ec.pem ec.pem` returns OK.
896    #[test]
897    fn verify_p256_self_signed() {
898        let der = include_bytes!("../tests/fixtures/ec-p256-sha256.der");
899        let cert = Certificate::from_der(der).expect("parse cert");
900
901        use der::Encode as _;
902        let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
903        let sig_bytes = cert.signature.raw_bytes();
904
905        // Self-signed cert: signer SPKI is the cert's own SPKI.
906        use spki::der::referenced::OwnedToRef as _;
907        let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
908
909        let verifier = EcdsaP256Verifier;
910        assert!(
911            verifier
912                .verify_signature(
913                    cert.signature_algorithm.owned_to_ref(),
914                    spki_ref,
915                    &tbs_der,
916                    sig_bytes,
917                )
918                .is_ok(),
919            "self-signed P-256 cert should verify"
920        );
921    }
922}
923
924#[cfg(all(test, feature = "rsa"))]
925mod tests_rsa {
926    use super::*;
927    use der::Decode;
928
929    /// Test vector: a real RSA-2048/SHA-256 self-signed cert generated by OpenSSL.
930    /// Oracle: `openssl verify -CAfile rsa.pem rsa.pem` returns OK.
931    #[test]
932    fn verify_rsa_pkcs1v15_sha256_self_signed() {
933        let der = include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der");
934        let cert = Certificate::from_der(der).expect("parse cert");
935
936        use der::Encode as _;
937        let tbs_der = cert.tbs_certificate.to_der().expect("encode tbs");
938        let sig_bytes = cert.signature.raw_bytes();
939
940        // Self-signed cert: signer SPKI is the cert's own SPKI.
941        use spki::der::referenced::OwnedToRef as _;
942        let spki_ref = cert.tbs_certificate.subject_public_key_info.owned_to_ref();
943
944        let verifier = RsaPkcs1v15Sha256Verifier;
945        assert!(
946            verifier
947                .verify_signature(
948                    cert.signature_algorithm.owned_to_ref(),
949                    spki_ref,
950                    &tbs_der,
951                    sig_bytes,
952                )
953                .is_ok(),
954            "self-signed RSA cert should verify"
955        );
956    }
957}
958
959// ---------------------------------------------------------------------------
960// NormalizedIter / names_match unit tests
961// ---------------------------------------------------------------------------
962#[cfg(test)]
963mod tests_normalized_iter {
964    use super::{normalized_eq, NormalizedIter};
965
966    /// Identical ASCII strings must compare equal.
967    #[test]
968    fn identical_strings_equal() {
969        assert!(normalized_eq(b"hello", b"hello"));
970    }
971
972    /// Case is folded to lowercase.
973    #[test]
974    fn case_folding() {
975        assert!(normalized_eq(b"Hello", b"hello"));
976        assert!(normalized_eq(b"HELLO WORLD", b"hello world"));
977    }
978
979    /// Leading spaces are stripped.
980    #[test]
981    fn leading_spaces_stripped() {
982        assert!(normalized_eq(b"  hello", b"hello"));
983    }
984
985    /// Trailing spaces are stripped.
986    ///
987    /// Regression test: NormalizedIter must not emit a trailing space for
988    /// input that ends with a space sequence.
989    #[test]
990    fn trailing_spaces_stripped() {
991        assert!(normalized_eq(b"hello  ", b"hello"));
992        assert!(normalized_eq(b"hello ", b"hello"));
993    }
994
995    /// Multiple consecutive internal spaces are collapsed to a single space.
996    ///
997    /// Regression test for the double-space bug: `pending_space` must not
998    /// cause two spaces to be emitted for a single space in the input.
999    #[test]
1000    fn internal_spaces_collapsed() {
1001        assert!(normalized_eq(b"hello  world", b"hello world"));
1002        assert!(normalized_eq(b"hello   world", b"hello world"));
1003    }
1004
1005    /// Combined: leading + trailing + internal spaces, case folding.
1006    #[test]
1007    fn combined_normalization() {
1008        assert!(normalized_eq(b"  Hello   World  ", b"hello world"));
1009    }
1010
1011    /// Empty string and all-spaces string must both yield zero bytes.
1012    #[test]
1013    fn empty_and_whitespace_only() {
1014        assert!(normalized_eq(b"", b""));
1015        assert!(normalized_eq(b"   ", b""));
1016        assert!(normalized_eq(b"   ", b"   "));
1017    }
1018
1019    /// Different strings must NOT compare equal after normalization.
1020    #[test]
1021    fn different_strings_not_equal() {
1022        assert!(!normalized_eq(b"hello", b"world"));
1023        assert!(!normalized_eq(b"ab", b"abc"));
1024    }
1025
1026    /// NormalizedIter: input ending with an internal space sequence followed by
1027    /// trailing spaces must emit the space and then stop (no double space, no
1028    /// trailing space).
1029    #[test]
1030    fn internal_then_trailing_space_no_trailing_emit() {
1031        // "ab  " → normalized → "ab" (one word, no trailing space)
1032        let collected: Vec<u8> = NormalizedIter::new(b"ab  ").collect();
1033        assert_eq!(collected, b"ab");
1034
1035        // "ab  cd  " → normalized → "ab cd" (one internal space, no trailing space)
1036        let collected: Vec<u8> = NormalizedIter::new(b"ab  cd  ").collect();
1037        assert_eq!(collected, b"ab cd");
1038    }
1039}
1040
1041// PKIX-h6z: validate_path public API tests.
1042#[cfg(all(test, feature = "p256"))]
1043mod tests_validate_path {
1044    use super::*;
1045    use der::Decode;
1046
1047    // Fixtures and time constants reused from tests_chain_walk.
1048    const GRY_NOW: u64 = 1_780_272_000; // 2026-06-01
1049
1050    fn load(bytes: &[u8]) -> Certificate {
1051        Certificate::from_der(bytes).expect("parse cert")
1052    }
1053
1054    fn policy_at(t: u64) -> ValidationPolicy {
1055        ValidationPolicy {
1056            current_time_unix: t,
1057            ..Default::default()
1058        }
1059    }
1060
1061    /// Happy-path 1-cert chain: self-signed cert is both chain and anchor.
1062    ///
1063    /// Expected: Ok(ValidatedPath { anchor_index: 0, depth: 0 })
1064    #[test]
1065    fn one_cert_chain_ok() {
1066        let cert = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
1067        let anchors = [TrustAnchor::from_cert(cert.clone())];
1068        let result = validate_path(&[cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
1069            .expect("1-cert chain must validate");
1070        assert_eq!(result.anchor_index, 0);
1071        assert_eq!(result.depth, 0);
1072    }
1073
1074    /// Happy-path 2-cert chain: leaf + intermediate, with root anchor.
1075    ///
1076    /// Oracle: openssl verify -CAfile gry-root.pem -untrusted gry-int.pem gry-leaf.pem → OK
1077    /// Expected: Ok(ValidatedPath { anchor_index: 0, depth: 1 })
1078    #[test]
1079    fn two_cert_chain_ok() {
1080        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1081        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1082        let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
1083        let anchors = [TrustAnchor::from_cert(root)];
1084        let result = validate_path(
1085            &[leaf, int_cert],
1086            &anchors,
1087            &policy_at(GRY_NOW),
1088            &EcdsaP256Verifier,
1089        )
1090        .expect("2-cert chain must validate");
1091        assert_eq!(result.anchor_index, 0);
1092        assert_eq!(result.depth, 1);
1093    }
1094
1095    /// Multiple anchors: correct anchor is second in the slice.
1096    ///
1097    /// Expected: Ok(ValidatedPath { anchor_index: 1, depth: 0 })
1098    #[test]
1099    fn correct_anchor_index_when_multiple_anchors() {
1100        let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
1101        let rsa = load(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"));
1102        // First anchor is the RSA cert (wrong name and SPKI for the P-256 chain).
1103        // Second anchor matches.
1104        let anchors = [
1105            TrustAnchor::from_cert(rsa),
1106            TrustAnchor::from_cert(p256.clone()),
1107        ];
1108        let result = validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier)
1109            .expect("must find second anchor");
1110        assert_eq!(result.anchor_index, 1);
1111        assert_eq!(result.depth, 0);
1112    }
1113
1114    /// Empty chain returns NoTrustedPath.
1115    #[test]
1116    fn empty_chain_returns_error() {
1117        let anchors = [TrustAnchor::from_cert(load(include_bytes!(
1118            "../tests/fixtures/ec-p256-sha256.der"
1119        )))];
1120        assert!(
1121            matches!(
1122                validate_path(&[], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
1123                Err(Error::NoTrustedPath)
1124            ),
1125            "empty chain must fail"
1126        );
1127    }
1128
1129    /// path_too_long: vxf chain [leaf, int] with max_path_len = 0.
1130    ///
1131    /// chain.len()=2 → 1 intermediate. 1 > max_path_len(0) → PathTooLong.
1132    #[test]
1133    fn path_too_long_returns_error() {
1134        let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
1135        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1136        let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
1137        let anchors = [TrustAnchor::from_cert(root)];
1138        let policy = ValidationPolicy {
1139            current_time_unix: GRY_NOW,
1140            max_path_len: 0,
1141            ..Default::default()
1142        };
1143        assert!(
1144            matches!(
1145                validate_path(&[leaf, int_cert], &anchors, &policy, &EcdsaP256Verifier),
1146                Err(Error::PathTooLong)
1147            ),
1148            "1 intermediate with max_path_len=0 must return PathTooLong"
1149        );
1150    }
1151
1152    /// no_trusted_path: vxf chain presented to an unrelated anchor (gry-root).
1153    ///
1154    /// vxf's last cert issuer name does not match gry-root's subject name.
1155    #[test]
1156    fn no_trusted_path_unrelated_anchor_returns_error() {
1157        let gry_root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1158        let vxf_int = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1159        let vxf_leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
1160        let anchors = [TrustAnchor::from_cert(gry_root)];
1161        assert!(
1162            matches!(
1163                validate_path(
1164                    &[vxf_leaf, vxf_int],
1165                    &anchors,
1166                    &policy_at(GRY_NOW),
1167                    &EcdsaP256Verifier
1168                ),
1169                Err(Error::NoTrustedPath)
1170            ),
1171            "vxf chain with gry anchor must return NoTrustedPath"
1172        );
1173    }
1174
1175    /// oid_mismatch: outer signatureAlgorithm OID differs from inner TBS signature OID.
1176    ///
1177    /// Patch the SECOND occurrence of the ECDSA-with-SHA256 OID bytes in vxf-leaf.der
1178    /// to ECDSA-with-SHA384. The inner TBS.signature remains SHA256.
1179    /// check_oid_consistency detects this → MalformedCertificate { index: 0 }.
1180    ///
1181    /// Oracle: RFC 5280 §4.1.1.2 requires outer and inner AlgorithmIdentifiers to be identical.
1182    #[test]
1183    fn oid_mismatch_outer_returns_malformed_certificate() {
1184        let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
1185        // ECDSA-with-SHA256 OID content bytes: 1.2.840.10045.4.3.2
1186        let oid_sha256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02];
1187        // ECDSA-with-SHA384 OID content bytes: 1.2.840.10045.4.3.3 (same length, last byte differs)
1188        let oid_sha384: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03];
1189        // In Certificate DER the inner TBS.signature OID appears FIRST (inside TBSCertificate)
1190        // and the outer signatureAlgorithm OID appears SECOND (after TBSCertificate). Patching
1191        // only the second occurrence changes the outer OID while leaving the inner intact.
1192        let first = leaf_der
1193            .windows(8)
1194            .position(|w| w == oid_sha256)
1195            .expect("inner SHA256 OID must be present in vxf-leaf.der");
1196        let second = leaf_der[first + 8..]
1197            .windows(8)
1198            .position(|w| w == oid_sha256)
1199            .map(|p| first + 8 + p)
1200            .expect("outer SHA256 OID must be present in vxf-leaf.der");
1201        leaf_der[second..second + 8].copy_from_slice(oid_sha384);
1202        let leaf = Certificate::from_der(&leaf_der).expect("patched DER must parse");
1203        assert_ne!(
1204            leaf.signature_algorithm, leaf.tbs_certificate.signature,
1205            "outer/inner OIDs must differ after patch"
1206        );
1207        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1208        let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
1209        let anchors = [TrustAnchor::from_cert(root)];
1210        assert!(
1211            matches!(
1212                validate_path(
1213                    &[leaf, int_cert],
1214                    &anchors,
1215                    &policy_at(GRY_NOW),
1216                    &EcdsaP256Verifier
1217                ),
1218                Err(Error::MalformedCertificate { index: 0 })
1219            ),
1220            "outer/inner OID mismatch must return MalformedCertificate {{ index: 0 }}"
1221        );
1222    }
1223
1224    /// intermediate_not_ca: nca-int has no BasicConstraints extension.
1225    ///
1226    /// Oracle: pyca/cryptography — nca-int built without any extensions.
1227    /// cert_is_ca(nca-int) returns None → NotCA { index: 1 }.
1228    #[test]
1229    fn intermediate_not_ca_returns_not_ca() {
1230        let root = load(include_bytes!("../tests/fixtures/nca-root.der"));
1231        let int_cert = load(include_bytes!("../tests/fixtures/nca-int.der"));
1232        let leaf = load(include_bytes!("../tests/fixtures/nca-leaf.der"));
1233        let anchors = [TrustAnchor::from_cert(root)];
1234        assert!(
1235            matches!(
1236                validate_path(
1237                    &[leaf, int_cert],
1238                    &anchors,
1239                    &policy_at(GRY_NOW),
1240                    &EcdsaP256Verifier
1241                ),
1242                Err(Error::NotCA { index: 1 })
1243            ),
1244            "intermediate without BasicConstraints CA flag must return NotCA {{ index: 1 }}"
1245        );
1246    }
1247
1248    /// key_usage_missing_cert_sign: kuf-int has KeyUsage with digitalSignature only.
1249    ///
1250    /// Oracle: pyca/cryptography — kuf-int KeyUsage.keyCertSign = False.
1251    /// Default policy has enforce_key_usage = true; chain_walk checks at i=1.
1252    #[test]
1253    fn key_usage_missing_cert_sign_returns_error() {
1254        let root = load(include_bytes!("../tests/fixtures/kuf-root.der"));
1255        let int_cert = load(include_bytes!("../tests/fixtures/kuf-int.der"));
1256        let leaf = load(include_bytes!("../tests/fixtures/kuf-leaf.der"));
1257        let anchors = [TrustAnchor::from_cert(root)];
1258        assert!(
1259            matches!(
1260                validate_path(&[leaf, int_cert], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
1261                Err(Error::KeyUsageMissing { index: 1 })
1262            ),
1263            "intermediate with KeyUsage but no keyCertSign must return KeyUsageMissing {{ index: 1 }}"
1264        );
1265    }
1266
1267    /// Security test: anchor with matching name but wrong SPKI must be rejected.
1268    ///
1269    /// Guards against a name-collision attack: an attacker who creates a root cert
1270    /// with the same DN as a trusted anchor but a different key must not be accepted.
1271    /// The self-issued SPKI guard in validate_path catches this.
1272    #[test]
1273    fn forged_anchor_name_match_spki_mismatch_rejected() {
1274        use der::Decode as _;
1275        let p256 = Certificate::from_der(include_bytes!("../tests/fixtures/ec-p256-sha256.der"))
1276            .expect("parse P-256 cert");
1277        let rsa =
1278            Certificate::from_der(include_bytes!("../tests/fixtures/rsa-pkcs1v15-sha256.der"))
1279                .expect("parse RSA cert");
1280        // Forged anchor: P-256 cert's subject name + RSA cert's SPKI.
1281        let forged = TrustAnchor::new(
1282            p256.tbs_certificate.subject.clone(),
1283            rsa.tbs_certificate.subject_public_key_info.clone(),
1284        );
1285        let anchors = [forged];
1286        assert!(
1287            matches!(
1288                validate_path(&[p256], &anchors, &policy_at(GRY_NOW), &EcdsaP256Verifier),
1289                Err(Error::NoTrustedPath)
1290            ),
1291            "anchor with matching name but wrong SPKI must return NoTrustedPath"
1292        );
1293    }
1294}
1295
1296// PKIX-vxf + PKIX-gry: chain_walk tests require the p256 feature.
1297#[cfg(all(test, feature = "p256"))]
1298mod tests_chain_walk {
1299    use super::*;
1300    use der::Decode;
1301
1302    // Fixtures (PKIX-vxf):
1303    //   vxf-root.der — self-signed root CA, CN=PKIX-vxf-root  (P-256)
1304    //   vxf-int.der  — intermediate CA, CN=PKIX-vxf-int, signed by vxf-root
1305    //   vxf-leaf.der — leaf cert, CN=PKIX-vxf-leaf, signed by vxf-int
1306    //   chk-root.der / chk-int.der / chk-leaf-wrong-issuer.der — ChainBroken test chain
1307    //
1308    // Fixtures (PKIX-gry):
1309    //   gry-root.der                  — root CA, CN=PKIX-gry-root (P-256)
1310    //   gry-int.der                   — intermediate CA, CN=PKIX-gry-int, valid 2026-2036
1311    //   gry-leaf.der                  — leaf, CN=PKIX-gry-leaf, valid 2026-2027 (short-lived)
1312    //   gry-leaf-unknown-crit.der     — leaf with unknown critical extension
1313    //
1314    // Unix timestamp constants for gry validity tests:
1315    //   GRY_NOW     = 1780272000  (2026-06-01, all gry certs valid)
1316    //   GRY_EXPIRED = 1830384000  (2028-01-02, gry-leaf expired; gry-int still valid)
1317    //   GRY_NOTYET  = 0           (1970-01-01, all gry certs not-yet-valid)
1318    //
1319    // Oracle:
1320    //   vxf chain: openssl verify -CAfile vxf-root.pem -untrusted vxf-int.pem vxf-leaf.pem → OK
1321    //   gry chain: pyca/cryptography; chain verifies at GRY_NOW
1322    //   chk-leaf-wrong-issuer: signature valid under chk-int key (pyca); issuer = PKIX-WRONG-ISSUER by design
1323
1324    const GRY_NOW: u64 = 1_780_272_000;
1325    const GRY_EXPIRED: u64 = 1_830_384_000;
1326    const GRY_NOTYET: u64 = 0;
1327
1328    fn load(bytes: &[u8]) -> Certificate {
1329        Certificate::from_der(bytes).expect("parse cert")
1330    }
1331
1332    fn policy_at(t: u64) -> ValidationPolicy {
1333        ValidationPolicy {
1334            current_time_unix: t,
1335            ..Default::default()
1336        }
1337    }
1338
1339    /// 1-cert chain: self-signed P-256 cert as both chain and anchor.
1340    #[test]
1341    fn single_cert_chain_ok() {
1342        let p256 = load(include_bytes!("../tests/fixtures/ec-p256-sha256.der"));
1343        let policy = policy_at(GRY_NOW);
1344        let anchor = TrustAnchor::from_cert(p256.clone());
1345        chain_walk(&[p256], &anchor, &policy, &EcdsaP256Verifier)
1346            .expect("1-cert chain must pass chain_walk");
1347    }
1348
1349    /// 2-cert chain (leaf + intermediate) with root as anchor.
1350    ///
1351    /// Oracle: openssl verify -CAfile vxf-root.pem -untrusted vxf-int.pem vxf-leaf.pem → OK
1352    #[test]
1353    fn two_cert_chain_ok() {
1354        let root = load(include_bytes!("../tests/fixtures/vxf-root.der"));
1355        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1356        let leaf = load(include_bytes!("../tests/fixtures/vxf-leaf.der"));
1357        let policy = policy_at(GRY_NOW);
1358        let anchor = TrustAnchor::from_cert(root);
1359        chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier)
1360            .expect("2-cert chain must pass chain_walk");
1361    }
1362
1363    /// Leaf with corrupted signature — last byte flipped.
1364    ///
1365    /// The DER structure remains valid; only the BIT STRING content is wrong.
1366    /// Expect SignatureInvalid at chain index 0.
1367    #[test]
1368    fn corrupted_signature_returns_signature_invalid() {
1369        let mut leaf_der = include_bytes!("../tests/fixtures/vxf-leaf.der").to_vec();
1370        *leaf_der.last_mut().unwrap() ^= 0xFF;
1371        let leaf = Certificate::from_der(&leaf_der).expect("parse still succeeds after bit flip");
1372        let int_cert = load(include_bytes!("../tests/fixtures/vxf-int.der"));
1373        let anchor = TrustAnchor::from_cert(load(include_bytes!("../tests/fixtures/vxf-root.der")));
1374        let policy = policy_at(GRY_NOW);
1375        assert!(
1376            matches!(
1377                chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1378                Err(Error::SignatureInvalid { index: 0 })
1379            ),
1380            "corrupted leaf signature must return SignatureInvalid {{ index: 0 }}"
1381        );
1382    }
1383
1384    /// Chain where the leaf's issuer field does not match the intermediate's subject.
1385    ///
1386    /// Oracle: chk-leaf-wrong-issuer was signed by chk-int's private key
1387    /// (signature IS valid), but its issuer field = "PKIX-WRONG-ISSUER" by design.
1388    #[test]
1389    fn wrong_issuer_name_returns_chain_broken() {
1390        let root = load(include_bytes!("../tests/fixtures/chk-root.der"));
1391        let int_cert = load(include_bytes!("../tests/fixtures/chk-int.der"));
1392        let leaf_wrong = load(include_bytes!(
1393            "../tests/fixtures/chk-leaf-wrong-issuer.der"
1394        ));
1395        let policy = policy_at(GRY_NOW);
1396        let anchor = TrustAnchor::from_cert(root);
1397        assert!(
1398            matches!(
1399                chain_walk(
1400                    &[leaf_wrong, int_cert],
1401                    &anchor,
1402                    &policy,
1403                    &EcdsaP256Verifier
1404                ),
1405                Err(Error::ChainBroken { index: 0 })
1406            ),
1407            "leaf with wrong issuer must return ChainBroken {{ index: 0 }}"
1408        );
1409    }
1410
1411    // --- PKIX-gry per-cert check tests ---
1412
1413    /// Expired leaf cert → ValidityPeriod at index 0.
1414    ///
1415    /// Oracle: gry-leaf.der has notAfter=2027-01-01; GRY_EXPIRED=2028-01-02.
1416    /// gry-int.der has notAfter=2036-01-01, which is still valid at GRY_EXPIRED.
1417    /// Reverse walk: i=1 (gry-int) passes validity, then i=0 (gry-leaf) fails.
1418    #[test]
1419    fn expired_leaf_returns_validity_period() {
1420        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1421        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1422        let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
1423        let policy = policy_at(GRY_EXPIRED);
1424        let anchor = TrustAnchor::from_cert(root);
1425        assert!(
1426            matches!(
1427                chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1428                Err(Error::ValidityPeriod { index: 0 })
1429            ),
1430            "expired leaf must return ValidityPeriod {{ index: 0 }}"
1431        );
1432    }
1433
1434    /// Not-yet-valid intermediate → ValidityPeriod at index 1.
1435    ///
1436    /// Oracle: gry-int.der has notBefore=2026-01-01; GRY_NOTYET=0 (1970-01-01).
1437    /// Reverse walk processes chain[1] (gry-int) first; it is not yet valid at time 0.
1438    #[test]
1439    fn notyet_valid_intermediate_returns_validity_period() {
1440        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1441        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1442        let leaf = load(include_bytes!("../tests/fixtures/gry-leaf.der"));
1443        let policy = policy_at(GRY_NOTYET);
1444        let anchor = TrustAnchor::from_cert(root);
1445        assert!(
1446            matches!(
1447                chain_walk(&[leaf, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1448                Err(Error::ValidityPeriod { index: 1 })
1449            ),
1450            "not-yet-valid intermediate must return ValidityPeriod {{ index: 1 }}"
1451        );
1452    }
1453
1454    /// Leaf with unknown critical extension → UnhandledCriticalExtension at index 0.
1455    ///
1456    /// Oracle: gry-leaf-unknown-crit.der was generated with OID 1.3.6.1.5.5.7.99.99 critical=true
1457    /// (not in HANDLED_CRITICAL_OIDS) using pyca/cryptography.
1458    #[test]
1459    fn unknown_critical_extension_returns_unhandled() {
1460        let root = load(include_bytes!("../tests/fixtures/gry-root.der"));
1461        let int_cert = load(include_bytes!("../tests/fixtures/gry-int.der"));
1462        let leaf_unk = load(include_bytes!(
1463            "../tests/fixtures/gry-leaf-unknown-crit.der"
1464        ));
1465        let policy = policy_at(GRY_NOW);
1466        let anchor = TrustAnchor::from_cert(root);
1467        assert!(
1468            matches!(
1469                chain_walk(&[leaf_unk, int_cert], &anchor, &policy, &EcdsaP256Verifier),
1470                Err(Error::UnhandledCriticalExtension { index: 0 })
1471            ),
1472            "unknown critical ext must return UnhandledCriticalExtension {{ index: 0 }}"
1473        );
1474    }
1475}