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