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