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