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