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